mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
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:
parent
cf30ee504e
commit
d97af76280
8 changed files with 166 additions and 47 deletions
|
|
@ -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) ###
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue