diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java index 06b8d6039f..04d91a3d05 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java @@ -457,6 +457,18 @@ public interface AudioSink { */ void flush(); + /** + * Flushes the sink, after which it is ready to receive buffers from a new playback position. + * + *

Does not release the {@link AudioTrack} held by the sink. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + *

Only for experimental use as part of {@link + * MediaCodecAudioRenderer#experimentalSetEnableKeepAudioTrackOnSeek(boolean)}. + */ + void experimentalFlushWithoutAudioTrackRelease(); + /** Resets the sink, releasing any resources that it currently holds. */ void reset(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java index bacac6c08c..11c6ca0ecb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java @@ -129,6 +129,7 @@ public abstract class DecoderAudioRenderer< private int encoderDelay; private int encoderPadding; + private boolean experimentalKeepAudioTrackOnSeek; private boolean firstStreamSampleRead; @Nullable private T decoder; @@ -208,6 +209,19 @@ public abstract class DecoderAudioRenderer< audioSinkNeedsConfigure = true; } + /** + * Sets whether to enable the experimental feature that keeps and flushes the {@link + * android.media.AudioTrack} when a seek occurs, as opposed to releasing and reinitialising. Off + * by default. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + * @param enableKeepAudioTrackOnSeek Whether to keep the {@link android.media.AudioTrack} on seek. + */ + public void experimentalSetEnableKeepAudioTrackOnSeek(boolean enableKeepAudioTrackOnSeek) { + this.experimentalKeepAudioTrackOnSeek = enableKeepAudioTrackOnSeek; + } + @Override @Nullable public MediaClock getMediaClock() { @@ -541,7 +555,12 @@ public abstract class DecoderAudioRenderer< @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - audioSink.flush(); + if (experimentalKeepAudioTrackOnSeek) { + audioSink.experimentalFlushWithoutAudioTrackRelease(); + } else { + audioSink.flush(); + } + currentPositionUs = positionUs; allowFirstBufferPositionDiscontinuity = true; allowPositionDiscontinuity = true; 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 cebe6957d5..de739678fb 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 @@ -907,7 +907,7 @@ public final class DefaultAudioSink implements AudioSink { // We're waiting for playout on the current audio track to finish. return false; } - flushAndReleaseAudioTrack(); + flush(); } else { // The current audio track can be reused for the new configuration. configuration = pendingConfiguration; @@ -1034,7 +1034,7 @@ public final class DefaultAudioSink implements AudioSink { if (audioTrackPositionTracker.isStalled(getWrittenFrames())) { Log.w(TAG, "Resetting stalled audio track"); - flushAndReleaseAudioTrack(); + flush(); return true; } @@ -1324,8 +1324,7 @@ public final class DefaultAudioSink implements AudioSink { // The audio attributes are ignored in tunneling mode, so no need to reset. return; } - // audioAttributes change requires the audioTrack to be recreated. - flushAndReleaseAudioTrack(); + flush(); } @Override @@ -1338,8 +1337,7 @@ public final class DefaultAudioSink implements AudioSink { if (this.audioSessionId != audioSessionId) { this.audioSessionId = audioSessionId; externalAudioSessionIdProvided = audioSessionId != C.AUDIO_SESSION_ID_UNSET; - // audioSessionId change requires the audioTrack to be recreated. - flushAndReleaseAudioTrack(); + flush(); } } @@ -1367,7 +1365,7 @@ public final class DefaultAudioSink implements AudioSink { Assertions.checkState(externalAudioSessionIdProvided); if (!tunneling) { tunneling = true; - flushAndReleaseAudioTrack(); + flush(); } } @@ -1375,7 +1373,7 @@ public final class DefaultAudioSink implements AudioSink { public void disableTunneling() { if (tunneling) { tunneling = false; - flushAndReleaseAudioTrack(); + flush(); } } @@ -1407,26 +1405,70 @@ public final class DefaultAudioSink implements AudioSink { @Override public void flush() { - if (!isAudioTrackInitialized()) { - return; - } + if (isAudioTrackInitialized()) { + resetSinkStateForFlush(); - // Prior to SDK 25, AudioTrack flush does not work as intended, so it must be released and - // reinitialized. (Internal reference: b/143500232) + if (audioTrackPositionTracker.isPlaying()) { + audioTrack.pause(); + } + if (isOffloadedPlayback(audioTrack)) { + checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack); + } + // AudioTrack.release can take some time, so we call it on a background thread. + final AudioTrack toRelease = audioTrack; + audioTrack = null; + if (Util.SDK_INT < 21 && !externalAudioSessionIdProvided) { + // Prior to API level 21, audio sessions are not kept alive once there are no components + // associated with them. If we generated the session ID internally, the only component + // associated with the session is the audio track that's being released, and therefore + // the session will not be kept alive. As a result, we need to generate a new session when + // we next create an audio track. + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + } + if (pendingConfiguration != null) { + configuration = pendingConfiguration; + pendingConfiguration = null; + } + audioTrackPositionTracker.reset(); + releasingConditionVariable.close(); + new Thread("ExoPlayer:AudioTrackReleaseThread") { + @Override + public void run() { + try { + toRelease.flush(); + toRelease.release(); + } finally { + releasingConditionVariable.open(); + } + } + }.start(); + } + writeExceptionPendingExceptionHolder.clear(); + initializationExceptionPendingExceptionHolder.clear(); + } + + @Override + public void experimentalFlushWithoutAudioTrackRelease() { + // Prior to SDK 25, AudioTrack flush does not work as intended, and therefore it must be + // released and reinitialized. (Internal reference: b/143500232) if (Util.SDK_INT < 25) { - flushAndReleaseAudioTrack(); + flush(); return; } writeExceptionPendingExceptionHolder.clear(); initializationExceptionPendingExceptionHolder.clear(); + if (!isAudioTrackInitialized()) { + return; + } + resetSinkStateForFlush(); if (audioTrackPositionTracker.isPlaying()) { audioTrack.pause(); } - audioTrack.flush(); + audioTrackPositionTracker.reset(); audioTrackPositionTracker.setAudioTrack( audioTrack, @@ -1434,12 +1476,13 @@ public final class DefaultAudioSink implements AudioSink { configuration.outputEncoding, configuration.outputPcmFrameSize, configuration.bufferSize); + startMediaTimeUsNeedsInit = true; } @Override public void reset() { - flushAndReleaseAudioTrack(); + flush(); for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) { audioProcessor.reset(); } @@ -1452,51 +1495,6 @@ public final class DefaultAudioSink implements AudioSink { // Internal methods. - private void flushAndReleaseAudioTrack() { - if (!isAudioTrackInitialized()) { - return; - } - - writeExceptionPendingExceptionHolder.clear(); - initializationExceptionPendingExceptionHolder.clear(); - - resetSinkStateForFlush(); - if (audioTrackPositionTracker.isPlaying()) { - audioTrack.pause(); - } - if (isOffloadedPlayback(audioTrack)) { - checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack); - } - // AudioTrack.release can take some time, so we call it on a background thread. - final AudioTrack toRelease = audioTrack; - audioTrack = null; - if (Util.SDK_INT < 21 && !externalAudioSessionIdProvided) { - // Prior to API level 21, audio sessions are not kept alive once there are no components - // associated with them. If we generated the session ID internally, the only component - // associated with the session is the audio track that's being released, and therefore - // the session will not be kept alive. As a result, we need to generate a new session when - // we next create an audio track. - audioSessionId = C.AUDIO_SESSION_ID_UNSET; - } - if (pendingConfiguration != null) { - configuration = pendingConfiguration; - pendingConfiguration = null; - } - audioTrackPositionTracker.reset(); - releasingConditionVariable.close(); - new Thread("ExoPlayer:AudioTrackReleaseThread") { - @Override - public void run() { - try { - toRelease.flush(); - toRelease.release(); - } finally { - releasingConditionVariable.open(); - } - } - }.start(); - } - private void resetSinkStateForFlush() { submittedPcmBytes = 0; submittedEncodedFrames = 0; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java index f8f8f073b6..06fdf5afd1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java @@ -163,6 +163,11 @@ public class ForwardingAudioSink implements AudioSink { sink.flush(); } + @Override + public void experimentalFlushWithoutAudioTrackRelease() { + sink.experimentalFlushWithoutAudioTrackRelease(); + } + @Override public void reset() { sink.reset(); 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 159a1b4412..d17d92da91 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 @@ -109,6 +109,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean allowPositionDiscontinuity; private boolean audioSinkNeedsConfigure; + private boolean experimentalKeepAudioTrackOnSeek; + @Nullable private WakeupListener wakeupListener; /** @@ -264,6 +266,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return TAG; } + /** + * Sets whether to enable the experimental feature that keeps and flushes the {@link + * android.media.AudioTrack} when a seek occurs, as opposed to releasing and reinitialising. Off + * by default. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + * @param enableKeepAudioTrackOnSeek Whether to keep the {@link android.media.AudioTrack} on seek. + */ + public void experimentalSetEnableKeepAudioTrackOnSeek(boolean enableKeepAudioTrackOnSeek) { + this.experimentalKeepAudioTrackOnSeek = enableKeepAudioTrackOnSeek; + } + @Override protected @Capabilities int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) throws DecoderQueryException { @@ -516,7 +531,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); - audioSink.flush(); + if (experimentalKeepAudioTrackOnSeek) { + audioSink.experimentalFlushWithoutAudioTrackRelease(); + } else { + audioSink.flush(); + } currentPositionUs = positionUs; allowFirstBufferPositionDiscontinuity = true; diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java index 68e3796b76..4b36260424 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java @@ -265,15 +265,15 @@ public final class DefaultAudioSinkTest { } @Test - public void handleBuffer_afterFlush_doesntThrow() throws Exception { - // This is demonstrating that no Exceptions are thrown as a result of handling a buffer after a - // flush. + public void handlesBufferAfterExperimentalFlush() throws Exception { + // This is demonstrating that no Exceptions are thrown as a result of handling a buffer after an + // experimental flush. configureDefaultAudioSink(CHANNEL_COUNT_STEREO); defaultAudioSink.handleBuffer( createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); - // After the flush we can successfully queue more input. - defaultAudioSink.flush(); + // After the experimental flush we can successfully queue more input. + defaultAudioSink.experimentalFlushWithoutAudioTrackRelease(); defaultAudioSink.handleBuffer( createDefaultSilenceBuffer(), /* presentationTimeUs= */ 5_000, @@ -281,13 +281,13 @@ public final class DefaultAudioSinkTest { } @Test - public void getCurrentPosition_afterFlush_returnsUnset() throws Exception { + public void getCurrentPosition_returnsUnset_afterExperimentalFlush() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); defaultAudioSink.handleBuffer( createDefaultSilenceBuffer(), /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, /* encodedAccessUnitCount= */ 1); - defaultAudioSink.flush(); + defaultAudioSink.experimentalFlushWithoutAudioTrackRelease(); assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false)) .isEqualTo(CURRENT_POSITION_NOT_SET); }