diff --git a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/AndroidMuxerTestUtil.java b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/AndroidMuxerTestUtil.java index bfd4c0acd6..5b79492e41 100644 --- a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/AndroidMuxerTestUtil.java +++ b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/AndroidMuxerTestUtil.java @@ -15,8 +15,18 @@ */ package androidx.media3.muxer; +import android.content.Context; +import android.media.MediaCodec; +import android.media.MediaExtractor; +import androidx.media3.common.util.MediaFormatUtil; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + /** Utilities for muxer test cases. */ /* package */ final class AndroidMuxerTestUtil { + private static final String MP4_FILE_ASSET_DIRECTORY = "media/mp4/"; private static final String DUMP_FILE_OUTPUT_DIRECTORY = "muxerdumps"; private static final String DUMP_FILE_EXTENSION = "dump"; @@ -25,4 +35,38 @@ package androidx.media3.muxer; public static String getExpectedDumpFilePath(String originalFileName) { return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION; } + + public static void feedInputDataToMuxer(Context context, Muxer muxer, String inputFileName) + throws IOException { + MediaExtractor extractor = new MediaExtractor(); + extractor.setDataSource( + context.getResources().getAssets().openFd(MP4_FILE_ASSET_DIRECTORY + inputFileName)); + + List addedTracks = new ArrayList<>(); + for (int i = 0; i < extractor.getTrackCount(); i++) { + Muxer.TrackToken trackToken = + muxer.addTrack(MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i))); + addedTracks.add(trackToken); + extractor.selectTrack(i); + } + + do { + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + bufferInfo.flags = extractor.getSampleFlags(); + bufferInfo.offset = 0; + bufferInfo.presentationTimeUs = extractor.getSampleTime(); + int sampleSize = (int) extractor.getSampleSize(); + bufferInfo.size = sampleSize; + + ByteBuffer sampleBuffer = ByteBuffer.allocateDirect(sampleSize); + extractor.readSampleData(sampleBuffer, /* offset= */ 0); + + sampleBuffer.rewind(); + + muxer.writeSampleData( + addedTracks.get(extractor.getSampleTrackIndex()), sampleBuffer, bufferInfo); + } while (extractor.advance()); + + extractor.release(); + } } diff --git a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/FragmentedMp4MuxerEndToEndAndroidTest.java b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/FragmentedMp4MuxerEndToEndAndroidTest.java new file mode 100644 index 0000000000..3bd3699232 --- /dev/null +++ b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/FragmentedMp4MuxerEndToEndAndroidTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.muxer; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.muxer.AndroidMuxerTestUtil.feedInputDataToMuxer; + +import android.content.Context; +import androidx.annotation.Nullable; +import androidx.media3.container.Mp4TimestampData; +import androidx.media3.extractor.mp4.FragmentedMp4Extractor; +import androidx.media3.test.utils.DumpFileAsserts; +import androidx.media3.test.utils.DumpableMp4Box; +import androidx.media3.test.utils.FakeExtractorOutput; +import androidx.media3.test.utils.TestUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +/** End to end instrumentation tests for {@link FragmentedMp4Muxer}. */ +@RunWith(AndroidJUnit4.class) +public class FragmentedMp4MuxerEndToEndAndroidTest { + private static final String H265_HDR10_MP4 = "hdr10-720p.mp4"; + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final Context context = ApplicationProvider.getApplicationContext(); + private @MonotonicNonNull String outputPath; + private @MonotonicNonNull FileOutputStream outputStream; + + @Before + public void setUp() throws Exception { + outputPath = temporaryFolder.newFile("muxeroutput.mp4").getPath(); + outputStream = new FileOutputStream(outputPath); + } + + @After + public void tearDown() throws IOException { + checkNotNull(outputStream).close(); + } + + @Test + public void createFragmentedMp4File_fromInputFileSampleData_matchesExpected() throws IOException { + @Nullable Muxer fragmentedMp4Muxer = null; + + try { + fragmentedMp4Muxer = new FragmentedMp4Muxer(checkNotNull(outputStream)); + fragmentedMp4Muxer.addMetadata( + new Mp4TimestampData( + /* creationTimestampSeconds= */ 100_000_000L, + /* modificationTimestampSeconds= */ 500_000_000L)); + feedInputDataToMuxer(context, fragmentedMp4Muxer, H265_HDR10_MP4); + } finally { + if (fragmentedMp4Muxer != null) { + fragmentedMp4Muxer.close(); + } + } + + FakeExtractorOutput fakeExtractorOutput = + TestUtil.extractAllSamplesFromFilePath( + new FragmentedMp4Extractor(), checkNotNull(outputPath)); + DumpFileAsserts.assertOutput( + context, + fakeExtractorOutput, + AndroidMuxerTestUtil.getExpectedDumpFilePath(H265_HDR10_MP4 + "_fragmented")); + } + + @Test + public void createFragmentedMp4File_fromInputFileSampleData_matchesExpectedBoxStructure() + throws IOException { + @Nullable Muxer fragmentedMp4Muxer = null; + + try { + fragmentedMp4Muxer = new FragmentedMp4Muxer(checkNotNull(outputStream)); + fragmentedMp4Muxer.addMetadata( + new Mp4TimestampData( + /* creationTimestampSeconds= */ 100_000_000L, + /* modificationTimestampSeconds= */ 500_000_000L)); + feedInputDataToMuxer(context, fragmentedMp4Muxer, H265_HDR10_MP4); + } finally { + if (fragmentedMp4Muxer != null) { + fragmentedMp4Muxer.close(); + } + } + + DumpableMp4Box dumpableMp4Box = + new DumpableMp4Box( + ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(checkNotNull(outputPath)))); + DumpFileAsserts.assertOutput( + context, + dumpableMp4Box, + AndroidMuxerTestUtil.getExpectedDumpFilePath(H265_HDR10_MP4 + "_fragmented_box_structure")); + } +} diff --git a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndAndroidTest.java b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndAndroidTest.java index 5eaed5ff9b..11a47f10b9 100644 --- a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndAndroidTest.java +++ b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndAndroidTest.java @@ -16,27 +16,20 @@ package androidx.media3.muxer; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.muxer.AndroidMuxerTestUtil.feedInputDataToMuxer; import static org.junit.Assume.assumeTrue; import android.content.Context; -import android.media.MediaCodec; -import android.media.MediaExtractor; import androidx.annotation.Nullable; -import androidx.media3.common.util.MediaFormatUtil; import androidx.media3.container.Mp4TimestampData; -import androidx.media3.extractor.mp4.FragmentedMp4Extractor; import androidx.media3.extractor.mp4.Mp4Extractor; import androidx.media3.test.utils.DumpFileAsserts; -import androidx.media3.test.utils.DumpableMp4Box; import androidx.media3.test.utils.FakeExtractorOutput; import androidx.media3.test.utils.TestUtil; import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.After; import org.junit.Before; @@ -64,7 +57,6 @@ public class Mp4MuxerEndToEndAndroidTest { @Parameter public @MonotonicNonNull String inputFile; @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - private static final String MP4_FILE_ASSET_DIRECTORY = "media/mp4/"; private final Context context = ApplicationProvider.getApplicationContext(); private @MonotonicNonNull String outputPath; private @MonotonicNonNull FileOutputStream outputStream; @@ -90,7 +82,7 @@ public class Mp4MuxerEndToEndAndroidTest { new Mp4TimestampData( /* creationTimestampSeconds= */ 100_000_000L, /* modificationTimestampSeconds= */ 500_000_000L)); - feedInputDataToMuxer(mp4Muxer, checkNotNull(inputFile)); + feedInputDataToMuxer(context, mp4Muxer, checkNotNull(inputFile)); } finally { if (mp4Muxer != null) { mp4Muxer.close(); @@ -114,7 +106,7 @@ public class Mp4MuxerEndToEndAndroidTest { new Mp4TimestampData( /* creationTimestampSeconds= */ 100_000_000L, /* modificationTimestampSeconds= */ 500_000_000L)); - feedInputDataToMuxer(mp4Muxer, inputFile); + feedInputDataToMuxer(context, mp4Muxer, inputFile); // Muxer not closed. @@ -128,100 +120,4 @@ public class Mp4MuxerEndToEndAndroidTest { fakeExtractorOutput, AndroidMuxerTestUtil.getExpectedDumpFilePath("partial_" + inputFile)); } - - @Test - public void createFragmentedMp4File_fromInputFileSampleData_matchesExpected() throws IOException { - // Test case doesn't need to be parameterized, so skip all but one input file to avoid creating - // many dump files. - assumeTrue(checkNotNull(inputFile).equals(H265_HDR10_MP4)); - @Nullable Mp4Muxer mp4Muxer = null; - - try { - mp4Muxer = - new Mp4Muxer.Builder(checkNotNull(outputStream)).setFragmentedMp4Enabled(true).build(); - mp4Muxer.addMetadata( - new Mp4TimestampData( - /* creationTimestampSeconds= */ 100_000_000L, - /* modificationTimestampSeconds= */ 500_000_000L)); - feedInputDataToMuxer(mp4Muxer, inputFile); - } finally { - if (mp4Muxer != null) { - mp4Muxer.close(); - } - } - - FakeExtractorOutput fakeExtractorOutput = - TestUtil.extractAllSamplesFromFilePath( - new FragmentedMp4Extractor(), checkNotNull(outputPath)); - DumpFileAsserts.assertOutput( - context, - fakeExtractorOutput, - AndroidMuxerTestUtil.getExpectedDumpFilePath(inputFile + "_fragmented")); - } - - @Test - public void createFragmentedMp4File_fromInputFileSampleData_matchesExpectedBoxStructure() - throws IOException { - // Test case doesn't need to be parameterized, so skip all but one input file to avoid creating - // many dump files. - assumeTrue(checkNotNull(inputFile).equals(H265_HDR10_MP4)); - @Nullable Mp4Muxer mp4Muxer = null; - - try { - mp4Muxer = - new Mp4Muxer.Builder(checkNotNull(outputStream)).setFragmentedMp4Enabled(true).build(); - mp4Muxer.addMetadata( - new Mp4TimestampData( - /* creationTimestampSeconds= */ 100_000_000L, - /* modificationTimestampSeconds= */ 500_000_000L)); - feedInputDataToMuxer(mp4Muxer, inputFile); - } finally { - if (mp4Muxer != null) { - mp4Muxer.close(); - } - } - - DumpableMp4Box dumpableMp4Box = - new DumpableMp4Box( - ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(checkNotNull(outputPath)))); - DumpFileAsserts.assertOutput( - context, - dumpableMp4Box, - AndroidMuxerTestUtil.getExpectedDumpFilePath(inputFile + "_fragmented_box_structure")); - } - - private void feedInputDataToMuxer(Mp4Muxer mp4Muxer, String inputFileName) throws IOException { - MediaExtractor extractor = new MediaExtractor(); - extractor.setDataSource( - context.getResources().getAssets().openFd(MP4_FILE_ASSET_DIRECTORY + inputFileName)); - - List addedTracks = new ArrayList<>(); - int sortKey = 0; - for (int i = 0; i < extractor.getTrackCount(); i++) { - Mp4Muxer.TrackToken trackToken = - mp4Muxer.addTrack( - sortKey++, MediaFormatUtil.createFormatFromMediaFormat(extractor.getTrackFormat(i))); - addedTracks.add(trackToken); - extractor.selectTrack(i); - } - - do { - MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - bufferInfo.flags = extractor.getSampleFlags(); - bufferInfo.offset = 0; - bufferInfo.presentationTimeUs = extractor.getSampleTime(); - int sampleSize = (int) extractor.getSampleSize(); - bufferInfo.size = sampleSize; - - ByteBuffer sampleBuffer = ByteBuffer.allocateDirect(sampleSize); - extractor.readSampleData(sampleBuffer, /* offset= */ 0); - - sampleBuffer.rewind(); - - mp4Muxer.writeSampleData( - addedTracks.get(extractor.getSampleTrackIndex()), sampleBuffer, bufferInfo); - } while (extractor.advance()); - - extractor.release(); - } } diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java new file mode 100644 index 0000000000..3c3882c031 --- /dev/null +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Muxer.java @@ -0,0 +1,144 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.muxer; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.media.MediaCodec; +import androidx.media3.common.Format; +import androidx.media3.common.Metadata; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.container.MdtaMetadataEntry; +import androidx.media3.container.Mp4LocationData; +import androidx.media3.container.Mp4OrientationData; +import androidx.media3.container.Mp4TimestampData; +import androidx.media3.container.XmpData; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A muxer for creating a fragmented MP4 file. + * + *

The muxer supports writing H264, H265 and AV1 video, AAC audio and metadata. + * + *

All the operations are performed on the caller thread. + * + *

To create a fragmented MP4 file, the caller must: + * + *

+ * + *

Some key points: + * + *

+ */ +@UnstableApi +public final class FragmentedMp4Muxer implements Muxer { + private static final int DEFAULT_FRAGMENT_DURATION_US = 2_000_000; + + private final FragmentedMp4Writer fragmentedMp4Writer; + private final MetadataCollector metadataCollector; + + /** Creates an instance with default fragment duration. */ + public FragmentedMp4Muxer(FileOutputStream fileOutputStream) { + this(fileOutputStream, DEFAULT_FRAGMENT_DURATION_US); + } + + /** + * Creates an instance. + * + * @param fileOutputStream The {@link FileOutputStream} to write the media data to. + * @param fragmentDurationUs The fragment duration (in microseconds). The muxer will attempt to + * create fragments of the given duration but the actual duration might be greater depending + * upon the frequency of sync samples. + */ + public FragmentedMp4Muxer(FileOutputStream fileOutputStream, int fragmentDurationUs) { + checkNotNull(fileOutputStream); + metadataCollector = new MetadataCollector(); + Mp4MoovStructure moovStructure = + new Mp4MoovStructure( + metadataCollector, Mp4Muxer.LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION); + fragmentedMp4Writer = + new FragmentedMp4Writer( + fileOutputStream, moovStructure, AnnexBToAvccConverter.DEFAULT, fragmentDurationUs); + } + + @Override + public TrackToken addTrack(Format format) { + return fragmentedMp4Writer.addTrack(/* sortKey= */ 1, format); + } + + /** + * {@inheritDoc} + * + *

The samples are cached and are written in batches so the caller must not change the {@link + * ByteBuffer} and the {@link MediaCodec.BufferInfo} after calling this method. + * + *

Note: Out of order B-frames are currently not supported. + * + * @param trackToken The {@link TrackToken} for which this sample is being written. + * @param byteBuffer The encoded sample. + * @param bufferInfo The {@link MediaCodec.BufferInfo} related to this sample. + * @throws IOException If there is any error while writing data to the disk. + */ + @Override + public void writeSampleData( + TrackToken trackToken, ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) + throws IOException { + fragmentedMp4Writer.writeSampleData(trackToken, byteBuffer, bufferInfo); + } + + /** + * {@inheritDoc} + * + *

List of supported {@linkplain Metadata.Entry metadata entries}: + * + *

+ * + * @param metadata The {@linkplain Metadata.Entry metadata}. An {@link IllegalArgumentException} + * is thrown if the {@linkplain Metadata.Entry metadata} is not supported. + */ + @Override + public void addMetadata(Metadata.Entry metadata) { + checkArgument(Mp4Utils.isMetadataSupported(metadata), "Unsupported metadata"); + metadataCollector.addMetadata(metadata); + } + + @Override + public void close() throws IOException { + fragmentedMp4Writer.close(); + } +} 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 76d3d893a4..c75f77d688 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java @@ -17,7 +17,6 @@ package androidx.media3.muxer; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.muxer.Mp4Utils.UNSIGNED_INT_MAX_VALUE; import static java.lang.annotation.ElementType.TYPE_USE; import android.media.MediaCodec.BufferInfo; @@ -25,14 +24,12 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.Format; import androidx.media3.common.Metadata; -import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; import androidx.media3.container.MdtaMetadataEntry; import androidx.media3.container.Mp4LocationData; import androidx.media3.container.Mp4OrientationData; import androidx.media3.container.Mp4TimestampData; import androidx.media3.container.XmpData; -import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.FileOutputStream; import java.io.IOException; @@ -96,8 +93,6 @@ public final class Mp4Muxer implements Muxer { private final FileOutputStream fileOutputStream; private @LastFrameDurationBehavior int lastFrameDurationBehavior; - private boolean fragmentedMp4Enabled; - private int fragmentDurationUs; @Nullable private AnnexBToAvccConverter annexBToAvccConverter; /** @@ -108,7 +103,6 @@ public final class Mp4Muxer implements Muxer { public Builder(FileOutputStream fileOutputStream) { this.fileOutputStream = checkNotNull(fileOutputStream); lastFrameDurationBehavior = LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME; - fragmentDurationUs = DEFAULT_FRAGMENT_DURATION_US; } /** @@ -136,37 +130,6 @@ public final class Mp4Muxer implements Muxer { return this; } - /** - * Sets whether to enable writing a fragmented MP4. - * - *

The default value is {@code false}. - */ - @CanIgnoreReturnValue - public Mp4Muxer.Builder setFragmentedMp4Enabled(boolean enabled) { - fragmentedMp4Enabled = enabled; - return this; - } - - /** - * Sets fragment duration for the {@linkplain #setFragmentedMp4Enabled(boolean) fragmented MP4}. - * - *

Muxer will attempt to create fragments of the given duration but the actual duration might - * be greater depending upon the frequency of sync samples. - * - *

The duration is ignored for {@linkplain #setFragmentedMp4Enabled(boolean) non fragmented - * MP4}. - * - *

The default value is {@link #DEFAULT_FRAGMENT_DURATION_US}. - * - * @param fragmentDurationUs The fragment duration in microseconds. - * @return The {@link Mp4Muxer.Builder}. - */ - @CanIgnoreReturnValue - public Mp4Muxer.Builder setFragmentDurationUs(int fragmentDurationUs) { - this.fragmentDurationUs = fragmentDurationUs; - return this; - } - /** Builds an {@link Mp4Muxer} instance. */ public Mp4Muxer build() { MetadataCollector metadataCollector = new MetadataCollector(); @@ -174,53 +137,20 @@ public final class Mp4Muxer implements Muxer { new Mp4MoovStructure(metadataCollector, lastFrameDurationBehavior); AnnexBToAvccConverter avccConverter = annexBToAvccConverter == null ? AnnexBToAvccConverter.DEFAULT : annexBToAvccConverter; - Mp4Writer mp4Writer = - fragmentedMp4Enabled - ? new FragmentedMp4Writer( - fileOutputStream, moovStructure, avccConverter, fragmentDurationUs) - : new BasicMp4Writer(fileOutputStream, moovStructure, avccConverter); + BasicMp4Writer mp4Writer = new BasicMp4Writer(fileOutputStream, moovStructure, avccConverter); return new Mp4Muxer(mp4Writer, metadataCollector); } } - /** A list of supported video sample mime types. */ - public static final ImmutableList SUPPORTED_VIDEO_SAMPLE_MIME_TYPES = - ImmutableList.of(MimeTypes.VIDEO_H264, MimeTypes.VIDEO_H265, MimeTypes.VIDEO_AV1); - - /** A list of supported audio sample mime types. */ - public static final ImmutableList SUPPORTED_AUDIO_SAMPLE_MIME_TYPES = - ImmutableList.of(MimeTypes.AUDIO_AAC); - - /** - * 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 BasicMp4Writer mp4Writer; private final MetadataCollector metadataCollector; - private Mp4Muxer(Mp4Writer mp4Writer, MetadataCollector metadataCollector) { + private Mp4Muxer(BasicMp4Writer mp4Writer, MetadataCollector metadataCollector) { this.mp4Writer = mp4Writer; this.metadataCollector = metadataCollector; } - /** - * Returns whether a given {@link Metadata.Entry metadata} is supported. - * - *

For the list of supported metadata refer to {@link Mp4Muxer#addMetadata(Metadata.Entry)}. - */ - public static boolean isMetadataSupported(Metadata.Entry metadata) { - return metadata instanceof Mp4OrientationData - || metadata instanceof Mp4LocationData - || (metadata instanceof Mp4TimestampData - && isMp4TimestampDataSupported((Mp4TimestampData) metadata)) - || (metadata instanceof MdtaMetadataEntry - && isMdtaMetadataEntrySupported((MdtaMetadataEntry) metadata)) - || metadata instanceof XmpData; - } - /** * {@inheritDoc} * @@ -258,8 +188,8 @@ public final class Mp4Muxer implements Muxer { /** * {@inheritDoc} * - *

The samples are cached and are written in batches so the caller must not change/release the - * {@link ByteBuffer} and the {@link BufferInfo} after calling this method. + *

The samples are cached and are written in batches so the caller must not change the {@link + * ByteBuffer} and the {@link BufferInfo} after calling this method. * *

Note: Out of order B-frames are currently not supported. * @@ -290,11 +220,11 @@ public final class Mp4Muxer implements Muxer { * * * @param metadata The {@linkplain Metadata.Entry metadata}. An {@link IllegalArgumentException} - * is throw if the {@linkplain Metadata.Entry metadata} is not supported. + * is thrown if the {@linkplain Metadata.Entry metadata} is not supported. */ @Override public void addMetadata(Metadata.Entry metadata) { - checkArgument(isMetadataSupported(metadata), "Unsupported metadata"); + checkArgument(Mp4Utils.isMetadataSupported(metadata), "Unsupported metadata"); metadataCollector.addMetadata(metadata); } @@ -302,14 +232,4 @@ public final class Mp4Muxer implements Muxer { public void close() throws IOException { mp4Writer.close(); } - - private static boolean isMdtaMetadataEntrySupported(MdtaMetadataEntry mdtaMetadataEntry) { - return mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_STRING - || mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32; - } - - private static boolean isMp4TimestampDataSupported(Mp4TimestampData timestampData) { - return timestampData.creationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE - && timestampData.modificationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE; - } } diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Utils.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Utils.java index be335e8b18..4c936a55e8 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Utils.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Utils.java @@ -15,6 +15,13 @@ */ package androidx.media3.muxer; +import androidx.media3.common.Metadata; +import androidx.media3.container.MdtaMetadataEntry; +import androidx.media3.container.Mp4LocationData; +import androidx.media3.container.Mp4OrientationData; +import androidx.media3.container.Mp4TimestampData; +import androidx.media3.container.XmpData; + /** Utilities for MP4 files. */ /* package */ final class Mp4Utils { /* Total number of bytes in an integer. */ @@ -48,4 +55,25 @@ package androidx.media3.muxer; public static long usFromVu(long timestampVu, long videoUnitTimebase) { return timestampVu * 1_000_000L / videoUnitTimebase; } + + /** Returns whether a given {@link Metadata.Entry metadata} is supported. */ + public static boolean isMetadataSupported(Metadata.Entry metadata) { + return metadata instanceof Mp4OrientationData + || metadata instanceof Mp4LocationData + || (metadata instanceof Mp4TimestampData + && isMp4TimestampDataSupported((Mp4TimestampData) metadata)) + || (metadata instanceof MdtaMetadataEntry + && isMdtaMetadataEntrySupported((MdtaMetadataEntry) metadata)) + || metadata instanceof XmpData; + } + + private static boolean isMdtaMetadataEntrySupported(MdtaMetadataEntry mdtaMetadataEntry) { + return mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_STRING + || mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32; + } + + private static boolean isMp4TimestampDataSupported(Mp4TimestampData timestampData) { + return timestampData.creationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE + && timestampData.modificationTimestampSeconds <= UNSIGNED_INT_MAX_VALUE; + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java index 5ea8111e03..7193e8aacc 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/InAppMuxer.java @@ -15,16 +15,18 @@ */ package androidx.media3.transformer; -import static androidx.media3.muxer.Mp4Muxer.SUPPORTED_AUDIO_SAMPLE_MIME_TYPES; -import static androidx.media3.muxer.Mp4Muxer.SUPPORTED_VIDEO_SAMPLE_MIME_TYPES; - import android.media.MediaCodec.BufferInfo; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Metadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; +import androidx.media3.container.MdtaMetadataEntry; +import androidx.media3.container.Mp4LocationData; import androidx.media3.container.Mp4OrientationData; +import androidx.media3.container.Mp4TimestampData; +import androidx.media3.container.XmpData; +import androidx.media3.muxer.FragmentedMp4Muxer; import androidx.media3.muxer.Mp4Muxer; import androidx.media3.muxer.Muxer.TrackToken; import com.google.common.collect.ImmutableList; @@ -39,7 +41,7 @@ import java.util.List; import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; -/** {@link Muxer} implementation that uses a {@link Mp4Muxer}. */ +/** {@link Muxer} implementation that uses an {@link Mp4Muxer} or {@link FragmentedMp4Muxer}. */ @UnstableApi public final class InAppMuxer implements Muxer { @@ -68,7 +70,7 @@ public final class InAppMuxer implements Muxer { /** Creates a {@link Builder} instance with default values. */ public Builder() { - fragmentDurationUs = Mp4Muxer.DEFAULT_FRAGMENT_DURATION_US; + fragmentDurationUs = C.LENGTH_UNSET; } /** @@ -85,14 +87,17 @@ public final class InAppMuxer implements Muxer { return this; } - /** See {@link Mp4Muxer.Builder#setFragmentedMp4Enabled(boolean)}. */ + /** Sets whether to produce a fragmented MP4. */ @CanIgnoreReturnValue public Builder setFragmentedMp4Enabled(boolean fragmentedMp4Enabled) { this.fragmentedMp4Enabled = fragmentedMp4Enabled; return this; } - /** See {@link Mp4Muxer.Builder#setFragmentDurationUs(int)}. */ + /** + * Sets the fragment duration if the output file is {@link #setFragmentedMp4Enabled(boolean) + * fragmented}. + */ @CanIgnoreReturnValue public Builder setFragmentDurationUs(int fragmentDurationUs) { this.fragmentDurationUs = fragmentDurationUs; @@ -105,6 +110,14 @@ public final class InAppMuxer implements Muxer { } } + /** A list of supported video sample MIME types. */ + private static final ImmutableList SUPPORTED_VIDEO_SAMPLE_MIME_TYPES = + ImmutableList.of(MimeTypes.VIDEO_H264, MimeTypes.VIDEO_H265, MimeTypes.VIDEO_AV1); + + /** A list of supported audio sample MIME types. */ + private static final ImmutableList SUPPORTED_AUDIO_SAMPLE_MIME_TYPES = + ImmutableList.of(MimeTypes.AUDIO_AAC); + private final @Nullable MetadataProvider metadataProvider; private final boolean fragmentedMp4Enabled; private final int fragmentDurationUs; @@ -127,12 +140,13 @@ public final class InAppMuxer implements Muxer { throw new MuxerException("Error creating file output stream", e); } - Mp4Muxer mp4Muxer = - new Mp4Muxer.Builder(outputStream) - .setFragmentedMp4Enabled(fragmentedMp4Enabled) - .setFragmentDurationUs(fragmentDurationUs) - .build(); - return new InAppMuxer(mp4Muxer, metadataProvider); + androidx.media3.muxer.Muxer muxer = + fragmentedMp4Enabled + ? fragmentDurationUs != C.LENGTH_UNSET + ? new FragmentedMp4Muxer(outputStream, fragmentDurationUs) + : new FragmentedMp4Muxer(outputStream) + : new Mp4Muxer.Builder(outputStream).build(); + return new InAppMuxer(muxer, metadataProvider); } @Override @@ -146,14 +160,15 @@ public final class InAppMuxer implements Muxer { } } - private final Mp4Muxer mp4Muxer; + private final androidx.media3.muxer.Muxer muxer; private final @Nullable MetadataProvider metadataProvider; private final List trackTokenList; private final BufferInfo bufferInfo; private final Set metadataEntries; - private InAppMuxer(Mp4Muxer mp4Muxer, @Nullable MetadataProvider metadataProvider) { - this.mp4Muxer = mp4Muxer; + private InAppMuxer( + androidx.media3.muxer.Muxer muxer, @Nullable MetadataProvider metadataProvider) { + this.muxer = muxer; this.metadataProvider = metadataProvider; trackTokenList = new ArrayList<>(); bufferInfo = new BufferInfo(); @@ -162,12 +177,11 @@ public final class InAppMuxer implements Muxer { @Override public int addTrack(Format format) { - // Keep same sort key as no specific sort order is required. - TrackToken trackToken = mp4Muxer.addTrack(/* sortKey= */ 0, format); + TrackToken trackToken = muxer.addTrack(format); trackTokenList.add(trackToken); if (MimeTypes.isVideo(format.sampleMimeType)) { - mp4Muxer.addMetadata(new Mp4OrientationData(format.rotationDegrees)); + muxer.addMetadata(new Mp4OrientationData(format.rotationDegrees)); } return trackTokenList.size() - 1; @@ -195,7 +209,7 @@ public final class InAppMuxer implements Muxer { bufferInfo.presentationTimeUs, bufferInfo.flags); - mp4Muxer.writeSampleData(trackTokenList.get(trackIndex), byteBufferCopy, bufferInfoCopy); + muxer.writeSampleData(trackTokenList.get(trackIndex), byteBufferCopy, bufferInfoCopy); } catch (IOException e) { throw new MuxerException( "Failed to write sample for trackIndex=" @@ -212,7 +226,7 @@ public final class InAppMuxer implements Muxer { public void addMetadata(Metadata metadata) { for (int i = 0; i < metadata.length(); i++) { Metadata.Entry entry = metadata.get(i); - if (Mp4Muxer.isMetadataSupported(entry)) { + if (isMetadataSupported(entry)) { metadataEntries.add(entry); } } @@ -223,7 +237,7 @@ public final class InAppMuxer implements Muxer { writeMetadata(); try { - mp4Muxer.close(); + muxer.close(); } catch (IOException e) { throw new MuxerException("Error closing muxer", e); } @@ -238,7 +252,22 @@ public final class InAppMuxer implements Muxer { } for (Metadata.Entry entry : metadataEntries) { - mp4Muxer.addMetadata(entry); + muxer.addMetadata(entry); } } + + /** Returns whether a given {@link Metadata.Entry metadata} is supported. */ + private static boolean isMetadataSupported(Metadata.Entry metadata) { + return metadata instanceof Mp4OrientationData + || metadata instanceof Mp4LocationData + || metadata instanceof Mp4TimestampData + || (metadata instanceof MdtaMetadataEntry + && isMdtaMetadataEntrySupported((MdtaMetadataEntry) metadata)) + || metadata instanceof XmpData; + } + + private static boolean isMdtaMetadataEntrySupported(MdtaMetadataEntry mdtaMetadataEntry) { + return mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_STRING + || mdtaMetadataEntry.typeIndicator == MdtaMetadataEntry.TYPE_INDICATOR_FLOAT32; + } }