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