diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 788a7a3b4d..b0f76c0afb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -362,6 +362,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 renderer, releasing any resources that it currently holds. */ void reset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java index c20de49f06..bc8237c911 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java @@ -112,6 +112,8 @@ public abstract class DecoderAudioRenderer< private int encoderDelay; private int encoderPadding; + private boolean experimentalKeepAudioTrackOnSeek; + @Nullable private T decoder; @Nullable private DecoderInputBuffer inputBuffer; @@ -185,6 +187,19 @@ public abstract class DecoderAudioRenderer< audioTrackNeedsConfigure = 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() { @@ -507,7 +522,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/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 491a0b0e11..590df93172 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -279,7 +279,9 @@ public final class DefaultAudioSink implements AudioSink { @MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29; @Nullable private Listener listener; - /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */ + /** + * Used to keep the audio session active on pre-V21 builds (see {@link #initializeAudioTrack()}). + */ @Nullable private AudioTrack keepSessionIdAudioTrack; @Nullable private Configuration pendingConfiguration; @@ -300,6 +302,7 @@ public final class DefaultAudioSink implements AudioSink { private long writtenEncodedFrames; private int framesPerEncodedSample; private boolean startMediaTimeUsNeedsSync; + private boolean startMediaTimeUsNeedsInit; private long startMediaTimeUs; private float volume; @@ -470,7 +473,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public long getCurrentPositionUs(boolean sourceEnded) { - if (!isInitialized()) { + if (!isAudioTrackInitialized() || startMediaTimeUsNeedsInit) { return CURRENT_POSITION_NOT_SET; } long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded); @@ -581,7 +584,7 @@ public final class DefaultAudioSink implements AudioSink { specifiedBufferSize, canApplyPlaybackParameters, availableAudioProcessors); - if (isInitialized()) { + if (isAudioTrackInitialized()) { this.pendingConfiguration = pendingConfiguration; } else { configuration = pendingConfiguration; @@ -612,7 +615,7 @@ public final class DefaultAudioSink implements AudioSink { } } - private void initialize(long presentationTimeUs) throws InitializationException { + private void initializeAudioTrack() throws InitializationException { // If we're asynchronously releasing a previous audio track then we block until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust @@ -647,17 +650,9 @@ public final class DefaultAudioSink implements AudioSink { } } - startMediaTimeUs = max(0, presentationTimeUs); - startMediaTimeUsNeedsSync = false; - - if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) { - setAudioTrackPlaybackSpeedV23(audioTrackPlaybackSpeed); - } - applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs); - audioTrackPositionTracker.setAudioTrack( audioTrack, - configuration.outputMode == OUTPUT_MODE_PASSTHROUGH, + /* isPassthrough= */ configuration.outputMode == OUTPUT_MODE_PASSTHROUGH, configuration.outputEncoding, configuration.outputPcmFrameSize, configuration.bufferSize); @@ -667,12 +662,14 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.attachAuxEffect(auxEffectInfo.effectId); audioTrack.setAuxEffectSendLevel(auxEffectInfo.sendLevel); } + + startMediaTimeUsNeedsInit = true; } @Override public void play() { playing = true; - if (isInitialized()) { + if (isAudioTrackInitialized()) { audioTrackPositionTracker.start(); audioTrack.play(); } @@ -716,8 +713,20 @@ public final class DefaultAudioSink implements AudioSink { applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs); } - if (!isInitialized()) { - initialize(presentationTimeUs); + if (!isAudioTrackInitialized()) { + initializeAudioTrack(); + } + + if (startMediaTimeUsNeedsInit) { + startMediaTimeUs = max(0, presentationTimeUs); + startMediaTimeUsNeedsSync = false; + startMediaTimeUsNeedsInit = false; + + if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) { + setAudioTrackPlaybackSpeedV23(audioTrackPlaybackSpeed); + } + applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs); + if (playing) { play(); } @@ -945,7 +954,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public void playToEndOfStream() throws WriteException { - if (!handledEndOfStream && isInitialized() && drainToEndOfStream()) { + if (!handledEndOfStream && isAudioTrackInitialized() && drainToEndOfStream()) { playPendingData(); handledEndOfStream = true; } @@ -987,12 +996,13 @@ public final class DefaultAudioSink implements AudioSink { @Override public boolean isEnded() { - return !isInitialized() || (handledEndOfStream && !hasPendingData()); + return !isAudioTrackInitialized() || (handledEndOfStream && !hasPendingData()); } @Override public boolean hasPendingData() { - return isInitialized() && audioTrackPositionTracker.hasPendingData(getWrittenFrames()); + return isAudioTrackInitialized() + && audioTrackPositionTracker.hasPendingData(getWrittenFrames()); } @Override @@ -1090,7 +1100,7 @@ public final class DefaultAudioSink implements AudioSink { } private void setVolumeInternal() { - if (!isInitialized()) { + if (!isAudioTrackInitialized()) { // Do nothing. } else if (Util.SDK_INT >= 21) { setVolumeInternalV21(audioTrack, volume); @@ -1102,14 +1112,14 @@ public final class DefaultAudioSink implements AudioSink { @Override public void pause() { playing = false; - if (isInitialized() && audioTrackPositionTracker.pause()) { + if (isAudioTrackInitialized() && audioTrackPositionTracker.pause()) { audioTrack.pause(); } } @Override public void flush() { - if (isInitialized()) { + if (isAudioTrackInitialized()) { resetSinkStateForFlush(); if (audioTrackPositionTracker.isPlaying()) { @@ -1141,6 +1151,36 @@ public final class DefaultAudioSink implements AudioSink { } } + @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) { + flush(); + return; + } + + if (!isAudioTrackInitialized()) { + return; + } + + resetSinkStateForFlush(); + if (audioTrackPositionTracker.isPlaying()) { + audioTrack.pause(); + } + audioTrack.flush(); + + audioTrackPositionTracker.reset(); + audioTrackPositionTracker.setAudioTrack( + audioTrack, + /* isPassthrough= */ configuration.outputMode == OUTPUT_MODE_PASSTHROUGH, + configuration.outputEncoding, + configuration.outputPcmFrameSize, + configuration.bufferSize); + + startMediaTimeUsNeedsInit = true; + } + @Override public void reset() { flush(); @@ -1204,7 +1244,7 @@ public final class DefaultAudioSink implements AudioSink { @RequiresApi(23) private void setAudioTrackPlaybackSpeedV23(float audioTrackPlaybackSpeed) { - if (isInitialized()) { + if (isAudioTrackInitialized()) { PlaybackParams playbackParams = new PlaybackParams() .allowDefaults() @@ -1233,7 +1273,7 @@ public final class DefaultAudioSink implements AudioSink { skipSilence, /* mediaTimeUs= */ C.TIME_UNSET, /* audioTrackPositionUs= */ C.TIME_UNSET); - if (isInitialized()) { + if (isAudioTrackInitialized()) { // Drain the audio processors so we can determine the frame position at which the new // parameters apply. this.afterDrainParameters = mediaPositionParameters; @@ -1313,7 +1353,7 @@ public final class DefaultAudioSink implements AudioSink { + configuration.framesToDurationUs(audioProcessorChain.getSkippedOutputFrameCount()); } - private boolean isInitialized() { + private boolean isAudioTrackInitialized() { return audioTrack != null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java index 93d890bc92..3f755a7130 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java @@ -147,6 +147,11 @@ public class ForwardingAudioSink implements AudioSink { sink.flush(); } + @Override + public void experimentalFlushWithoutAudioTrackRelease() { + sink.experimentalFlushWithoutAudioTrackRelease(); + } + @Override public void reset() { sink.reset(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 3f0241ee5a..91c0f946ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -97,6 +97,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; + private boolean experimentalKeepAudioTrackOnSeek; + @Nullable private WakeupListener wakeupListener; /** @@ -205,6 +207,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 @Capabilities protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) @@ -465,7 +480,12 @@ 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; allowPositionDiscontinuity = true; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java index 4c00e672cf..54628f91be 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.audio.AudioSink.CURRENT_POSITION_NOT_SET; import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY; import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_WITH_TRANSCODING; import static com.google.common.truth.Truth.assertThat; @@ -275,6 +276,34 @@ public final class DefaultAudioSinkTest { assertThat(defaultAudioSink.supportsFormat(aacLcFormat)).isFalse(); } + @Test + 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 experimental flush we can successfully queue more input. + defaultAudioSink.experimentalFlushWithoutAudioTrackRelease(); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 5_000, + /* encodedAccessUnitCount= */ 1); + } + + @Test + public void getCurrentPosition_returnsUnset_afterExperimentalFlush() throws Exception { + configureDefaultAudioSink(CHANNEL_COUNT_STEREO); + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, + /* encodedAccessUnitCount= */ 1); + defaultAudioSink.experimentalFlushWithoutAudioTrackRelease(); + assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false)) + .isEqualTo(CURRENT_POSITION_NOT_SET); + } + private void configureDefaultAudioSink(int channelCount) throws AudioSink.ConfigurationException { configureDefaultAudioSink(channelCount, /* trimStartFrames= */ 0, /* trimEndFrames= */ 0); }