Implement repeat mode for CompositionPlayer.

-----

Context:
* Each sequence is wrapped as a single MediaSource, each being played
by an underlying ExoPlayer.
* Repeat mode is typically implemented in Players as a seek to the next
item in the playlist.

-----

This CL:

Repeat mode is triggered by listening for when the main input player
sees a play when ready change due to the end of the media item.

There is a slight delay at the end of the playback, before it repeats.
Setting repeat mode on the underlying players addresses this, but means
that the players will seek without waiting for the CompositionPlayer,
and as such previewAudioPipeline does not get the correct signals
around blocking/flushing.

PreviewAudioPipeline - The seek position can validly be C.TIME_UNSET,
however preview pipeline did not handle this case.

CompositionPlayer getContentPosition is given (through a lambda) as a
supplier to the State object, which means any comparisons between
previous/new state for this value does not work. In SimpleBasePlayer,
there is logic to use the positionDiscontinuityPositionUs for the
position change (see getPositionInfo called from
updateStateAndInformListeners), however this logic is not considered in
getMediaItemTransitionReason, so a condition needed to be added for
this case.

-----

Tests:
* Dump files clearly show the position and data is repeated.
* Assertions on the reasons for transitions or position
discontinuities.
PiperOrigin-RevId: 653210278
This commit is contained in:
samrobinson 2024-07-17 06:19:07 -07:00 committed by Copybara-Service
parent 29a2486ce3
commit d0afb96c40
10 changed files with 608 additions and 11 deletions

View File

@ -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();

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.
*
* <p>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
* <p>{@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()}.
*
* <p>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<MediaItemData> 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

View File

@ -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);

View File

@ -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);

View File

@ -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();