diff --git a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndNonParameterizedAndroidTest.java b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndNonParameterizedAndroidTest.java
index 4bf58117ea..3db14aced9 100644
--- a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndNonParameterizedAndroidTest.java
+++ b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndNonParameterizedAndroidTest.java
@@ -113,4 +113,63 @@ public class Mp4MuxerEndToEndNonParameterizedAndroidTest {
/*DumpFileAsserts.assertOutput(
context, fakeExtractorOutput, AndroidMuxerTestUtil.getExpectedDumpFilePath(vp9Mp4));*/
}
+
+ @Test
+ public void createMp4File_withSampleBatchingDisabled_matchesExpected() throws Exception {
+ @Nullable Mp4Muxer mp4Muxer = null;
+
+ try {
+ mp4Muxer =
+ new Mp4Muxer.Builder(checkNotNull(outputStream)).setSampleBatchingEnabled(false).build();
+ mp4Muxer.addMetadataEntry(
+ new Mp4TimestampData(
+ /* creationTimestampSeconds= */ 100_000_000L,
+ /* modificationTimestampSeconds= */ 500_000_000L));
+ feedInputDataToMuxer(context, mp4Muxer, checkNotNull(H265_HDR10_MP4));
+ } finally {
+ if (mp4Muxer != null) {
+ mp4Muxer.close();
+ }
+ }
+
+ FakeExtractorOutput fakeExtractorOutput =
+ TestUtil.extractAllSamplesFromFilePath(
+ new Mp4Extractor(new DefaultSubtitleParserFactory()), checkNotNull(outputPath));
+ DumpFileAsserts.assertOutput(
+ context,
+ fakeExtractorOutput,
+ AndroidMuxerTestUtil.getExpectedDumpFilePath("sample_batching_disabled_" + H265_HDR10_MP4));
+ }
+
+ @Test
+ public void createMp4File_withSampleBatchingAndAttemptStreamableOutputDisabled_matchesExpected()
+ throws Exception {
+ @Nullable Mp4Muxer mp4Muxer = null;
+
+ try {
+ mp4Muxer =
+ new Mp4Muxer.Builder(checkNotNull(outputStream))
+ .setSampleBatchingEnabled(false)
+ .setAttemptStreamableOutputEnabled(false)
+ .build();
+ mp4Muxer.addMetadataEntry(
+ new Mp4TimestampData(
+ /* creationTimestampSeconds= */ 100_000_000L,
+ /* modificationTimestampSeconds= */ 500_000_000L));
+ feedInputDataToMuxer(context, mp4Muxer, checkNotNull(H265_HDR10_MP4));
+ } finally {
+ if (mp4Muxer != null) {
+ mp4Muxer.close();
+ }
+ }
+
+ FakeExtractorOutput fakeExtractorOutput =
+ TestUtil.extractAllSamplesFromFilePath(
+ new Mp4Extractor(new DefaultSubtitleParserFactory()), checkNotNull(outputPath));
+ DumpFileAsserts.assertOutput(
+ context,
+ fakeExtractorOutput,
+ AndroidMuxerTestUtil.getExpectedDumpFilePath(
+ "sample_batching_and_attempt_streamable_output_disabled_" + H265_HDR10_MP4));
+ }
}
diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java
index 7222971350..4e5af47d52 100644
--- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java
+++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java
@@ -200,6 +200,7 @@ public final class Mp4Muxer implements Muxer {
private @LastSampleDurationBehavior int lastSampleDurationBehavior;
@Nullable private AnnexBToAvccConverter annexBToAvccConverter;
private boolean sampleCopyEnabled;
+ private boolean sampleBatchingEnabled;
private boolean attemptStreamableOutputEnabled;
private @FileFormat int outputFileFormat;
@Nullable private EditableVideoParameters editableVideoParameters;
@@ -214,6 +215,7 @@ public final class Mp4Muxer implements Muxer {
lastSampleDurationBehavior =
LAST_SAMPLE_DURATION_BEHAVIOR_SET_FROM_END_OF_STREAM_BUFFER_OR_DUPLICATE_PREVIOUS;
sampleCopyEnabled = true;
+ sampleBatchingEnabled = true;
attemptStreamableOutputEnabled = true;
outputFileFormat = FILE_FORMAT_DEFAULT;
}
@@ -260,6 +262,21 @@ public final class Mp4Muxer implements Muxer {
return this;
}
+ /**
+ * Sets whether to enable sample batching.
+ *
+ *
If sample batching is enabled, samples are {@linkplain #writeSampleData(TrackToken,
+ * ByteBuffer, BufferInfo) written} in batches for each track, otherwise samples are written as
+ * they arrive.
+ *
+ *
The default value is {@code true}.
+ */
+ @CanIgnoreReturnValue
+ public Mp4Muxer.Builder setSampleBatchingEnabled(boolean enabled) {
+ this.sampleBatchingEnabled = enabled;
+ return this;
+ }
+
/**
* Sets whether to attempt to write a file where the metadata is stored at the start, which can
* make the file more efficient to read sequentially.
@@ -309,6 +326,7 @@ public final class Mp4Muxer implements Muxer {
lastSampleDurationBehavior,
annexBToAvccConverter == null ? AnnexBToAvccConverter.DEFAULT : annexBToAvccConverter,
sampleCopyEnabled,
+ sampleBatchingEnabled,
attemptStreamableOutputEnabled,
outputFileFormat,
editableVideoParameters);
@@ -322,6 +340,7 @@ public final class Mp4Muxer implements Muxer {
private final @LastSampleDurationBehavior int lastSampleDurationBehavior;
private final AnnexBToAvccConverter annexBToAvccConverter;
private final boolean sampleCopyEnabled;
+ private final boolean sampleBatchingEnabled;
private final boolean attemptStreamableOutputEnabled;
private final @FileFormat int outputFileFormat;
@Nullable private final EditableVideoParameters editableVideoParameters;
@@ -339,6 +358,7 @@ public final class Mp4Muxer implements Muxer {
@LastSampleDurationBehavior int lastFrameDurationBehavior,
AnnexBToAvccConverter annexBToAvccConverter,
boolean sampleCopyEnabled,
+ boolean sampleBatchingEnabled,
boolean attemptStreamableOutputEnabled,
@FileFormat int outputFileFormat,
@Nullable EditableVideoParameters editableVideoParameters) {
@@ -347,6 +367,7 @@ public final class Mp4Muxer implements Muxer {
this.lastSampleDurationBehavior = lastFrameDurationBehavior;
this.annexBToAvccConverter = annexBToAvccConverter;
this.sampleCopyEnabled = sampleCopyEnabled;
+ this.sampleBatchingEnabled = sampleBatchingEnabled;
this.attemptStreamableOutputEnabled = attemptStreamableOutputEnabled;
this.outputFileFormat = outputFileFormat;
this.editableVideoParameters = editableVideoParameters;
@@ -358,6 +379,7 @@ public final class Mp4Muxer implements Muxer {
annexBToAvccConverter,
lastFrameDurationBehavior,
sampleCopyEnabled,
+ sampleBatchingEnabled,
attemptStreamableOutputEnabled);
editableVideoTracks = new ArrayList<>();
}
@@ -415,10 +437,13 @@ public final class Mp4Muxer implements Muxer {
/**
* {@inheritDoc}
*
- *
Samples are written to the file in batches. If {@link Builder#setSampleCopyEnabled(boolean)
- * sample copying} is disabled, the {@code byteBuffer} and the {@code bufferInfo} must not be
- * modified after calling this method. Otherwise, they are copied and it is safe to modify them
- * after this method returns.
+ *
When sample batching is {@linkplain Mp4Muxer.Builder#setSampleBatchingEnabled(boolean)
+ * enabled}, provide sample data ({@link ByteBuffer}, {@link BufferInfo}) that won't be modified
+ * after calling the {@link #writeSampleData(TrackToken, ByteBuffer, BufferInfo)} method, unless
+ * sample copying is also {@linkplain Mp4Muxer.Builder#setSampleCopyEnabled(boolean) enabled}.
+ * This ensures data integrity within the batch. If sample copying is {@linkplain
+ * Mp4Muxer.Builder#setSampleCopyEnabled(boolean) enabled}, it's safe to modify the data after the
+ * method returns, as the muxer internally creates a sample copy.
*
* @param trackToken The {@link TrackToken} for which this sample is being written.
* @param byteBuffer The encoded sample. The muxer takes ownership of the buffer if {@link
@@ -522,6 +547,7 @@ public final class Mp4Muxer implements Muxer {
annexBToAvccConverter,
lastSampleDurationBehavior,
sampleCopyEnabled,
+ sampleBatchingEnabled,
attemptStreamableOutputEnabled);
}
}
diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Writer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Writer.java
index 2a5639c39d..6fd67dd2d6 100644
--- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Writer.java
+++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Writer.java
@@ -43,6 +43,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
/** Writes all media samples into a single mdat box. */
/* package */ final class Mp4Writer {
private static final long INTERLEAVE_DURATION_US = 1_000_000L;
+ // Used for updating the moov box periodically when sample batching is disabled.
+ private static final long MOOV_BOX_UPDATE_INTERVAL_US = 1_000_000L;
private static final int DEFAULT_MOOV_BOX_SIZE_BYTES = 400_000;
private static final String FREE_BOX_TYPE = "free";
@@ -51,6 +53,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final AnnexBToAvccConverter annexBToAvccConverter;
private final @Mp4Muxer.LastSampleDurationBehavior int lastSampleDurationBehavior;
private final boolean sampleCopyEnabled;
+ private final boolean sampleBatchingEnabled;
private final List