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. */
|
/** 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;
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
@ -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 =
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user