diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d1ff1e1fdd..d8929589a3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,6 +33,8 @@ * Add `media3.extractor.heif.HeifExtractor`. * Audio: * Add support for Opus gapless metadata during offload playback. + * Allow renderer recovery by disabling offload if failed at first write + ([#627](https://github.com/androidx/media/issues/627)). * Video: * Text: * Metadata: diff --git a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java index 26dba9755c..44ff185d4e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java @@ -220,6 +220,10 @@ public class PlaybackException extends Exception implements Bundleable { /** Caused by an AudioTrack write operation failure. */ public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002; + // TODO(b/299907254): Stabilize error code, remove @UnstableApi annotation, and add to IntDef + /** Caused by an AudioTrack write operation failure in offload mode. */ + @UnstableApi public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED = 5003; + // DRM errors (6xxx). /** Caused by an unspecified error related to DRM protection. */ @@ -324,6 +328,8 @@ public class PlaybackException extends Exception implements Bundleable { return "ERROR_CODE_AUDIO_TRACK_INIT_FAILED"; case ERROR_CODE_AUDIO_TRACK_WRITE_FAILED: return "ERROR_CODE_AUDIO_TRACK_WRITE_FAILED"; + case ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED: + return "ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED"; case ERROR_CODE_DRM_UNSPECIFIED: return "ERROR_CODE_DRM_UNSPECIFIED"; case ERROR_CODE_DRM_SCHEME_UNSUPPORTED: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index c99c25940f..a13313ab50 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -618,9 +618,19 @@ import java.util.concurrent.atomic.AtomicBoolean; e = e.copyWithMediaPeriodId(readingPeriod.info.id); } } - if (e.isRecoverable && pendingRecoverableRendererError == null) { + if (e.isRecoverable + && (pendingRecoverableRendererError == null + || e.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED)) { + // If pendingRecoverableRendererError != null and error was + // ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED then upon retry, renderer will attempt with + // offload disabled. Log.w(TAG, "Recoverable renderer error", e); - pendingRecoverableRendererError = e; + if (pendingRecoverableRendererError != null) { + pendingRecoverableRendererError.addSuppressed(e); + e = pendingRecoverableRendererError; + } else { + pendingRecoverableRendererError = 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. handler.sendMessageAtFrontOfQueue( diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 20d72e186c..f50a2146fb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -1163,9 +1163,17 @@ public final class DefaultAudioSink implements AudioSink { int error = bytesWrittenOrError; // Treat a write error on a previously successful offload channel as recoverable - // without disabling offload. Offload will be disabled when a new AudioTrack is created, - // if no longer supported. - boolean isRecoverable = isAudioTrackDeadObject(error) && getWrittenFrames() > 0; + // without disabling offload. Offload will be disabled if offload channel was not successfully + // written to or when a new AudioTrack is created, if no longer supported. + boolean isRecoverable = false; + if (isAudioTrackDeadObject(error)) { + if (getWrittenFrames() > 0) { + isRecoverable = true; + } else if (isOffloadedPlayback(audioTrack)) { + maybeDisableOffload(); + isRecoverable = true; + } + } WriteException e = new WriteException(error, configuration.inputFormat, isRecoverable); if (listener != null) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index 51c651991f..a3f7b28a21 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -754,7 +754,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media e, inputFormat, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED); } catch (WriteException e) { throw createRendererException( - e, format, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED); + e, + format, + e.isRecoverable, + isBypassEnabled() + && getConfiguration().offloadModePreferred != AudioSink.OFFLOAD_MODE_DISABLED + ? PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED + : PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED); } if (fullyConsumed) { @@ -774,7 +780,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { throw createRendererException( - e, e.format, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED); + e, + e.format, + e.isRecoverable, + isBypassEnabled() + ? PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED + : PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED); } }