SUPPORTED_AUDIO_SAMPLE_MIME_TYPES =
+ ImmutableList.of(
+ MimeTypes.AUDIO_AAC,
+ MimeTypes.AUDIO_AMR_NB,
+ MimeTypes.AUDIO_AMR_WB,
+ MimeTypes.AUDIO_OPUS,
+ MimeTypes.AUDIO_VORBIS);
+
+ private final long fragmentDurationMs;
+
+ private long videoDurationUs;
+
+ /** Creates an instance with default values. */
+ public Factory() {
+ this(C.TIME_UNSET);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param fragmentDurationMs The fragment duration (in milliseconds).
+ */
+ public Factory(long fragmentDurationMs) {
+ this.fragmentDurationMs = fragmentDurationMs;
+ videoDurationUs = C.TIME_UNSET;
+ }
+
+ /**
+ * Sets the duration of the video track (in microseconds) in the output.
+ *
+ * Only the duration of the last sample is adjusted to achieve the given duration. Duration
+ * of the other samples remains unchanged.
+ *
+ *
The default is {@link C#TIME_UNSET} to not set any duration in the output. In this case
+ * the video track duration is determined by the samples written to it and the duration of the
+ * last sample will be same as that of the sample before that.
+ *
+ * @param videoDurationUs The duration of the video track (in microseconds) in the output, or
+ * {@link C#TIME_UNSET} to not set any duration. Only applicable when a video track is
+ * {@linkplain #addTrack(Format) added}.
+ * @return This factory.
+ */
+ @CanIgnoreReturnValue
+ public Factory setVideoDurationUs(long videoDurationUs) {
+ this.videoDurationUs = videoDurationUs;
+ return this;
+ }
+
+ @Override
+ public InAppFragmentedMp4Muxer create(String path) throws MuxerException {
+ FileOutputStream outputStream;
+ try {
+ outputStream = new FileOutputStream(path);
+ } catch (FileNotFoundException e) {
+ throw new MuxerException("Error creating file output stream", e);
+ }
+
+ FragmentedMp4Muxer.Builder builder = new FragmentedMp4Muxer.Builder(outputStream);
+ if (fragmentDurationMs != C.TIME_UNSET) {
+ builder.setFragmentDurationMs(fragmentDurationMs);
+ }
+ FragmentedMp4Muxer muxer = builder.build();
+
+ return new InAppFragmentedMp4Muxer(muxer, videoDurationUs);
+ }
+
+ @Override
+ public ImmutableList getSupportedSampleMimeTypes(@C.TrackType int trackType) {
+ if (trackType == C.TRACK_TYPE_VIDEO) {
+ return SUPPORTED_VIDEO_SAMPLE_MIME_TYPES;
+ } else if (trackType == C.TRACK_TYPE_AUDIO) {
+ return SUPPORTED_AUDIO_SAMPLE_MIME_TYPES;
+ }
+ return ImmutableList.of();
+ }
+ }
+
+ private static final String TAG = "InAppFragmentedMp4Muxer";
+ private static final int TRACK_ID_UNSET = -1;
+
+ private final FragmentedMp4Muxer muxer;
+ private final long videoDurationUs;
+
+ private int videoTrackId;
+
+ private InAppFragmentedMp4Muxer(FragmentedMp4Muxer muxer, long videoDurationUs) {
+ this.muxer = muxer;
+ this.videoDurationUs = videoDurationUs;
+ videoTrackId = TRACK_ID_UNSET;
+ }
+
+ @Override
+ public int addTrack(Format format) {
+ int trackId = muxer.addTrack(format);
+ if (MimeTypes.isVideo(format.sampleMimeType)) {
+ muxer.addMetadataEntry(new Mp4OrientationData(format.rotationDegrees));
+ videoTrackId = trackId;
+ }
+ return trackId;
+ }
+
+ @Override
+ public void writeSampleData(int trackId, ByteBuffer byteBuffer, BufferInfo bufferInfo)
+ throws MuxerException {
+ if (videoDurationUs != C.TIME_UNSET
+ && trackId == videoTrackId
+ && bufferInfo.presentationTimeUs > videoDurationUs) {
+ Log.w(
+ TAG,
+ String.format(
+ Locale.US,
+ "Skipped sample with presentation time (%d) > video duration (%d)",
+ bufferInfo.presentationTimeUs,
+ videoDurationUs));
+ return;
+ }
+ muxer.writeSampleData(trackId, byteBuffer, bufferInfo);
+ }
+
+ @Override
+ public void addMetadataEntry(Metadata.Entry metadataEntry) {
+ if (MuxerUtil.isMetadataSupported(metadataEntry)) {
+ muxer.addMetadataEntry(metadataEntry);
+ }
+ }
+
+ @Override
+ public void close() throws MuxerException {
+ if (videoDurationUs != C.TIME_UNSET && videoTrackId != TRACK_ID_UNSET) {
+ BufferInfo bufferInfo = new BufferInfo();
+ bufferInfo.set(
+ /* newOffset= */ 0,
+ /* newSize= */ 0,
+ videoDurationUs,
+ MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ writeSampleData(videoTrackId, ByteBuffer.allocateDirect(0), bufferInfo);
+ }
+ muxer.close();
+ }
+}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMp4Muxer.java
similarity index 69%
rename from libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java
rename to libraries/transformer/src/main/java/androidx/media3/transformer/InAppMp4Muxer.java
index ddfd1fc325..61a7c77e91 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMp4Muxer.java
@@ -27,7 +27,6 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.container.Mp4OrientationData;
-import androidx.media3.muxer.FragmentedMp4Muxer;
import androidx.media3.muxer.Mp4Muxer;
import androidx.media3.muxer.Muxer;
import androidx.media3.muxer.MuxerException;
@@ -41,10 +40,9 @@ import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
-/** {@link Muxer} implementation that uses an {@link Mp4Muxer} or {@link FragmentedMp4Muxer}. */
+/** {@link Muxer} implementation that uses an {@link Mp4Muxer}. */
@UnstableApi
-public final class InAppMuxer implements Muxer {
-
+public final class InAppMp4Muxer implements Muxer {
/** Provides {@linkplain Metadata.Entry metadata} to add in the output MP4 file. */
public interface MetadataProvider {
@@ -60,57 +58,9 @@ public final class InAppMuxer implements Muxer {
void updateMetadataEntries(Set metadataEntries);
}
- /** {@link Muxer.Factory} for {@link InAppMuxer}. */
+ /** {@link Muxer.Factory} for {@link InAppMp4Muxer}. */
public static final class Factory implements Muxer.Factory {
-
- /** A builder for {@link Factory} instances. */
- public static final class Builder {
- @Nullable private MetadataProvider metadataProvider;
- private boolean outputFragmentedMp4;
- private long fragmentDurationMs;
-
- /** Creates a {@link Builder} instance with default values. */
- public Builder() {
- fragmentDurationMs = C.TIME_UNSET;
- }
-
- /**
- * Sets an implementation of {@link MetadataProvider}.
- *
- * The default value is {@code null}.
- *
- *
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;
- }
-
- /** Sets whether to output a fragmented MP4. */
- @CanIgnoreReturnValue
- public Builder setOutputFragmentedMp4(boolean outputFragmentedMp4) {
- this.outputFragmentedMp4 = outputFragmentedMp4;
- return this;
- }
-
- /**
- * Sets the fragment duration (in milliseconds) if the output file is {@link
- * #setOutputFragmentedMp4(boolean) fragmented}.
- */
- @CanIgnoreReturnValue
- public Builder setFragmentDurationMs(long fragmentDurationMs) {
- this.fragmentDurationMs = fragmentDurationMs;
- return this;
- }
-
- /** Builds a {@link Factory} instance. */
- public Factory build() {
- return new Factory(metadataProvider, outputFragmentedMp4, fragmentDurationMs);
- }
- }
-
+ // TODO: b/372417042 - Move these lists to Mp4Muxer.
/** A list of supported video sample MIME types. */
private static final ImmutableList SUPPORTED_VIDEO_SAMPLE_MIME_TYPES =
ImmutableList.of(
@@ -130,18 +80,21 @@ public final class InAppMuxer implements Muxer {
MimeTypes.AUDIO_VORBIS);
@Nullable private final MetadataProvider metadataProvider;
- private final boolean outputFragmentedMp4;
- private final long fragmentDurationMs;
private long videoDurationUs;
- private Factory(
- @Nullable MetadataProvider metadataProvider,
- boolean outputFragmentedMp4,
- long fragmentDurationMs) {
+ /** Creates an instance with default values. */
+ public Factory() {
+ this(/* metadataProvider= */ null);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param metadataProvider A {@link MetadataProvider}.
+ */
+ public Factory(@Nullable MetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider;
- this.outputFragmentedMp4 = outputFragmentedMp4;
- this.fragmentDurationMs = fragmentDurationMs;
videoDurationUs = C.TIME_UNSET;
}
@@ -167,7 +120,7 @@ public final class InAppMuxer implements Muxer {
}
@Override
- public InAppMuxer create(String path) throws MuxerException {
+ public InAppMp4Muxer create(String path) throws MuxerException {
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(path);
@@ -175,23 +128,14 @@ public final class InAppMuxer implements Muxer {
throw new MuxerException("Error creating file output stream", e);
}
- Muxer muxer = null;
- if (outputFragmentedMp4) {
- FragmentedMp4Muxer.Builder builder = new FragmentedMp4Muxer.Builder(outputStream);
- if (fragmentDurationMs != C.TIME_UNSET) {
- builder.setFragmentDurationMs(fragmentDurationMs);
- }
- muxer = builder.build();
- } else {
- Mp4Muxer.Builder builder = new Mp4Muxer.Builder(outputStream);
- if (videoDurationUs != C.TIME_UNSET) {
- builder.setLastSampleDurationBehavior(
- LAST_SAMPLE_DURATION_BEHAVIOR_SET_FROM_END_OF_STREAM_BUFFER_OR_DUPLICATE_PREVIOUS);
- }
- muxer = builder.build();
+ Mp4Muxer.Builder builder = new Mp4Muxer.Builder(outputStream);
+ if (videoDurationUs != C.TIME_UNSET) {
+ builder.setLastSampleDurationBehavior(
+ LAST_SAMPLE_DURATION_BEHAVIOR_SET_FROM_END_OF_STREAM_BUFFER_OR_DUPLICATE_PREVIOUS);
}
+ Mp4Muxer muxer = builder.build();
- return new InAppMuxer(muxer, metadataProvider, videoDurationUs);
+ return new InAppMp4Muxer(muxer, metadataProvider, videoDurationUs);
}
@Override
@@ -205,18 +149,18 @@ public final class InAppMuxer implements Muxer {
}
}
- private static final String TAG = "InAppMuxer";
+ private static final String TAG = "InAppMp4Muxer";
private static final int TRACK_ID_UNSET = -1;
- private final Muxer muxer;
+ private final Mp4Muxer muxer;
@Nullable private final MetadataProvider metadataProvider;
private final long videoDurationUs;
private final Set metadataEntries;
private int videoTrackId;
- private InAppMuxer(
- Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) {
+ private InAppMp4Muxer(
+ Mp4Muxer muxer, @Nullable MetadataProvider metadataProvider, long videoDurationUs) {
this.muxer = muxer;
this.metadataProvider = metadataProvider;
this.videoDurationUs = videoDurationUs;
diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java
index b81ae93a95..7970c89dc7 100644
--- a/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java
+++ b/libraries/transformer/src/test/java/androidx/media3/transformer/EncodedSampleExporterTest.java
@@ -66,7 +66,7 @@ public final class EncodedSampleExporterTest {
new TransformationRequest.Builder().build(),
new MuxerWrapper(
/* outputPath= */ "unused",
- new InAppMuxer.Factory.Builder().build(),
+ new InAppMp4Muxer.Factory(),
mock(MuxerWrapper.Listener.class),
MuxerWrapper.MUXER_MODE_DEFAULT,
/* dropSamplesBeforeFirstVideoSample= */ false,
diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndParameterizedTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndParameterizedTest.java
similarity index 88%
rename from libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndParameterizedTest.java
rename to libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndParameterizedTest.java
index 1113e5f21a..8bf9b0ed42 100644
--- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndParameterizedTest.java
+++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndParameterizedTest.java
@@ -39,9 +39,9 @@ import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
-/** End to end parameterized tests for {@link Transformer} with {@link InAppMuxer}. */
+/** End to end parameterized tests for {@link Transformer} with {@link InAppMp4Muxer}. */
@RunWith(ParameterizedRobolectricTestRunner.class)
-public class TransformerWithInAppMuxerEndToEndParameterizedTest {
+public class TransformerWithInAppMp4MuxerEndToEndParameterizedTest {
private static final String H263_3GP = "mp4/bbb_176x144_128kbps_15fps_h263.3gp";
private static final String H264_MP4 = "mp4/sample_no_bframes.mp4";
@@ -84,15 +84,13 @@ public class TransformerWithInAppMuxerEndToEndParameterizedTest {
@Test
public void transmux_mp4File_outputMatchesExpected() throws Exception {
Muxer.Factory inAppMuxerFactory =
- new InAppMuxer.Factory.Builder()
- .setMetadataProvider(
- metadataEntries ->
- // Add timestamp to make output file deterministic.
- metadataEntries.add(
- new Mp4TimestampData(
- /* creationTimestampSeconds= */ 3_000_000_000L,
- /* modificationTimestampSeconds= */ 4_000_000_000L)))
- .build();
+ new InAppMp4Muxer.Factory(
+ metadataEntries ->
+ // Add timestamp to make output file deterministic.
+ metadataEntries.add(
+ new Mp4TimestampData(
+ /* creationTimestampSeconds= */ 3_000_000_000L,
+ /* modificationTimestampSeconds= */ 4_000_000_000L)));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndTest.java
similarity index 86%
rename from libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java
rename to libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndTest.java
index 39f2abc4b4..0baa1ca0ef 100644
--- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMuxerEndToEndNonParameterizedTest.java
+++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerWithInAppMp4MuxerEndToEndTest.java
@@ -47,9 +47,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
-/** End-to-end test for {@link Transformer} with {@link InAppMuxer}. */
+/** End-to-end test for {@link Transformer} with {@link InAppMp4Muxer}. */
@RunWith(AndroidJUnit4.class)
-public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
+public class TransformerWithInAppMp4MuxerEndToEndTest {
private static final String MP4_FILE_PATH = "asset:///media/mp4/sample_no_bframes.mp4";
@Rule public final TemporaryFolder outputDir = new TemporaryFolder();
@@ -67,15 +67,13 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
String tsFilePath = "asset:///media/ts/sample_h264.ts";
String tsFileName = "ts/sample_h264.ts";
Muxer.Factory inAppMuxerFactory =
- new InAppMuxer.Factory.Builder()
- .setMetadataProvider(
- metadataEntries ->
- // Add timestamp to make output file deterministic.
- metadataEntries.add(
- new Mp4TimestampData(
- /* creationTimestampSeconds= */ 3_000_000_000L,
- /* modificationTimestampSeconds= */ 4_000_000_000L)))
- .build();
+ new InAppMp4Muxer.Factory(
+ metadataEntries ->
+ // Add timestamp to make output file deterministic.
+ metadataEntries.add(
+ new Mp4TimestampData(
+ /* creationTimestampSeconds= */ 3_000_000_000L,
+ /* modificationTimestampSeconds= */ 4_000_000_000L)));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
@@ -104,14 +102,11 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
Mp4LocationData expectedLocationData =
new Mp4LocationData(/* latitude= */ 45f, /* longitude= */ -90f);
Muxer.Factory inAppMuxerFactory =
- new InAppMuxer.Factory.Builder()
- .setMetadataProvider(
- metadataEntries -> {
- metadataEntries.removeIf(
- (Metadata.Entry entry) -> entry instanceof Mp4LocationData);
- metadataEntries.add(expectedLocationData);
- })
- .build();
+ new InAppMp4Muxer.Factory(
+ metadataEntries -> {
+ metadataEntries.removeIf((Metadata.Entry entry) -> entry instanceof Mp4LocationData);
+ metadataEntries.add(expectedLocationData);
+ });
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@@ -130,9 +125,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
String xmpSampleData = "media/xmp/sample_datetime_xmp.xmp";
byte[] xmpData = androidx.media3.test.utils.TestUtil.getByteArray(context, xmpSampleData);
Muxer.Factory inAppMuxerFactory =
- new InAppMuxer.Factory.Builder()
- .setMetadataProvider(metadataEntries -> metadataEntries.add(new XmpData(xmpData)))
- .build();
+ new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(new XmpData(xmpData)));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@@ -153,9 +146,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
/* value= */ Util.toByteArray(captureFps),
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32);
Muxer.Factory inAppMuxerFactory =
- new InAppMuxer.Factory.Builder()
- .setMetadataProvider(metadataEntries -> metadataEntries.add(expectedCaptureFps))
- .build();
+ new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(expectedCaptureFps));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@@ -181,9 +172,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
/* creationTimestampSeconds= */ 3_000_000_000L,
/* modificationTimestampSeconds= */ 4_000_000_000L);
Muxer.Factory inAppMuxerFactory =
- new InAppMuxer.Factory.Builder()
- .setMetadataProvider(metadataEntries -> metadataEntries.add(expectedTimestampData))
- .build();
+ new InAppMp4Muxer.Factory(metadataEntries -> metadataEntries.add(expectedTimestampData));
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
@@ -212,13 +201,11 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
/* value= */ Util.toByteArray(600.0f),
MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32);
Muxer.Factory inAppMuxerFactory =
- new InAppMuxer.Factory.Builder()
- .setMetadataProvider(
- metadataEntries -> {
- metadataEntries.add(expectedStringMetadata);
- metadataEntries.add(expectedFloatMetadata);
- })
- .build();
+ new InAppMp4Muxer.Factory(
+ metadataEntries -> {
+ metadataEntries.add(expectedStringMetadata);
+ metadataEntries.add(expectedFloatMetadata);
+ });
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(inAppMuxerFactory).build();
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(MP4_FILE_PATH));
@@ -248,7 +235,7 @@ public class TransformerWithInAppMuxerEndToEndNonParameterizedTest {
@Test
public void transmux_withSettingVideoDuration_writesCorrectVideoDuration() throws Exception {
- InAppMuxer.Factory inAppMuxerFactory = new InAppMuxer.Factory.Builder().build();
+ InAppMp4Muxer.Factory inAppMuxerFactory = new InAppMp4Muxer.Factory();
long expectedDurationUs = 2_000_000L;
inAppMuxerFactory.setVideoDurationUs(expectedDurationUs);
Transformer transformer =