From babc2dd416b1e12df8b41391357020c9b3d57a81 Mon Sep 17 00:00:00 2001 From: claincly Date: Fri, 7 Feb 2025 04:24:24 -0800 Subject: [PATCH] Match with ExoPlayer seeking when using CompositionPlayer In a image sequence, seek to after the image duration should render the final image frame. But currently it hangs as ConstantRateTimestampIterator doesn't output any timestamp in this case PiperOrigin-RevId: 724295475 --- .../util/ConstantRateTimestampIterator.java | 4 +- .../ConstantRateTimestampIteratorTest.java | 12 +++--- .../CompositionPlayerSeekTest.java | 41 +++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/ConstantRateTimestampIterator.java b/libraries/common/src/main/java/androidx/media3/common/util/ConstantRateTimestampIterator.java index b2bf6c3c4f..321b26f9df 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/ConstantRateTimestampIterator.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/ConstantRateTimestampIterator.java @@ -17,6 +17,7 @@ package androidx.media3.common.util; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkState; +import static java.lang.Math.max; import static java.lang.Math.round; import androidx.annotation.FloatRange; @@ -70,7 +71,8 @@ public final class ConstantRateTimestampIterator implements TimestampIterator { this.endPositionUs = endPositionUs; this.frameRate = frameRate; float durationSecs = (endPositionUs - startPositionUs) / (float) C.MICROS_PER_SECOND; - this.totalNumberOfFramesToAdd = round(frameRate * durationSecs); + // Generate at least one timestamp so that at least one frame is produced when seeking. + this.totalNumberOfFramesToAdd = max(round(frameRate * durationSecs), 1); framesDurationUs = C.MICROS_PER_SECOND / frameRate; } diff --git a/libraries/common/src/test/java/androidx/media3/common/util/ConstantRateTimestampIteratorTest.java b/libraries/common/src/test/java/androidx/media3/common/util/ConstantRateTimestampIteratorTest.java index 1010b20af6..683d27ab99 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/ConstantRateTimestampIteratorTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/ConstantRateTimestampIteratorTest.java @@ -67,12 +67,12 @@ public class ConstantRateTimestampIteratorTest { } @Test - public void timestampIterator_smallDuration_generatesEmptyIterator() { + public void timestampIterator_smallDuration_generatesOneTimestamp() { ConstantRateTimestampIterator constantRateTimestampIterator = new ConstantRateTimestampIterator(/* durationUs= */ 1, /* frameRate= */ 2); - assertThat(generateList(constantRateTimestampIterator)).isEmpty(); - assertThat(constantRateTimestampIterator.getLastTimestampUs()).isEqualTo(C.TIME_UNSET); + assertThat(generateList(constantRateTimestampIterator)).containsExactly(0L); + assertThat(constantRateTimestampIterator.getLastTimestampUs()).isEqualTo(0L); } @Test @@ -110,15 +110,15 @@ public class ConstantRateTimestampIteratorTest { } @Test - public void timestampIterator_withNoTimestampsWithinParameters_generatesNoTimestamp() { + public void timestampIterator_withNoTimestampsWithinParameters_generatesOneTimestamp() { ConstantRateTimestampIterator constantRateTimestampIterator = new ConstantRateTimestampIterator( /* startPositionUs= */ 900_000L, /* endPositionUs= */ C.MICROS_PER_SECOND, /* frameRate= */ 3); - assertThat(generateList(constantRateTimestampIterator)).isEmpty(); - assertThat(constantRateTimestampIterator.getLastTimestampUs()).isEqualTo(C.TIME_UNSET); + assertThat(generateList(constantRateTimestampIterator)).containsExactly(900_000L); + assertThat(constantRateTimestampIterator.getLastTimestampUs()).isEqualTo(900_000L); } private static List generateList(TimestampIterator iterator) { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java index f13f857200..9cbb8ca2f0 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/CompositionPlayerSeekTest.java @@ -355,6 +355,47 @@ public class CompositionPlayerSeekTest { .isEqualTo(sequenceTimestampsUs); } + @Test + public void seekToEndOfSecondImage_afterPlayingSingleSequenceOfTwoImages() throws Exception { + // Seeks to the end of the second image + long seekTimeMs = usToMs(2 * IMAGE_DURATION_US); + ImmutableList sequenceTimestampsUs = + new ImmutableList.Builder() + // Plays the first image + .addAll(IMAGE_TIMESTAMPS_US) + // Plays the second image + .addAll( + transform(IMAGE_TIMESTAMPS_US, timestampUs -> (IMAGE_DURATION_US + timestampUs))) + // Plays the last image frame, which is one microsecond smaller than the total duration. + .add(399_999L) + .build(); + + assertThat( + playSequenceUntilEndedAndSeekAndGetTimestampsUs( + ImmutableList.of(IMAGE_MEDIA_ITEM, IMAGE_MEDIA_ITEM), seekTimeMs)) + .isEqualTo(sequenceTimestampsUs); + } + + @Test + public void seekToAfterEndOfSecondImage_afterPlayingSingleSequenceOfTwoImages() throws Exception { + long seekTimeMs = usToMs(3 * IMAGE_DURATION_US); + ImmutableList sequenceTimestampsUs = + new ImmutableList.Builder() + // Plays the first image + .addAll(IMAGE_TIMESTAMPS_US) + // Plays the second image + .addAll( + transform(IMAGE_TIMESTAMPS_US, timestampUs -> (IMAGE_DURATION_US + timestampUs))) + // Plays the last image frame, which is one microsecond smaller than the total duration. + .add(399_999L) + .build(); + + assertThat( + playSequenceUntilEndedAndSeekAndGetTimestampsUs( + ImmutableList.of(IMAGE_MEDIA_ITEM, IMAGE_MEDIA_ITEM), seekTimeMs)) + .isEqualTo(sequenceTimestampsUs); + } + @Test public void seekToZero_afterPlayingSingleSequenceOfVideoAndImage() throws Exception { maybeSkipTest();