29
29
import androidx .media3 .common .Effect ;
30
30
import androidx .media3 .common .MediaItem ;
31
31
import androidx .media3 .common .Player ;
32
+ import androidx .media3 .common .Player .State ;
32
33
import androidx .media3 .common .Timeline ;
33
34
import androidx .media3 .common .audio .AudioProcessor ;
34
- import androidx .media3 .common .audio .BaseAudioProcessor ;
35
35
import androidx .media3 .common .audio .SpeedProvider ;
36
36
import androidx .media3 .common .util .ConditionVariable ;
37
- import androidx .media3 .common .util .Util ;
38
37
import androidx .media3 .effect .GlEffect ;
39
38
import androidx .test .ext .junit .rules .ActivityScenarioRule ;
40
39
import androidx .test .ext .junit .runners .AndroidJUnit4 ;
41
40
import com .google .common .collect .ImmutableList ;
42
41
import com .google .common .collect .Iterables ;
43
- import java .nio .ByteBuffer ;
44
- import java .util .Collections ;
45
- import java .util .List ;
46
42
import java .util .concurrent .CopyOnWriteArrayList ;
47
43
import java .util .concurrent .atomic .AtomicBoolean ;
48
- import java .util .concurrent .atomic .AtomicLong ;
49
44
import org .checkerframework .checker .nullness .qual .MonotonicNonNull ;
50
45
import org .junit .After ;
51
46
import org .junit .Before ;
56
51
/** Tests for setting {@link Composition} on {@link CompositionPlayer}. */
57
52
@ RunWith (AndroidJUnit4 .class )
58
53
public class CompositionPlayerSetCompositionTest {
59
- // TODO: b/412585856: Keep tests focused or make them parameterized.
60
54
private static final long TEST_TIMEOUT_MS = isRunningOnEmulator () ? 20_000 : 10_000 ;
61
55
62
56
private @ MonotonicNonNull CompositionPlayer compositionPlayer ;
@@ -128,6 +122,55 @@ public void composition_changeComposition() throws Exception {
128
122
.hasSize (2 );
129
123
}
130
124
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
+
131
174
@ Test
132
175
public void setComposition_withChangedSpeed_playbackCompletes () throws Exception {
133
176
EditedMediaItem fastMediaItem = createEditedMediaItemWithSpeed (MP4_ASSET , 3.f );
@@ -164,228 +207,6 @@ public void onTimelineChanged(Timeline timeline, int reason) {
164
207
assertThat (playerDurations ).containsExactly (341333L , 3071999L ).inOrder ();
165
208
}
166
209
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
-
389
210
private static EditedMediaItem createEditedMediaItemWithSpeed (
390
211
AndroidTestUtil .AssetInfo assetInfo , float speed ) {
391
212
Pair <AudioProcessor , Effect > speedChangingEffect =
@@ -399,19 +220,16 @@ private static EditedMediaItem createEditedMediaItemWithSpeed(
399
220
.build ();
400
221
}
401
222
402
- private static Composition createSingleSequenceComposition (
403
- List <EditedMediaItem > editedMediaItems ) {
404
- return new Composition .Builder (new EditedMediaItemSequence .Builder (editedMediaItems ).build ())
405
- .build ();
406
- }
407
-
408
223
private static Composition createSingleSequenceComposition (
409
224
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 ();
415
233
}
416
234
417
235
private static final class SimpleSpeedProvider implements SpeedProvider {
@@ -433,20 +251,4 @@ public long getNextSpeedChangeTimeUs(long timeUs) {
433
251
return C .TIME_UNSET ;
434
252
}
435
253
}
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
- }
452
254
}
0 commit comments