diff --git a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java index edf1f4a1ea..0be5081996 100644 --- a/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java +++ b/demos/composition/src/main/java/androidx/media3/demo/composition/CompositionPreviewActivity.java @@ -220,6 +220,7 @@ public final class CompositionPreviewActivity extends AppCompatActivity { Log.e(TAG, "Preview error", error); } }); + player.setRepeatMode(Player.REPEAT_MODE_ALL); player.setComposition(composition); player.prepare(); player.play(); diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index f95d7a5537..18ff39859e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -4008,10 +4008,14 @@ public abstract class SimpleBasePlayer extends BasePlayer { } // Only mark changes within the current item as a transition if we are repeating automatically // or via a seek to next/previous. - if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION - && getContentPositionMsInternal(previousState, window) - > getContentPositionMsInternal(newState, window)) { - return MEDIA_ITEM_TRANSITION_REASON_REPEAT; + if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) { + if ((getContentPositionMsInternal(previousState, window) + > getContentPositionMsInternal(newState, window)) + || (newState.hasPositionDiscontinuity + && newState.discontinuityPositionMs == C.TIME_UNSET + && isRepeatingCurrentItem)) { + return MEDIA_ITEM_TRANSITION_REASON_REPEAT; + } } if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK && isRepeatingCurrentItem) { return MEDIA_ITEM_TRANSITION_REASON_SEEK; diff --git a/libraries/test_data/src/test/assets/audiosinkdumps/wav/compositionOf_sample.wav-clipped__sample_rf64.wav.dump b/libraries/test_data/src/test/assets/audiosinkdumps/wav/compositionOf_sample.wav-clipped__sample_rf64.wav.dump new file mode 100644 index 0000000000..d3f2a741bd --- /dev/null +++ b/libraries/test_data/src/test/assets/audiosinkdumps/wav/compositionOf_sample.wav-clipped__sample_rf64.wav.dump @@ -0,0 +1,39 @@ +AudioSink: + buffer count = 11 + config: + pcmEncoding = 2 + channelCount = 1 + sampleRate = 44100 + buffer #0: + time = 0 + data = -419876658 + buffer #1: + time = 100000 + data = -1236081112 + buffer #2: + time = 200000 + data = -1630460924 + buffer #3: + time = 300000 + data = 1478130841 + buffer #4: + time = 348616 + data = -2449 + buffer #5: + time = 348639 + data = 590036013 + buffer #6: + time = 448639 + data = -61907402 + buffer #7: + time = 500000 + data = -404977619 + buffer #8: + time = 648639 + data = -1276039913 + buffer #9: + time = 697256 + data = -1085 + buffer #10: + time = 697278 + data = 1110417718 diff --git a/libraries/test_data/src/test/assets/audiosinkdumps/wav/repeatedCompositionOf_sample.wav-clipped__sample_rf64.wav.dump b/libraries/test_data/src/test/assets/audiosinkdumps/wav/repeatedCompositionOf_sample.wav-clipped__sample_rf64.wav.dump new file mode 100644 index 0000000000..c1287f26e9 --- /dev/null +++ b/libraries/test_data/src/test/assets/audiosinkdumps/wav/repeatedCompositionOf_sample.wav-clipped__sample_rf64.wav.dump @@ -0,0 +1,72 @@ +AudioSink: + buffer count = 22 + config: + pcmEncoding = 2 + channelCount = 1 + sampleRate = 44100 + buffer #0: + time = 0 + data = -419876658 + buffer #1: + time = 100000 + data = -1236081112 + buffer #2: + time = 200000 + data = -1630460924 + buffer #3: + time = 300000 + data = 1478130841 + buffer #4: + time = 348616 + data = -2449 + buffer #5: + time = 348639 + data = 590036013 + buffer #6: + time = 448639 + data = -61907402 + buffer #7: + time = 500000 + data = -404977619 + buffer #8: + time = 648639 + data = -1276039913 + buffer #9: + time = 697256 + data = -1085 + buffer #10: + time = 697278 + data = 1110417718 + buffer #11: + time = 0 + data = -419876658 + buffer #12: + time = 100000 + data = -1236081112 + buffer #13: + time = 200000 + data = -1630460924 + buffer #14: + time = 300000 + data = 1478130841 + buffer #15: + time = 348616 + data = -2449 + buffer #16: + time = 348639 + data = 590036013 + buffer #17: + time = 448639 + data = -61907402 + buffer #18: + time = 500000 + data = -404977619 + buffer #19: + time = 648639 + data = -1276039913 + buffer #20: + time = 697256 + data = -1085 + buffer #21: + time = 697278 + data = 1110417718 diff --git a/libraries/test_data/src/test/assets/audiosinkdumps/wav/sample.wav_repeated.dump b/libraries/test_data/src/test/assets/audiosinkdumps/wav/sample.wav_repeated.dump new file mode 100644 index 0000000000..4a1a236ba2 --- /dev/null +++ b/libraries/test_data/src/test/assets/audiosinkdumps/wav/sample.wav_repeated.dump @@ -0,0 +1,66 @@ +AudioSink: + buffer count = 20 + config: + pcmEncoding = 2 + channelCount = 1 + sampleRate = 44100 + buffer #0: + time = 0 + data = -85819864 + buffer #1: + time = 100000 + data = 566487491 + buffer #2: + time = 200000 + data = -1256531710 + buffer #3: + time = 300000 + data = 793455796 + buffer #4: + time = 400000 + data = -268235582 + buffer #5: + time = 500000 + data = -8136122 + buffer #6: + time = 600000 + data = 1750866613 + buffer #7: + time = 700000 + data = -1100753636 + buffer #8: + time = 800000 + data = 507833230 + buffer #9: + time = 900000 + data = 1472467506 + buffer #10: + time = 0 + data = -85819864 + buffer #11: + time = 100000 + data = 566487491 + buffer #12: + time = 200000 + data = -1256531710 + buffer #13: + time = 300000 + data = 793455796 + buffer #14: + time = 400000 + data = -268235582 + buffer #15: + time = 500000 + data = -8136122 + buffer #16: + time = 600000 + data = 1750866613 + buffer #17: + time = 700000 + data = -1100753636 + buffer #18: + time = 800000 + data = 507833230 + buffer #19: + time = 900000 + data = 1472467506 diff --git a/libraries/test_data/src/test/assets/audiosinkdumps/wav/sample.wav_then_sample_rf64.wav_repeated.dump b/libraries/test_data/src/test/assets/audiosinkdumps/wav/sample.wav_then_sample_rf64.wav_repeated.dump new file mode 100644 index 0000000000..646b26acc5 --- /dev/null +++ b/libraries/test_data/src/test/assets/audiosinkdumps/wav/sample.wav_then_sample_rf64.wav_repeated.dump @@ -0,0 +1,96 @@ +AudioSink: + buffer count = 30 + config: + pcmEncoding = 2 + channelCount = 1 + sampleRate = 44100 + buffer #0: + time = 0 + data = -85819864 + buffer #1: + time = 100000 + data = 566487491 + buffer #2: + time = 200000 + data = -1256531710 + buffer #3: + time = 300000 + data = 793455796 + buffer #4: + time = 400000 + data = -268235582 + buffer #5: + time = 500000 + data = -8136122 + buffer #6: + time = 600000 + data = 1750866613 + buffer #7: + time = 700000 + data = -1100753636 + buffer #8: + time = 800000 + data = 507833230 + buffer #9: + time = 900000 + data = 1472467506 + buffer #10: + time = 1000000 + data = 1785344804 + buffer #11: + time = 1100000 + data = 458152960 + buffer #12: + time = 1200000 + data = -2129352270 + buffer #13: + time = 1300000 + data = 1572219123 + buffer #14: + time = 1348616 + data = -2263 + buffer #15: + time = 0 + data = -85819864 + buffer #16: + time = 100000 + data = 566487491 + buffer #17: + time = 200000 + data = -1256531710 + buffer #18: + time = 300000 + data = 793455796 + buffer #19: + time = 400000 + data = -268235582 + buffer #20: + time = 500000 + data = -8136122 + buffer #21: + time = 600000 + data = 1750866613 + buffer #22: + time = 700000 + data = -1100753636 + buffer #23: + time = 800000 + data = 507833230 + buffer #24: + time = 900000 + data = 1472467506 + buffer #25: + time = 1000000 + data = 1785344804 + buffer #26: + time = 1100000 + data = 458152960 + buffer #27: + time = 1200000 + data = -2129352270 + buffer #28: + time = 1300000 + data = 1572219123 + buffer #29: + time = 1348616 + data = -2263 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 548ec6e2b1..2e78ba6c84 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java @@ -80,14 +80,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * The {@link Composition} specifies how the assets should be arranged, and the audio and video * effects to apply to them. * - *

CompositionPlayer instances must be accessed from a single application thread. For the vast - * majority of cases this should be the application's main thread. The thread on which a + *

{@code CompositionPlayer} instances must be accessed from a single application thread. For the + * vast majority of cases this should be the application's main thread. The thread on which a * CompositionPlayer instance must be accessed can be explicitly specified by passing a {@link * Looper} when creating the player. If no {@link Looper} is specified, then the {@link Looper} of * the thread that the player is created on is used, or if that thread does not have a {@link * Looper}, the {@link Looper} of the application's main thread is used. In all cases the {@link * Looper} of the thread from which the player must be accessed can be queried using {@link * #getApplicationLooper()}. + * + *

This player only supports setting the {@linkplain #setRepeatMode(int) repeat mode} as + * {@linkplain Player#REPEAT_MODE_ALL all} of the {@link Composition}, or {@linkplain + * Player#REPEAT_MODE_OFF off}. */ @UnstableApi @RestrictTo(LIBRARY_GROUP) @@ -243,10 +247,12 @@ public final class CompositionPlayer extends SimpleBasePlayer COMMAND_PREPARE, COMMAND_STOP, COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, + COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_BACK, COMMAND_SEEK_FORWARD, COMMAND_GET_CURRENT_MEDIA_ITEM, COMMAND_GET_TIMELINE, + COMMAND_SET_REPEAT_MODE, COMMAND_SET_VIDEO_SURFACE, COMMAND_GET_VOLUME, COMMAND_SET_VOLUME, @@ -258,11 +264,11 @@ public final class CompositionPlayer extends SimpleBasePlayer EVENT_PLAYBACK_STATE_CHANGED, EVENT_PLAY_WHEN_READY_CHANGED, EVENT_PLAYER_ERROR, - EVENT_POSITION_DISCONTINUITY + EVENT_POSITION_DISCONTINUITY, + EVENT_MEDIA_ITEM_TRANSITION, }; private static final int MAX_SUPPORTED_SEQUENCES = 2; - private static final String TAG = "CompositionPlayer"; private final Context context; @@ -283,6 +289,7 @@ public final class CompositionPlayer extends SimpleBasePlayer private long compositionDurationUs; private boolean playWhenReady; private @PlayWhenReadyChangeReason int playWhenReadyChangeReason; + private @RepeatMode int repeatMode; private float volume; private boolean renderedFirstFrame; @Nullable private Object videoOutput; @@ -290,6 +297,7 @@ public final class CompositionPlayer extends SimpleBasePlayer private @Player.State int playbackState; @Nullable private SurfaceHolder surfaceHolder; @Nullable private Surface displaySurface; + private boolean repeatingCompositionSeekInProgress; private CompositionPlayer(Builder builder) { super(checkNotNull(builder.looper), builder.clock); @@ -427,15 +435,19 @@ public final class CompositionPlayer extends SimpleBasePlayer .setPlaybackState(playbackState) .setPlayerError(playbackException) .setPlayWhenReady(playWhenReady, playWhenReadyChangeReason) + .setRepeatMode(repeatMode) .setVolume(volume) .setContentPositionMs(this::getContentPositionMs) .setContentBufferedPositionMs(this::getBufferedPositionMs) .setTotalBufferedDurationMs(this::getTotalBufferedDurationMs) .setNewlyRenderedFirstFrame(getRenderedFirstFrameAndReset()); + if (repeatingCompositionSeekInProgress) { + state.setPositionDiscontinuity(DISCONTINUITY_REASON_AUTO_TRANSITION, C.TIME_UNSET); + repeatingCompositionSeekInProgress = false; + } if (playlist != null) { // Update the playlist only after it has been set so that SimpleBasePlayer announces a - // timeline - // change with reason TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED. + // timeline change with reason TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED. state.setPlaylist(playlist); } return state.build(); @@ -467,6 +479,14 @@ public final class CompositionPlayer extends SimpleBasePlayer return Futures.immediateVoidFuture(); } + @Override + protected ListenableFuture handleSetRepeatMode(@RepeatMode int repeatMode) { + // Composition is treated as a single item, so only supports being repeated as a whole. + checkArgument(repeatMode != REPEAT_MODE_ONE); + this.repeatMode = repeatMode; + return Futures.immediateVoidFuture(); + } + @Override protected ListenableFuture handleStop() { for (int i = 0; i < players.size(); i++) { @@ -534,7 +554,8 @@ public final class CompositionPlayer extends SimpleBasePlayer } @Override - protected ListenableFuture handleSeek(int mediaItemIndex, long positionMs, int seekCommand) { + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { CompositionPlayerInternal compositionPlayerInternal = checkStateNotNull(this.compositionPlayerInternal); compositionPlayerInternal.startSeek(positionMs); @@ -643,6 +664,7 @@ public final class CompositionPlayer extends SimpleBasePlayer ExoPlayer player = playerBuilder.build(); player.addListener(new PlayerListener(i)); player.addAnalyticsListener(new EventLogger()); + player.setPauseAtEndOfMediaItems(true); setPlayerSequence(player, editedMediaItemSequence, /* shouldGenerateSilence= */ i == 0); players.add(player); if (i == 0) { @@ -790,6 +812,15 @@ public final class CompositionPlayer extends SimpleBasePlayer } } + private void repeatCompositionPlayback() { + repeatingCompositionSeekInProgress = true; + seekTo( + getCurrentMediaItemIndex(), + /* positionMs= */ C.TIME_UNSET, + Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, + /* isRepeatingCurrentItem= */ true); + } + private ImmutableList createPlaylist() { checkNotNull(compositionDurationUs != C.TIME_UNSET); return ImmutableList.of( @@ -895,6 +926,11 @@ public final class CompositionPlayer extends SimpleBasePlayer @Override public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { playWhenReadyChangeReason = reason; + if (reason == PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM + && repeatMode != REPEAT_MODE_OFF + && playerIndex == 0) { + repeatCompositionPlayback(); + } } @Override diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/PreviewAudioPipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/PreviewAudioPipeline.java index 44444d813a..89afedd949 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/PreviewAudioPipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/PreviewAudioPipeline.java @@ -18,6 +18,7 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Util.sampleCountToDurationUs; +import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.audio.AudioProcessor; import androidx.media3.common.audio.AudioProcessor.AudioFormat; @@ -138,6 +139,9 @@ import java.util.Objects; * @param positionUs The seek position, in microseconds. */ public void startSeek(long positionUs) { + if (positionUs == C.TIME_UNSET) { + positionUs = 0; + } finalAudioSink.pause(); audioGraph.blockInput(); audioGraph.setPendingStartTimeUs(positionUs); diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerAudioPlaybackTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerAudioPlaybackTest.java index af2e080a90..ac982bb5a5 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerAudioPlaybackTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerAudioPlaybackTest.java @@ -33,6 +33,7 @@ import androidx.media3.test.utils.robolectric.TestPlayerRunHelper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -195,6 +196,147 @@ public final class CompositionPlayerAudioPlaybackTest { "audiosinkdumps/wav/sample.wav_clipped_then_sample_rf64_clipped.wav.dump"); } + @Test + public void multiSequenceCompositionPlayback_outputsCorrectSamples() throws Exception { + CompositionPlayer player = createCompositionPlayer(context, capturingAudioSink); + EditedMediaItem editedMediaItem1 = + new EditedMediaItem.Builder( + new MediaItem.Builder() + .setUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(0) + .setEndPositionUs(696_000) + .build()) + .build()) + .setDurationUs(1_000_000L) + .build(); + EditedMediaItem editedMediaItem2 = + new EditedMediaItem.Builder( + MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_STEREO_48000KHZ)) + .setDurationUs(348_000L) + .build(); + Composition composition = + new Composition.Builder( + new EditedMediaItemSequence(editedMediaItem1), + new EditedMediaItemSequence(editedMediaItem2, editedMediaItem2)) + .build(); + player.setComposition(composition); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + DumpFileAsserts.assertOutput( + context, + capturingAudioSink, + "audiosinkdumps/wav/compositionOf_sample.wav-clipped__sample_rf64.wav.dump"); + } + + @Test + public void playback_withOneRepeat_outputsCorrectSamples() throws Exception { + CompositionPlayer player = createCompositionPlayer(context, capturingAudioSink); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW)) + .setDurationUs(1_000_000L) + .build(); + EditedMediaItemSequence sequence = new EditedMediaItemSequence(editedMediaItem); + Composition composition = new Composition.Builder(sequence).build(); + player.setComposition(composition); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + DumpFileAsserts.assertOutput( + context, capturingAudioSink, "audiosinkdumps/wav/sample.wav_repeated.dump"); + } + + @Test + public void sequencePlayback_withOneRepeat_outputsCorrectSamples() throws Exception { + CompositionPlayer player = createCompositionPlayer(context, capturingAudioSink); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + EditedMediaItem editedMediaItem1 = + new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW)) + .setDurationUs(1_000_000L) + .build(); + EditedMediaItem editedMediaItem2 = + new EditedMediaItem.Builder( + MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_STEREO_48000KHZ)) + .setDurationUs(348_000L) + .build(); + EditedMediaItemSequence sequence = + new EditedMediaItemSequence(editedMediaItem1, editedMediaItem2); + Composition composition = new Composition.Builder(sequence).build(); + player.setComposition(composition); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + + player.setRepeatMode(Player.REPEAT_MODE_OFF); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + DumpFileAsserts.assertOutput( + context, + capturingAudioSink, + "audiosinkdumps/wav/sample.wav_then_sample_rf64.wav_repeated.dump"); + } + + // TODO - b/320014878: Enable this test. + @Ignore("Preview audio is not fed to the sink in deterministic buffers - see b/320014878.") + @Test + public void multiSequenceCompositionPlayback_withOneRepeat_outputsCorrectSamples() + throws Exception { + CompositionPlayer player = createCompositionPlayer(context, capturingAudioSink); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + EditedMediaItem editedMediaItem1 = + new EditedMediaItem.Builder( + new MediaItem.Builder() + .setUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(0) + .setEndPositionUs(696_000) + .build()) + .build()) + .setDurationUs(1_000_000L) + .build(); + EditedMediaItem editedMediaItem2 = + new EditedMediaItem.Builder( + MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_STEREO_48000KHZ)) + .setDurationUs(348_000L) + .build(); + Composition composition = + new Composition.Builder( + new EditedMediaItemSequence(editedMediaItem1), + new EditedMediaItemSequence(editedMediaItem2, editedMediaItem2)) + .build(); + player.setComposition(composition); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + + player.setRepeatMode(Player.REPEAT_MODE_OFF); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + DumpFileAsserts.assertOutput( + context, + capturingAudioSink, + "audiosinkdumps/wav/repeatedCompositionOf_sample.wav-clipped__sample_rf64.wav.dump"); + } + @Test public void seekTo_outputsCorrectSamples() throws Exception { CompositionPlayer player = createCompositionPlayer(context, capturingAudioSink); 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 0585e11abd..0a4dd10c10 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/CompositionPlayerTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; @@ -232,11 +233,13 @@ public class CompositionPlayerTest { Player.COMMAND_PREPARE, Player.COMMAND_STOP, Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, + Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, Player.COMMAND_SEEK_BACK, Player.COMMAND_SEEK_FORWARD, Player.COMMAND_GET_CURRENT_MEDIA_ITEM, Player.COMMAND_GET_TIMELINE, Player.COMMAND_SET_VIDEO_SURFACE, + Player.COMMAND_SET_REPEAT_MODE, Player.COMMAND_GET_VOLUME, Player.COMMAND_SET_VOLUME, Player.COMMAND_RELEASE); @@ -541,6 +544,140 @@ public class CompositionPlayerTest { .isEqualTo(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); } + @Test + public void playSequence_withRepeatModeOff_doesNotReportRepeatMediaItemTransition() + throws Exception { + CompositionPlayer player = buildCompositionPlayer(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + player.setComposition(buildComposition()); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + + verify(mockListener, never()) + .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT)); + verify(mockListener, never()) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + } + + @Test + public void playSequence_withRepeatModeAll_reportsRepeatReasonForMediaItemTransition() + throws Exception { + CompositionPlayer player = buildCompositionPlayer(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + player.setComposition(buildComposition()); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + + verify(mockListener, times(3)) + .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT)); + verify(mockListener, never()) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + } + + @Test + public void playComposition_withRepeatModeOff_doesNotReportRepeatMediaItemTransition() + throws Exception { + CompositionPlayer player = buildCompositionPlayer(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + EditedMediaItem editedMediaItem1 = + new EditedMediaItem.Builder( + new MediaItem.Builder() + .setUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(0) + .setEndPositionUs(696_000) + .build()) + .build()) + .setDurationUs(1_000_000L) + .build(); + EditedMediaItem editedMediaItem2 = + new EditedMediaItem.Builder( + MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_STEREO_48000KHZ)) + .setDurationUs(348_000L) + .build(); + Composition composition = + new Composition.Builder( + new EditedMediaItemSequence(editedMediaItem1), + new EditedMediaItemSequence(editedMediaItem2, editedMediaItem2)) + .build(); + + player.setComposition(composition); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + + verify(mockListener, never()) + .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT)); + verify(mockListener, never()) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + } + + @Test + public void playComposition_withRepeatModeAll_reportsRepeatReasonForMediaItemTransition() + throws Exception { + CompositionPlayer player = buildCompositionPlayer(); + Player.Listener mockListener = mock(Player.Listener.class); + player.addListener(mockListener); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + EditedMediaItem editedMediaItem1 = + new EditedMediaItem.Builder( + new MediaItem.Builder() + .setUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW) + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(0) + .setEndPositionUs(696_000) + .build()) + .build()) + .setDurationUs(1_000_000L) + .build(); + EditedMediaItem editedMediaItem2 = + new EditedMediaItem.Builder( + MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_STEREO_48000KHZ)) + .setDurationUs(348_000L) + .build(); + Composition composition = + new Composition.Builder( + new EditedMediaItemSequence(editedMediaItem1), + new EditedMediaItemSequence(editedMediaItem2, editedMediaItem2)) + .build(); + player.setComposition(composition); + player.prepare(); + player.play(); + + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + + verify(mockListener, times(3)) + .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT)); + verify(mockListener, never()) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + } + @Test public void seekPastDuration_ends() throws Exception { CompositionPlayer player = buildCompositionPlayer();