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