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:
parent
29a2486ce3
commit
d0afb96c40
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user