Add a way in the SampleConsumer to indicate that it couldn't consume

- To support looping EditedMediaItemSequences, we need a way to tell the
AssetLoader that a sample couldn't be consumed and that it should retry
later. This is necessary in case we don't know yet whether the looping
sequence should load more samples because the other sequences haven't
made sufficient progress yet.
- The decision on whether to consume a sample is based on its timestamp
so it needs to be available.

PiperOrigin-RevId: 516546026
This commit is contained in:
kimvde 2023-03-14 16:25:28 +00:00 committed by tonihei
parent f29da34dc4
commit df724b517e
11 changed files with 108 additions and 69 deletions

View File

@ -170,9 +170,10 @@ import org.checkerframework.dataflow.qual.Pure;
}
@Override
public void queueInputBuffer() {
public boolean queueInputBuffer() {
DecoderInputBuffer inputBuffer = availableInputBuffers.remove();
pendingInputBuffers.add(inputBuffer);
return true;
}
@Override

View File

@ -78,7 +78,7 @@ import java.util.concurrent.atomic.AtomicLong;
}
@Override
public void queueInputBuffer() {
public boolean queueInputBuffer() {
DecoderInputBuffer inputBuffer = availableInputBuffers.remove();
if (inputBuffer.isEndOfStream()) {
inputEnded = true;
@ -86,6 +86,7 @@ import java.util.concurrent.atomic.AtomicLong;
inputBuffer.timeUs += mediaItemOffsetUs;
pendingInputBuffers.add(inputBuffer);
}
return true;
}
@Override

View File

@ -63,26 +63,28 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return false;
}
if (decoder.isEnded()) {
checkNotNull(sampleConsumerInputBuffer.data).limit(0);
sampleConsumerInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
sampleConsumer.queueInputBuffer();
isEnded = true;
return false;
ByteBuffer sampleConsumerInputData = checkNotNull(sampleConsumerInputBuffer.data);
if (sampleConsumerInputData.position() == 0) {
if (decoder.isEnded()) {
sampleConsumerInputData.limit(0);
sampleConsumerInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
isEnded = sampleConsumer.queueInputBuffer();
return false;
}
ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer();
if (decoderOutputBuffer == null) {
return false;
}
sampleConsumerInputBuffer.ensureSpaceForWrite(decoderOutputBuffer.limit());
sampleConsumerInputBuffer.data.put(decoderOutputBuffer).flip();
MediaCodec.BufferInfo bufferInfo = checkNotNull(decoder.getOutputBufferInfo());
sampleConsumerInputBuffer.timeUs = bufferInfo.presentationTimeUs;
sampleConsumerInputBuffer.setFlags(bufferInfo.flags);
decoder.releaseOutputBuffer(/* render= */ false);
}
ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer();
if (decoderOutputBuffer == null) {
return false;
}
sampleConsumerInputBuffer.ensureSpaceForWrite(decoderOutputBuffer.limit());
sampleConsumerInputBuffer.data.put(decoderOutputBuffer).flip();
MediaCodec.BufferInfo bufferInfo = checkNotNull(decoder.getOutputBufferInfo());
sampleConsumerInputBuffer.timeUs = bufferInfo.presentationTimeUs;
sampleConsumerInputBuffer.setFlags(bufferInfo.flags);
decoder.releaseOutputBuffer(/* render= */ false);
sampleConsumer.queueInputBuffer();
return true;
return sampleConsumer.queueInputBuffer();
}
}

View File

@ -309,16 +309,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return false;
}
if (!readInput(sampleConsumerInputBuffer)) {
if (checkNotNull(sampleConsumerInputBuffer.data).position() == 0
&& !sampleConsumerInputBuffer.isEndOfStream()) {
if (!readInput(sampleConsumerInputBuffer)) {
return false;
}
if (shouldDropInputBuffer(sampleConsumerInputBuffer)) {
return true;
}
}
boolean isInputEnded = sampleConsumerInputBuffer.isEndOfStream();
if (!sampleConsumer.queueInputBuffer()) {
return false;
}
if (shouldDropInputBuffer(sampleConsumerInputBuffer)) {
return true;
}
isEnded = sampleConsumerInputBuffer.isEndOfStream();
sampleConsumer.queueInputBuffer();
isEnded = isInputEnded;
return !isEnded;
}

View File

@ -142,7 +142,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return false;
}
sampleConsumer.registerVideoFrame();
if (!sampleConsumer.registerVideoFrame(decoderOutputBufferInfo.presentationTimeUs)) {
return false;
}
decoder.releaseOutputBuffer(/* render= */ true);
return true;
}

View File

@ -72,6 +72,7 @@ public final class ImageAssetLoader implements AssetLoader {
private final Listener listener;
private final ScheduledExecutorService scheduledExecutorService;
@Nullable private SampleConsumer sampleConsumer;
private @Transformer.ProgressState int progressState;
private volatile int progress;
@ -87,6 +88,8 @@ public final class ImageAssetLoader implements AssetLoader {
}
@Override
// Ignore Future returned by scheduledExecutorService because failures are already handled in the
// runnable.
@SuppressWarnings("FutureReturnValueIgnored")
public void start() {
progressState = PROGRESS_STATE_AVAILABLE;
@ -149,19 +152,23 @@ public final class ImageAssetLoader implements AssetLoader {
scheduledExecutorService.shutdownNow();
}
// Ignore Future returned by scheduledExecutorService because failures are already handled in the
// runnable.
@SuppressWarnings("FutureReturnValueIgnored")
private void queueBitmapInternal(Bitmap bitmap, Format format) {
try {
@Nullable SampleConsumer sampleConsumer = listener.onOutputFormat(format);
if (sampleConsumer == null) {
sampleConsumer = listener.onOutputFormat(format);
}
// TODO(b/262693274): consider using listener.onDurationUs() or the MediaItem change
// callback rather than setting duration here.
if (sampleConsumer == null
|| !sampleConsumer.queueInputBitmap(
bitmap, editedMediaItem.durationUs, editedMediaItem.frameRate)) {
scheduledExecutorService.schedule(
() -> queueBitmapInternal(bitmap, format), QUEUE_BITMAP_INTERVAL_MS, MILLISECONDS);
return;
}
// TODO(b/262693274): consider using listener.onDurationUs() or the MediaItem change
// callback rather than setting duration here.
sampleConsumer.queueInputBitmap(
bitmap, editedMediaItem.durationUs, editedMediaItem.frameRate);
sampleConsumer.signalEndOfVideoInput();
progress = 100;
} catch (ExportException e) {

View File

@ -25,11 +25,18 @@ import com.google.android.exoplayer2.video.ColorInfo;
public interface SampleConsumer {
/**
* Returns a buffer if the consumer is ready to accept input, and {@code null} otherwise.
* Returns a {@link DecoderInputBuffer}, if available.
*
* <p>If the consumer is ready to accept input and this method is called multiple times before
* {@linkplain #queueInputBuffer() queuing} input, the same {@link DecoderInputBuffer} instance is
* returned.
* <p>This {@linkplain DecoderInputBuffer buffer} should be filled with new input data and
* {@linkplain #queueInputBuffer() queued} to the consumer.
*
* <p>If this method returns a non-null buffer:
*
* <ul>
* <li>The buffer's {@linkplain DecoderInputBuffer#data data} is non-null.
* <li>The same buffer instance is returned if this method is called multiple times before
* {@linkplain #queueInputBuffer() queuing} input.
* </ul>
*
* <p>Should only be used for compressed data and raw audio data.
*/
@ -39,30 +46,35 @@ public interface SampleConsumer {
}
/**
* Informs the consumer that its input buffer contains new input.
* Attempts to queue new input to the consumer.
*
* <p>Should be called after filling the input buffer from {@link #getInputBuffer()} with new
* input.
* <p>The input buffer from {@link #getInputBuffer()} should be filled with the new input before
* calling this method.
*
* <p>An input buffer should not be used anymore after it has been queued.
* <p>An input buffer should not be used anymore after it has been successfully queued.
*
* <p>Should only be used for compressed data and raw audio data.
*
* @return Whether the input was successfully queued. If {@code false}, the caller should try
* again later.
*/
default void queueInputBuffer() {
default boolean queueInputBuffer() {
throw new UnsupportedOperationException();
}
/**
* Provides an input {@link Bitmap} to the consumer.
* Attempts to provide an input {@link Bitmap} to the consumer.
*
* <p>Should only be used for image data.
*
* @param inputBitmap The {@link Bitmap} queued to the consumer.
* @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.
*/
default void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
default boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
throw new UnsupportedOperationException();
}
@ -88,8 +100,8 @@ public interface SampleConsumer {
/**
* Returns the number of input video frames pending in the consumer. Pending input frames are
* frames that have been {@linkplain #registerVideoFrame() registered} but not processed off the
* {@linkplain #getInputSurface() input surface} yet.
* frames that have been {@linkplain #registerVideoFrame(long) registered} but not processed off
* the {@linkplain #getInputSurface() input surface} yet.
*
* <p>Should only be used for raw video data.
*/
@ -98,14 +110,18 @@ public interface SampleConsumer {
}
/**
* Informs the consumer that a frame will be queued to the {@linkplain #getInputSurface() input
* surface}.
* Attempts to register a video frame to the consumer.
*
* <p>Must be called before rendering a frame to the input surface.
* <p>Each frame to consume should be registered using this method. After a frame is successfully
* registered, it should be rendered to the {@linkplain #getInputSurface() input surface}.
*
* <p>Should only be used for raw video data.
*
* @param presentationTimeUs The presentation time of the frame to register, in microseconds.
* @return Whether the frame was successfully registered. If {@code false}, the caller should try
* again later.
*/
default void registerVideoFrame() {
default boolean registerVideoFrame(long presentationTimeUs) {
throw new UnsupportedOperationException();
}

View File

@ -329,34 +329,31 @@ import java.util.concurrent.atomic.AtomicInteger;
@Nullable
@Override
public DecoderInputBuffer getInputBuffer() {
DecoderInputBuffer inputBuffer = sampleConsumer.getInputBuffer();
if (inputBuffer != null && inputBuffer.isEndOfStream()) {
inputBuffer.clear();
inputBuffer.timeUs = 0;
}
return inputBuffer;
return sampleConsumer.getInputBuffer();
}
@Override
public void queueInputBuffer() {
public boolean queueInputBuffer() {
DecoderInputBuffer inputBuffer = checkStateNotNull(sampleConsumer.getInputBuffer());
if (inputBuffer.isEndOfStream()) {
nonEndedTracks.decrementAndGet();
if (currentMediaItemIndex.get() < editedMediaItems.size() - 1) {
inputBuffer.clear();
inputBuffer.timeUs = 0;
if (nonEndedTracks.get() == 0) {
switchAssetLoader();
}
return;
return true;
}
}
sampleConsumer.queueInputBuffer();
return sampleConsumer.queueInputBuffer();
}
// TODO(b/262693274): Test that concatenate 2 images or an image and a video works as expected
// once ImageAssetLoader implementation is complete.
@Override
public void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate);
public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
return sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate);
}
@Override
@ -375,8 +372,8 @@ import java.util.concurrent.atomic.AtomicInteger;
}
@Override
public void registerVideoFrame() {
sampleConsumer.registerVideoFrame();
public boolean registerVideoFrame(long presentationTimeUs) {
return sampleConsumer.registerVideoFrame(presentationTimeUs);
}
@Override

View File

@ -206,8 +206,9 @@ import org.checkerframework.dataflow.qual.Pure;
}
@Override
public void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
videoFrameProcessor.queueInputBitmap(inputBitmap, durationUs, frameRate);
return true;
}
@Override
@ -221,8 +222,9 @@ import org.checkerframework.dataflow.qual.Pure;
}
@Override
public void registerVideoFrame() {
public boolean registerVideoFrame(long presentationTimeUs) {
videoFrameProcessor.registerInputFrame();
return true;
}
@Override

View File

@ -165,6 +165,8 @@ public class ExoPlayerAssetLoaderTest {
}
@Override
public void queueInputBuffer() {}
public boolean queueInputBuffer() {
return true;
}
}
}

View File

@ -133,7 +133,9 @@ public class ImageAssetLoaderTest {
private static final class FakeSampleConsumer implements SampleConsumer {
@Override
public void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {}
public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
return true;
}
@Override
public void signalEndOfVideoInput() {}