diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerSeekTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerSeekTest.java index 179e9f5911..def7ed5481 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerSeekTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/performance/CompositionPlayerSeekTest.java @@ -16,6 +16,8 @@ package androidx.media3.transformer.mh.performance; +import static androidx.media3.common.util.Util.usToMs; +import static com.google.common.collect.Iterables.getLast; import static com.google.common.truth.Truth.assertThat; import android.app.Instrumentation; @@ -38,6 +40,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -56,6 +59,12 @@ public class CompositionPlayerSeekTest { private static final long TEST_TIMEOUT_MS = 10_000; private static final String MP4_ASSET = "asset:///media/mp4/sample.mp4"; + private static final long MP4_ASSET_DURATION_US = 1_024_000L; + private static final ImmutableList MP4_ASSET_TIMESTAMPS_US = + ImmutableList.of( + 0L, 33366L, 66733L, 100100L, 133466L, 166833L, 200200L, 233566L, 266933L, 300300L, + 333666L, 367033L, 400400L, 433766L, 467133L, 500500L, 533866L, 567233L, 600600L, 633966L, + 667333L, 700700L, 734066L, 767433L, 800800L, 834166L, 867533L, 900900L, 934266L, 967633L); @Rule public ActivityScenarioRule rule = @@ -114,84 +123,170 @@ public class CompositionPlayerSeekTest { instrumentation.runOnMainSync(() -> compositionPlayer.seekTo(0)); listener.waitUntilPlayerEnded(); - ImmutableList timestampsUsOfOneSequence = - ImmutableList.of( - 0L, - 33366L, - 66733L, - 100100L, - 133466L, - 166833L, - 200200L, - 233566L, - 266933L, - 300300L, - 333666L, - 367033L, - 400400L, - 433766L, - 467133L, - 500500L, - 533866L, - 567233L, - 600600L, - 633966L, - 667333L, - 700700L, - 734066L, - 767433L, - 800800L, - 834166L, - 867533L, - 900900L, - 934266L, - 967633L, - // Second video starts here. - 1024000L, - 1057366L, - 1090733L, - 1124100L, - 1157466L, - 1190833L, - 1224200L, - 1257566L, - 1290933L, - 1324300L, - 1357666L, - 1391033L, - 1424400L, - 1457766L, - 1491133L, - 1524500L, - 1557866L, - 1591233L, - 1624600L, - 1657966L, - 1691333L, - 1724700L, - 1758066L, - 1791433L, - 1824800L, - 1858166L, - 1891533L, - 1924900L, - 1958266L, - 1991633L); + ImmutableList sequenceTimestampsUs = + new ImmutableList.Builder() + // Plays the first video + .addAll(MP4_ASSET_TIMESTAMPS_US) + // Plays the second video + .addAll( + Iterables.transform( + MP4_ASSET_TIMESTAMPS_US, timestampUs -> (MP4_ASSET_DURATION_US + timestampUs))) + .build(); + // Seeked after the first playback ends, so the timestamps are repeated twice. + ImmutableList expectedTimestampsUs = + new ImmutableList.Builder() + .addAll(sequenceTimestampsUs) + .addAll(sequenceTimestampsUs) + .build(); assertThat(inputTimestampRecordingShaderProgram.getInputTimestampsUs()) - // Seeked after the first playback ends, so the timestamps are repeated twice. - .containsExactlyElementsIn( - new ImmutableList.Builder() - .addAll(timestampsUsOfOneSequence) - .addAll(timestampsUsOfOneSequence) - .build()) - .inOrder(); + .isEqualTo(expectedTimestampsUs); } @Test - public void seekToZero_after15framesInSingleSequenceOfTwoVideos() throws Exception { + public void seekToZero_duringPlayingFirstVideoInSingleSequenceOfTwoVideos() throws Exception { PlayerTestListener listener = new PlayerTestListener(TEST_TIMEOUT_MS); - ResettableCountDownLatch framesReceivedLatch = new ResettableCountDownLatch(15); + int numberOfFramesBeforeSeeking = 15; + + ImmutableList expectedTimestampsUs = + new ImmutableList.Builder() + // Plays the first 15 frames of the first video + .addAll( + Iterables.limit( + MP4_ASSET_TIMESTAMPS_US, /* limitSize= */ numberOfFramesBeforeSeeking)) + // Seek to zero, plays the first video again + .addAll(MP4_ASSET_TIMESTAMPS_US) + // Plays the second video + .addAll( + Iterables.transform( + MP4_ASSET_TIMESTAMPS_US, timestampUs -> (MP4_ASSET_DURATION_US + timestampUs))) + .build(); + + ImmutableList actualTimestampsUs = + playCompositionOfTwoVideosAndGetTimestamps( + listener, numberOfFramesBeforeSeeking, /* seekTimeMs= */ 0); + + assertThat(actualTimestampsUs).isEqualTo(expectedTimestampsUs); + } + + @Test + public void seekToSecondMedia_duringPlayingFirstVideoInSingleSequenceOfTwoVideos() + throws Exception { + PlayerTestListener listener = new PlayerTestListener(TEST_TIMEOUT_MS); + int numberOfFramesBeforeSeeking = 15; + // 100ms into the second video, should skip the first three frames. + long seekTimeMs = 1124; + + ImmutableList expectedTimestampsUs = + new ImmutableList.Builder() + // Plays the first 15 frames of the first video + .addAll( + Iterables.limit( + MP4_ASSET_TIMESTAMPS_US, /* limitSize= */ numberOfFramesBeforeSeeking)) + // Skipping the first three frames of the second video + .addAll( + Iterables.transform( + Iterables.skip(MP4_ASSET_TIMESTAMPS_US, /* numberToSkip= */ 3), + timestampUs -> (MP4_ASSET_DURATION_US + timestampUs))) + .build(); + + ImmutableList actualTimestampsUs = + playCompositionOfTwoVideosAndGetTimestamps( + listener, numberOfFramesBeforeSeeking, seekTimeMs); + + assertThat(actualTimestampsUs).isEqualTo(expectedTimestampsUs); + } + + @Test + public void seekToFirstMedia_duringPlayingSecondVideoInSingleSequenceOfTwoVideos() + throws Exception { + PlayerTestListener listener = new PlayerTestListener(TEST_TIMEOUT_MS); + int numberOfFramesBeforeSeeking = 45; + // 100ms into the first video, should skip the first three frames. + long seekTimeMs = 100; + + ImmutableList expectedTimestampsUs = + new ImmutableList.Builder() + // Play first video + .addAll(MP4_ASSET_TIMESTAMPS_US) + // Play the first 15 frames of the seconds video + .addAll( + Iterables.transform( + Iterables.limit(MP4_ASSET_TIMESTAMPS_US, /* limitSize= */ 15), + timestampUs -> (MP4_ASSET_DURATION_US + timestampUs))) + // Seek to the first, skipping the first three frames. + .addAll(Iterables.skip(MP4_ASSET_TIMESTAMPS_US, /* numberToSkip= */ 3)) + // Plays the second video + .addAll( + Iterables.transform( + MP4_ASSET_TIMESTAMPS_US, timestampUs -> (MP4_ASSET_DURATION_US + timestampUs))) + .build(); + + ImmutableList actualTimestampsUs = + playCompositionOfTwoVideosAndGetTimestamps( + listener, numberOfFramesBeforeSeeking, seekTimeMs); + + assertThat(actualTimestampsUs).isEqualTo(expectedTimestampsUs); + } + + @Test + public void seekToEndOfFirstMedia_duringPlayingFirstVideoInSingleSequenceOfTwoVideos() + throws Exception { + PlayerTestListener listener = new PlayerTestListener(TEST_TIMEOUT_MS); + int numberOfFramesBeforeSeeking = 15; + // Seek to the duration of the first video. + long seekTimeMs = usToMs(MP4_ASSET_DURATION_US); + + ImmutableList expectedTimestampsUs = + new ImmutableList.Builder() + // Play the first 15 frames of the first video + .addAll(Iterables.limit(MP4_ASSET_TIMESTAMPS_US, /* limitSize= */ 15)) + // Plays the second video + .addAll( + Iterables.transform( + MP4_ASSET_TIMESTAMPS_US, timestampUs -> (MP4_ASSET_DURATION_US + timestampUs))) + .build(); + + ImmutableList actualTimestampsUs = + playCompositionOfTwoVideosAndGetTimestamps( + listener, numberOfFramesBeforeSeeking, seekTimeMs); + + assertThat(actualTimestampsUs).isEqualTo(expectedTimestampsUs); + } + + @Test + public void seekToEndOfSecondVideo_duringPlayingFirstVideoInSingleSequenceOfTwoVideos() + throws Exception { + PlayerTestListener listener = new PlayerTestListener(TEST_TIMEOUT_MS); + int numberOfFramesBeforeSeeking = 15; + // Seek to after the composition ends. + long seekTimeMs = 10_000_000L; + + ImmutableList expectedTimestampsUs = + new ImmutableList.Builder() + // Play the first 15 frames of the first video + .addAll(Iterables.limit(MP4_ASSET_TIMESTAMPS_US, /* limitSize= */ 15)) + // Seeking to/beyond the end plays the last frame. + .add(MP4_ASSET_DURATION_US + getLast(MP4_ASSET_TIMESTAMPS_US)) + .build(); + + ImmutableList actualTimestampsUs = + playCompositionOfTwoVideosAndGetTimestamps( + listener, numberOfFramesBeforeSeeking, seekTimeMs); + + assertThat(actualTimestampsUs).isEqualTo(expectedTimestampsUs); + } + + /** + * Plays the {@link #MP4_ASSET} for {@code videoLoopCount} times, seeks after {@code + * numberOfFramesBeforeSeeking} frames to {@code seekTimeMs}, and returns the timestamps of the + * processed frames, in microsecond. + */ + private ImmutableList playCompositionOfTwoVideosAndGetTimestamps( + PlayerTestListener listener, int numberOfFramesBeforeSeeking, long seekTimeMs) + throws Exception { + ResettableCountDownLatch framesReceivedLatch = + new ResettableCountDownLatch(numberOfFramesBeforeSeeking); AtomicBoolean shaderProgramShouldBlockInput = new AtomicBoolean(); InputTimestampRecordingShaderProgram inputTimestampRecordingShaderProgram = @@ -226,10 +321,15 @@ public class CompositionPlayerSeekTest { framesReceivedLatch.reset(Integer.MAX_VALUE); } }; - EditedMediaItem video = - createEditedMediaItem( - /* videoEffects= */ ImmutableList.of( - (GlEffect) (context, useHdr) -> inputTimestampRecordingShaderProgram)); + + ImmutableList editedMediaItems = + ImmutableList.of( + createEditedMediaItem( + ImmutableList.of( + (GlEffect) (context, useHdr) -> inputTimestampRecordingShaderProgram)), + createEditedMediaItem( + ImmutableList.of( + (GlEffect) (context, useHdr) -> inputTimestampRecordingShaderProgram))); instrumentation.runOnMainSync( () -> { @@ -239,99 +339,16 @@ public class CompositionPlayerSeekTest { compositionPlayer.setVideoSurfaceView(surfaceView); compositionPlayer.addListener(listener); compositionPlayer.setComposition( - new Composition.Builder(new EditedMediaItemSequence(video, video)).build()); + new Composition.Builder(new EditedMediaItemSequence(editedMediaItems)).build()); compositionPlayer.prepare(); compositionPlayer.play(); }); // Wait until the number of frames are received, block further input on the shader program. framesReceivedLatch.await(); - instrumentation.runOnMainSync(() -> compositionPlayer.seekTo(0)); + instrumentation.runOnMainSync(() -> compositionPlayer.seekTo(seekTimeMs)); listener.waitUntilPlayerEnded(); - - ImmutableList expectedTimestampsUs = - ImmutableList.of( - 0L, - 33366L, - 66733L, - 100100L, - 133466L, - 166833L, - 200200L, - 233566L, - 266933L, - 300300L, - 333666L, - 367033L, - 400400L, - 433766L, - 467133L, - // 15 frames, seek - 0L, - 33366L, - 66733L, - 100100L, - 133466L, - 166833L, - 200200L, - 233566L, - 266933L, - 300300L, - 333666L, - 367033L, - 400400L, - 433766L, - 467133L, - 500500L, - 533866L, - 567233L, - 600600L, - 633966L, - 667333L, - 700700L, - 734066L, - 767433L, - 800800L, - 834166L, - 867533L, - 900900L, - 934266L, - 967633L, - // Second video starts here. - 1024000L, - 1057366L, - 1090733L, - 1124100L, - 1157466L, - 1190833L, - 1224200L, - 1257566L, - 1290933L, - 1324300L, - 1357666L, - 1391033L, - 1424400L, - 1457766L, - 1491133L, - 1524500L, - 1557866L, - 1591233L, - 1624600L, - 1657966L, - 1691333L, - 1724700L, - 1758066L, - 1791433L, - 1824800L, - 1858166L, - 1891533L, - 1924900L, - 1958266L, - 1991633L); - - assertThat(inputTimestampRecordingShaderProgram.getInputTimestampsUs()) - .containsExactlyElementsIn(expectedTimestampsUs) - .inOrder(); + return inputTimestampRecordingShaderProgram.getInputTimestampsUs(); } private static EditedMediaItem createEditedMediaItem(List videoEffects) {