Skip to content

Commit 50df607

Browse files
clainclycopybara-github
authored andcommitted
Allow setComposition() to start from any position
PiperOrigin-RevId: 787083120
1 parent df6b9a0 commit 50df607

File tree

5 files changed

+444
-176
lines changed

5 files changed

+444
-176
lines changed

libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSetCompositionTest.java

Lines changed: 256 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,23 @@
2929
import androidx.media3.common.Effect;
3030
import androidx.media3.common.MediaItem;
3131
import androidx.media3.common.Player;
32-
import androidx.media3.common.Player.State;
3332
import androidx.media3.common.Timeline;
3433
import androidx.media3.common.audio.AudioProcessor;
34+
import androidx.media3.common.audio.BaseAudioProcessor;
3535
import androidx.media3.common.audio.SpeedProvider;
3636
import androidx.media3.common.util.ConditionVariable;
37+
import androidx.media3.common.util.Util;
3738
import androidx.media3.effect.GlEffect;
3839
import androidx.test.ext.junit.rules.ActivityScenarioRule;
3940
import androidx.test.ext.junit.runners.AndroidJUnit4;
4041
import com.google.common.collect.ImmutableList;
4142
import com.google.common.collect.Iterables;
43+
import java.nio.ByteBuffer;
44+
import java.util.Collections;
45+
import java.util.List;
4246
import java.util.concurrent.CopyOnWriteArrayList;
4347
import java.util.concurrent.atomic.AtomicBoolean;
48+
import java.util.concurrent.atomic.AtomicLong;
4449
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
4550
import org.junit.After;
4651
import org.junit.Before;
@@ -51,6 +56,7 @@
5156
/** Tests for setting {@link Composition} on {@link CompositionPlayer}. */
5257
@RunWith(AndroidJUnit4.class)
5358
public 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

Comments
 (0)