From 404a259295ee7e4870edd337cf9b4ced3bda67b4 Mon Sep 17 00:00:00 2001 From: tofunmi Date: Wed, 23 Aug 2023 12:27:28 +0100 Subject: [PATCH] Remove queueInputBitmap by framerate and duration interfaces PiperOrigin-RevId: 559384011 --- RELEASENOTES.md | 4 + .../media3/common/VideoFrameProcessor.java | 20 ---- .../util/ConstantRateTimestampIterator.java | 9 ++ .../media3/common/util/TimestampIterator.java | 18 +-- ...deoFrameProcessorImageFrameOutputTest.java | 32 ++++- .../media3/effect/BitmapTextureManager.java | 113 +++++++++--------- .../effect/DefaultVideoFrameProcessor.java | 35 +----- .../media3/effect/TextureManager.java | 10 +- .../utils/VideoFrameProcessorTestRunner.java | 7 +- .../transformer/TransformerEndToEndTest.java | 4 +- .../media3/transformer/EditedMediaItem.java | 2 +- .../media3/transformer/ImageAssetLoader.java | 5 +- .../media3/transformer/SampleConsumer.java | 18 --- .../transformer/SequenceAssetLoader.java | 109 ++++++++--------- .../transformer/SingleInputVideoGraph.java | 6 - .../transformer/ImageAssetLoaderTest.java | 3 +- 16 files changed, 168 insertions(+), 227 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4c788ab74f..7d0ea338b9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ object request (`nor`) and next range request (`nrr`) ([#8699](https://github.com/google/ExoPlayer/issues/8699)). * Transformer: + * Changed `frameRate` and `durationUs` parameters of + `SampleConsumer.queueInputBitmap` to `TimestampIterator`. * Track Selection: * Extractors: * Audio: @@ -22,6 +24,8 @@ * Metadata: * DRM: * Effect: + * Changed `frameRate` and `durationUs` parameters of + `VideoFrameProcessor.queueInputBitmap` to `TimestampIterator`. * Muxers: * IMA extension: * Session: diff --git a/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java b/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java index ab42421743..7a63521ee0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/VideoFrameProcessor.java @@ -150,26 +150,6 @@ public interface VideoFrameProcessor { /** Indicates the frame should be dropped after {@link #renderOutputFrame(long)} is invoked. */ long DROP_OUTPUT_FRAME = -2; - /** - * Provides an input {@link Bitmap} to the {@link VideoFrameProcessor} to generate an input stream - * of frames. - * - *

Each call must be made after {@linkplain #registerInputStream registering a new input - * stream}. - * - *

Can be called on any thread. - * - * @param inputBitmap The {@link Bitmap} queued to the {@code VideoFrameProcessor}. - * @param durationUs The duration for which to display the {@code inputBitmap}, in microseconds. - * @param frameRate The frame rate at which to display the {@code inputBitmap}, in frames per - * second. - * @throws UnsupportedOperationException If the {@code VideoFrameProcessor} does not accept - * {@linkplain #INPUT_TYPE_BITMAP bitmap input}. - */ - // TODO(b/262693274): Delete this method and usages in favor of the one below (Note it is not - // deprecated because transformer still relies on this method for frame duplication). - void queueInputBitmap(Bitmap inputBitmap, long durationUs, float frameRate); - /** * Provides an input {@link Bitmap} to the {@link VideoFrameProcessor}. * 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 fda73e61b4..5bba78b939 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 @@ -30,6 +30,8 @@ import androidx.media3.common.C; @UnstableApi public final class ConstantRateTimestampIterator implements TimestampIterator { + private final long durationUs; + private final float frameRate; private final double framesDurationUs; private double currentTimestampUs; private int framesToAdd; @@ -45,6 +47,8 @@ public final class ConstantRateTimestampIterator implements TimestampIterator { @FloatRange(from = 0, fromInclusive = false) float frameRate) { checkArgument(durationUs > 0); checkArgument(frameRate > 0); + this.durationUs = durationUs; + this.frameRate = frameRate; framesToAdd = round(frameRate * (durationUs / (float) C.MICROS_PER_SECOND)); framesDurationUs = C.MICROS_PER_SECOND / frameRate; } @@ -62,4 +66,9 @@ public final class ConstantRateTimestampIterator implements TimestampIterator { currentTimestampUs += framesDurationUs; return next; } + + @Override + public ConstantRateTimestampIterator copyOf() { + return new ConstantRateTimestampIterator(durationUs, frameRate); + } } diff --git a/libraries/common/src/main/java/androidx/media3/common/util/TimestampIterator.java b/libraries/common/src/main/java/androidx/media3/common/util/TimestampIterator.java index 913dcc9c63..40ccbd875a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/TimestampIterator.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/TimestampIterator.java @@ -15,8 +15,6 @@ */ package androidx.media3.common.util; -import java.util.Iterator; - /** A primitive long iterator used for generating sequences of timestamps. */ @UnstableApi public interface TimestampIterator { @@ -27,18 +25,6 @@ public interface TimestampIterator { /** Returns the next timestamp. */ long next(); - /** Creates TimestampIterator */ - static TimestampIterator createFromLongIterator(Iterator iterator) { - return new TimestampIterator() { - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public long next() { - return iterator.next(); - } - }; - } + /** Returns fresh copy of the iterator. */ + TimestampIterator copyOf(); } diff --git a/libraries/effect/src/androidTest/java/androidx/media3/effect/DefaultVideoFrameProcessorImageFrameOutputTest.java b/libraries/effect/src/androidTest/java/androidx/media3/effect/DefaultVideoFrameProcessorImageFrameOutputTest.java index 408eaf62cb..12331fe3c8 100644 --- a/libraries/effect/src/androidTest/java/androidx/media3/effect/DefaultVideoFrameProcessorImageFrameOutputTest.java +++ b/libraries/effect/src/androidTest/java/androidx/media3/effect/DefaultVideoFrameProcessorImageFrameOutputTest.java @@ -16,7 +16,6 @@ package androidx.media3.effect; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.TimestampIterator.createFromLongIterator; import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap; import static com.google.common.truth.Truth.assertThat; @@ -24,9 +23,12 @@ import android.graphics.Bitmap; import android.util.Pair; import androidx.media3.common.C; import androidx.media3.common.ColorInfo; +import androidx.media3.common.util.TimestampIterator; import androidx.media3.test.utils.VideoFrameProcessorTestRunner; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.util.Iterator; +import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; @@ -210,14 +212,36 @@ public class DefaultVideoFrameProcessorImageFrameOutputTest { videoFrameProcessorTestRunner.queueInputBitmaps( bitmap1.getWidth(), bitmap1.getHeight(), - Pair.create(bitmap1, createFromLongIterator(ImmutableList.of(offset1).iterator())), - Pair.create( - bitmap2, createFromLongIterator(ImmutableList.of(offset2, offset3).iterator()))); + Pair.create(bitmap1, createTimestampIterator(ImmutableList.of(offset1))), + Pair.create(bitmap2, createTimestampIterator(ImmutableList.of(offset2, offset3)))); videoFrameProcessorTestRunner.endFrameProcessing(); assertThat(actualPresentationTimesUs).containsExactly(offset1, offset2, offset3).inOrder(); } + private static TimestampIterator createTimestampIterator(List elements) { + + Iterator elementsIterator = elements.iterator(); + + return new TimestampIterator() { + @Override + public boolean hasNext() { + return elementsIterator.hasNext(); + } + + @Override + public long next() { + return elementsIterator.next(); + } + + @Override + public TimestampIterator copyOf() { + // Method not needed for effects tests. + throw new UnsupportedOperationException(); + } + }; + } + private VideoFrameProcessorTestRunner.Builder getDefaultFrameProcessorTestRunnerBuilder( String testId) { return new VideoFrameProcessorTestRunner.Builder() diff --git a/libraries/effect/src/main/java/androidx/media3/effect/BitmapTextureManager.java b/libraries/effect/src/main/java/androidx/media3/effect/BitmapTextureManager.java index 83dce85b97..a9b8141504 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/BitmapTextureManager.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/BitmapTextureManager.java @@ -18,7 +18,6 @@ package androidx.media3.effect; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; -import static java.lang.Math.round; import android.graphics.Bitmap; import android.opengl.GLES20; @@ -30,6 +29,7 @@ import androidx.media3.common.GlObjectsProvider; import androidx.media3.common.GlTextureInfo; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.util.GlUtil; +import androidx.media3.common.util.TimestampIterator; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import java.util.Queue; @@ -57,10 +57,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private @MonotonicNonNull GlTextureInfo currentGlTextureInfo; private int downstreamShaderProgramCapacity; - private int framesToQueueForCurrentBitmap; - private double currentPresentationTimeUs; private boolean useHdr; private boolean currentInputStreamEnded; + private boolean isNextFrameInTexture; /** * Creates a new instance. @@ -92,10 +91,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void queueInputBitmap( - Bitmap inputBitmap, long durationUs, FrameInfo frameInfo, float frameRate, boolean useHdr) { + Bitmap inputBitmap, + FrameInfo frameInfo, + TimestampIterator inStreamOffsetsUs, + boolean useHdr) { videoFrameProcessingTaskExecutor.submit( () -> { - setupBitmap(inputBitmap, durationUs, frameInfo, frameRate, useHdr); + setupBitmap(inputBitmap, frameInfo, inStreamOffsetsUs, useHdr); currentInputStreamEnded = false; }); } @@ -110,7 +112,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public void signalEndOfCurrentInputStream() { videoFrameProcessingTaskExecutor.submit( () -> { - if (framesToQueueForCurrentBitmap == 0 && pendingBitmaps.isEmpty()) { + if (pendingBitmaps.isEmpty()) { shaderProgram.signalEndOfCurrentInputStream(); DebugTraceUtil.logEvent( DebugTraceUtil.EVENT_BITMAP_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE); @@ -137,7 +139,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Methods that must be called on the GL thread. private void setupBitmap( - Bitmap bitmap, long durationUs, FrameInfo frameInfo, float frameRate, boolean useHdr) + Bitmap bitmap, FrameInfo frameInfo, TimestampIterator inStreamOffsetsUs, boolean useHdr) throws VideoFrameProcessingException { if (Util.SDK_INT >= 26) { checkState( @@ -147,17 +149,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; checkState( !bitmap.getConfig().equals(Bitmap.Config.RGBA_1010102), UNSUPPORTED_IMAGE_CONFIGURATION); } - this.useHdr = useHdr; - // TODO(b/262693274): move frame duplication logic out of the texture manager. Note this will - // involve removing the BitmapFrameSequenceInfo queue and using the FrameConsumptionManager - // instead. It will also remove the framesToAdd variable - int framesToAdd = round(frameRate * (durationUs / (float) C.MICROS_PER_SECOND)); - // framestoAdd > 0 otherwise the VFP will hang. - checkArgument(framesToAdd > 0); - double frameDurationUs = C.MICROS_PER_SECOND / frameRate; - pendingBitmaps.add( - new BitmapFrameSequenceInfo(bitmap, frameInfo, frameDurationUs, framesToAdd)); + checkArgument(inStreamOffsetsUs.hasNext(), "Bitmap queued but no timestamps provided."); + pendingBitmaps.add(new BitmapFrameSequenceInfo(bitmap, frameInfo, inStreamOffsetsUs)); maybeQueueToShaderProgram(); } @@ -167,51 +161,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } BitmapFrameSequenceInfo currentBitmapInfo = checkNotNull(pendingBitmaps.peek()); - if (framesToQueueForCurrentBitmap == 0) { - Bitmap bitmap = currentBitmapInfo.bitmap; - framesToQueueForCurrentBitmap = currentBitmapInfo.numberOfFrames; - currentPresentationTimeUs = currentBitmapInfo.frameInfo.offsetToAddUs; - int currentTexId; - try { - if (currentGlTextureInfo != null) { - currentGlTextureInfo.release(); - } - currentTexId = - GlUtil.createTexture( - currentBitmapInfo.frameInfo.width, - currentBitmapInfo.frameInfo.height, - /* useHighPrecisionColorComponents= */ useHdr); - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, currentTexId); - GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0); - GlUtil.checkGlError(); - } catch (GlUtil.GlException e) { - throw VideoFrameProcessingException.from(e); - } - - currentGlTextureInfo = - new GlTextureInfo( - currentTexId, - /* fboId= */ C.INDEX_UNSET, - /* rboId= */ C.INDEX_UNSET, - currentBitmapInfo.frameInfo.width, - currentBitmapInfo.frameInfo.height); + FrameInfo currentFrameInfo = currentBitmapInfo.frameInfo; + TimestampIterator inStreamOffsetsUs = currentBitmapInfo.inStreamOffsetsUs; + checkState(currentBitmapInfo.inStreamOffsetsUs.hasNext()); + long currentPresentationTimeUs = + currentBitmapInfo.frameInfo.offsetToAddUs + inStreamOffsetsUs.next(); + if (!isNextFrameInTexture) { + isNextFrameInTexture = true; + updateCurrentGlTextureInfo(currentFrameInfo, currentBitmapInfo.bitmap); } - framesToQueueForCurrentBitmap--; downstreamShaderProgramCapacity--; shaderProgram.queueInputFrame( - glObjectsProvider, checkNotNull(currentGlTextureInfo), round(currentPresentationTimeUs)); + glObjectsProvider, checkNotNull(currentGlTextureInfo), currentPresentationTimeUs); DebugTraceUtil.logEvent( DebugTraceUtil.EVENT_VFP_QUEUE_BITMAP, - (long) currentPresentationTimeUs, - /* extra= */ currentBitmapInfo.frameInfo.width + "x" + currentBitmapInfo.frameInfo.height); - currentPresentationTimeUs += currentBitmapInfo.frameDurationUs; + currentPresentationTimeUs, + /* extra= */ currentFrameInfo.width + "x" + currentFrameInfo.height); - if (framesToQueueForCurrentBitmap == 0) { + if (!currentBitmapInfo.inStreamOffsetsUs.hasNext()) { + isNextFrameInTexture = false; pendingBitmaps.remove(); if (pendingBitmaps.isEmpty() && currentInputStreamEnded) { // Only signal end of stream after all pending bitmaps are processed. - // TODO(b/269424561): Call signalEndOfCurrentInputStream on every bitmap shaderProgram.signalEndOfCurrentInputStream(); DebugTraceUtil.logEvent( DebugTraceUtil.EVENT_BITMAP_TEXTURE_MANAGER_SIGNAL_EOS, C.TIME_END_OF_SOURCE); @@ -220,19 +192,42 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - /** Information to generate all the frames associated with a specific {@link Bitmap}. */ + /** Information needed to generate all the frames associated with a specific {@link Bitmap}. */ private static final class BitmapFrameSequenceInfo { public final Bitmap bitmap; - public final FrameInfo frameInfo; - public final double frameDurationUs; - public final int numberOfFrames; + private final FrameInfo frameInfo; + private final TimestampIterator inStreamOffsetsUs; public BitmapFrameSequenceInfo( - Bitmap bitmap, FrameInfo frameInfo, double frameDurationUs, int numberOfFrames) { + Bitmap bitmap, FrameInfo frameInfo, TimestampIterator inStreamOffsetsUs) { this.bitmap = bitmap; this.frameInfo = frameInfo; - this.frameDurationUs = frameDurationUs; - this.numberOfFrames = numberOfFrames; + this.inStreamOffsetsUs = inStreamOffsetsUs; } } + + private void updateCurrentGlTextureInfo(FrameInfo frameInfo, Bitmap bitmap) + throws VideoFrameProcessingException { + int currentTexId; + try { + if (currentGlTextureInfo != null) { + currentGlTextureInfo.release(); + } + currentTexId = + GlUtil.createTexture( + frameInfo.width, frameInfo.height, /* useHighPrecisionColorComponents= */ useHdr); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, currentTexId); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0); + GlUtil.checkGlError(); + } catch (GlUtil.GlException e) { + throw VideoFrameProcessingException.from(e); + } + currentGlTextureInfo = + new GlTextureInfo( + currentTexId, + /* fboId= */ C.INDEX_UNSET, + /* rboId= */ C.INDEX_UNSET, + frameInfo.width, + frameInfo.height); + } } diff --git a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java index 2d3e87b40d..c5bd3b897b 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java @@ -349,7 +349,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { private volatile @MonotonicNonNull CountDownLatch latch; private volatile @MonotonicNonNull FrameInfo nextInputFrameInfo; private volatile boolean inputStreamEnded; - private volatile boolean hasRefreshedNextInputFrameInfo; private DefaultVideoFrameProcessor( Context context, @@ -422,39 +421,15 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { } @Override - public void queueInputBitmap(Bitmap inputBitmap, long durationUs, float frameRate) { - checkState( - hasRefreshedNextInputFrameInfo, - "registerInputStream must be called before queueing another bitmap"); + public void queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { + FrameInfo frameInfo = checkNotNull(this.nextInputFrameInfo); inputSwitcher .activeTextureManager() .queueInputBitmap( inputBitmap, - durationUs, - checkNotNull(nextInputFrameInfo), - frameRate, + new FrameInfo.Builder(frameInfo).setOffsetToAddUs(frameInfo.offsetToAddUs).build(), + inStreamOffsetsUs, /* useHdr= */ false); - hasRefreshedNextInputFrameInfo = false; - } - - @Override - public void queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { - FrameInfo frameInfo = checkNotNull(this.nextInputFrameInfo); - // TODO(b/262693274): move frame duplication logic out of the texture manager so - // textureManager.queueInputBitmap() frame rate and duration parameters be removed. - while (inStreamOffsetsUs.hasNext()) { - long inStreamOffsetUs = inStreamOffsetsUs.next(); - inputSwitcher - .activeTextureManager() - .queueInputBitmap( - inputBitmap, - /* durationUs= */ C.MICROS_PER_SECOND, - new FrameInfo.Builder(frameInfo) - .setOffsetToAddUs(frameInfo.offsetToAddUs + inStreamOffsetUs) - .build(), - /* frameRate= */ 1, - /* useHdr= */ false); - } } @Override @@ -482,7 +457,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { "InputType %s - %dx%d", getInputTypeString(inputType), frameInfo.width, frameInfo.height)); nextInputFrameInfo = adjustForPixelWidthHeightRatio(frameInfo); - hasRefreshedNextInputFrameInfo = true; synchronized (lock) { if (!processingInput) { videoFrameProcessingTaskExecutor.submitAndBlock(() -> configureEffects(effects)); @@ -524,7 +498,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { nextInputFrameInfo, "registerInputStream must be called before registering input frames"); inputSwitcher.activeTextureManager().registerInputFrame(nextInputFrameInfo); - hasRefreshedNextInputFrameInfo = false; } @Override diff --git a/libraries/effect/src/main/java/androidx/media3/effect/TextureManager.java b/libraries/effect/src/main/java/androidx/media3/effect/TextureManager.java index 9c5f6bfeb9..3bfee8aedd 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/TextureManager.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/TextureManager.java @@ -25,6 +25,7 @@ import androidx.media3.common.FrameInfo; import androidx.media3.common.OnInputFrameProcessedListener; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.VideoFrameProcessor; +import androidx.media3.common.util.TimestampIterator; /** Handles {@code DefaultVideoFrameProcessor}'s input. */ /* package */ interface TextureManager extends GlShaderProgram.InputListener { @@ -42,13 +43,16 @@ import androidx.media3.common.VideoFrameProcessor; * Provides an input {@link Bitmap} to put into the video frames. * * @param inputBitmap The {@link Bitmap} queued to the {@code VideoFrameProcessor}. - * @param durationUs The duration of the bitmap in the composition, in microseconds. * @param frameInfo Information about the bitmap being queued. - * @param frameRate The rate at which to generate frames with the bitmap, in frames per second. + * @param inStreamOffsetsUs The times within the current stream that the bitmap should be shown + * at. The timestamps should be monotonically increasing. * @param useHdr Whether input and/or output colors are HDR. */ default void queueInputBitmap( - Bitmap inputBitmap, long durationUs, FrameInfo frameInfo, float frameRate, boolean useHdr) { + Bitmap inputBitmap, + FrameInfo frameInfo, + TimestampIterator inStreamOffsetsUs, + boolean useHdr) { throw new UnsupportedOperationException(); } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java index 5f67c2c4e7..92b05fe6fc 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/VideoFrameProcessorTestRunner.java @@ -44,6 +44,7 @@ import androidx.media3.common.GlTextureInfo; import androidx.media3.common.SurfaceInfo; import androidx.media3.common.VideoFrameProcessingException; import androidx.media3.common.VideoFrameProcessor; +import androidx.media3.common.util.ConstantRateTimestampIterator; import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.TimestampIterator; import androidx.media3.common.util.UnstableApi; @@ -254,8 +255,7 @@ public final class VideoFrameProcessorTestRunner { private final AtomicReference videoFrameProcessingException; private final VideoFrameProcessor videoFrameProcessor; private final ImmutableList effects; - - private @MonotonicNonNull BitmapReader bitmapReader; + private final @MonotonicNonNull BitmapReader bitmapReader; private VideoFrameProcessorTestRunner( String testId, @@ -356,7 +356,8 @@ public final class VideoFrameProcessorTestRunner { .setPixelWidthHeightRatio(pixelWidthHeightRatio) .setOffsetToAddUs(offsetToAddUs) .build()); - videoFrameProcessor.queueInputBitmap(inputBitmap, durationUs, frameRate); + videoFrameProcessor.queueInputBitmap( + inputBitmap, new ConstantRateTimestampIterator(durationUs, frameRate)); } public void queueInputBitmaps(int width, int height, Pair... frames) { diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index 5912440e30..4e5c615026 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -622,8 +622,8 @@ public class TransformerEndToEndTest { assertThat(result.exportResult.processedInputs).hasSize(7); assertThat(result.exportResult.channelCount).isEqualTo(1); - assertThat(result.exportResult.videoFrameCount).isEqualTo(94); - assertThat(result.exportResult.durationMs).isEqualTo(3100); + assertThat(result.exportResult.durationMs).isEqualTo(3133); + assertThat(result.exportResult.videoFrameCount).isEqualTo(95); } @Test diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java index d40102e4c5..28fcd6808a 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EditedMediaItem.java @@ -216,7 +216,7 @@ public final class EditedMediaItem { public final long durationUs; /** The frame rate of the image in the output video, in frames per second. */ - @IntRange(from = 0) + @IntRange(from = 1) public final int frameRate; /** The {@link Effects} to apply to the {@link #mediaItem}. */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java index 4ad4a57bcf..7c86ac5d36 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java @@ -36,6 +36,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.BitmapLoader; +import androidx.media3.common.util.ConstantRateTimestampIterator; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; @@ -178,7 +179,9 @@ public final class ImageAssetLoader implements AssetLoader { // callback rather than setting duration here. if (sampleConsumer == null || !sampleConsumer.queueInputBitmap( - bitmap, editedMediaItem.durationUs, editedMediaItem.frameRate)) { + bitmap, + new ConstantRateTimestampIterator( + editedMediaItem.durationUs, editedMediaItem.frameRate))) { scheduledExecutorService.schedule( () -> queueBitmapInternal(bitmap, format), QUEUE_BITMAP_INTERVAL_MS, MILLISECONDS); return; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SampleConsumer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SampleConsumer.java index c6df629523..a1358da33d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SampleConsumer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SampleConsumer.java @@ -66,24 +66,6 @@ public interface SampleConsumer { throw new UnsupportedOperationException(); } - /** - * Attempts to provide an input {@link Bitmap} to the consumer. - * - *

Should only be used for image data. - * - * @param inputBitmap The {@link Bitmap} to queue to the consumer. - * @param durationUs The duration for which to display the {@code inputBitmap}, in microseconds. - * @param frameRate The frame rate at which to display the {@code inputBitmap}, in frames per - * second. - * @return Whether the {@link Bitmap} was successfully queued. If {@code false}, the caller should - * try again later. - */ - // TODO(b/262693274): Delete this method and usages in favor of the one below (Note it is not - // deprecated because transformer still relies on this method for frame duplication). - default boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) { - throw new UnsupportedOperationException(); - } - /** * Attempts to provide an input {@link Bitmap} to the consumer. * diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceAssetLoader.java index e9a7a92824..f8c4c09edc 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceAssetLoader.java @@ -348,37 +348,6 @@ import java.util.concurrent.atomic.AtomicInteger; sequenceAssetLoaderListener.onError(exportException); } - /** - * Wraps a {@link TimestampIterator}, providing all the values in the original timestamp iterator - * (in the same order) up to and including the first occurrence of the {@code clippingValue}. - */ - private static final class ClippingIterator implements TimestampIterator { - - private final TimestampIterator iterator; - private final long clippingValue; - private boolean hasReachedClippingValue; - - public ClippingIterator(TimestampIterator iterator, long clippingValue) { - this.iterator = iterator; - this.clippingValue = clippingValue; - } - - @Override - public boolean hasNext() { - return !hasReachedClippingValue && iterator.hasNext(); - } - - @Override - public long next() { - checkState(hasNext()); - long next = iterator.next(); - if (clippingValue == next) { - hasReachedClippingValue = true; - } - return next; - } - } - // Classes accessed from AssetLoader threads. private final class SampleConsumerWrapper implements SampleConsumer { @@ -428,51 +397,31 @@ import java.util.concurrent.atomic.AtomicInteger; return true; } - @Override - public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) { - if (isLooping && totalDurationUs + durationUs > maxSequenceDurationUs) { - if (!isMaxSequenceDurationUsFinal) { - return false; - } - durationUs = maxSequenceDurationUs - totalDurationUs; - if (durationUs == 0) { - if (!videoLoopingEnded) { - videoLoopingEnded = true; - signalEndOfVideoInput(); - } - return false; - } - videoLoopingEnded = true; - } - - return sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate); - } - @Override public boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { - TimestampIterator iteratorToUse = inStreamOffsetsUs; if (isLooping) { - long durationLeftUs = maxSequenceDurationUs - totalDurationUs; - if (durationLeftUs <= 0) { - if (!videoLoopingEnded) { - videoLoopingEnded = true; - signalEndOfVideoInput(); - } - return false; - } + long lastOffsetUs = C.TIME_UNSET; while (inStreamOffsetsUs.hasNext()) { long offsetUs = inStreamOffsetsUs.next(); if (totalDurationUs + offsetUs > maxSequenceDurationUs) { if (!isMaxSequenceDurationUsFinal) { return false; } - iteratorToUse = new ClippingIterator(inStreamOffsetsUs, offsetUs); + if (lastOffsetUs == C.TIME_UNSET) { + if (!videoLoopingEnded) { + videoLoopingEnded = true; + signalEndOfVideoInput(); + } + return false; + } + inStreamOffsetsUs = new ClippingIterator(inStreamOffsetsUs.copyOf(), lastOffsetUs); videoLoopingEnded = true; break; } + lastOffsetUs = offsetUs; } } - return sampleConsumer.queueInputBitmap(inputBitmap, iteratorToUse); + return sampleConsumer.queueInputBitmap(inputBitmap, inStreamOffsetsUs.copyOf()); } @Override @@ -564,4 +513,40 @@ import java.util.concurrent.atomic.AtomicInteger; }); } } + + /** + * Wraps a {@link TimestampIterator}, providing all the values in the original timestamp iterator + * (in the same order) up to and including the first occurrence of the {@code clippingValue}. + */ + private static final class ClippingIterator implements TimestampIterator { + + private final TimestampIterator iterator; + private final long clippingValue; + private boolean hasReachedClippingValue; + + public ClippingIterator(TimestampIterator iterator, long clippingValue) { + this.iterator = iterator; + this.clippingValue = clippingValue; + } + + @Override + public boolean hasNext() { + return !hasReachedClippingValue && iterator.hasNext(); + } + + @Override + public long next() { + checkState(hasNext()); + long next = iterator.next(); + if (clippingValue <= next) { + hasReachedClippingValue = true; + } + return next; + } + + @Override + public TimestampIterator copyOf() { + return new ClippingIterator(iterator.copyOf(), clippingValue); + } + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SingleInputVideoGraph.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SingleInputVideoGraph.java index f21460f74a..3c436fca53 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SingleInputVideoGraph.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SingleInputVideoGraph.java @@ -168,12 +168,6 @@ import java.util.concurrent.atomic.AtomicLong; mediaItemOffsetUs.addAndGet(durationUs); } - @Override - public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) { - videoFrameProcessor.queueInputBitmap(inputBitmap, durationUs, frameRate); - return true; - } - @Override public boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { videoFrameProcessor.queueInputBitmap(inputBitmap, inStreamOffsetsUs); diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/ImageAssetLoaderTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/ImageAssetLoaderTest.java index 7a8948cdf7..7a690e33cc 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/ImageAssetLoaderTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/ImageAssetLoaderTest.java @@ -22,6 +22,7 @@ import android.graphics.Bitmap; import android.os.Looper; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; +import androidx.media3.common.util.TimestampIterator; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.time.Duration; @@ -126,7 +127,7 @@ public class ImageAssetLoaderTest { private static final class FakeSampleConsumer implements SampleConsumer { @Override - public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) { + public boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { return true; }