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:
parent
dc3481fca7
commit
b118565730
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user