2929import androidx .media3 .common .Effect ;
3030import androidx .media3 .common .MediaItem ;
3131import androidx .media3 .common .Player ;
32- import androidx .media3 .common .Player .State ;
3332import androidx .media3 .common .Timeline ;
3433import androidx .media3 .common .audio .AudioProcessor ;
34+ import androidx .media3 .common .audio .BaseAudioProcessor ;
3535import androidx .media3 .common .audio .SpeedProvider ;
3636import androidx .media3 .common .util .ConditionVariable ;
37+ import androidx .media3 .common .util .Util ;
3738import androidx .media3 .effect .GlEffect ;
3839import androidx .test .ext .junit .rules .ActivityScenarioRule ;
3940import androidx .test .ext .junit .runners .AndroidJUnit4 ;
4041import com .google .common .collect .ImmutableList ;
4142import com .google .common .collect .Iterables ;
43+ import java .nio .ByteBuffer ;
44+ import java .util .Collections ;
45+ import java .util .List ;
4246import java .util .concurrent .CopyOnWriteArrayList ;
4347import java .util .concurrent .atomic .AtomicBoolean ;
48+ import java .util .concurrent .atomic .AtomicLong ;
4449import org .checkerframework .checker .nullness .qual .MonotonicNonNull ;
4550import org .junit .After ;
4651import org .junit .Before ;
5156/** Tests for setting {@link Composition} on {@link CompositionPlayer}. */
5257@ RunWith (AndroidJUnit4 .class )
5358public class CompositionPlayerSetCompositionTest {
59+ // TODO: b/412585856: Keep tests focused or make them parameterized.
5460 private static final long TEST_TIMEOUT_MS = isRunningOnEmulator () ? 20_000 : 10_000 ;
5561
5662 private @ MonotonicNonNull CompositionPlayer compositionPlayer ;
@@ -122,55 +128,6 @@ public void composition_changeComposition() throws Exception {
122128 .hasSize (2 );
123129 }
124130
125- @ Test
126- public void setComposition_withChangedRemoveAudio_playbackCompletes () throws Exception {
127- EditedMediaItem mediaItem =
128- new EditedMediaItem .Builder (MediaItem .fromUri (MP4_ASSET .uri ))
129- .setDurationUs (MP4_ASSET .videoDurationUs )
130- .build ();
131- EditedMediaItem mediaItemRemoveAudio = mediaItem .buildUpon ().setRemoveAudio (true ).build ();
132- AtomicBoolean changedComposition = new AtomicBoolean ();
133- ConditionVariable playerEnded = new ConditionVariable ();
134- CopyOnWriteArrayList <Integer > playerStates = new CopyOnWriteArrayList <>();
135-
136- instrumentation .runOnMainSync (
137- () -> {
138- compositionPlayer = new CompositionPlayer .Builder (context ).build ();
139- compositionPlayer .setVideoSurfaceView (surfaceView );
140- compositionPlayer .addListener (playerTestListener );
141- compositionPlayer .addListener (
142- new Player .Listener () {
143- @ Override
144- public void onPlaybackStateChanged (@ State int playbackState ) {
145- playerStates .add (playbackState );
146- if (playbackState == Player .STATE_READY ) {
147- if (!changedComposition .get ()) {
148- compositionPlayer .setComposition (
149- createSingleSequenceComposition (
150- mediaItemRemoveAudio , mediaItemRemoveAudio ));
151- compositionPlayer .play ();
152- changedComposition .set (true );
153- }
154- } else if (playbackState == Player .STATE_ENDED ) {
155- playerEnded .open ();
156- }
157- }
158- });
159- compositionPlayer .setComposition (createSingleSequenceComposition (mediaItem , mediaItem ));
160- compositionPlayer .prepare ();
161- });
162-
163- // Wait until the final state is added to playerStates.
164- playerEnded .block (TEST_TIMEOUT_MS );
165- // waitUntilPlayerEnded should return immediate and will throw any player error.
166- playerTestListener .waitUntilPlayerEnded ();
167- // Asserts that changing removeAudio does not cause the player to get back to buffering state,
168- // because the player should not be re-prepared.
169- assertThat (playerStates )
170- .containsExactly (Player .STATE_BUFFERING , Player .STATE_READY , Player .STATE_ENDED )
171- .inOrder ();
172- }
173-
174131 @ Test
175132 public void setComposition_withChangedSpeed_playbackCompletes () throws Exception {
176133 EditedMediaItem fastMediaItem = createEditedMediaItemWithSpeed (MP4_ASSET , 3.f );
@@ -207,6 +164,228 @@ public void onTimelineChanged(Timeline timeline, int reason) {
207164 assertThat (playerDurations ).containsExactly (341333L , 3071999L ).inOrder ();
208165 }
209166
167+ @ Test
168+ public void setComposition_withStartPosition_playbackStartsFromSetPosition () throws Exception {
169+ assertThat (
170+ getFirstVideoFrameTimestampUsWithStartPosition (
171+ /* startPositionUs= */ 500_000L , /* numberOfItemsInSequence= */ 1 ))
172+ .isEqualTo (500_500L );
173+ }
174+
175+ @ Test
176+ public void setComposition_withZeroStartPosition_playbackStartsFromZero () throws Exception {
177+ assertThat (
178+ getFirstVideoFrameTimestampUsWithStartPosition (
179+ /* startPositionUs= */ 0 , /* numberOfItemsInSequence= */ 1 ))
180+ .isEqualTo (0 );
181+ }
182+
183+ @ Test
184+ public void setComposition_withStartPositionPastVideoDuration_playbackStopsAtLastFrame ()
185+ throws Exception {
186+ assertThat (
187+ getFirstVideoFrameTimestampUsWithStartPosition (
188+ /* startPositionUs= */ 100_000_000L , /* numberOfItemsInSequence= */ 1 ))
189+ .isEqualTo (967633L );
190+ }
191+
192+ @ Test
193+ public void
194+ setComposition_withStartPositionPastVideoDurationInMultiItemSequence_playbackStopsAtLastFrame ()
195+ throws Exception {
196+ assertThat (
197+ getFirstVideoFrameTimestampUsWithStartPosition (
198+ /* startPositionUs= */ 100_000_000L , /* numberOfItemsInSequence= */ 5 ))
199+ .isEqualTo (5_063_633L );
200+ }
201+
202+ @ Test
203+ public void setComposition_withStartPositionInMultiItemSequence_playbackStartsFromSetPosition ()
204+ throws Exception {
205+ assertThat (
206+ getFirstVideoFrameTimestampUsWithStartPosition (
207+ /* startPositionUs= */ 1_500_000L , /* numberOfItemsInSequence= */ 2 ))
208+ .isEqualTo (1_524_500 );
209+ }
210+
211+ @ Test
212+ public void
213+ setComposition_withStartPositionSingleItemAudioSequence_reportsCorrectAudioProcessorPositionOffset ()
214+ throws Exception {
215+ Pair <Long , Long > lastAudioPositionOffsetWithStartPosition =
216+ getLastAudioPositionOffsetWithStartPosition (
217+ /* startPositionUs= */ 500_000L , /* numberOfItemsInSequence= */ 1 );
218+
219+ assertThat (lastAudioPositionOffsetWithStartPosition .first ).isEqualTo (500_000 );
220+ assertThat (lastAudioPositionOffsetWithStartPosition .second ).isEqualTo (500_000 );
221+ }
222+
223+ @ Test
224+ public void
225+ setComposition_withStartPositionTwoItemsAudioSequence_reportsCorrectAudioProcessorPositionOffset ()
226+ throws Exception {
227+ Pair <Long , Long > lastAudioPositionOffsetWithStartPosition =
228+ getLastAudioPositionOffsetWithStartPosition (
229+ /* startPositionUs= */ 1_500_000L , /* numberOfItemsInSequence= */ 2 );
230+
231+ assertThat (lastAudioPositionOffsetWithStartPosition .first ).isEqualTo (500_000 );
232+ assertThat (lastAudioPositionOffsetWithStartPosition .second ).isEqualTo (1_500_000 );
233+ }
234+
235+ @ Test
236+ public void setComposition_withNewCompositionAudioProcessor_recreatesAudioPipeline ()
237+ throws Exception {
238+ AtomicBoolean firstCompositionSentDataToAudioPipeline = new AtomicBoolean ();
239+ AtomicBoolean secondCompositionSentDataToAudioPipeline = new AtomicBoolean ();
240+ ConditionVariable firstCompositionProcessedData = new ConditionVariable ();
241+ PassthroughAudioProcessor firstCompositionAudioProcessor =
242+ new PassthroughAudioProcessor () {
243+ @ Override
244+ public void queueInput (ByteBuffer inputBuffer ) {
245+ super .queueInput (inputBuffer );
246+ firstCompositionSentDataToAudioPipeline .set (true );
247+ firstCompositionProcessedData .open ();
248+ }
249+ };
250+ PassthroughAudioProcessor secondCompositionAudioProcessor =
251+ new PassthroughAudioProcessor () {
252+ @ Override
253+ public void queueInput (ByteBuffer inputBuffer ) {
254+ super .queueInput (inputBuffer );
255+ secondCompositionSentDataToAudioPipeline .set (true );
256+ }
257+ };
258+ EditedMediaItem editedMediaItem =
259+ new EditedMediaItem .Builder (MediaItem .fromUri (AndroidTestUtil .WAV_ASSET .uri ))
260+ .setDurationUs (1_000_000L )
261+ .setEffects (
262+ new Effects (
263+ /* audioProcessors= */ ImmutableList .of (firstCompositionAudioProcessor ),
264+ /* videoEffects= */ ImmutableList .of ()))
265+ .build ();
266+ Composition firstComposition =
267+ new Composition .Builder (
268+ new EditedMediaItemSequence .Builder (Collections .nCopies (5 , editedMediaItem ))
269+ .build ())
270+ .setEffects (
271+ new Effects (
272+ /* audioProcessors= */ ImmutableList .of (firstCompositionAudioProcessor ),
273+ /* videoEffects= */ ImmutableList .of ()))
274+ .build ();
275+ Composition secondComposition =
276+ new Composition .Builder (
277+ new EditedMediaItemSequence .Builder (Collections .nCopies (5 , editedMediaItem ))
278+ .build ())
279+ .setEffects (
280+ new Effects (
281+ /* audioProcessors= */ ImmutableList .of (secondCompositionAudioProcessor ),
282+ /* videoEffects= */ ImmutableList .of ()))
283+ .build ();
284+
285+ getInstrumentation ()
286+ .runOnMainSync (
287+ () -> {
288+ compositionPlayer = new CompositionPlayer .Builder (context ).build ();
289+ compositionPlayer .addListener (playerTestListener );
290+ compositionPlayer .setComposition (firstComposition );
291+ compositionPlayer .prepare ();
292+ });
293+ playerTestListener .waitUntilPlayerReady ();
294+ firstCompositionProcessedData .block (TEST_TIMEOUT_MS );
295+ assertThat (firstCompositionSentDataToAudioPipeline .get ()).isTrue ();
296+ assertThat (secondCompositionSentDataToAudioPipeline .get ()).isFalse ();
297+
298+ playerTestListener .resetStatus ();
299+ getInstrumentation ()
300+ .runOnMainSync (
301+ () -> {
302+ compositionPlayer .setComposition (secondComposition );
303+ compositionPlayer .play ();
304+ });
305+ playerTestListener .waitUntilPlayerEnded ();
306+
307+ assertThat (secondCompositionSentDataToAudioPipeline .get ()).isTrue ();
308+ }
309+
310+ private Pair <Long , Long > getLastAudioPositionOffsetWithStartPosition (
311+ long startPositionUs , int numberOfItemsInSequence ) throws Exception {
312+ AtomicLong lastItemPositionOffsetUs = new AtomicLong (C .TIME_UNSET );
313+ AtomicLong lastCompositionPositionOffsetUs = new AtomicLong (C .TIME_UNSET );
314+ PassthroughAudioProcessor itemAudioProcessor =
315+ new PassthroughAudioProcessor () {
316+ @ Override
317+ protected void onFlush (AudioProcessor .StreamMetadata streamMetadata ) {
318+ lastItemPositionOffsetUs .set (streamMetadata .positionOffsetUs );
319+ }
320+ };
321+ PassthroughAudioProcessor compositionAudioProcessor =
322+ new PassthroughAudioProcessor () {
323+ @ Override
324+ protected void onFlush (AudioProcessor .StreamMetadata streamMetadata ) {
325+ lastCompositionPositionOffsetUs .set (streamMetadata .positionOffsetUs );
326+ }
327+ };
328+ EditedMediaItem editedMediaItem =
329+ new EditedMediaItem .Builder (MediaItem .fromUri (AndroidTestUtil .WAV_ASSET .uri ))
330+ .setDurationUs (1_000_000L )
331+ .setEffects (
332+ new Effects (
333+ /* audioProcessors= */ ImmutableList .of (itemAudioProcessor ),
334+ /* videoEffects= */ ImmutableList .of ()))
335+ .build ();
336+ final Composition composition =
337+ new Composition .Builder (
338+ new EditedMediaItemSequence .Builder (
339+ Collections .nCopies (numberOfItemsInSequence , editedMediaItem ))
340+ .build ())
341+ .setEffects (
342+ new Effects (
343+ /* audioProcessors= */ ImmutableList .of (compositionAudioProcessor ),
344+ /* videoEffects= */ ImmutableList .of ()))
345+ .build ();
346+
347+ getInstrumentation ()
348+ .runOnMainSync (
349+ () -> {
350+ compositionPlayer = new CompositionPlayer .Builder (context ).build ();
351+ compositionPlayer .addListener (playerTestListener );
352+ compositionPlayer .setComposition (composition , Util .usToMs (startPositionUs ));
353+ compositionPlayer .prepare ();
354+ });
355+ playerTestListener .waitUntilPlayerReady ();
356+ return Pair .create (lastItemPositionOffsetUs .get (), lastCompositionPositionOffsetUs .get ());
357+ }
358+
359+ private long getFirstVideoFrameTimestampUsWithStartPosition (
360+ long startPositionUs , int numberOfItemsInSequence ) throws Exception {
361+ EditedMediaItem editedMediaItem =
362+ new EditedMediaItem .Builder (MediaItem .fromUri (MP4_ASSET .uri ))
363+ .setDurationUs (MP4_ASSET .videoDurationUs )
364+ .build ();
365+ AtomicLong firstFrameTimestampUs = new AtomicLong (C .TIME_UNSET );
366+
367+ instrumentation .runOnMainSync (
368+ () -> {
369+ compositionPlayer = new CompositionPlayer .Builder (context ).build ();
370+ compositionPlayer .setVideoSurfaceView (surfaceView );
371+ compositionPlayer .addListener (playerTestListener );
372+ compositionPlayer .setVideoFrameMetadataListener (
373+ (presentationTimeUs , releaseTimeNs , format , mediaFormat ) -> {
374+ if (firstFrameTimestampUs .compareAndSet (C .TIME_UNSET , presentationTimeUs )) {
375+ instrumentation .runOnMainSync (compositionPlayer ::play );
376+ }
377+ });
378+ compositionPlayer .setComposition (
379+ createSingleSequenceComposition (
380+ Collections .nCopies (numberOfItemsInSequence , editedMediaItem )),
381+ Util .usToMs (startPositionUs ));
382+ compositionPlayer .prepare ();
383+ });
384+
385+ playerTestListener .waitUntilPlayerEnded ();
386+ return firstFrameTimestampUs .get ();
387+ }
388+
210389 private static EditedMediaItem createEditedMediaItemWithSpeed (
211390 AndroidTestUtil .AssetInfo assetInfo , float speed ) {
212391 Pair <AudioProcessor , Effect > speedChangingEffect =
@@ -221,17 +400,20 @@ private static EditedMediaItem createEditedMediaItemWithSpeed(
221400 }
222401
223402 private static Composition createSingleSequenceComposition (
224- EditedMediaItem editedMediaItem , EditedMediaItem ... moreEditedMediaItems ) {
225- return new Composition .Builder (
226- new EditedMediaItemSequence .Builder (
227- new ImmutableList .Builder <EditedMediaItem >()
228- .add (editedMediaItem )
229- .add (moreEditedMediaItems )
230- .build ())
231- .build ())
403+ List <EditedMediaItem > editedMediaItems ) {
404+ return new Composition .Builder (new EditedMediaItemSequence .Builder (editedMediaItems ).build ())
232405 .build ();
233406 }
234407
408+ private static Composition createSingleSequenceComposition (
409+ EditedMediaItem editedMediaItem , EditedMediaItem ... moreEditedMediaItems ) {
410+ return createSingleSequenceComposition (
411+ new ImmutableList .Builder <EditedMediaItem >()
412+ .add (editedMediaItem )
413+ .add (moreEditedMediaItems )
414+ .build ());
415+ }
416+
235417 private static final class SimpleSpeedProvider implements SpeedProvider {
236418
237419 private final float speed ;
@@ -251,4 +433,20 @@ public long getNextSpeedChangeTimeUs(long timeUs) {
251433 return C .TIME_UNSET ;
252434 }
253435 }
436+
437+ private static class PassthroughAudioProcessor extends BaseAudioProcessor {
438+ @ Override
439+ public void queueInput (ByteBuffer inputBuffer ) {
440+ if (!inputBuffer .hasRemaining ()) {
441+ return ;
442+ }
443+ ByteBuffer buffer = this .replaceOutputBuffer (inputBuffer .remaining ());
444+ buffer .put (inputBuffer ).flip ();
445+ }
446+
447+ @ Override
448+ protected AudioFormat onConfigure (AudioFormat inputAudioFormat ) {
449+ return inputAudioFormat ;
450+ }
451+ }
254452}
0 commit comments