From d97af76280b834e4279bcddac966e21153430740 Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 24 Sep 2020 15:11:06 +0100 Subject: [PATCH] Retry after offload playback failure Do that by adding a recoverable state to the ExoPlaybackException marking when it is needed to recreate the renderers. PiperOrigin-RevId: 333507849 --- RELEASENOTES.md | 2 + .../android/exoplayer2/BaseRenderer.java | 15 +++- .../exoplayer2/ExoPlaybackException.java | 70 +++++++++++++++---- .../exoplayer2/ExoPlayerImplInternal.java | 39 ++++++++++- .../android/exoplayer2/audio/AudioSink.java | 44 +++++++----- .../audio/DecoderAudioRenderer.java | 13 ++-- .../exoplayer2/audio/DefaultAudioSink.java | 19 +++-- .../audio/MediaCodecAudioRenderer.java | 11 ++- 8 files changed, 166 insertions(+), 47 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6c94ec1d86..c7555d768b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,8 @@ ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). +* Audio: + * Retry playback after some types of `AudioTrack` error. ### 2.12.0 (2020-09-11) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 351f6c50f2..315431c6e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -341,6 +341,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { */ protected final ExoPlaybackException createRendererException( Exception cause, @Nullable Format format) { + return createRendererException(cause, format, /* isRecoverable= */ false); + } + + /** + * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for + * this renderer. + * + * @param cause The cause of the exception. + * @param format The current format used by the renderer. May be null. + * @param isRecoverable If the error is recoverable by disabling and re-enabling the renderer. + */ + protected final ExoPlaybackException createRendererException( + Exception cause, @Nullable Format format, boolean isRecoverable) { @FormatSupport int formatSupport = RendererCapabilities.FORMAT_HANDLED; if (format != null && !throwRendererExceptionIsExecuting) { // Prevent recursive re-entry from subclass supportsFormat implementations. @@ -354,7 +367,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } } return ExoPlaybackException.createForRenderer( - cause, getName(), getIndex(), format, formatSupport); + cause, getName(), getIndex(), format, formatSupport, isRecoverable); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index 93fb4b0118..d69b747f8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -29,9 +29,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.TimeoutException; -/** - * Thrown when a non-recoverable playback failure occurs. - */ +/** Thrown when a non locally recoverable playback failure occurs. */ public final class ExoPlaybackException extends Exception { /** @@ -138,6 +136,17 @@ public final class ExoPlaybackException extends Exception { */ @Nullable public final MediaSource.MediaPeriodId mediaPeriodId; + /** + * Whether the error may be recoverable. + * + *

This is only used internally by ExoPlayer to try to recover from some errors and should not + * be used by apps. + * + *

If the {@link #type} is {@link #TYPE_RENDERER}, it may be possible to recover from the error + * by disabling and re-enabling the renderers. + */ + /* package */ final boolean isRecoverable; + @Nullable private final Throwable cause; /** @@ -167,6 +176,34 @@ public final class ExoPlaybackException extends Exception { int rendererIndex, @Nullable Format rendererFormat, @FormatSupport int rendererFormatSupport) { + return createForRenderer( + cause, + rendererName, + rendererIndex, + rendererFormat, + rendererFormatSupport, + /* isRecoverable= */ false); + } + + /** + * Creates an instance of type {@link #TYPE_RENDERER}. + * + * @param cause The cause of the failure. + * @param rendererIndex The index of the renderer in which the failure occurred. + * @param rendererFormat The {@link Format} the renderer was using at the time of the exception, + * or null if the renderer wasn't using a {@link Format}. + * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code + * rendererFormat}. Ignored if {@code rendererFormat} is null. + * @param isRecoverable If the failure can be recovered by disabling and re-enabling the renderer. + * @return The created instance. + */ + public static ExoPlaybackException createForRenderer( + Exception cause, + String rendererName, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport, + boolean isRecoverable) { return new ExoPlaybackException( TYPE_RENDERER, cause, @@ -175,7 +212,8 @@ public final class ExoPlaybackException extends Exception { rendererIndex, rendererFormat, rendererFormat == null ? RendererCapabilities.FORMAT_HANDLED : rendererFormatSupport, - TIMEOUT_OPERATION_UNDEFINED); + TIMEOUT_OPERATION_UNDEFINED, + isRecoverable); } /** @@ -225,7 +263,8 @@ public final class ExoPlaybackException extends Exception { /* rendererIndex= */ C.INDEX_UNSET, /* rendererFormat= */ null, /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED, - timeoutOperation); + timeoutOperation, + /* isRecoverable= */ false); } private ExoPlaybackException(@Type int type, Throwable cause) { @@ -237,7 +276,8 @@ public final class ExoPlaybackException extends Exception { /* rendererIndex= */ C.INDEX_UNSET, /* rendererFormat= */ null, /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED, - TIMEOUT_OPERATION_UNDEFINED); + TIMEOUT_OPERATION_UNDEFINED, + /* isRecoverable= */ false); } private ExoPlaybackException(@Type int type, String message) { @@ -249,7 +289,8 @@ public final class ExoPlaybackException extends Exception { /* rendererIndex= */ C.INDEX_UNSET, /* rendererFormat= */ null, /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED, - /* timeoutOperation= */ TIMEOUT_OPERATION_UNDEFINED); + /* timeoutOperation= */ TIMEOUT_OPERATION_UNDEFINED, + /* isRecoverable= */ false); } private ExoPlaybackException( @@ -260,7 +301,8 @@ public final class ExoPlaybackException extends Exception { int rendererIndex, @Nullable Format rendererFormat, @FormatSupport int rendererFormatSupport, - @TimeoutOperation int timeoutOperation) { + @TimeoutOperation int timeoutOperation, + boolean isRecoverable) { this( deriveMessage( type, @@ -277,7 +319,8 @@ public final class ExoPlaybackException extends Exception { rendererFormatSupport, /* mediaPeriodId= */ null, timeoutOperation, - /* timestampMs= */ SystemClock.elapsedRealtime()); + /* timestampMs= */ SystemClock.elapsedRealtime(), + isRecoverable); } private ExoPlaybackException( @@ -290,7 +333,8 @@ public final class ExoPlaybackException extends Exception { @FormatSupport int rendererFormatSupport, @Nullable MediaSource.MediaPeriodId mediaPeriodId, @TimeoutOperation int timeoutOperation, - long timestampMs) { + long timestampMs, + boolean isRecoverable) { super(message, cause); this.type = type; this.cause = cause; @@ -301,6 +345,7 @@ public final class ExoPlaybackException extends Exception { this.mediaPeriodId = mediaPeriodId; this.timeoutOperation = timeoutOperation; this.timestampMs = timestampMs; + this.isRecoverable = isRecoverable; } /** @@ -360,7 +405,7 @@ public final class ExoPlaybackException extends Exception { * @return The copied exception. */ @CheckResult - /* package= */ ExoPlaybackException copyWithMediaPeriodId( + /* package */ ExoPlaybackException copyWithMediaPeriodId( @Nullable MediaSource.MediaPeriodId mediaPeriodId) { return new ExoPlaybackException( getMessage(), @@ -372,7 +417,8 @@ public final class ExoPlaybackException extends Exception { rendererFormatSupport, mediaPeriodId, timeoutOperation, - timestampMs); + timestampMs, + isRecoverable); } @Nullable diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 0752c08949..45af6d601f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -142,6 +142,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22; private static final int MSG_SET_PAUSE_AT_END_OF_WINDOW = 23; private static final int MSG_SET_OFFLOAD_SCHEDULING_ENABLED = 24; + private static final int MSG_ATTEMPT_ERROR_RECOVERY = 25; private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; @@ -196,6 +197,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private long rendererPositionUs; private int nextPendingMessageIndexHint; private boolean deliverPendingMessageAtStartPositionRequired; + @Nullable private ExoPlaybackException pendingRecoverableError; private long releaseTimeoutMs; private boolean throwWhenStuckBuffering; @@ -525,6 +527,9 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_SET_OFFLOAD_SCHEDULING_ENABLED: setOffloadSchedulingEnabledInternal(msg.arg1 == 1); break; + case MSG_ATTEMPT_ERROR_RECOVERY: + attemptErrorRecovery((ExoPlaybackException) msg.obj); + break; case MSG_RELEASE: releaseInternal(); // Return immediately to not send playback info updates after release. @@ -542,9 +547,22 @@ import java.util.concurrent.atomic.AtomicBoolean; e = e.copyWithMediaPeriodId(readingPeriod.info.id); } } - Log.e(TAG, "Playback error", e); - stopInternal(/* forceResetRenderers= */ true, /* acknowledgeStop= */ false); - playbackInfo = playbackInfo.copyWithPlaybackError(e); + if (e.isRecoverable && pendingRecoverableError == null) { + Log.w(TAG, "Recoverable playback error", e); + pendingRecoverableError = e; + Message message = handler.obtainMessage(MSG_ATTEMPT_ERROR_RECOVERY, e); + // Given that the player is now in an unhandled exception state, the error needs to be + // recovered or the player stopped before any other message is handled. + message.getTarget().sendMessageAtFrontOfQueue(message); + } else { + if (pendingRecoverableError != null) { + e.addSuppressed(pendingRecoverableError); + pendingRecoverableError = null; + } + Log.e(TAG, "Playback error", e); + stopInternal(/* forceResetRenderers= */ true, /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(e); + } maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { ExoPlaybackException error = ExoPlaybackException.createForSource(e); @@ -572,6 +590,19 @@ import java.util.concurrent.atomic.AtomicBoolean; // Private methods. + private void attemptErrorRecovery(ExoPlaybackException exceptionToRecoverFrom) + throws ExoPlaybackException { + Assertions.checkArgument( + exceptionToRecoverFrom.isRecoverable + && exceptionToRecoverFrom.type == ExoPlaybackException.TYPE_RENDERER); + try { + seekToCurrentPosition(/* sendDiscontinuity= */ true); + } catch (Exception e) { + exceptionToRecoverFrom.addSuppressed(e); + throw exceptionToRecoverFrom; + } + } + /** * Blocks the current thread until a condition becomes true. * @@ -929,6 +960,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } else if (playbackInfo.playbackState == Player.STATE_BUFFERING && shouldTransitionToReadyState(renderersAllowPlayback)) { setState(Player.STATE_READY); + pendingRecoverableError = null; // Any pending error was successfully recovered from. if (shouldPlayWhenReady()) { startRenderers(); } @@ -1318,6 +1350,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (releaseMediaSourceList) { mediaSourceList.release(); } + pendingRecoverableError = null; } private Pair getPlaceholderFirstMediaPeriodPosition(Timeline timeline) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index b7d375fd9d..4f9e007d86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -136,34 +136,41 @@ public interface AudioSink { } - /** - * Thrown when a failure occurs initializing the sink. - */ + /** Thrown when a failure occurs initializing the sink. */ final class InitializationException extends Exception { - /** - * The underlying {@link AudioTrack}'s state, if applicable. - */ + /** The underlying {@link AudioTrack}'s state. */ public final int audioTrackState; + /** If the exception can be recovered by recreating the sink. */ + public final boolean isRecoverable; /** - * @param audioTrackState The underlying {@link AudioTrack}'s state, if applicable. * @param sampleRate The requested sample rate in Hz. * @param channelConfig The requested channel configuration. * @param bufferSize The requested buffer size in bytes. + * @param audioTrackException Exception thrown during the creation of the {@link AudioTrack}. */ - public InitializationException(int audioTrackState, int sampleRate, int channelConfig, - int bufferSize) { - super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " - + channelConfig + ", " + bufferSize + ")"); + public InitializationException( + int audioTrackState, + int sampleRate, + int channelConfig, + int bufferSize, + boolean isRecoverable, + @Nullable Exception audioTrackException) { + super( + "AudioTrack init failed " + + audioTrackState + + " " + + ("Config(" + sampleRate + ", " + channelConfig + ", " + bufferSize + ")") + + (isRecoverable ? " (recoverable)" : ""), + audioTrackException); this.audioTrackState = audioTrackState; + this.isRecoverable = isRecoverable; } } - /** - * Thrown when a failure occurs writing to the sink. - */ + /** Thrown when a failure occurs writing to the sink. */ final class WriteException extends Exception { /** @@ -173,12 +180,13 @@ public interface AudioSink { * Otherwise, the meaning of the error code depends on the sink implementation. */ public final int errorCode; + /** If the exception can be recovered by recreating the sink. */ + public final boolean isRecoverable; - /** - * @param errorCode The error value returned from the sink implementation. - */ - public WriteException(int errorCode) { + /** @param errorCode The error value returned from the sink implementation. */ + public WriteException(int errorCode, boolean isRecoverable) { super("AudioTrack write failed: " + errorCode); + this.isRecoverable = isRecoverable; this.errorCode = errorCode; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java index 1c1e593e22..0391fc95c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java @@ -257,7 +257,7 @@ public abstract class DecoderAudioRenderer< try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw createRendererException(e, inputFormat); + throw createRendererException(e, inputFormat, e.isRecoverable); } return; } @@ -296,11 +296,12 @@ public abstract class DecoderAudioRenderer< while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (DecoderException - | AudioSink.ConfigurationException - | AudioSink.InitializationException - | AudioSink.WriteException e) { + } catch (DecoderException | AudioSink.ConfigurationException e) { throw createRendererException(e, inputFormat); + } catch (AudioSink.InitializationException e) { + throw createRendererException(e, inputFormat, e.isRecoverable); + } catch (AudioSink.WriteException e) { + throw createRendererException(e, inputFormat, e.isRecoverable); } decoderCounters.ensureUpdated(); } @@ -383,7 +384,7 @@ public abstract class DecoderAudioRenderer< try { processEndOfStream(); } catch (AudioSink.WriteException e) { - throw createRendererException(e, getOutputFormat(decoder)); + throw createRendererException(e, getOutputFormat(decoder), e.isRecoverable); } } return false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 5ced4afd7d..78a62ed8b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -908,7 +908,7 @@ public final class DefaultAudioSink implements AudioSink { if (isRecoverable) { maybeDisableOffload(); } - throw new WriteException(bytesWritten); + throw new WriteException(bytesWritten, isRecoverable); } if (isOffloadedPlayback(audioTrack)) { @@ -1883,9 +1883,14 @@ public final class DefaultAudioSink implements AudioSink { AudioTrack audioTrack; try { audioTrack = createAudioTrack(tunneling, audioAttributes, audioSessionId); - } catch (UnsupportedOperationException e) { + } catch (UnsupportedOperationException | IllegalArgumentException e) { throw new InitializationException( - AudioTrack.STATE_UNINITIALIZED, outputSampleRate, outputChannelConfig, bufferSize); + AudioTrack.STATE_UNINITIALIZED, + outputSampleRate, + outputChannelConfig, + bufferSize, + /* isRecoverable= */ outputModeIsOffload(), + e); } int state = audioTrack.getState(); @@ -1896,7 +1901,13 @@ public final class DefaultAudioSink implements AudioSink { // The track has already failed to initialize, so it wouldn't be that surprising if // release were to fail too. Swallow the exception. } - throw new InitializationException(state, outputSampleRate, outputChannelConfig, bufferSize); + throw new InitializationException( + state, + outputSampleRate, + outputChannelConfig, + bufferSize, + /* isRecoverable= */ outputModeIsOffload(), + /* audioTrackException= */ null); } return audioTrack; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 2d034335c8..75bc7d3b1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -37,6 +37,8 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; +import com.google.android.exoplayer2.audio.AudioSink.InitializationException; +import com.google.android.exoplayer2.audio.AudioSink.WriteException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; @@ -616,8 +618,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media boolean fullyConsumed; try { fullyConsumed = audioSink.handleBuffer(buffer, bufferPresentationTimeUs, sampleCount); - } catch (AudioSink.InitializationException | AudioSink.WriteException e) { - throw createRendererException(e, format); + } catch (InitializationException e) { + throw createRendererException(e, format, e.isRecoverable); + } catch (WriteException e) { + throw createRendererException(e, format, e.isRecoverable); } if (fullyConsumed) { @@ -637,7 +641,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { @Nullable Format outputFormat = getOutputFormat(); - throw createRendererException(e, outputFormat != null ? outputFormat : getInputFormat()); + throw createRendererException( + e, outputFormat != null ? outputFormat : getInputFormat(), e.isRecoverable); } }