diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java index faeae7da86..ba73b266ea 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java @@ -70,7 +70,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class CompositionPlayerSeekTest { - private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000; + private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 2000_000 : 1000_000; private static final MediaItem VIDEO_MEDIA_ITEM = MediaItem.fromUri(MP4_ASSET.uri); private static final long VIDEO_DURATION_US = MP4_ASSET.videoDurationUs; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerTest.java index 9c6f7257dc..f80f5dd5b6 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerTest.java @@ -527,8 +527,6 @@ public class CompositionPlayerTest { playerTestListener.waitUntilPlayerEnded(); instrumentation.runOnMainSync(compositionPlayer::release); - - playerTestListener.waitUntilPlayerIdle(); } private static final class TestImageDecoderFactory implements ImageDecoder.Factory { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraph.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraph.java index 9d976163b9..0bfbcf08d6 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraph.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraph.java @@ -35,8 +35,6 @@ import java.util.Objects; /** Processes raw audio samples. */ /* package */ final class AudioGraph { - private static final String TAG = "AudioGraph"; - private final List inputInfos; private final AudioMixer mixer; private final AudioProcessingPipeline audioProcessingPipeline; @@ -190,7 +188,6 @@ import java.util.Objects; inputInfos.clear(); mixer.reset(); audioProcessingPipeline.reset(); - finishedInputs = 0; mixerOutput = EMPTY_BUFFER; mixerAudioFormat = AudioFormat.NOT_SET; @@ -280,6 +277,7 @@ import java.util.Objects; } private boolean isMixerEnded() { + // return !mixerOutput.hasRemaining() && activeInputCount == 0 && mixer.isEnded(); return !mixerOutput.hasRemaining() && finishedInputs >= inputInfos.size() && mixer.isEnded(); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraphInputAudioSink.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraphInputAudioSink.java index 6acafb6423..1969f396a3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraphInputAudioSink.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioGraphInputAudioSink.java @@ -20,6 +20,8 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.getPcmFrameSize; +import static androidx.media3.common.util.Util.sampleCountToDurationUs; import android.media.AudioTrack; import androidx.annotation.Nullable; @@ -78,18 +80,6 @@ import java.util.Objects; * @return The playback position relative to the start of playback, in microseconds. */ long getCurrentPositionUs(boolean sourceEnded); - - /** Returns whether the controller is ended. */ - boolean isEnded(); - - /** See {@link #play()}. */ - default void onPlay() {} - - /** See {@link #pause()}. */ - default void onPause() {} - - /** See {@link #reset()}. */ - default void onReset() {} } private final Controller controller; @@ -100,6 +90,7 @@ import java.util.Objects; private boolean signalledEndOfStream; @Nullable private EditedMediaItemInfo currentEditedMediaItemInfo; private long offsetToCompositionTimeUs; + private long inputPositionUs; public AudioGraphInputAudioSink(Controller controller) { this.controller = controller; @@ -144,11 +135,8 @@ import java.util.Objects; if (currentInputFormat == null) { // Sink not configured. return inputStreamEnded; } - // If we are playing the last media item in the sequence, we must also check that the controller - // is ended. - return inputStreamEnded - && (!checkStateNotNull(currentEditedMediaItemInfo).isLastInSequence - || controller.isEnded()); + + return inputStreamEnded && getCompositionPlayerPositionUs() >= inputPositionUs; } @Override @@ -156,6 +144,7 @@ import java.util.Objects; ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount) throws InitializationException { checkState(!inputStreamEnded); + EditedMediaItem editedMediaItem = checkStateNotNull(currentEditedMediaItemInfo).editedMediaItem; if (outputGraphInput == null) { @@ -219,23 +208,17 @@ import java.util.Objects; @Override public long getCurrentPositionUs(boolean sourceEnded) { - long currentPositionUs = controller.getCurrentPositionUs(sourceEnded); - if (currentPositionUs != CURRENT_POSITION_NOT_SET) { - // Reset the position to the one expected by the player. - currentPositionUs -= offsetToCompositionTimeUs; + if (isEnded()) { + return inputPositionUs; } - return currentPositionUs; + return getCompositionPlayerPositionUs(); } @Override - public void play() { - controller.onPlay(); - } + public void play() {} @Override - public void pause() { - controller.onPause(); - } + public void pause() {} @Override public void flush() { @@ -248,7 +231,6 @@ import java.util.Objects; flush(); currentInputFormat = null; currentEditedMediaItemInfo = null; - controller.onReset(); } // Unsupported interface functionality. @@ -301,6 +283,15 @@ import java.util.Objects; // Internal methods + private long getCompositionPlayerPositionUs() { + long currentPositionUs = controller.getCurrentPositionUs(/* sourceEnded= */ inputStreamEnded); + if (currentPositionUs != CURRENT_POSITION_NOT_SET) { + // Reset the position to the one expected by the player. + currentPositionUs -= offsetToCompositionTimeUs; + } + return currentPositionUs; + } + private boolean handleBufferInternal(ByteBuffer buffer, long presentationTimeUs, int flags) { checkStateNotNull(currentInputFormat); checkState(!signalledEndOfStream); @@ -310,7 +301,8 @@ import java.util.Objects; if (outputBuffer == null) { return false; } - outputBuffer.ensureSpaceForWrite(buffer.remaining()); + int bytesToWrite = buffer.remaining(); + outputBuffer.ensureSpaceForWrite(bytesToWrite); checkNotNull(outputBuffer.data).put(buffer).flip(); outputBuffer.timeUs = presentationTimeUs == C.TIME_END_OF_SOURCE @@ -318,7 +310,18 @@ import java.util.Objects; : presentationTimeUs + offsetToCompositionTimeUs; outputBuffer.setFlags(flags); - return outputGraphInput.queueInputBuffer(); + boolean bufferQueued = outputGraphInput.queueInputBuffer(); + if (bufferQueued) { + Format currentInputFormat = checkNotNull(this.currentInputFormat); + inputPositionUs = + presentationTimeUs + + sampleCountToDurationUs( + /* sampleCount= */ bytesToWrite + / getPcmFrameSize( + currentInputFormat.pcmEncoding, currentInputFormat.channelCount), + /* sampleRate= */ currentInputFormat.sampleRate); + } + return bufferQueued; } private static final class EditedMediaItemInfo { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java index ea1739b0fd..198a924bd7 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java @@ -317,6 +317,7 @@ public final class CompositionPlayer extends SimpleBasePlayer private LivePositionSupplier positionSupplier; private LivePositionSupplier bufferedPositionSupplier; private LivePositionSupplier totalBufferedDurationSupplier; + private boolean isSeeking; // "this" reference for position suppliers. @SuppressWarnings("initialization:methodref.receiver.bound.invalid") @@ -443,20 +444,6 @@ public final class CompositionPlayer extends SimpleBasePlayer @Override protected State getState() { - @Player.State int oldPlaybackState = playbackState; - updatePlaybackState(); - if (oldPlaybackState != STATE_READY && playbackState == STATE_READY && playWhenReady) { - for (int i = 0; i < players.size(); i++) { - players.get(i).setPlayWhenReady(true); - } - } else if (oldPlaybackState == STATE_READY - && playWhenReady - && playbackState == STATE_BUFFERING) { - // We were playing but a player got in buffering state, pause the players. - for (int i = 0; i < players.size(); i++) { - players.get(i).setPlayWhenReady(false); - } - } // TODO: b/328219481 - Report video size change to app. State.Builder state = new State.Builder() @@ -501,6 +488,11 @@ public final class CompositionPlayer extends SimpleBasePlayer this.playWhenReady = playWhenReady; playWhenReadyChangeReason = PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; if (playbackState == STATE_READY) { + if (playWhenReady) { + finalAudioSink.play(); + } else { + finalAudioSink.pause(); + } for (int i = 0; i < players.size(); i++) { players.get(i).setPlayWhenReady(playWhenReady); } @@ -588,6 +580,7 @@ public final class CompositionPlayer extends SimpleBasePlayer resetLivePositionSuppliers(); CompositionPlayerInternal compositionPlayerInternal = checkStateNotNull(this.compositionPlayerInternal); + isSeeking = true; compositionPlayerInternal.startSeek(positionMs); for (int i = 0; i < players.size(); i++) { players.get(i).seekTo(positionMs); @@ -640,6 +633,8 @@ public final class CompositionPlayer extends SimpleBasePlayer return; } + @Player.State int oldPlaybackState = playbackState; + int idleCount = 0; int bufferingCount = 0; int endedCount = 0; @@ -666,10 +661,28 @@ public final class CompositionPlayer extends SimpleBasePlayer playbackState = STATE_IDLE; } else if (bufferingCount > 0) { playbackState = STATE_BUFFERING; + if (oldPlaybackState == STATE_READY && playWhenReady) { + // We were playing but a player got in buffering state, pause the players. + for (int i = 0; i < players.size(); i++) { + players.get(i).setPlayWhenReady(false); + } + if (!isSeeking) { + // The finalAudioSink cannot be paused more than once. The audio pipeline pauses it during + // a seek, so don't pause here when seeking. + finalAudioSink.pause(); + } + } } else if (endedCount == players.size()) { playbackState = STATE_ENDED; } else { playbackState = STATE_READY; + isSeeking = false; + if (oldPlaybackState != STATE_READY && playWhenReady) { + for (int i = 0; i < players.size(); i++) { + players.get(i).setPlayWhenReady(true); + } + finalAudioSink.play(); + } } } @@ -955,6 +968,8 @@ public final class CompositionPlayer extends SimpleBasePlayer for (int i = 0; i < players.size(); i++) { players.get(i).stop(); } + updatePlaybackState(); + // Invalidate the parent class state. invalidateState(); } else { Log.w(TAG, errorMessage, cause); @@ -1107,6 +1122,11 @@ public final class CompositionPlayer extends SimpleBasePlayer } } + @Override + public void onPlaybackStateChanged(int playbackState) { + updatePlaybackState(); + } + @Override public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { playWhenReadyChangeReason = reason; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/PlaybackAudioGraphWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/PlaybackAudioGraphWrapper.java index fc5c2503f2..fca8afcbf4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/PlaybackAudioGraphWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/PlaybackAudioGraphWrapper.java @@ -43,7 +43,6 @@ import java.util.Objects; private int audioGraphInputsCreated; private int inputAudioSinksCreated; - private int inputAudioSinksPlaying; private boolean hasRegisteredPrimaryFormat; private AudioFormat outputAudioFormat; private long outputFramesWritten; @@ -74,7 +73,6 @@ import java.util.Objects; finalAudioSink.release(); audioGraphInputsCreated = 0; inputAudioSinksCreated = 0; - inputAudioSinksPlaying = 0; } /** Returns an {@link AudioSink} for a single sequence of non-overlapping raw PCM audio. */ @@ -162,7 +160,6 @@ import java.util.Objects; private final class SinkController implements AudioGraphInputAudioSink.Controller { private final boolean isSequencePrimary; - private boolean playing; public SinkController(int inputIndex) { this.isSequencePrimary = inputIndex == PRIMARY_SEQUENCE_INDEX; @@ -191,41 +188,5 @@ import java.util.Objects; public long getCurrentPositionUs(boolean sourceEnded) { return finalAudioSink.getCurrentPositionUs(sourceEnded); } - - @Override - public boolean isEnded() { - return finalAudioSink.isEnded(); - } - - @Override - public void onPlay() { - if (playing) { - return; - } - playing = true; - - inputAudioSinksPlaying++; - if (inputAudioSinksCreated == inputAudioSinksPlaying) { - finalAudioSink.play(); - } - } - - @Override - public void onPause() { - if (!playing) { - return; - } - playing = false; - - if (inputAudioSinksCreated == inputAudioSinksPlaying) { - finalAudioSink.pause(); - } - inputAudioSinksPlaying--; - } - - @Override - public void onReset() { - onPause(); - } } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerTest.java index 0ff224b934..4957500d39 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerTest.java @@ -620,9 +620,10 @@ public class CompositionPlayerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); inOrder.verify(listener).onPlaybackStateChanged(Player.STATE_ENDED); - player.release(); + player.stop(); TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE); inOrder.verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + player.release(); assertThat(playbackStates) .containsExactly( diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/PlaybackAudioGraphWrapperTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/PlaybackAudioGraphWrapperTest.java deleted file mode 100644 index 0f13d0c1cf..0000000000 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/PlaybackAudioGraphWrapperTest.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.media3.transformer; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.atMostOnce; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import androidx.media3.exoplayer.audio.AudioSink; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.common.collect.ImmutableList; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -/** Unit tests for {@link PlaybackAudioGraphWrapper}. */ -@RunWith(AndroidJUnit4.class) -public class PlaybackAudioGraphWrapperTest { - @Rule public final MockitoRule mockito = MockitoJUnit.rule(); - - private PlaybackAudioGraphWrapper playbackAudioGraphWrapper; - @Mock AudioSink outputAudioSink; - - @Before - public void setUp() { - playbackAudioGraphWrapper = - new PlaybackAudioGraphWrapper( - new DefaultAudioMixer.Factory(), /* effects= */ ImmutableList.of(), outputAudioSink); - } - - @After - public void tearDown() { - playbackAudioGraphWrapper.release(); - } - - @Test - public void processData_noAudioSinksCreated_returnsFalse() throws Exception { - assertThat(playbackAudioGraphWrapper.processData()).isFalse(); - } - - @Test - public void processData_audioSinkHasNotConfiguredYet_returnsFalse() throws Exception { - AudioGraphInputAudioSink unused = playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - - assertThat(playbackAudioGraphWrapper.processData()).isFalse(); - } - - @Test - public void inputPlay_withOneInput_playsOutputSink() throws Exception { - AudioGraphInputAudioSink inputAudioSink = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - - inputAudioSink.play(); - - verify(outputAudioSink).play(); - } - - @Test - public void inputPause_withOneInput_pausesOutputSink() throws Exception { - AudioGraphInputAudioSink inputAudioSink = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - - inputAudioSink.play(); - inputAudioSink.pause(); - - verify(outputAudioSink).pause(); - } - - @Test - public void inputReset_withOneInput_pausesOutputSink() { - AudioGraphInputAudioSink inputAudioSink = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - - inputAudioSink.play(); - inputAudioSink.reset(); - - verify(outputAudioSink).pause(); - } - - @Test - public void inputPlay_whenPlaying_doesNotPlayOutputSink() throws Exception { - AudioGraphInputAudioSink inputAudioSink = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - inputAudioSink.play(); - inputAudioSink.play(); - - verify(outputAudioSink, atMostOnce()).play(); - } - - @Test - public void inputPause_whenNotPlaying_doesNotPauseOutputSink() throws Exception { - AudioGraphInputAudioSink inputAudioSink = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - - inputAudioSink.pause(); - - verify(outputAudioSink, never()).pause(); - } - - @Test - public void someInputPlay_withMultipleInputs_doesNotPlayOutputSink() throws Exception { - AudioGraphInputAudioSink inputAudioSink1 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - AudioGraphInputAudioSink inputAudioSink2 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 1); - AudioGraphInputAudioSink unused = playbackAudioGraphWrapper.createInput(/* inputIndex= */ 2); - - inputAudioSink1.play(); - inputAudioSink2.play(); - verify(outputAudioSink, never()).play(); - } - - @Test - public void allInputPlay_withMultipleInputs_playsOutputSinkOnce() throws Exception { - AudioGraphInputAudioSink inputAudioSink1 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - AudioGraphInputAudioSink inputAudioSink2 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 1); - AudioGraphInputAudioSink inputAudioSink3 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 2); - - inputAudioSink1.play(); - inputAudioSink2.play(); - inputAudioSink3.play(); - - verify(outputAudioSink, atMostOnce()).play(); - } - - @Test - public void firstInputPause_withMultipleInputs_pausesOutputSink() throws Exception { - InOrder inOrder = inOrder(outputAudioSink); - AudioGraphInputAudioSink inputAudioSink1 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - AudioGraphInputAudioSink inputAudioSink2 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 1); - AudioGraphInputAudioSink inputAudioSink3 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 2); - - inputAudioSink1.play(); - inputAudioSink2.play(); - inputAudioSink3.play(); - inputAudioSink2.pause(); - - inOrder.verify(outputAudioSink).pause(); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void allInputPause_withMultipleInputs_pausesOutputSinkOnce() throws Exception { - AudioGraphInputAudioSink inputAudioSink1 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - AudioGraphInputAudioSink inputAudioSink2 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 1); - AudioGraphInputAudioSink inputAudioSink3 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 2); - - inputAudioSink1.play(); - inputAudioSink2.play(); - inputAudioSink3.play(); - inputAudioSink2.pause(); - inputAudioSink1.pause(); - inputAudioSink3.pause(); - - verify(outputAudioSink, atMostOnce()).pause(); - } - - @Test - public void inputPlayAfterPause_withMultipleInputs_playsOutputSink() throws Exception { - InOrder inOrder = inOrder(outputAudioSink); - AudioGraphInputAudioSink inputAudioSink1 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 0); - AudioGraphInputAudioSink inputAudioSink2 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 1); - AudioGraphInputAudioSink inputAudioSink3 = - playbackAudioGraphWrapper.createInput(/* inputIndex= */ 2); - - inputAudioSink1.play(); - inputAudioSink2.play(); - inputAudioSink3.play(); - inputAudioSink2.pause(); - inputAudioSink1.pause(); - inputAudioSink2.play(); - inputAudioSink1.play(); - - inOrder.verify(outputAudioSink).play(); - inOrder.verify(outputAudioSink).pause(); - inOrder.verify(outputAudioSink).play(); - Mockito.verifyNoMoreInteractions(outputAudioSink); - } -}