Allow creating fragmented MP4 file via InAppMuxer

PiperOrigin-RevId: 594431665
This commit is contained in:
sheenachhabra 2023-12-29 04:11:30 -08:00 committed by Copybara-Service
parent e0257f403f
commit 27ae6d974e
6 changed files with 140 additions and 80 deletions

View File

@ -88,13 +88,6 @@ public final class Mp4Muxer {
/** A builder for {@link Mp4Muxer} instances. */ /** A builder for {@link Mp4Muxer} instances. */
public static final class Builder { public static final class Builder {
// TODO: b/262704382 - Optimize the default duration.
/**
* The default fragment duration for the {@linkplain #setFragmentedMp4Enabled(boolean)
* fragmented MP4}.
*/
public static final int DEFAULT_FRAGMENT_DURATION_US = 2_000_000;
private final FileOutputStream fileOutputStream; private final FileOutputStream fileOutputStream;
private @LastFrameDurationBehavior int lastFrameDurationBehavior; private @LastFrameDurationBehavior int lastFrameDurationBehavior;
@ -194,6 +187,13 @@ public final class Mp4Muxer {
public static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES = public static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES =
ImmutableList.of(MimeTypes.AUDIO_AAC); ImmutableList.of(MimeTypes.AUDIO_AAC);
// TODO: b/262704382 - Optimize the default duration.
/**
* The default fragment duration for the {@linkplain Builder#setFragmentedMp4Enabled(boolean)
* fragmented MP4}.
*/
public static final int DEFAULT_FRAGMENT_DURATION_US = 2_000_000;
private final Mp4Writer mp4Writer; private final Mp4Writer mp4Writer;
private final MetadataCollector metadataCollector; private final MetadataCollector metadataCollector;

View File

@ -65,7 +65,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
return; return;
} }
Transformer transformer = Transformer transformer =
new Transformer.Builder(context).setMuxerFactory(new InAppMuxer.Factory()).build(); new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.build();
ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter()); ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter());
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFile)); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFile));
EditedMediaItem editedMediaItem = EditedMediaItem editedMediaItem =
@ -85,7 +87,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
assumeTrue(checkNotNull(inputFile).equals(H264_MP4)); assumeTrue(checkNotNull(inputFile).equals(H264_MP4));
String testId = "audioEditing_completesSuccessfully"; String testId = "audioEditing_completesSuccessfully";
Transformer transformer = Transformer transformer =
new Transformer.Builder(context).setMuxerFactory(new InAppMuxer.Factory()).build(); new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.build();
ChannelMixingAudioProcessor channelMixingAudioProcessor = new ChannelMixingAudioProcessor(); ChannelMixingAudioProcessor channelMixingAudioProcessor = new ChannelMixingAudioProcessor();
channelMixingAudioProcessor.putChannelMixingMatrix( channelMixingAudioProcessor.putChannelMixingMatrix(
ChannelMixingMatrix.create(/* inputChannelCount= */ 1, /* outputChannelCount= */ 2)); ChannelMixingMatrix.create(/* inputChannelCount= */ 1, /* outputChannelCount= */ 2));

View File

@ -49,7 +49,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
return; return;
} }
Transformer transformer = Transformer transformer =
new Transformer.Builder(context).setMuxerFactory(new InAppMuxer.Factory()).build(); new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.build();
ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter()); ImmutableList<Effect> videoEffects = ImmutableList.of(RgbFilter.createGrayscaleFilter());
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_AV1_VIDEO_URI_STRING)); MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_AV1_VIDEO_URI_STRING));
EditedMediaItem editedMediaItem = EditedMediaItem editedMediaItem =

View File

@ -33,6 +33,7 @@ import androidx.media3.container.XmpData;
import androidx.media3.muxer.Mp4Muxer; import androidx.media3.muxer.Mp4Muxer;
import androidx.media3.muxer.Mp4Muxer.TrackToken; import androidx.media3.muxer.Mp4Muxer.TrackToken;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@ -70,34 +71,76 @@ public final class InAppMuxer implements Muxer {
/** {@link Muxer.Factory} for {@link InAppMuxer}. */ /** {@link Muxer.Factory} for {@link InAppMuxer}. */
public static final class Factory implements Muxer.Factory { public static final class Factory implements Muxer.Factory {
private final long maxDelayBetweenSamplesMs;
private final @Nullable MetadataProvider metadataProvider;
/** /** A builder for {@link Factory} instances. */
* Creates an instance with {@link Muxer#getMaxDelayBetweenSamplesMs() maxDelayBetweenSamplesMs} public static final class Builder {
* set to {@link DefaultMuxer.Factory#DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS} and {@link private long maxDelayBetweenSamplesMs;
* #metadataProvider} set to {@code null}. private @Nullable MetadataProvider metadataProvider;
* private boolean fragmentedMp4Enabled;
* <p>If the {@link #metadataProvider} is not set then the {@linkplain Metadata.Entry metadata} private int fragmentDurationUs;
* from the input file is set as it is in the output file.
*/ /** Creates a {@link Builder} instance with default values. */
public Factory() { public Builder() {
this( maxDelayBetweenSamplesMs = DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS;
/* maxDelayBetweenSamplesMs= */ DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, fragmentDurationUs = Mp4Muxer.DEFAULT_FRAGMENT_DURATION_US;
/* metadataProvider= */ null); }
/** See {@link Muxer#getMaxDelayBetweenSamplesMs()}. */
@CanIgnoreReturnValue
public Builder setMaxDelayBetweenSamplesMs(long maxDelayBetweenSamplesMs) {
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
return this;
}
/**
* Sets an implementation of {@link MetadataProvider}.
*
* <p>The default value is {@code null}.
*
* <p>If the value is not set then the {@linkplain Metadata.Entry metadata} from the input
* file is set as it is in the output file.
*/
@CanIgnoreReturnValue
public Builder setMetadataProvider(MetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider;
return this;
}
/** See {@link Mp4Muxer.Builder#setFragmentedMp4Enabled(boolean)}. */
@CanIgnoreReturnValue
public Builder setFragmentedMp4Enabled(boolean fragmentedMp4Enabled) {
this.fragmentedMp4Enabled = fragmentedMp4Enabled;
return this;
}
/** See {@link Mp4Muxer.Builder#setFragmentDurationUs(int)}. */
@CanIgnoreReturnValue
public Builder setFragmentDurationUs(int fragmentDurationUs) {
this.fragmentDurationUs = fragmentDurationUs;
return this;
}
/** Builds a {@link Factory} instance. */
public Factory build() {
return new Factory(
maxDelayBetweenSamplesMs, metadataProvider, fragmentedMp4Enabled, fragmentDurationUs);
}
} }
/** private final long maxDelayBetweenSamplesMs;
* {@link Muxer.Factory} for {@link InAppMuxer}. private final @Nullable MetadataProvider metadataProvider;
* private final boolean fragmentedMp4Enabled;
* @param maxDelayBetweenSamplesMs See {@link Muxer#getMaxDelayBetweenSamplesMs()}. private final int fragmentDurationUs;
* @param metadataProvider A {@link MetadataProvider} implementation. If the value is set to
* {@code null} then the {@linkplain Metadata.Entry metadata} from the input file is set as private Factory(
* it is in the output file. long maxDelayBetweenSamplesMs,
*/ @Nullable MetadataProvider metadataProvider,
public Factory(long maxDelayBetweenSamplesMs, @Nullable MetadataProvider metadataProvider) { boolean fragmentedMp4Enabled,
int fragmentDurationUs) {
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs; this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
this.metadataProvider = metadataProvider; this.metadataProvider = metadataProvider;
this.fragmentedMp4Enabled = fragmentedMp4Enabled;
this.fragmentDurationUs = fragmentDurationUs;
} }
@Override @Override
@ -109,7 +152,11 @@ public final class InAppMuxer implements Muxer {
throw new MuxerException("Error creating file output stream", e); throw new MuxerException("Error creating file output stream", e);
} }
Mp4Muxer mp4Muxer = new Mp4Muxer.Builder(outputStream).build(); Mp4Muxer mp4Muxer =
new Mp4Muxer.Builder(outputStream)
.setFragmentedMp4Enabled(fragmentedMp4Enabled)
.setFragmentDurationUs(fragmentDurationUs)
.build();
return new InAppMuxer(mp4Muxer, maxDelayBetweenSamplesMs, metadataProvider); return new InAppMuxer(mp4Muxer, maxDelayBetweenSamplesMs, metadataProvider);
} }

View File

@ -65,7 +65,7 @@ public final class EncodedSampleExporterTest {
new TransformationRequest.Builder().build(), new TransformationRequest.Builder().build(),
new MuxerWrapper( new MuxerWrapper(
/* outputPath= */ "unused", /* outputPath= */ "unused",
new InAppMuxer.Factory(), new InAppMuxer.Factory.Builder().build(),
mock(MuxerWrapper.Listener.class), mock(MuxerWrapper.Listener.class),
MuxerWrapper.MUXER_MODE_DEFAULT, MuxerWrapper.MUXER_MODE_DEFAULT,
/* dropSamplesBeforeFirstVideoSample= */ false), /* dropSamplesBeforeFirstVideoSample= */ false),

View File

@ -57,12 +57,15 @@ public class TransformerWithInAppMuxerEndToEndTest {
@Test @Test
public void transmux_withLocationMetadata_outputMatchesExpected() throws Exception { public void transmux_withLocationMetadata_outputMatchesExpected() throws Exception {
Muxer.Factory inAppMuxerFactory = Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory( new InAppMuxer.Factory.Builder()
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, .setMetadataProvider(
metadataEntries -> { metadataEntries -> {
metadataEntries.removeIf((Metadata.Entry entry) -> entry instanceof Mp4LocationData); metadataEntries.removeIf(
metadataEntries.add(new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f)); (Metadata.Entry entry) -> entry instanceof Mp4LocationData);
}); metadataEntries.add(
new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f));
})
.build();
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setClock(new FakeClock(/* isAutoAdvancing= */ true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true))
@ -90,9 +93,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
String xmpSampleData = "media/xmp/sample_datetime_xmp.xmp"; String xmpSampleData = "media/xmp/sample_datetime_xmp.xmp";
byte[] xmpData = androidx.media3.test.utils.TestUtil.getByteArray(context, xmpSampleData); byte[] xmpData = androidx.media3.test.utils.TestUtil.getByteArray(context, xmpSampleData);
Muxer.Factory inAppMuxerFactory = Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory( new InAppMuxer.Factory.Builder()
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, .setMetadataProvider(metadataEntries -> metadataEntries.add(new XmpData(xmpData)))
metadataEntries -> metadataEntries.add(new XmpData(xmpData))); .build();
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setClock(new FakeClock(/* isAutoAdvancing= */ true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true))
@ -110,17 +113,18 @@ public class TransformerWithInAppMuxerEndToEndTest {
@Test @Test
public void transmux_withCaptureFps_outputMatchesExpected() throws Exception { public void transmux_withCaptureFps_outputMatchesExpected() throws Exception {
Muxer.Factory inAppMuxerFactory = Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory( new InAppMuxer.Factory.Builder()
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, .setMetadataProvider(
metadataEntries -> { metadataEntries -> {
float captureFps = 60.0f; float captureFps = 60.0f;
metadataEntries.add( metadataEntries.add(
new MdtaMetadataEntry( new MdtaMetadataEntry(
MdtaMetadataEntry.KEY_ANDROID_CAPTURE_FPS, MdtaMetadataEntry.KEY_ANDROID_CAPTURE_FPS,
/* value= */ Util.toByteArray(captureFps), /* value= */ Util.toByteArray(captureFps),
/* localeIndicator= */ 0, /* localeIndicator= */ 0,
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32)); MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32));
}); })
.build();
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setClock(new FakeClock(/* isAutoAdvancing= */ true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true))
@ -145,11 +149,13 @@ public class TransformerWithInAppMuxerEndToEndTest {
@Test @Test
public void transmux_withCreationTime_outputMatchesExpected() throws Exception { public void transmux_withCreationTime_outputMatchesExpected() throws Exception {
Muxer.Factory inAppMuxerFactory = Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory( new InAppMuxer.Factory.Builder()
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, .setMetadataProvider(
metadataEntries -> metadataEntries ->
metadataEntries.add( metadataEntries.add(
new Mp4TimestampData(/* creationTimestampSeconds= */ 2_000_000_000L))); new Mp4TimestampData(/* creationTimestampSeconds= */ 2_000_000_000L)))
.build();
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setClock(new FakeClock(/* isAutoAdvancing= */ true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true))
@ -175,26 +181,27 @@ public class TransformerWithInAppMuxerEndToEndTest {
@Test @Test
public void transmux_withCustomeMetadata_outputMatchesExpected() throws Exception { public void transmux_withCustomeMetadata_outputMatchesExpected() throws Exception {
Muxer.Factory inAppMuxerFactory = Muxer.Factory inAppMuxerFactory =
new InAppMuxer.Factory( new InAppMuxer.Factory.Builder()
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS, .setMetadataProvider(
metadataEntries -> { metadataEntries -> {
String stringKey = "StringKey"; String stringKey = "StringKey";
String stringValue = "StringValue"; String stringValue = "StringValue";
metadataEntries.add( metadataEntries.add(
new MdtaMetadataEntry( new MdtaMetadataEntry(
stringKey, stringKey,
Util.getUtf8Bytes(stringValue), Util.getUtf8Bytes(stringValue),
/* localeIndicator= */ 0, /* localeIndicator= */ 0,
MdtaMetadataEntry.TYPE_INDICATOR_STRING)); MdtaMetadataEntry.TYPE_INDICATOR_STRING));
String floatKey = "FloatKey"; String floatKey = "FloatKey";
float floatValue = 600.0f; float floatValue = 600.0f;
metadataEntries.add( metadataEntries.add(
new MdtaMetadataEntry( new MdtaMetadataEntry(
floatKey, floatKey,
Util.toByteArray(floatValue), Util.toByteArray(floatValue),
/* localeIndicator= */ 0, /* localeIndicator= */ 0,
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32)); MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32));
}); })
.build();
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setClock(new FakeClock(/* isAutoAdvancing= */ true)) .setClock(new FakeClock(/* isAutoAdvancing= */ true))