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