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
This commit is contained in:
krocard 2020-09-24 15:11:06 +01:00 committed by kim-vde
parent cf30ee504e
commit d97af76280
8 changed files with 166 additions and 47 deletions

View file

@ -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) ###

View file

@ -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);
}
/**

View file

@ -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.
*
* <p>This is only used internally by ExoPlayer to try to recover from some errors and should not
* be used by apps.
*
* <p>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

View file

@ -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<MediaPeriodId, Long> getPlaceholderFirstMediaPeriodPosition(Timeline timeline) {

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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);
}
}