Add an API entry point for looping a sequence

Also
- Add unit tests
- Fix bug discovered by unit tests

PiperOrigin-RevId: 519092249
This commit is contained in:
kimvde 2023-03-24 09:38:07 +00:00 committed by Tianyi Feng
parent dc3481fca7
commit b118565730
7 changed files with 199 additions and 12 deletions

View File

@ -478,6 +478,9 @@ public final class AndroidTestUtil {
.setFrameRate(23.163f) .setFrameRate(23.163f)
.setCodecs("hvc1.1.6.L183.B0") .setCodecs("hvc1.1.6.L183.B0")
.build(); .build();
public static final String MP3_ASSET_URI_STRING = "asset:///media/mp3/test.mp3";
/** /**
* Log in logcat and in an analysis file that this test was skipped. * Log in logcat and in an analysis file that this test was skipped.
* *

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.transformer.AndroidTestUtil.MP3_ASSET_URI_STRING;
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING;
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_URI_STRING;
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET_URI_STRING;
@ -227,10 +228,9 @@ public class TransformerEndToEndTest {
} }
@Test @Test
public void start_audioVideoTranscodedFromDifferentSequences_producesExpectedResult() public void audioVideoTranscodedFromDifferentSequences_producesExpectedResult() throws Exception {
throws Exception {
Transformer transformer = new Transformer.Builder(context).build(); Transformer transformer = new Transformer.Builder(context).build();
String testId = "start_audioVideoTranscodedFromDifferentSequences_producesExpectedResult"; String testId = "audioVideoTranscodedFromDifferentSequences_producesExpectedResult";
ImmutableList<AudioProcessor> audioProcessors = ImmutableList.of(new SonicAudioProcessor()); ImmutableList<AudioProcessor> audioProcessors = ImmutableList.of(new SonicAudioProcessor());
ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter()); ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter());
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_URI_STRING)); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_URI_STRING));
@ -271,6 +271,99 @@ public class TransformerEndToEndTest {
assertThat(result.exportResult.durationMs).isEqualTo(expectedResult.exportResult.durationMs); assertThat(result.exportResult.durationMs).isEqualTo(expectedResult.exportResult.durationMs);
} }
@Test
public void loopingTranscodedAudio_producesExpectedResult() throws Exception {
Transformer transformer = new Transformer.Builder(context).build();
String testId = "loopingTranscodedAudio_producesExpectedResult";
EditedMediaItem audioEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(MP3_ASSET_URI_STRING)).build();
EditedMediaItemSequence audioSequence =
new EditedMediaItemSequence(
ImmutableList.of(audioEditedMediaItem, audioEditedMediaItem), /* isLooping= */ true);
EditedMediaItem videoEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(MP4_ASSET_URI_STRING))
.setRemoveAudio(true)
.build();
EditedMediaItemSequence videoSequence =
new EditedMediaItemSequence(
ImmutableList.of(videoEditedMediaItem, videoEditedMediaItem, videoEditedMediaItem));
Composition composition =
new Composition.Builder(ImmutableList.of(audioSequence, videoSequence))
.setTransmuxVideo(true)
.build();
ExportTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, composition);
assertThat(result.exportResult.processedInputs).hasSize(6);
assertThat(result.exportResult.channelCount).isEqualTo(1);
assertThat(result.exportResult.videoFrameCount).isEqualTo(90);
assertThat(result.exportResult.durationMs).isEqualTo(3015);
}
@Test
public void loopingTranscodedVideo_producesExpectedResult() throws Exception {
Transformer transformer = new Transformer.Builder(context).build();
String testId = "loopingTranscodedVideo_producesExpectedResult";
EditedMediaItem audioEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(MP3_ASSET_URI_STRING)).build();
EditedMediaItemSequence audioSequence =
new EditedMediaItemSequence(
ImmutableList.of(audioEditedMediaItem, audioEditedMediaItem, audioEditedMediaItem));
EditedMediaItem videoEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(MP4_ASSET_URI_STRING))
.setRemoveAudio(true)
.build();
EditedMediaItemSequence videoSequence =
new EditedMediaItemSequence(
ImmutableList.of(videoEditedMediaItem, videoEditedMediaItem), /* isLooping= */ true);
Composition composition =
new Composition.Builder(ImmutableList.of(audioSequence, videoSequence)).build();
ExportTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, composition);
assertThat(result.exportResult.processedInputs).hasSize(7);
assertThat(result.exportResult.channelCount).isEqualTo(1);
assertThat(result.exportResult.videoFrameCount).isEqualTo(92);
assertThat(result.exportResult.durationMs).isEqualTo(3105);
}
@Test
public void loopingImage_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(
ImmutableList.of(audioEditedMediaItem, audioEditedMediaItem, audioEditedMediaItem));
EditedMediaItem imageEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(PNG_ASSET_URI_STRING))
.setDurationUs(1_000_000)
.setFrameRate(30)
.build();
EditedMediaItemSequence imageSequence =
new EditedMediaItemSequence(
ImmutableList.of(imageEditedMediaItem, imageEditedMediaItem), /* isLooping= */ true);
Composition composition =
new Composition.Builder(ImmutableList.of(audioSequence, imageSequence)).build();
ExportTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, composition);
assertThat(result.exportResult.processedInputs).hasSize(7);
assertThat(result.exportResult.channelCount).isEqualTo(1);
assertThat(result.exportResult.videoFrameCount).isEqualTo(94);
assertThat(result.exportResult.durationMs).isEqualTo(3100);
}
private static final class VideoUnsupportedEncoderFactory implements Codec.EncoderFactory { private static final class VideoUnsupportedEncoderFactory implements Codec.EncoderFactory {
private final Codec.EncoderFactory encoderFactory; private final Codec.EncoderFactory encoderFactory;

View File

@ -35,8 +35,17 @@ public final class EditedMediaItemSequence {
* <p>This list must not be empty. * <p>This list must not be empty.
*/ */
public final ImmutableList<EditedMediaItem> editedMediaItems; public final ImmutableList<EditedMediaItem> editedMediaItems;
/**
/* package */ final boolean isLooping; * Whether this sequence is looping.
*
* <p>This value indicates whether to loop over the {@link EditedMediaItem} instances in this
* sequence until all the non-looping sequences in the {@link Composition} have ended.
*
* <p>A looping sequence ends at the same time as the longest non-looping sequence. This means
* that the last exported {@link EditedMediaItem} from a looping sequence can be only partially
* exported.
*/
public final boolean isLooping;
/** /**
* Creates an instance. * Creates an instance.
@ -44,8 +53,18 @@ public final class EditedMediaItemSequence {
* @param editedMediaItems The {@link #editedMediaItems}. * @param editedMediaItems The {@link #editedMediaItems}.
*/ */
public EditedMediaItemSequence(List<EditedMediaItem> editedMediaItems) { public EditedMediaItemSequence(List<EditedMediaItem> editedMediaItems) {
this(editedMediaItems, /* isLooping= */ false);
}
/**
* Creates an instance.
*
* @param editedMediaItems The {@link #editedMediaItems}.
* @param isLooping Whether the sequence {@linkplain #isLooping is looping}.
*/
public EditedMediaItemSequence(List<EditedMediaItem> editedMediaItems, boolean isLooping) {
checkArgument(!editedMediaItems.isEmpty()); checkArgument(!editedMediaItems.isEmpty());
this.editedMediaItems = ImmutableList.copyOf(editedMediaItems); this.editedMediaItems = ImmutableList.copyOf(editedMediaItems);
isLooping = false; this.isLooping = isLooping;
} }
} }

View File

@ -31,6 +31,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final Codec.DecoderFactory decoderFactory; private final Codec.DecoderFactory decoderFactory;
private boolean hasPendingConsumerInput;
public ExoAssetLoaderAudioRenderer( public ExoAssetLoaderAudioRenderer(
Codec.DecoderFactory decoderFactory, Codec.DecoderFactory decoderFactory,
TransformerMediaClock mediaClock, TransformerMediaClock mediaClock,
@ -63,10 +65,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return false; return false;
} }
ByteBuffer sampleConsumerInputData = checkNotNull(sampleConsumerInputBuffer.data); if (!hasPendingConsumerInput) {
if (sampleConsumerInputData.position() == 0) {
if (decoder.isEnded()) { if (decoder.isEnded()) {
sampleConsumerInputData.limit(0); checkNotNull(sampleConsumerInputBuffer.data).limit(0);
sampleConsumerInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); sampleConsumerInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
isEnded = sampleConsumer.queueInputBuffer(); isEnded = sampleConsumer.queueInputBuffer();
return false; return false;
@ -83,8 +84,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleConsumerInputBuffer.timeUs = bufferInfo.presentationTimeUs; sampleConsumerInputBuffer.timeUs = bufferInfo.presentationTimeUs;
sampleConsumerInputBuffer.setFlags(bufferInfo.flags); sampleConsumerInputBuffer.setFlags(bufferInfo.flags);
decoder.releaseOutputBuffer(/* render= */ false); decoder.releaseOutputBuffer(/* render= */ false);
hasPendingConsumerInput = true;
} }
return sampleConsumer.queueInputBuffer(); if (!sampleConsumer.queueInputBuffer()) {
return false;
}
hasPendingConsumerInput = false;
return true;
} }
} }

View File

@ -54,6 +54,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private boolean isRunning; private boolean isRunning;
private long streamStartPositionUs; private long streamStartPositionUs;
private boolean shouldInitDecoder; private boolean shouldInitDecoder;
private boolean hasPendingConsumerInput;
public ExoAssetLoaderBaseRenderer( public ExoAssetLoaderBaseRenderer(
@C.TrackType int trackType, @C.TrackType int trackType,
@ -309,14 +310,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return false; return false;
} }
if (checkNotNull(sampleConsumerInputBuffer.data).position() == 0 if (!hasPendingConsumerInput) {
&& !sampleConsumerInputBuffer.isEndOfStream()) {
if (!readInput(sampleConsumerInputBuffer)) { if (!readInput(sampleConsumerInputBuffer)) {
return false; return false;
} }
if (shouldDropInputBuffer(sampleConsumerInputBuffer)) { if (shouldDropInputBuffer(sampleConsumerInputBuffer)) {
return true; return true;
} }
hasPendingConsumerInput = true;
} }
boolean isInputEnded = sampleConsumerInputBuffer.isEndOfStream(); boolean isInputEnded = sampleConsumerInputBuffer.isEndOfStream();
@ -324,6 +325,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return false; return false;
} }
hasPendingConsumerInput = false;
isEnded = isInputEnded; isEnded = isInputEnded;
return !isEnded; return !isEnded;
} }

View File

@ -16,7 +16,9 @@
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX; import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_ONLY;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO; import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO;
import static androidx.media3.transformer.TestUtil.FILE_VIDEO_ONLY;
import static androidx.media3.transformer.TestUtil.createTransformerBuilder; import static androidx.media3.transformer.TestUtil.createTransformerBuilder;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
@ -88,4 +90,64 @@ public class CompositionExportTest {
assertThat(exportResult.videoFrameCount).isEqualTo(expectedExportResult.videoFrameCount); assertThat(exportResult.videoFrameCount).isEqualTo(expectedExportResult.videoFrameCount);
assertThat(exportResult.durationMs).isEqualTo(expectedExportResult.durationMs); assertThat(exportResult.durationMs).isEqualTo(expectedExportResult.durationMs);
} }
@Test
public void start_loopingTransmuxedAudio_producesExpectedResult() throws Exception {
Transformer transformer =
createTransformerBuilder(testMuxerHolder, /* enableFallback= */ false).build();
EditedMediaItem audioEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_ONLY)).build();
EditedMediaItemSequence audioSequence =
new EditedMediaItemSequence(
ImmutableList.of(audioEditedMediaItem, audioEditedMediaItem), /* isLooping= */ true);
EditedMediaItem videoEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY)).build();
EditedMediaItemSequence videoSequence =
new EditedMediaItemSequence(
ImmutableList.of(videoEditedMediaItem, videoEditedMediaItem, videoEditedMediaItem));
Composition composition =
new Composition.Builder(ImmutableList.of(audioSequence, videoSequence))
.setTransmuxAudio(true)
.setTransmuxVideo(true)
.build();
transformer.start(composition, outputPath);
ExportResult exportResult = TransformerTestRunner.runLooper(transformer);
assertThat(exportResult.processedInputs).hasSize(6);
assertThat(exportResult.channelCount).isEqualTo(1);
assertThat(exportResult.videoFrameCount).isEqualTo(90);
assertThat(exportResult.durationMs).isEqualTo(2977);
assertThat(exportResult.fileSizeBytes).isEqualTo(293660);
}
@Test
public void start_loopingTransmuxedVideo_producesExpectedResult() throws Exception {
Transformer transformer =
createTransformerBuilder(testMuxerHolder, /* enableFallback= */ false).build();
EditedMediaItem audioEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_ONLY)).build();
EditedMediaItemSequence audioSequence =
new EditedMediaItemSequence(
ImmutableList.of(audioEditedMediaItem, audioEditedMediaItem, audioEditedMediaItem));
EditedMediaItem videoEditedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY)).build();
EditedMediaItemSequence videoSequence =
new EditedMediaItemSequence(
ImmutableList.of(videoEditedMediaItem, videoEditedMediaItem), /* isLooping= */ true);
Composition composition =
new Composition.Builder(ImmutableList.of(audioSequence, videoSequence))
.setTransmuxAudio(true)
.setTransmuxVideo(true)
.build();
transformer.start(composition, outputPath);
ExportResult exportResult = TransformerTestRunner.runLooper(transformer);
assertThat(exportResult.processedInputs).hasSize(7);
assertThat(exportResult.channelCount).isEqualTo(1);
assertThat(exportResult.videoFrameCount).isEqualTo(93);
assertThat(exportResult.durationMs).isEqualTo(3108);
assertThat(exportResult.fileSizeBytes).isEqualTo(337308);
}
} }

View File

@ -145,6 +145,7 @@ public final class TestUtil {
public static final String ASSET_URI_PREFIX = "asset:///media/"; public static final String ASSET_URI_PREFIX = "asset:///media/";
public static final String FILE_VIDEO_ONLY = "mp4/sample_18byte_nclx_colr.mp4"; public static final String FILE_VIDEO_ONLY = "mp4/sample_18byte_nclx_colr.mp4";
public static final String FILE_AUDIO_ONLY = "mp3/test.mp3";
public static final String FILE_AUDIO_VIDEO = "mp4/sample.mp4"; public static final String FILE_AUDIO_VIDEO = "mp4/sample.mp4";
public static final String FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S = public static final String FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S =
"mp4/sample_with_increasing_timestamps_320w_240h.mp4"; "mp4/sample_with_increasing_timestamps_320w_240h.mp4";