Fix SequenceAssetLoader signalling End Of Video Input twice

PiperOrigin-RevId: 560170216
This commit is contained in:
tofunmi 2023-08-25 12:44:22 -07:00 committed by Copybara-Service
parent 99ae44efa4
commit 667103f2bd
8 changed files with 135 additions and 29 deletions

View File

@ -626,6 +626,33 @@ public class TransformerEndToEndTest {
assertThat(result.exportResult.videoFrameCount).isEqualTo(95); assertThat(result.exportResult.videoFrameCount).isEqualTo(95);
} }
@Test
public void loopingImage_loopingSequenceIsLongest_producesExpectedResult() throws Exception {
Transformer transformer = new Transformer.Builder(context).build();
String testId = "loopingImage_producesExpectedResult";
EditedMediaItem audioEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(MP3_ASSET_URI_STRING)).build();
EditedMediaItemSequence audioSequence = new EditedMediaItemSequence(audioEditedMediaItem);
EditedMediaItem imageEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(PNG_ASSET_URI_STRING))
.setDurationUs(1_050_000)
.setFrameRate(20)
.build();
EditedMediaItemSequence loopingImageSequence =
new EditedMediaItemSequence(
ImmutableList.of(imageEditedMediaItem, imageEditedMediaItem), /* isLooping= */ true);
Composition composition = new Composition.Builder(audioSequence, loopingImageSequence).build();
ExportTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, composition);
assertThat(result.exportResult.processedInputs).hasSize(3);
assertThat(result.exportResult.channelCount).isEqualTo(1);
assertThat(result.exportResult.durationMs).isEqualTo(1000);
}
@Test @Test
public void audioTranscode_processesInInt16Pcm() throws Exception { public void audioTranscode_processesInInt16Pcm() throws Exception {
String testId = "audioTranscode_processesInInt16Pcm"; String testId = "audioTranscode_processesInInt16Pcm";

View File

@ -20,6 +20,9 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.transformer.ExportException.ERROR_CODE_IO_UNSPECIFIED; import static androidx.media3.transformer.ExportException.ERROR_CODE_IO_UNSPECIFIED;
import static androidx.media3.transformer.ExportException.ERROR_CODE_UNSPECIFIED; import static androidx.media3.transformer.ExportException.ERROR_CODE_UNSPECIFIED;
import static androidx.media3.transformer.SampleConsumer.INPUT_RESULT_END_OF_STREAM;
import static androidx.media3.transformer.SampleConsumer.INPUT_RESULT_SUCCESS;
import static androidx.media3.transformer.SampleConsumer.INPUT_RESULT_TRY_AGAIN_LATER;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE; import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED; import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -42,6 +45,7 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.datasource.DataSourceBitmapLoader;
import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.transformer.SampleConsumer.InputResult;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
@ -174,20 +178,34 @@ public final class ImageAssetLoader implements AssetLoader {
try { try {
if (sampleConsumer == null) { if (sampleConsumer == null) {
sampleConsumer = listener.onOutputFormat(format); 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,
new ConstantRateTimestampIterator(
editedMediaItem.durationUs, editedMediaItem.frameRate))) {
scheduledExecutorService.schedule( scheduledExecutorService.schedule(
() -> queueBitmapInternal(bitmap, format), QUEUE_BITMAP_INTERVAL_MS, MILLISECONDS); () -> queueBitmapInternal(bitmap, format), QUEUE_BITMAP_INTERVAL_MS, MILLISECONDS);
return; return;
} }
sampleConsumer.signalEndOfVideoInput(); // TODO(b/262693274): consider using listener.onDurationUs() or the MediaItem change
progress = 100; // callback rather than setting duration here.
@InputResult
int result =
sampleConsumer.queueInputBitmap(
bitmap,
new ConstantRateTimestampIterator(
editedMediaItem.durationUs, editedMediaItem.frameRate));
switch (result) {
case INPUT_RESULT_SUCCESS:
progress = 100;
sampleConsumer.signalEndOfVideoInput();
break;
case INPUT_RESULT_TRY_AGAIN_LATER:
scheduledExecutorService.schedule(
() -> queueBitmapInternal(bitmap, format), QUEUE_BITMAP_INTERVAL_MS, MILLISECONDS);
break;
case INPUT_RESULT_END_OF_STREAM:
progress = 100;
break;
default:
throw new IllegalStateException();
}
} catch (ExportException e) { } catch (ExportException e) {
listener.onError(e); listener.onError(e);
} catch (RuntimeException e) { } catch (RuntimeException e) {

View File

@ -15,19 +15,59 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
import androidx.media3.common.OnInputFrameProcessedListener; import androidx.media3.common.OnInputFrameProcessedListener;
import androidx.media3.common.util.TimestampIterator; import androidx.media3.common.util.TimestampIterator;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Consumer of encoded media samples, raw audio or raw video frames. */ /** Consumer of encoded media samples, raw audio or raw video frames. */
@UnstableApi @UnstableApi
public interface SampleConsumer { public interface SampleConsumer {
/**
* Specifies the result of an input operation. One of {@link #INPUT_RESULT_SUCCESS}, {@link
* #INPUT_RESULT_TRY_AGAIN_LATER} or {@link #INPUT_RESULT_END_OF_STREAM}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({INPUT_RESULT_SUCCESS, INPUT_RESULT_TRY_AGAIN_LATER, INPUT_RESULT_END_OF_STREAM})
@interface InputResult {}
/**
* The operation of queueing input was successful.
*
* <p>The caller can queue more input or signal {@link #signalEndOfVideoInput() signal end of
* input}.
*/
int INPUT_RESULT_SUCCESS = 1;
/**
* The operation of queueing/registering input was unsuccessful.
*
* <p>The caller should queue try again later.
*/
int INPUT_RESULT_TRY_AGAIN_LATER = 2;
/**
* The operation of queueing input successful and end of input has been automatically signalled.
*
* <p>The caller should not {@link #signalEndOfVideoInput() signal end of input} as this has
* already been done internally.
*/
int INPUT_RESULT_END_OF_STREAM = 3;
/** /**
* Returns a {@link DecoderInputBuffer}, if available. * Returns a {@link DecoderInputBuffer}, if available.
* *
@ -74,8 +114,10 @@ public interface SampleConsumer {
* @param inputBitmap The {@link Bitmap} to queue to the consumer. * @param inputBitmap The {@link Bitmap} to queue to the consumer.
* @param inStreamOffsetsUs The times within the current stream that the bitmap should be * @param inStreamOffsetsUs The times within the current stream that the bitmap should be
* displayed at. The timestamps should be monotonically increasing. * displayed at. The timestamps should be monotonically increasing.
* @return The {@link InputResult} describing the result of the operation.
*/ */
default boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { default @InputResult int queueInputBitmap(
Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -99,10 +141,9 @@ public interface SampleConsumer {
* *
* @param texId The ID of the texture to queue to the consumer. * @param texId The ID of the texture to queue to the consumer.
* @param presentationTimeUs The presentation time for the texture, in microseconds. * @param presentationTimeUs The presentation time for the texture, in microseconds.
* @return Whether the texture was successfully queued. If {@code false}, the caller should try * @return The {@link InputResult} describing the result of the operation.
* again later.
*/ */
default boolean queueInputTexture(int texId, long presentationTimeUs) { default @InputResult int queueInputTexture(int texId, long presentationTimeUs) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -398,21 +398,23 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
@Override @Override
public boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { public @InputResult int queueInputBitmap(
Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) {
if (isLooping) { if (isLooping) {
long lastOffsetUs = C.TIME_UNSET; long lastOffsetUs = C.TIME_UNSET;
while (inStreamOffsetsUs.hasNext()) { while (inStreamOffsetsUs.hasNext()) {
long offsetUs = inStreamOffsetsUs.next(); long offsetUs = inStreamOffsetsUs.next();
if (totalDurationUs + offsetUs > maxSequenceDurationUs) { if (totalDurationUs + offsetUs > maxSequenceDurationUs) {
if (!isMaxSequenceDurationUsFinal) { if (!isMaxSequenceDurationUsFinal) {
return false; return INPUT_RESULT_TRY_AGAIN_LATER;
} }
if (lastOffsetUs == C.TIME_UNSET) { if (lastOffsetUs == C.TIME_UNSET) {
if (!videoLoopingEnded) { if (!videoLoopingEnded) {
videoLoopingEnded = true; videoLoopingEnded = true;
signalEndOfVideoInput(); signalEndOfVideoInput();
return INPUT_RESULT_END_OF_STREAM;
} }
return false; return INPUT_RESULT_TRY_AGAIN_LATER;
} }
inStreamOffsetsUs = new ClippingIterator(inStreamOffsetsUs.copyOf(), lastOffsetUs); inStreamOffsetsUs = new ClippingIterator(inStreamOffsetsUs.copyOf(), lastOffsetUs);
videoLoopingEnded = true; videoLoopingEnded = true;
@ -430,14 +432,15 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
@Override @Override
public boolean queueInputTexture(int texId, long presentationTimeUs) { public @InputResult int queueInputTexture(int texId, long presentationTimeUs) {
long globalTimestampUs = totalDurationUs + presentationTimeUs; long globalTimestampUs = totalDurationUs + presentationTimeUs;
if (isLooping && globalTimestampUs >= maxSequenceDurationUs) { if (isLooping && globalTimestampUs >= maxSequenceDurationUs) {
if (isMaxSequenceDurationUsFinal && !videoLoopingEnded) { if (isMaxSequenceDurationUsFinal && !videoLoopingEnded) {
videoLoopingEnded = true; videoLoopingEnded = true;
signalEndOfVideoInput(); signalEndOfVideoInput();
return INPUT_RESULT_END_OF_STREAM;
} }
return false; return INPUT_RESULT_TRY_AGAIN_LATER;
} }
return sampleConsumer.queueInputTexture(texId, presentationTimeUs); return sampleConsumer.queueInputTexture(texId, presentationTimeUs);
} }

View File

@ -215,8 +215,11 @@ import java.util.concurrent.atomic.AtomicLong;
} }
@Override @Override
public boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { public @InputResult int queueInputBitmap(
return videoFrameProcessor.queueInputBitmap(inputBitmap, inStreamOffsetsUs); Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) {
return videoFrameProcessor.queueInputBitmap(inputBitmap, inStreamOffsetsUs)
? INPUT_RESULT_SUCCESS
: INPUT_RESULT_TRY_AGAIN_LATER;
} }
@Override @Override
@ -225,8 +228,10 @@ import java.util.concurrent.atomic.AtomicLong;
} }
@Override @Override
public boolean queueInputTexture(int texId, long presentationTimeUs) { public @InputResult int queueInputTexture(int texId, long presentationTimeUs) {
return videoFrameProcessor.queueInputTexture(texId, presentationTimeUs); return videoFrameProcessor.queueInputTexture(texId, presentationTimeUs)
? INPUT_RESULT_SUCCESS
: INPUT_RESULT_TRY_AGAIN_LATER;
} }
@Override @Override

View File

@ -18,6 +18,8 @@ package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.transformer.ExportException.ERROR_CODE_UNSPECIFIED; import static androidx.media3.transformer.ExportException.ERROR_CODE_UNSPECIFIED;
import static androidx.media3.transformer.SampleConsumer.INPUT_RESULT_END_OF_STREAM;
import static androidx.media3.transformer.SampleConsumer.INPUT_RESULT_TRY_AGAIN_LATER;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE; import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED; import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import static java.lang.Math.round; import static java.lang.Math.round;
@ -28,6 +30,7 @@ import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.OnInputFrameProcessedListener; import androidx.media3.common.OnInputFrameProcessedListener;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.transformer.SampleConsumer.InputResult;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -50,6 +53,7 @@ public final class TextureAssetLoader implements AssetLoader {
private @MonotonicNonNull SampleConsumer sampleConsumer; private @MonotonicNonNull SampleConsumer sampleConsumer;
private @Transformer.ProgressState int progressState; private @Transformer.ProgressState int progressState;
private boolean isTrackAdded; private boolean isTrackAdded;
private boolean isEndOfStreamSignaled;
private volatile boolean isStarted; private volatile boolean isStarted;
private volatile long lastQueuedPresentationTimeUs; private volatile long lastQueuedPresentationTimeUs;
@ -136,9 +140,13 @@ public final class TextureAssetLoader implements AssetLoader {
sampleConsumer.setOnInputFrameProcessedListener(frameProcessedListener); sampleConsumer.setOnInputFrameProcessedListener(frameProcessedListener);
} }
} }
if (!sampleConsumer.queueInputTexture(texId, presentationTimeUs)) { @InputResult int result = sampleConsumer.queueInputTexture(texId, presentationTimeUs);
if (result == INPUT_RESULT_TRY_AGAIN_LATER) {
return false; return false;
} }
if (result == INPUT_RESULT_END_OF_STREAM) {
isEndOfStreamSignaled = true;
}
lastQueuedPresentationTimeUs = presentationTimeUs; lastQueuedPresentationTimeUs = presentationTimeUs;
return true; return true;
} catch (ExportException e) { } catch (ExportException e) {
@ -156,7 +164,10 @@ public final class TextureAssetLoader implements AssetLoader {
*/ */
public void signalEndOfVideoInput() { public void signalEndOfVideoInput() {
try { try {
checkNotNull(sampleConsumer).signalEndOfVideoInput(); if (!isEndOfStreamSignaled) {
isEndOfStreamSignaled = true;
checkNotNull(sampleConsumer).signalEndOfVideoInput();
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
assetLoaderListener.onError(ExportException.createForAssetLoader(e, ERROR_CODE_UNSPECIFIED)); assetLoaderListener.onError(ExportException.createForAssetLoader(e, ERROR_CODE_UNSPECIFIED));
} }

View File

@ -127,8 +127,9 @@ public class ImageAssetLoaderTest {
private static final class FakeSampleConsumer implements SampleConsumer { private static final class FakeSampleConsumer implements SampleConsumer {
@Override @Override
public boolean queueInputBitmap(Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) { public @InputResult int queueInputBitmap(
return true; Bitmap inputBitmap, TimestampIterator inStreamOffsetsUs) {
return INPUT_RESULT_SUCCESS;
} }
@Override @Override

View File

@ -141,8 +141,8 @@ public class TextureAssetLoaderTest {
public void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener) {} public void setOnInputFrameProcessedListener(OnInputFrameProcessedListener listener) {}
@Override @Override
public boolean queueInputTexture(int texId, long presentationTimeUs) { public @InputResult int queueInputTexture(int texId, long presentationTimeUs) {
return true; return INPUT_RESULT_SUCCESS;
} }
@Override @Override