fix(android): recover ExoPlayer audio failures
This commit is contained in:
@@ -43,6 +43,7 @@ import androidx.media3.datasource.cronet.CronetDataSource
|
||||
import androidx.media3.exoplayer.DecoderReuseEvaluation
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlaybackException
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
||||
@@ -96,6 +97,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
private const val WATCHDOG_CHECK_INTERVAL_MS = 1000L
|
||||
private const val WATCHDOG_TIMEOUT_MS = 8000L
|
||||
private const val DECODER_HANG_TIMEOUT_MS = 5000L
|
||||
private const val MAX_AUDIO_RECOVERY_ATTEMPTS = 2
|
||||
private const val FPS_SAMPLE_COUNT = 8
|
||||
private const val TS_TIMESTAMP_SEARCH_PACKETS = 1800
|
||||
|
||||
@@ -136,8 +138,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
private var tunnelingDisabledForAudioCodec: Boolean = false
|
||||
private var tunnelingDisabledForVideoCodec: Boolean = false
|
||||
private var tunnelingDisabledForDecodedTrueHdPcm: Boolean = false
|
||||
private var tunnelingDisabledForAudioRecovery: Boolean = false
|
||||
private val tunnelingDisabledForCodec: Boolean
|
||||
get() = tunnelingDisabledForAudioCodec || tunnelingDisabledForVideoCodec || tunnelingDisabledForDecodedTrueHdPcm
|
||||
get() = tunnelingDisabledForAudioCodec || tunnelingDisabledForVideoCodec || tunnelingDisabledForDecodedTrueHdPcm || tunnelingDisabledForAudioRecovery
|
||||
private var currentTunneledPlayback: Boolean = false
|
||||
private var lastSeekable: Boolean? = null
|
||||
|
||||
@@ -153,6 +156,13 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
private var decoderHangRunnable: Runnable? = null
|
||||
private var decoderInitName: String? = null
|
||||
private var audioDecoderInitName: String? = null
|
||||
private var lastAudioTrackConfig: AudioSink.AudioTrackConfig? = null
|
||||
private val directAudioOutputBlockedAfterFailure = mutableSetOf<String>()
|
||||
private val loggedDirectAudioRecoveryBlocks = mutableSetOf<String>()
|
||||
private var audioRecoveryAttempts: Int = 0
|
||||
private var lastAudioRecoveryAction: String? = null
|
||||
private var lastAudioRecoveryReason: String? = null
|
||||
private var lastAudioSinkError: String? = null
|
||||
private var loggedEwasteEac3Workaround: Boolean = false
|
||||
private var lastTrueHdDirectOutputLogKey: String? = null
|
||||
private var loggedDecodedTrueHdTunnelingGuard: Boolean = false
|
||||
@@ -439,7 +449,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
// Use DefaultRenderersFactory with FFmpeg fallback for unsupported or blocked audio codecs.
|
||||
val renderersFactory = PlezyRenderersFactory(activity).apply {
|
||||
audioDiagnosticsLogger = { level, prefix, message -> emitLog(level, prefix, message) }
|
||||
shouldBlockDirectTrueHd = { format -> this@ExoPlayerCore.shouldBlockDirectTrueHd(format, "sink support") }
|
||||
shouldBlockDirectAudioOutput = { format -> this@ExoPlayerCore.shouldBlockDirectAudioOutput(format, "sink support") }
|
||||
onAudioCapabilitiesChanged = { updateAudioDecoderPolicy("audio capabilities changed") }
|
||||
setEnableDecoderFallback(true)
|
||||
setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
@@ -953,6 +963,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
return
|
||||
}
|
||||
|
||||
if (retryAfterAudioTrackError(error, causeChain)) return
|
||||
|
||||
if (currentMediaUri != null) {
|
||||
Log.w(TAG, "ExoPlayer error (code ${error.errorCode}) - attempting fallback to MPV")
|
||||
val handled = delegate?.onFormatUnsupported(
|
||||
@@ -974,6 +986,95 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
)
|
||||
}
|
||||
|
||||
private fun retryAfterAudioTrackError(error: PlaybackException, causeChain: String): Boolean {
|
||||
if (!isAudioTrackError(error.errorCode)) return false
|
||||
|
||||
val player = exoPlayer ?: return false
|
||||
val uri = currentMediaUri ?: return false
|
||||
if (audioRecoveryAttempts >= MAX_AUDIO_RECOVERY_ATTEMPTS) {
|
||||
emitLog(
|
||||
"warn",
|
||||
"audio-recovery",
|
||||
"ExoPlayer audio recovery exhausted after $audioRecoveryAttempts attempts for ${PlaybackException.getErrorCodeName(error.errorCode)}"
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
val selectedFormat = selectedAudioFormat()
|
||||
val errorFormat = (error as? ExoPlaybackException)?.rendererFormat?.takeIf { format ->
|
||||
format.sampleMimeType?.startsWith("audio/") == true
|
||||
}
|
||||
val recoveryFormat = selectedFormat ?: errorFormat
|
||||
val actions = mutableListOf<String>()
|
||||
|
||||
recoveryFormat?.sampleMimeType
|
||||
?.takeIf { isEncodedAudioMimeType(it) }
|
||||
?.let { mimeType ->
|
||||
if (directAudioOutputBlockedAfterFailure.add(mimeType)) {
|
||||
actions.add("force-decoded-pcm($mimeType)")
|
||||
}
|
||||
}
|
||||
|
||||
if (!tunnelingDisabledForAudioRecovery) {
|
||||
tunnelingDisabledForAudioRecovery = true
|
||||
actions.add("disable-tunneling")
|
||||
}
|
||||
if (actions.isEmpty()) actions.add("reload")
|
||||
|
||||
audioRecoveryAttempts++
|
||||
val savedPosition = maxOf(player.currentPosition, lastPosition, pendingStartPositionMs)
|
||||
val savedPlayWhenReady = player.playWhenReady
|
||||
val previousAudioTrackConfig = lastAudioTrackConfig
|
||||
pendingStartPositionMs = savedPosition
|
||||
pendingPlayWhenReady = savedPlayWhenReady
|
||||
audioDecoderInitName = null
|
||||
lastAudioTrackConfig = null
|
||||
lastAudioRecoveryAction = actions.joinToString(",")
|
||||
lastAudioRecoveryReason = "${PlaybackException.getErrorCodeName(error.errorCode)}: ${error.message ?: causeChain.ifEmpty { "unknown" }}"
|
||||
|
||||
stopFrameWatchdog()
|
||||
cancelDecoderHangCheck()
|
||||
applyTrackSelectorPolicy(reason = "audio recovery", forceSelector = true)
|
||||
|
||||
emitLog(
|
||||
"warn",
|
||||
"audio-recovery",
|
||||
"Retrying in ExoPlayer ($audioRecoveryAttempts/$MAX_AUDIO_RECOVERY_ATTEMPTS) at ${savedPosition}ms; " +
|
||||
"format=${recoveryFormat?.let { formatAudioSummary(it) } ?: "unknown"}, " +
|
||||
"lastOutput=${describeAudioTrackConfig(previousAudioTrackConfig)}, actions=$lastAudioRecoveryAction"
|
||||
)
|
||||
|
||||
if (!setCurrentMediaForRetry(player, uri, savedPosition)) return false
|
||||
player.prepare()
|
||||
player.playWhenReady = savedPlayWhenReady
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isAudioTrackError(errorCode: Int): Boolean = when (errorCode) {
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED,
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED,
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED,
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun isEncodedAudioMimeType(mimeType: String): Boolean = mimeType.startsWith("audio/") && mimeType != MimeTypes.AUDIO_RAW
|
||||
|
||||
private fun setCurrentMediaForRetry(player: ExoPlayer, uri: String, positionMs: Long): Boolean {
|
||||
if (currentMediaIsLive) {
|
||||
val factory = dataSourceFactory ?: return false
|
||||
val extractorsFactory = androidx.media3.extractor.ExtractorsFactory {
|
||||
arrayOf(MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES))
|
||||
}
|
||||
val mediaSource = ProgressiveMediaSource.Factory(factory, extractorsFactory)
|
||||
.createMediaSource(MediaItem.fromUri(uri))
|
||||
player.setMediaSource(mediaSource, positionMs)
|
||||
} else {
|
||||
player.setMediaItem(buildMediaItem(uri), positionMs)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isDvProfile7Format(format: Format): Boolean {
|
||||
val codecs = format.codecs?.lowercase() ?: return false
|
||||
return codecs.startsWith("dvhe.07") || codecs.startsWith("dvh1.07")
|
||||
@@ -1473,6 +1574,21 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
return decision.blockDirectOutput
|
||||
}
|
||||
|
||||
private fun shouldBlockDirectAudioOutput(format: Format, reason: String): Boolean {
|
||||
val mimeType = format.sampleMimeType ?: return false
|
||||
if (directAudioOutputBlockedAfterFailure.contains(mimeType)) {
|
||||
if (loggedDirectAudioRecoveryBlocks.add("$mimeType|$reason")) {
|
||||
emitLog(
|
||||
"info",
|
||||
"audio-recovery",
|
||||
"Direct $mimeType output disabled after AudioTrack failure (reason=$reason); decoded PCM will be preferred"
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return mimeType == MimeTypes.AUDIO_TRUEHD && shouldBlockDirectTrueHd(format, reason)
|
||||
}
|
||||
|
||||
private fun updateAudioDecoderPolicy(reason: String, format: Format? = null) {
|
||||
if (format != null && format.sampleMimeType != MimeTypes.AUDIO_TRUEHD) return
|
||||
val decision = evaluateTrueHdDirectOutput(format)
|
||||
@@ -1825,7 +1941,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
currentTunneledPlayback = shouldTunnel
|
||||
val speed = exoPlayer?.playbackParameters?.speed ?: 1f
|
||||
val audioDelayActive = (renderersFactory?.audioDelayUs?.get() ?: 0L) != 0L
|
||||
emitLog("info", "tunneling", "Toggling tunneling=$shouldTunnel (reason=$reason, user=$tunnelingUserEnabled, speed=$speed, audioCodecDisabled=$tunnelingDisabledForAudioCodec, videoCodecDisabled=$tunnelingDisabledForVideoCodec, decodedTrueHdPcmDisabled=$tunnelingDisabledForDecodedTrueHdPcm, audioDelay=$audioDelayActive)")
|
||||
emitLog("info", "tunneling", "Toggling tunneling=$shouldTunnel (reason=$reason, user=$tunnelingUserEnabled, speed=$speed, audioCodecDisabled=$tunnelingDisabledForAudioCodec, videoCodecDisabled=$tunnelingDisabledForVideoCodec, decodedTrueHdPcmDisabled=$tunnelingDisabledForDecodedTrueHdPcm, audioRecoveryDisabled=$tunnelingDisabledForAudioRecovery, audioDelay=$audioDelayActive)")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1892,6 +2008,13 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
return parts.joinToString(", ")
|
||||
}
|
||||
|
||||
private fun describeAudioTrackConfig(config: AudioSink.AudioTrackConfig?): String {
|
||||
if (config == null) return "none"
|
||||
return "encoding=${config.encoding}, channels=${Integer.bitCount(config.channelConfig)}, " +
|
||||
"channelConfig=0x${config.channelConfig.toString(16)}, buffer=${config.bufferSize}B, " +
|
||||
"tunneling=${config.tunneling}, offload=${config.offload}"
|
||||
}
|
||||
|
||||
// Decoder hang detection via AnalyticsListener:
|
||||
// Tracks the gap between onVideoDecoderInitialized and onRenderedFirstFrame.
|
||||
// If the decoder is initialized and fed input but never produces output, it's hung
|
||||
@@ -1950,6 +2073,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
}
|
||||
|
||||
override fun onAudioSinkError(eventTime: AnalyticsListener.EventTime, audioSinkError: Exception) {
|
||||
lastAudioSinkError = "${audioSinkError.javaClass.name}: ${audioSinkError.message}"
|
||||
emitLog("warn", "audio", "Sink error: ${audioSinkError.javaClass.name}: ${audioSinkError.message}")
|
||||
}
|
||||
|
||||
@@ -1961,14 +2085,14 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
eventTime: AnalyticsListener.EventTime,
|
||||
audioTrackConfig: AudioSink.AudioTrackConfig
|
||||
) {
|
||||
lastAudioTrackConfig = audioTrackConfig
|
||||
val audioFormat = selectedAudioFormat()
|
||||
emitLog(
|
||||
"info",
|
||||
"audio",
|
||||
"AudioTrack initialized: encoding=${audioTrackConfig.encoding}, sampleRate=${audioTrackConfig.sampleRate}, " +
|
||||
"channelConfig=0x${audioTrackConfig.channelConfig.toString(16)}, buffer=${audioTrackConfig.bufferSize}B, " +
|
||||
"tunneling=${audioTrackConfig.tunneling}, offload=${audioTrackConfig.offload}"
|
||||
"AudioTrack initialized: input=${audioFormat?.let { formatAudioSummary(it) } ?: "unknown"}, " +
|
||||
"decoder=${audioDecoderInitName ?: "direct/bypass"}, ${describeAudioTrackConfig(audioTrackConfig)}"
|
||||
)
|
||||
val audioFormat = selectedAudioFormat()
|
||||
if (audioFormat?.sampleMimeType == MimeTypes.AUDIO_TRUEHD) {
|
||||
if (audioTrackConfig.tunneling && isPcmEncoding(audioTrackConfig.encoding)) {
|
||||
if (!loggedDecodedTrueHdTunnelingGuard) {
|
||||
@@ -2149,6 +2273,13 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
|
||||
decoderInitName = null
|
||||
audioDecoderInitName = null
|
||||
lastAudioTrackConfig = null
|
||||
lastAudioSinkError = null
|
||||
audioRecoveryAttempts = 0
|
||||
lastAudioRecoveryAction = null
|
||||
lastAudioRecoveryReason = null
|
||||
directAudioOutputBlockedAfterFailure.clear()
|
||||
loggedDirectAudioRecoveryBlocks.clear()
|
||||
currentVideoFormat = null
|
||||
loggedNativeDvSelectionKey = null
|
||||
loggedNativeDvFirstFrame = false
|
||||
@@ -2188,6 +2319,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
tunnelingDisabledForAudioCodec = false
|
||||
tunnelingDisabledForVideoCodec = false
|
||||
tunnelingDisabledForDecodedTrueHdPcm = false
|
||||
tunnelingDisabledForAudioRecovery = false
|
||||
currentTunneledPlayback = false
|
||||
pendingStartPositionMs = startPositionMs
|
||||
pendingPlayWhenReady = autoPlay
|
||||
@@ -2634,6 +2766,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
val player = exoPlayer ?: return emptyMap()
|
||||
val videoFormat = player.videoFormat
|
||||
val audioFormat = player.audioFormat
|
||||
val audioTrackConfig = lastAudioTrackConfig
|
||||
|
||||
// Get decoder info from the format's codecs field and check if hardware accelerated
|
||||
val videoDecoderInfo = getVideoDecoderInfo(videoFormat)
|
||||
@@ -2661,6 +2794,17 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
"audioChannels" to audioFormat?.channelCount,
|
||||
"audioBitrate" to audioFormat?.bitrate,
|
||||
"audioDecoderName" to audioDecoderInitName,
|
||||
"audioOutputEncoding" to audioTrackConfig?.encoding,
|
||||
"audioOutputChannels" to audioTrackConfig?.channelConfig?.let { Integer.bitCount(it) },
|
||||
"audioOutputChannelConfig" to audioTrackConfig?.channelConfig,
|
||||
"audioOutputTunneling" to audioTrackConfig?.tunneling,
|
||||
"audioOutputOffload" to audioTrackConfig?.offload,
|
||||
"audioOutputBufferSize" to audioTrackConfig?.bufferSize,
|
||||
"audioLastSinkError" to lastAudioSinkError,
|
||||
"audioRecoveryAttempts" to audioRecoveryAttempts,
|
||||
"audioRecoveryLastAction" to lastAudioRecoveryAction,
|
||||
"audioRecoveryLastReason" to lastAudioRecoveryReason,
|
||||
"audioDirectOutputBlockedMimes" to directAudioOutputBlockedAfterFailure.toList(),
|
||||
// Tunneling
|
||||
"tunneledPlayback" to currentTunneledPlayback,
|
||||
"tunnelingStatus" to getTunnelingStatus(player),
|
||||
@@ -2725,6 +2869,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
if (currentTunneledPlayback) return "Active"
|
||||
if (!tunnelingUserEnabled) return "Disabled by user"
|
||||
if (player.playbackParameters.speed != 1f) return "Off (speed ≠ 1×)"
|
||||
if (tunnelingDisabledForAudioRecovery) return "Off (audio recovery)"
|
||||
if (tunnelingDisabledForDecodedTrueHdPcm) return "Off (decoded TrueHD PCM)"
|
||||
if (tunnelingDisabledForVideoCodec) return "Off (video codec unsupported)"
|
||||
if (tunnelingDisabledForAudioCodec) return "Off (no HW audio decoder)"
|
||||
@@ -2761,9 +2906,17 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
|
||||
|
||||
decoderInitName = null
|
||||
audioDecoderInitName = null
|
||||
lastAudioTrackConfig = null
|
||||
lastAudioSinkError = null
|
||||
audioRecoveryAttempts = 0
|
||||
lastAudioRecoveryAction = null
|
||||
lastAudioRecoveryReason = null
|
||||
directAudioOutputBlockedAfterFailure.clear()
|
||||
loggedDirectAudioRecoveryBlocks.clear()
|
||||
tunnelingDisabledForAudioCodec = false
|
||||
tunnelingDisabledForVideoCodec = false
|
||||
tunnelingDisabledForDecodedTrueHdPcm = false
|
||||
tunnelingDisabledForAudioRecovery = false
|
||||
currentTunneledPlayback = false
|
||||
pendingStartPositionMs = 0L
|
||||
pendingPlayWhenReady = null
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.media.AudioDeviceInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@@ -29,8 +28,8 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
|
||||
/** Audio delay in microseconds. Shared with PositionFixAudioSink for live updates. */
|
||||
val audioDelayUs = AtomicLong(0L)
|
||||
|
||||
/** Returns whether direct TrueHD output should be hidden so decoded PCM output can be selected. */
|
||||
var shouldBlockDirectTrueHd: ((Format) -> Boolean)? = null
|
||||
/** Returns whether direct encoded output should be hidden so decoded PCM output can be selected. */
|
||||
var shouldBlockDirectAudioOutput: ((Format) -> Boolean)? = null
|
||||
|
||||
/** Called before Media3 reacts to an audio route/capability change. */
|
||||
var onAudioCapabilitiesChanged: (() -> Unit)? = null
|
||||
@@ -68,7 +67,7 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
|
||||
defaultSink,
|
||||
rawPositionUs,
|
||||
audioDelayUs,
|
||||
shouldBlockDirectTrueHd,
|
||||
shouldBlockDirectAudioOutput,
|
||||
onAudioCapabilitiesChanged,
|
||||
audioDiagnosticsLogger
|
||||
)
|
||||
@@ -101,7 +100,7 @@ private class PositionFixAudioSink(
|
||||
sink: AudioSink,
|
||||
private val rawPositionUs: AtomicLong,
|
||||
private val audioDelayUs: AtomicLong,
|
||||
private val shouldBlockDirectTrueHd: ((Format) -> Boolean)?,
|
||||
private val shouldBlockDirectAudioOutput: ((Format) -> Boolean)?,
|
||||
private val onAudioCapabilitiesChanged: (() -> Unit)?,
|
||||
private val log: ((String, String, String) -> Unit)?
|
||||
) : ForwardingAudioSink(sink) {
|
||||
@@ -117,27 +116,27 @@ private class PositionFixAudioSink(
|
||||
// Transient counter-offset: after seek/flush, ramp offset from 0 to full
|
||||
// over recoveryDurationUs to prevent video frame drops.
|
||||
private var recoveryDurationUs = 0L
|
||||
private var loggedTrueHdDirectBlock = false
|
||||
private val loggedDirectBlockMimeTypes = mutableSetOf<String>()
|
||||
private var loggedFirstBuffer = false
|
||||
|
||||
override fun supportsFormat(format: Format): Boolean {
|
||||
if (blocksDirectTrueHd(format)) return false
|
||||
if (blocksDirectAudioOutput(format)) return false
|
||||
return super.supportsFormat(format)
|
||||
}
|
||||
|
||||
override fun getFormatSupport(format: Format): Int {
|
||||
if (blocksDirectTrueHd(format)) return AudioSink.SINK_FORMAT_UNSUPPORTED
|
||||
if (blocksDirectAudioOutput(format)) return AudioSink.SINK_FORMAT_UNSUPPORTED
|
||||
return super.getFormatSupport(format)
|
||||
}
|
||||
|
||||
private fun blocksDirectTrueHd(format: Format): Boolean {
|
||||
if (format.sampleMimeType != MimeTypes.AUDIO_TRUEHD || shouldBlockDirectTrueHd?.invoke(format) != true) return false
|
||||
if (!loggedTrueHdDirectBlock) {
|
||||
loggedTrueHdDirectBlock = true
|
||||
private fun blocksDirectAudioOutput(format: Format): Boolean {
|
||||
if (shouldBlockDirectAudioOutput?.invoke(format) != true) return false
|
||||
val mimeType = format.sampleMimeType ?: "unknown"
|
||||
if (loggedDirectBlockMimeTypes.add(mimeType)) {
|
||||
log?.invoke(
|
||||
"info",
|
||||
"audio",
|
||||
"Blocking direct TrueHD output; route does not report TrueHD bitstream support, decoded PCM output will be preferred"
|
||||
"Blocking direct $mimeType output; decoded PCM output will be preferred"
|
||||
)
|
||||
}
|
||||
return true
|
||||
@@ -349,6 +348,7 @@ private class RawPositionAudioOutput(
|
||||
private var loggedFirstWrite = false
|
||||
private var writeCount = 0L
|
||||
private var writtenBytes = 0L
|
||||
private var failed = false
|
||||
|
||||
override fun getPositionUs(): Long {
|
||||
val pos = delegate.getPositionUs()
|
||||
@@ -362,7 +362,14 @@ private class RawPositionAudioOutput(
|
||||
@Throws(AudioOutput.WriteException::class)
|
||||
override fun write(buffer: ByteBuffer, size: Int, presentationTimeUs: Long): Boolean {
|
||||
val before = buffer.position()
|
||||
val handled = delegate.write(buffer, size, presentationTimeUs)
|
||||
val handled = try {
|
||||
delegate.write(buffer, size, presentationTimeUs)
|
||||
} catch (e: AudioOutput.WriteException) {
|
||||
failed = true
|
||||
rawPositionUs.set(Long.MIN_VALUE)
|
||||
log?.invoke("warn", "audio-output", "Write failed; AudioTrack will not be reused: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
val written = buffer.position() - before
|
||||
if (written > 0) {
|
||||
writeCount++
|
||||
@@ -391,6 +398,10 @@ private class RawPositionAudioOutput(
|
||||
|
||||
override fun release() {
|
||||
rawPositionUs.set(Long.MIN_VALUE)
|
||||
if (failed) {
|
||||
delegate.release()
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
delegate.stop()
|
||||
delegate.flush()
|
||||
|
||||
Reference in New Issue
Block a user