2929import androidx .media3 .common .Effect ;
3030import androidx .media3 .common .MediaItem ;
3131import androidx .media3 .common .Player ;
32+ import androidx .media3 .common .Player .State ;
3233import androidx .media3 .common .Timeline ;
3334import 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 ;
3837import androidx .media3 .effect .GlEffect ;
3938import androidx .test .ext .junit .rules .ActivityScenarioRule ;
4039import androidx .test .ext .junit .runners .AndroidJUnit4 ;
4140import com .google .common .collect .ImmutableList ;
4241import com .google .common .collect .Iterables ;
43- import java .nio .ByteBuffer ;
44- import java .util .Collections ;
45- import java .util .List ;
4642import java .util .concurrent .CopyOnWriteArrayList ;
4743import java .util .concurrent .atomic .AtomicBoolean ;
48- import java .util .concurrent .atomic .AtomicLong ;
4944import org .checkerframework .checker .nullness .qual .MonotonicNonNull ;
5045import org .junit .After ;
5146import org .junit .Before ;
5651/** Tests for setting {@link Composition} on {@link CompositionPlayer}. */
5752@ RunWith (AndroidJUnit4 .class )
5853public class CompositionPlayerSetCompositionTest {
59- // TODO: b/412585856: Keep tests focused or make them parameterized.
6054 private static final long TEST_TIMEOUT_MS = isRunningOnEmulator () ? 20_000 : 10_000 ;
6155
6256 private @ MonotonicNonNull CompositionPlayer compositionPlayer ;
@@ -128,6 +122,55 @@ public void composition_changeComposition() throws Exception {
128122 .hasSize (2 );
129123 }
130124
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+
131174 @ Test
132175 public void setComposition_withChangedSpeed_playbackCompletes () throws Exception {
133176 EditedMediaItem fastMediaItem = createEditedMediaItemWithSpeed (MP4_ASSET , 3.f );
@@ -164,228 +207,6 @@ public void onTimelineChanged(Timeline timeline, int reason) {
164207 assertThat (playerDurations ).containsExactly (341333L , 3071999L ).inOrder ();
165208 }
166209
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-
389210 private static EditedMediaItem createEditedMediaItemWithSpeed (
390211 AndroidTestUtil .AssetInfo assetInfo , float speed ) {
391212 Pair <AudioProcessor , Effect > speedChangingEffect =
@@ -399,19 +220,16 @@ private static EditedMediaItem createEditedMediaItemWithSpeed(
399220 .build ();
400221 }
401222
402- private static Composition createSingleSequenceComposition (
403- List <EditedMediaItem > editedMediaItems ) {
404- return new Composition .Builder (new EditedMediaItemSequence .Builder (editedMediaItems ).build ())
405- .build ();
406- }
407-
408223 private static Composition createSingleSequenceComposition (
409224 EditedMediaItem editedMediaItem , EditedMediaItem ... moreEditedMediaItems ) {
410- return createSingleSequenceComposition (
411- new ImmutableList .Builder <EditedMediaItem >()
412- .add (editedMediaItem )
413- .add (moreEditedMediaItems )
414- .build ());
225+ return new Composition .Builder (
226+ new EditedMediaItemSequence .Builder (
227+ new ImmutableList .Builder <EditedMediaItem >()
228+ .add (editedMediaItem )
229+ .add (moreEditedMediaItems )
230+ .build ())
231+ .build ())
232+ .build ();
415233 }
416234
417235 private static final class SimpleSpeedProvider implements SpeedProvider {
@@ -433,20 +251,4 @@ public long getNextSpeedChangeTimeUs(long timeUs) {
433251 return C .TIME_UNSET ;
434252 }
435253 }
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- }
452254}
0 commit comments