Allow creating fragmented MP4 file via InAppMuxer
PiperOrigin-RevId: 594431665
This commit is contained in:
parent
e0257f403f
commit
27ae6d974e
@ -88,13 +88,6 @@ public final class Mp4Muxer {
|
||||
|
||||
/** A builder for {@link Mp4Muxer} instances. */
|
||||
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 @LastFrameDurationBehavior int lastFrameDurationBehavior;
|
||||
@ -194,6 +187,13 @@ public final class Mp4Muxer {
|
||||
public static final ImmutableList<String> SUPPORTED_AUDIO_SAMPLE_MIME_TYPES =
|
||||
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 MetadataCollector metadataCollector;
|
||||
|
||||
|
@ -65,7 +65,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
return;
|
||||
}
|
||||
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());
|
||||
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_ASSET_DIRECTORY + inputFile));
|
||||
EditedMediaItem editedMediaItem =
|
||||
@ -85,7 +87,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
assumeTrue(checkNotNull(inputFile).equals(H264_MP4));
|
||||
String testId = "audioEditing_completesSuccessfully";
|
||||
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.putChannelMixingMatrix(
|
||||
ChannelMixingMatrix.create(/* inputChannelCount= */ 1, /* outputChannelCount= */ 2));
|
||||
|
@ -49,7 +49,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
return;
|
||||
}
|
||||
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());
|
||||
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_ASSET_AV1_VIDEO_URI_STRING));
|
||||
EditedMediaItem editedMediaItem =
|
||||
|
@ -33,6 +33,7 @@ import androidx.media3.container.XmpData;
|
||||
import androidx.media3.muxer.Mp4Muxer;
|
||||
import androidx.media3.muxer.Mp4Muxer.TrackToken;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -70,34 +71,76 @@ public final class InAppMuxer implements Muxer {
|
||||
|
||||
/** {@link Muxer.Factory} for {@link InAppMuxer}. */
|
||||
public static final class Factory implements Muxer.Factory {
|
||||
private final long maxDelayBetweenSamplesMs;
|
||||
private final @Nullable MetadataProvider metadataProvider;
|
||||
|
||||
/**
|
||||
* Creates an instance with {@link Muxer#getMaxDelayBetweenSamplesMs() maxDelayBetweenSamplesMs}
|
||||
* set to {@link DefaultMuxer.Factory#DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS} and {@link
|
||||
* #metadataProvider} set to {@code null}.
|
||||
*
|
||||
* <p>If the {@link #metadataProvider} is not set then the {@linkplain Metadata.Entry metadata}
|
||||
* from the input file is set as it is in the output file.
|
||||
*/
|
||||
public Factory() {
|
||||
this(
|
||||
/* maxDelayBetweenSamplesMs= */ DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
|
||||
/* metadataProvider= */ null);
|
||||
/** A builder for {@link Factory} instances. */
|
||||
public static final class Builder {
|
||||
private long maxDelayBetweenSamplesMs;
|
||||
private @Nullable MetadataProvider metadataProvider;
|
||||
private boolean fragmentedMp4Enabled;
|
||||
private int fragmentDurationUs;
|
||||
|
||||
/** Creates a {@link Builder} instance with default values. */
|
||||
public Builder() {
|
||||
maxDelayBetweenSamplesMs = DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS;
|
||||
fragmentDurationUs = Mp4Muxer.DEFAULT_FRAGMENT_DURATION_US;
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Muxer.Factory} for {@link InAppMuxer}.
|
||||
*
|
||||
* @param maxDelayBetweenSamplesMs See {@link Muxer#getMaxDelayBetweenSamplesMs()}.
|
||||
* @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
|
||||
* it is in the output file.
|
||||
*/
|
||||
public Factory(long maxDelayBetweenSamplesMs, @Nullable MetadataProvider metadataProvider) {
|
||||
private final long maxDelayBetweenSamplesMs;
|
||||
private final @Nullable MetadataProvider metadataProvider;
|
||||
private final boolean fragmentedMp4Enabled;
|
||||
private final int fragmentDurationUs;
|
||||
|
||||
private Factory(
|
||||
long maxDelayBetweenSamplesMs,
|
||||
@Nullable MetadataProvider metadataProvider,
|
||||
boolean fragmentedMp4Enabled,
|
||||
int fragmentDurationUs) {
|
||||
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
|
||||
this.metadataProvider = metadataProvider;
|
||||
this.fragmentedMp4Enabled = fragmentedMp4Enabled;
|
||||
this.fragmentDurationUs = fragmentDurationUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -109,7 +152,11 @@ public final class InAppMuxer implements Muxer {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ public final class EncodedSampleExporterTest {
|
||||
new TransformationRequest.Builder().build(),
|
||||
new MuxerWrapper(
|
||||
/* outputPath= */ "unused",
|
||||
new InAppMuxer.Factory(),
|
||||
new InAppMuxer.Factory.Builder().build(),
|
||||
mock(MuxerWrapper.Listener.class),
|
||||
MuxerWrapper.MUXER_MODE_DEFAULT,
|
||||
/* dropSamplesBeforeFirstVideoSample= */ false),
|
||||
|
@ -57,12 +57,15 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
@Test
|
||||
public void transmux_withLocationMetadata_outputMatchesExpected() throws Exception {
|
||||
Muxer.Factory inAppMuxerFactory =
|
||||
new InAppMuxer.Factory(
|
||||
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
|
||||
metadataEntries -> {
|
||||
metadataEntries.removeIf((Metadata.Entry entry) -> entry instanceof Mp4LocationData);
|
||||
metadataEntries.add(new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f));
|
||||
});
|
||||
new InAppMuxer.Factory.Builder()
|
||||
.setMetadataProvider(
|
||||
metadataEntries -> {
|
||||
metadataEntries.removeIf(
|
||||
(Metadata.Entry entry) -> entry instanceof Mp4LocationData);
|
||||
metadataEntries.add(
|
||||
new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f));
|
||||
})
|
||||
.build();
|
||||
Transformer transformer =
|
||||
new Transformer.Builder(context)
|
||||
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||
@ -90,9 +93,9 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
String xmpSampleData = "media/xmp/sample_datetime_xmp.xmp";
|
||||
byte[] xmpData = androidx.media3.test.utils.TestUtil.getByteArray(context, xmpSampleData);
|
||||
Muxer.Factory inAppMuxerFactory =
|
||||
new InAppMuxer.Factory(
|
||||
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
|
||||
metadataEntries -> metadataEntries.add(new XmpData(xmpData)));
|
||||
new InAppMuxer.Factory.Builder()
|
||||
.setMetadataProvider(metadataEntries -> metadataEntries.add(new XmpData(xmpData)))
|
||||
.build();
|
||||
Transformer transformer =
|
||||
new Transformer.Builder(context)
|
||||
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||
@ -110,17 +113,18 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
@Test
|
||||
public void transmux_withCaptureFps_outputMatchesExpected() throws Exception {
|
||||
Muxer.Factory inAppMuxerFactory =
|
||||
new InAppMuxer.Factory(
|
||||
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
|
||||
metadataEntries -> {
|
||||
float captureFps = 60.0f;
|
||||
metadataEntries.add(
|
||||
new MdtaMetadataEntry(
|
||||
MdtaMetadataEntry.KEY_ANDROID_CAPTURE_FPS,
|
||||
/* value= */ Util.toByteArray(captureFps),
|
||||
/* localeIndicator= */ 0,
|
||||
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32));
|
||||
});
|
||||
new InAppMuxer.Factory.Builder()
|
||||
.setMetadataProvider(
|
||||
metadataEntries -> {
|
||||
float captureFps = 60.0f;
|
||||
metadataEntries.add(
|
||||
new MdtaMetadataEntry(
|
||||
MdtaMetadataEntry.KEY_ANDROID_CAPTURE_FPS,
|
||||
/* value= */ Util.toByteArray(captureFps),
|
||||
/* localeIndicator= */ 0,
|
||||
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32));
|
||||
})
|
||||
.build();
|
||||
Transformer transformer =
|
||||
new Transformer.Builder(context)
|
||||
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||
@ -145,11 +149,13 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
@Test
|
||||
public void transmux_withCreationTime_outputMatchesExpected() throws Exception {
|
||||
Muxer.Factory inAppMuxerFactory =
|
||||
new InAppMuxer.Factory(
|
||||
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
|
||||
metadataEntries ->
|
||||
metadataEntries.add(
|
||||
new Mp4TimestampData(/* creationTimestampSeconds= */ 2_000_000_000L)));
|
||||
new InAppMuxer.Factory.Builder()
|
||||
.setMetadataProvider(
|
||||
metadataEntries ->
|
||||
metadataEntries.add(
|
||||
new Mp4TimestampData(/* creationTimestampSeconds= */ 2_000_000_000L)))
|
||||
.build();
|
||||
|
||||
Transformer transformer =
|
||||
new Transformer.Builder(context)
|
||||
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||
@ -175,26 +181,27 @@ public class TransformerWithInAppMuxerEndToEndTest {
|
||||
@Test
|
||||
public void transmux_withCustomeMetadata_outputMatchesExpected() throws Exception {
|
||||
Muxer.Factory inAppMuxerFactory =
|
||||
new InAppMuxer.Factory(
|
||||
DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS,
|
||||
metadataEntries -> {
|
||||
String stringKey = "StringKey";
|
||||
String stringValue = "StringValue";
|
||||
metadataEntries.add(
|
||||
new MdtaMetadataEntry(
|
||||
stringKey,
|
||||
Util.getUtf8Bytes(stringValue),
|
||||
/* localeIndicator= */ 0,
|
||||
MdtaMetadataEntry.TYPE_INDICATOR_STRING));
|
||||
String floatKey = "FloatKey";
|
||||
float floatValue = 600.0f;
|
||||
metadataEntries.add(
|
||||
new MdtaMetadataEntry(
|
||||
floatKey,
|
||||
Util.toByteArray(floatValue),
|
||||
/* localeIndicator= */ 0,
|
||||
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32));
|
||||
});
|
||||
new InAppMuxer.Factory.Builder()
|
||||
.setMetadataProvider(
|
||||
metadataEntries -> {
|
||||
String stringKey = "StringKey";
|
||||
String stringValue = "StringValue";
|
||||
metadataEntries.add(
|
||||
new MdtaMetadataEntry(
|
||||
stringKey,
|
||||
Util.getUtf8Bytes(stringValue),
|
||||
/* localeIndicator= */ 0,
|
||||
MdtaMetadataEntry.TYPE_INDICATOR_STRING));
|
||||
String floatKey = "FloatKey";
|
||||
float floatValue = 600.0f;
|
||||
metadataEntries.add(
|
||||
new MdtaMetadataEntry(
|
||||
floatKey,
|
||||
Util.toByteArray(floatValue),
|
||||
/* localeIndicator= */ 0,
|
||||
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32));
|
||||
})
|
||||
.build();
|
||||
Transformer transformer =
|
||||
new Transformer.Builder(context)
|
||||
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||
|
Loading…
x
Reference in New Issue
Block a user