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 05511fc03c..a78b92ed62 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SampleConsumer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SampleConsumer.java @@ -22,6 +22,7 @@ import androidx.media3.common.ColorInfo; import androidx.media3.common.OnInputFrameProcessedListener; import androidx.media3.common.util.UnstableApi; import androidx.media3.decoder.DecoderInputBuffer; +import java.util.Iterator; /** Consumer of encoded media samples, raw audio or raw video frames. */ @UnstableApi @@ -77,10 +78,25 @@ public interface SampleConsumer { * @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. + * + *

Should only be used for image data. + * + * @param inputBitmap The {@link Bitmap} to queue to the consumer. + * @param inStreamOffsetsUs The times within the current stream that the bitmap should be + * displayed at. The timestamps should be monotonically increasing. + */ + default boolean queueInputBitmap(Bitmap inputBitmap, Iterator inStreamOffsetsUs) { + throw new UnsupportedOperationException(); + } + // Methods to pass raw video input. /** 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 fb31d56d18..5342087c92 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceAssetLoader.java @@ -39,6 +39,7 @@ import androidx.media3.decoder.DecoderInputBuffer; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -347,6 +348,38 @@ import java.util.concurrent.atomic.AtomicInteger; sequenceAssetLoaderListener.onError(exportException); } + /** + * Given an {@link Iterator}, creates an iterator that includes all the values in the original + * iterator (in the same order) up to and including the first occurrence of the {@code + * clippingValue}. + */ + private static final class ClippingIterator implements Iterator { + + private final Iterator iterator; + private final long clippingValue; + private boolean hasReachedClippingValue; + + public ClippingIterator(Iterator 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 { @@ -396,8 +429,6 @@ import java.util.concurrent.atomic.AtomicInteger; return true; } - // TODO(b/262693274): Test that concatenate 2 images or an image and a video works as expected - // once ImageAssetLoader implementation is complete. @Override public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) { if (isLooping && totalDurationUs + durationUs > maxSequenceDurationUs) { @@ -418,6 +449,33 @@ import java.util.concurrent.atomic.AtomicInteger; return sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate); } + @Override + public boolean queueInputBitmap(Bitmap inputBitmap, Iterator inStreamOffsetsUs) { + Iterator iteratorToUse = inStreamOffsetsUs; + if (isLooping) { + long durationLeftUs = maxSequenceDurationUs - totalDurationUs; + if (durationLeftUs <= 0) { + if (!videoLoopingEnded) { + videoLoopingEnded = true; + signalEndOfVideoInput(); + } + return false; + } + while (inStreamOffsetsUs.hasNext()) { + long offsetUs = inStreamOffsetsUs.next(); + if (totalDurationUs + offsetUs > maxSequenceDurationUs) { + if (!isMaxSequenceDurationUsFinal) { + return false; + } + iteratorToUse = new ClippingIterator(inStreamOffsetsUs, offsetUs); + videoLoopingEnded = true; + break; + } + } + } + return sampleConsumer.queueInputBitmap(inputBitmap, iteratorToUse); + } + @Override public void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener) { sampleConsumer.setOnInputFrameProcessedListener(listener); 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 ac15d05570..8c781df6e6 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SingleInputVideoGraph.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SingleInputVideoGraph.java @@ -39,6 +39,7 @@ import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Size; import androidx.media3.effect.Presentation; import com.google.common.collect.ImmutableList; +import java.util.Iterator; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; @@ -173,6 +174,12 @@ import java.util.concurrent.atomic.AtomicLong; return true; } + @Override + public boolean queueInputBitmap(Bitmap inputBitmap, Iterator inStreamOffsetsUs) { + videoFrameProcessor.queueInputBitmap(inputBitmap, inStreamOffsetsUs); + return true; + } + @Override public void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener) { videoFrameProcessor.setOnInputFrameProcessedListener(listener);