From e0257f403fe891994d8cf211d7b01d059fba8e66 Mon Sep 17 00:00:00 2001 From: sheenachhabra Date: Fri, 29 Dec 2023 03:36:49 -0800 Subject: [PATCH] Implement fragmented MP4 (fMP4) in the Mp4Muxer Changes includes; 1. Public API to enable fMP4 and to pass fragment duration. 2. Added `FragmentedMp4Writer`. 3. Added logic to create fragments based on given fragment duration. 4. Write "moov" box only once in the beginning. 3. Add all the required boxes for current implementation. 4. Unit tests for all the new boxes. 5. E2E test for generating fMP4. Note: The output file is un seek-able with this first implementation. PiperOrigin-RevId: 594426486 --- .../media3/muxer/Mp4MuxerEndToEndTest.java | 54 +- .../java/androidx/media3/muxer/Boxes.java | 85 ++ .../media3/muxer/DefaultMp4Writer.java | 3 +- .../media3/muxer/FragmentedMp4Writer.java | 240 +++ .../media3/muxer/Mp4MoovStructure.java | 186 +-- .../java/androidx/media3/muxer/Mp4Muxer.java | 54 +- .../java/androidx/media3/muxer/BoxesTest.java | 47 + .../muxerdumps/hdr10-720p.mp4_fragmented.dump | 1325 +++++++++++++++++ ...r10-720p.mp4_fragmented_box_structure.dump | 90 ++ .../src/test/assets/muxerdumps/mfhd_box.dump | 2 + .../src/test/assets/muxerdumps/tfhd_box.dump | 2 + .../src/test/assets/muxerdumps/trex_box.dump | 2 + .../src/test/assets/muxerdumps/trun_box.dump | 2 + .../media3/test/utils/DumpableMp4Box.java | 3 +- 14 files changed, 1995 insertions(+), 100 deletions(-) create mode 100644 libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java create mode 100644 libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump create mode 100644 libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented_box_structure.dump create mode 100644 libraries/test_data/src/test/assets/muxerdumps/mfhd_box.dump create mode 100644 libraries/test_data/src/test/assets/muxerdumps/tfhd_box.dump create mode 100644 libraries/test_data/src/test/assets/muxerdumps/trex_box.dump create mode 100644 libraries/test_data/src/test/assets/muxerdumps/trun_box.dump diff --git a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndTest.java b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndTest.java index 6f8ec784b5..3da4925802 100644 --- a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndTest.java +++ b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndTest.java @@ -20,9 +20,12 @@ import static androidx.media3.common.util.Assertions.checkNotNull; 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.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; @@ -77,7 +80,7 @@ public class Mp4MuxerEndToEndTest { @Test public void createMp4File_fromInputFileSampleData_matchesExpected() throws IOException { - Mp4Muxer mp4Muxer = null; + @Nullable Mp4Muxer mp4Muxer = null; try { mp4Muxer = new Mp4Muxer.Builder(checkNotNull(outputStream)).build(); @@ -114,6 +117,55 @@ public class Mp4MuxerEndToEndTest { AndroidMuxerTestUtil.getExpectedDumpFilePath("partial_" + H265_HDR10_MP4)); } + @Test + public void createFragmentedMp4File_fromInputFileSampleData_matchesExpected() throws IOException { + @Nullable Mp4Muxer mp4Muxer = null; + + try { + mp4Muxer = + new Mp4Muxer.Builder(checkNotNull(outputStream)).setFragmentedMp4Enabled(true).build(); + mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L); + feedInputDataToMuxer(mp4Muxer, H265_HDR10_MP4); + } finally { + if (mp4Muxer != null) { + mp4Muxer.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 Mp4Muxer mp4Muxer = null; + + try { + mp4Muxer = + new Mp4Muxer.Builder(checkNotNull(outputStream)).setFragmentedMp4Enabled(true).build(); + mp4Muxer.setModificationTime(/* timestampMs= */ 500_000_000L); + feedInputDataToMuxer(mp4Muxer, H265_HDR10_MP4); + } finally { + if (mp4Muxer != null) { + mp4Muxer.close(); + } + } + + DumpableMp4Box dumpableMp4Box = + new DumpableMp4Box( + ByteBuffer.wrap(TestUtil.getByteArrayFromFilePath(checkNotNull(outputPath)))); + DumpFileAsserts.assertOutput( + context, + dumpableMp4Box, + AndroidMuxerTestUtil.getExpectedDumpFilePath(H265_HDR10_MP4 + "_fragmented_box_structure")); + } + private void feedInputDataToMuxer(Mp4Muxer mp4Muxer, String inputFileName) throws IOException { MediaExtractor extractor = new MediaExtractor(); extractor.setDataSource( diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java index 8c6edc3acf..59a80d3126 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java @@ -31,6 +31,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Util; import androidx.media3.container.NalUnitUtil; +import androidx.media3.muxer.FragmentedMp4Writer.SampleMetadata; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.primitives.Bytes; @@ -48,6 +49,13 @@ import java.util.Locale; * buffers}. */ /* package */ final class Boxes { + private static final int BYTES_PER_INTEGER = 4; + // unsigned int(2) sample_depends_on = 2 (bit index 25 and 24) + private static final int TRUN_BOX_SYNC_SAMPLE_FLAGS = 0b00000010_00000000_00000000_00000000; + // unsigned int(2) sample_depends_on = 1 (bit index 25 and 24) + // bit(1) sample_is_non_sync_sample = 1 (bit index 16) + private static final int TRUN_BOX_NON_SYNC_SAMPLE_FLAGS = 0b00000001_00000001_00000000_00000000; + private Boxes() {} public static final ImmutableList XMP_UUID = @@ -598,6 +606,7 @@ import java.util.Locale; * @return A list of all the sample durations. */ // TODO: b/280084657 - Add support for setting last sample duration. + // TODO: b/317373578 - Consider changing return type to List. public static List convertPresentationTimestampsToDurationsVu( List samplesInfo, long firstSamplePresentationTimeUs, @@ -805,6 +814,82 @@ import java.util.Locale; return BoxUtils.wrapBoxesIntoBox("ftyp", boxBytes); } + /** Returns the movie fragment (moof) box. */ + public static ByteBuffer moof(ByteBuffer mfhdBox, List trafBoxes) { + return BoxUtils.wrapBoxesIntoBox( + "moof", new ImmutableList.Builder().add(mfhdBox).addAll(trafBoxes).build()); + } + + /** Returns the movie fragment header (mfhd) box. */ + public static ByteBuffer mfhd(int sequenceNumber) { + ByteBuffer contents = ByteBuffer.allocate(2 * BYTES_PER_INTEGER); + contents.putInt(0x0); // version and flags + contents.putInt(sequenceNumber); // An unsigned int(32) + contents.flip(); + return BoxUtils.wrapIntoBox("mfhd", contents); + } + + /** Returns a track fragment (traf) box. */ + public static ByteBuffer traf(ByteBuffer tfhdBox, ByteBuffer trunBox) { + return BoxUtils.wrapBoxesIntoBox("traf", ImmutableList.of(tfhdBox, trunBox)); + } + + /** Returns a track fragment header (tfhd) box. */ + public static ByteBuffer tfhd(int trackId) { + ByteBuffer contents = ByteBuffer.allocate(2 * BYTES_PER_INTEGER); + contents.putInt(0x0); // version and flags + contents.putInt(trackId); + contents.flip(); + return BoxUtils.wrapIntoBox("tfhd", contents); + } + + /** Returns a track fragment run (trun) box. */ + public static ByteBuffer trun(List samplesMetadata) { + // 3 integers are required for each sample's metadata. + ByteBuffer contents = + ByteBuffer.allocate(2 * BYTES_PER_INTEGER + 3 * samplesMetadata.size() * BYTES_PER_INTEGER); + + // 0x000100 sample-duration-present: indicates that each sample has its own duration, otherwise + // the default is used. + // 0x000200 sample-size-present: indicates that each sample has its own size, otherwise the + // default is used. + // 0x000400 sample-flags-present: indicates that each sample has its own flags, otherwise the + // default is used. + // Version is 0x0. + int versionAndFlags = 0x0 | 0x000100 | 0x000200 | 0x000400; + contents.putInt(versionAndFlags); + contents.putInt(samplesMetadata.size()); // An unsigned int(32) + for (int i = 0; i < samplesMetadata.size(); i++) { + SampleMetadata currentSampleMetadata = samplesMetadata.get(i); + contents.putInt((int) currentSampleMetadata.durationVu); // An unsigned int(32) + contents.putInt(currentSampleMetadata.size); // An unsigned int(32) + contents.putInt( + (currentSampleMetadata.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0 + ? TRUN_BOX_SYNC_SAMPLE_FLAGS + : TRUN_BOX_NON_SYNC_SAMPLE_FLAGS); + } + contents.flip(); + return BoxUtils.wrapIntoBox("trun", contents); + } + + /** Returns a movie extends (mvex) box. */ + public static ByteBuffer mvex(List trexBoxes) { + return BoxUtils.wrapBoxesIntoBox("mvex", trexBoxes); + } + + /** Returns a track extends (trex) box. */ + public static ByteBuffer trex(int trackId) { + ByteBuffer contents = ByteBuffer.allocate(6 * BYTES_PER_INTEGER); + contents.putInt(0x0); // version and flags + contents.putInt(trackId); + contents.putInt(1); // default_sample_description_index + contents.putInt(0); // default_sample_duration + contents.putInt(0); // default_sample_size + contents.putInt(0); // default_sample_flags + contents.flip(); + return BoxUtils.wrapIntoBox("trex", contents); + } + // TODO: b/317117431 - Change this method to getLastSampleDuration(). /** Adjusts the duration of the very last sample if needed. */ private static void adjustLastSampleDuration( diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/DefaultMp4Writer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/DefaultMp4Writer.java index 7212b24c1f..be88416aab 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/DefaultMp4Writer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/DefaultMp4Writer.java @@ -132,7 +132,8 @@ import java.util.concurrent.atomic.AtomicBoolean; ByteBuffer moovHeader; if (minInputPtsUs != Long.MAX_VALUE) { - moovHeader = moovGenerator.moovMetadataHeader(tracks, minInputPtsUs); + moovHeader = + moovGenerator.moovMetadataHeader(tracks, minInputPtsUs, /* isFragmentedMp4= */ false); } else { // Skip moov box, if there are no samples. moovHeader = ByteBuffer.allocate(0); diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java new file mode 100644 index 0000000000..08b3fed7fd --- /dev/null +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/FragmentedMp4Writer.java @@ -0,0 +1,240 @@ +/* + * Copyright 2023 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 static java.lang.Math.max; +import static java.lang.Math.min; + +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Util; +import androidx.media3.muxer.Mp4Muxer.TrackToken; +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; + +/** + * An {@link Mp4Writer} implementation which writes samples into multiple fragments as per the + * fragmented MP4 (ISO/IEC 14496-12) standard. + */ +/* package */ final class FragmentedMp4Writer extends Mp4Writer { + /** Provides a limited set of sample metadata. */ + public static class SampleMetadata { + public final long durationVu; + public final int size; + public final int flags; + + public SampleMetadata(long durationsVu, int size, int flags) { + this.durationVu = durationsVu; + this.size = size; + this.flags = flags; + } + } + + private final int fragmentDurationUs; + + private @MonotonicNonNull Track videoTrack; + private int currentFragmentSequenceNumber; + private boolean headerCreated; + private long minInputPresentationTimeUs; + private long maxTrackDurationUs; + + public FragmentedMp4Writer( + FileOutputStream outputStream, + Mp4MoovStructure moovGenerator, + AnnexBToAvccConverter annexBToAvccConverter, + int fragmentDurationUs) { + super(outputStream, moovGenerator, annexBToAvccConverter); + this.fragmentDurationUs = fragmentDurationUs; + minInputPresentationTimeUs = Long.MAX_VALUE; + currentFragmentSequenceNumber = 1; + } + + @Override + public TrackToken addTrack(int sortKey, Format format) { + Track track = new Track(format); + tracks.add(track); + if (MimeTypes.isVideo(format.sampleMimeType)) { + videoTrack = track; + } + return track; + } + + @Override + public void writeSampleData( + TrackToken token, ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) + throws IOException { + checkArgument(token instanceof Track); + if (!headerCreated) { + createHeader(); + headerCreated = true; + } + Track track = (Track) token; + if (shouldFlushPendingSamples(track, bufferInfo)) { + createFragment(); + } + track.writeSampleData(byteBuffer, bufferInfo); + BufferInfo firstPendingSample = checkNotNull(track.pendingSamplesBufferInfo.peekFirst()); + BufferInfo lastPendingSample = checkNotNull(track.pendingSamplesBufferInfo.peekLast()); + minInputPresentationTimeUs = + min(minInputPresentationTimeUs, firstPendingSample.presentationTimeUs); + maxTrackDurationUs = + max( + maxTrackDurationUs, + lastPendingSample.presentationTimeUs - firstPendingSample.presentationTimeUs); + } + + @Override + public void close() throws IOException { + try { + createFragment(); + } finally { + output.close(); + outputStream.close(); + } + } + + private void createHeader() throws IOException { + output.position(0L); + output.write(Boxes.ftyp()); + // TODO: b/262704382 - Add some free space in the moov box to fit any newly added metadata and + // write moov box again in the close() method. + // The minInputPtsUs is actually ignored as there are no pending samples to write. + output.write( + moovGenerator.moovMetadataHeader( + tracks, /* minInputPtsUs= */ 0L, /* isFragmentedMp4= */ true)); + } + + private boolean shouldFlushPendingSamples( + Track track, MediaCodec.BufferInfo nextSampleBufferInfo) { + // If video track is present then fragment will be created based on group of pictures and + // track's duration so far. + if (videoTrack != null) { + // Video samples can be written only when complete group of pictures are present. + if (track.equals(videoTrack) + && track.hadKeyframe + && ((nextSampleBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) > 0)) { + BufferInfo firstPendingSample = checkNotNull(track.pendingSamplesBufferInfo.peekFirst()); + BufferInfo lastPendingSample = checkNotNull(track.pendingSamplesBufferInfo.peekLast()); + return lastPendingSample.presentationTimeUs - firstPendingSample.presentationTimeUs + >= fragmentDurationUs; + } + return false; + } else { + return maxTrackDurationUs >= fragmentDurationUs; + } + } + + private void createFragment() throws IOException { + // Write moof box. + List trafBoxes = createTrafBoxes(); + if (trafBoxes.isEmpty()) { + return; + } + output.write(Boxes.moof(Boxes.mfhd(currentFragmentSequenceNumber), trafBoxes)); + + writeMdatBox(); + + currentFragmentSequenceNumber++; + } + + private List createTrafBoxes() { + List trafBoxes = new ArrayList<>(); + for (int i = 0; i < tracks.size(); i++) { + Track currentTrack = tracks.get(i); + if (!currentTrack.pendingSamplesBufferInfo.isEmpty()) { + List samplesMetadata = + processPendingSamplesBufferInfo(currentTrack, currentFragmentSequenceNumber); + ByteBuffer trun = Boxes.trun(samplesMetadata); + trafBoxes.add(Boxes.traf(Boxes.tfhd(/* trackId= */ i + 1), trun)); + } + } + return trafBoxes; + } + + private void writeMdatBox() throws IOException { + long mdatStartPosition = output.position(); + // 4 bytes (indicating a 64-bit length field) + 4 bytes (box name) + 8 bytes (the actual length) + ByteBuffer header = ByteBuffer.allocate(16); + // This 32-bit integer in general contains the total length of the box. Here value 1 indicates + // that the actual length is stored as 64-bit integer after the box name. + header.putInt(1); + header.put(Util.getUtf8Bytes("mdat")); + header.putLong(16); // The total box length so far. + header.flip(); + output.write(header); + + long bytesWritten = 0; + for (int i = 0; i < tracks.size(); i++) { + Track currentTrack = tracks.get(i); + while (!currentTrack.pendingSamplesByteBuffer.isEmpty()) { + ByteBuffer currentSampleByteBuffer = currentTrack.pendingSamplesByteBuffer.removeFirst(); + + // Convert the H.264/H.265 samples from Annex-B format (output by MediaCodec) to + // Avcc format (required by MP4 container). + if (MimeTypes.isVideo(currentTrack.format.sampleMimeType)) { + annexBToAvccConverter.process(currentSampleByteBuffer); + } + bytesWritten += output.write(currentSampleByteBuffer); + } + } + + long currentPosition = output.position(); + + // Skip 4 bytes (64-bit length indication) + 4 bytes (box name). + output.position(mdatStartPosition + 8); + ByteBuffer mdatSize = ByteBuffer.allocate(8); // 64-bit length. + // Additional 4 bytes (64-bit length indication) + 4 bytes (box name) + 8 bytes (actual length). + mdatSize.putLong(bytesWritten + 16); + mdatSize.flip(); + output.write(mdatSize); + output.position(currentPosition); + } + + private List processPendingSamplesBufferInfo( + Track track, int fragmentSequenceNumber) { + List sampleBufferInfos = new ArrayList<>(track.pendingSamplesBufferInfo); + + List sampleDurations = + Boxes.convertPresentationTimestampsToDurationsVu( + sampleBufferInfos, + /* firstSamplePresentationTimeUs= */ fragmentSequenceNumber == 1 + ? minInputPresentationTimeUs + : sampleBufferInfos.get(0).presentationTimeUs, + track.videoUnitTimebase(), + Mp4Muxer.LAST_FRAME_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION); + + List pendingSamplesMetadata = new ArrayList<>(sampleBufferInfos.size()); + for (int i = 0; i < sampleBufferInfos.size(); i++) { + pendingSamplesMetadata.add( + new SampleMetadata( + sampleDurations.get(i), + sampleBufferInfos.get(i).size, + sampleBufferInfos.get(i).flags)); + } + + // Clear the queue. + track.pendingSamplesBufferInfo.clear(); + return pendingSamplesMetadata; + } +} diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java index dee215f30c..88ad25e5f1 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java @@ -59,105 +59,104 @@ import org.checkerframework.checker.nullness.qual.PolyNull; /** Generates a mdat header. */ @SuppressWarnings("InlinedApi") public ByteBuffer moovMetadataHeader( - List tracks, long minInputPtsUs) { + List tracks, long minInputPtsUs, boolean isFragmentedMp4) { List trakBoxes = new ArrayList<>(); + List trexBoxes = new ArrayList<>(); int nextTrackId = 1; long videoDurationUs = 0L; for (int i = 0; i < tracks.size(); i++) { TrackMetadataProvider track = tracks.get(i); - if (!track.writtenSamples().isEmpty()) { - Format format = track.format(); - String languageCode = bcp47LanguageTagToIso3(format.language); + Format format = track.format(); + String languageCode = bcp47LanguageTagToIso3(format.language); - // Generate the sample durations to calculate the total duration for tkhd box. - List sampleDurationsVu = - Boxes.convertPresentationTimestampsToDurationsVu( - track.writtenSamples(), - minInputPtsUs, - track.videoUnitTimebase(), - lastFrameDurationBehavior); + // Generate the sample durations to calculate the total duration for tkhd box. + List sampleDurationsVu = + Boxes.convertPresentationTimestampsToDurationsVu( + track.writtenSamples(), + minInputPtsUs, + track.videoUnitTimebase(), + lastFrameDurationBehavior); - long trackDurationInTrackUnitsVu = 0; - for (int j = 0; j < sampleDurationsVu.size(); j++) { - trackDurationInTrackUnitsVu += sampleDurationsVu.get(j); - } - - long trackDurationUs = - Mp4Utils.usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase()); - - @C.TrackType int trackType = MimeTypes.getTrackType(format.sampleMimeType); - ByteBuffer stts = Boxes.stts(sampleDurationsVu); - ByteBuffer stsz = Boxes.stsz(track.writtenSamples()); - ByteBuffer stsc = Boxes.stsc(track.writtenChunkSampleCounts()); - ByteBuffer co64 = Boxes.co64(track.writtenChunkOffsets()); - - String handlerType; - String handlerName; - ByteBuffer mhdBox; - ByteBuffer sampleEntryBox; - ByteBuffer stsdBox; - ByteBuffer stblBox; - - switch (trackType) { - case C.TRACK_TYPE_VIDEO: - handlerType = "vide"; - handlerName = "VideoHandle"; - mhdBox = Boxes.vmhd(); - sampleEntryBox = Boxes.videoSampleEntry(format); - stsdBox = Boxes.stsd(sampleEntryBox); - stblBox = - Boxes.stbl(stsdBox, stts, stsz, stsc, co64, Boxes.stss(track.writtenSamples())); - break; - case C.TRACK_TYPE_AUDIO: - handlerType = "soun"; - handlerName = "SoundHandle"; - mhdBox = Boxes.smhd(); - sampleEntryBox = Boxes.audioSampleEntry(format); - stsdBox = Boxes.stsd(sampleEntryBox); - stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64); - break; - case C.TRACK_TYPE_METADATA: - // TODO: (b/280443593) - Check if we can identify a metadata track type from a custom - // mime type. - case C.TRACK_TYPE_UNKNOWN: - handlerType = "meta"; - handlerName = "MetaHandle"; - mhdBox = Boxes.nmhd(); - sampleEntryBox = Boxes.textMetaDataSampleEntry(format); - stsdBox = Boxes.stsd(sampleEntryBox); - stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64); - break; - default: - throw new IllegalArgumentException("Unsupported track type"); - } - - // The below statement is also a description of how a mdat box looks like, with all the - // inner boxes and what they actually store. Although they're technically instance methods, - // everything that is written to a box is visible in the argument list. - ByteBuffer trakBox = - Boxes.trak( - Boxes.tkhd( - nextTrackId, - // Using the time base of the entire file, not that of the track; otherwise, - // Quicktime will stretch the audio accordingly, see b/158120042. - (int) Mp4Utils.vuFromUs(trackDurationUs, MVHD_TIMEBASE), - metadataCollector.modificationTimestampSeconds, - metadataCollector.orientation, - format), - Boxes.mdia( - Boxes.mdhd( - trackDurationInTrackUnitsVu, - track.videoUnitTimebase(), - metadataCollector.modificationTimestampSeconds, - languageCode), - Boxes.hdlr(handlerType, handlerName), - Boxes.minf(mhdBox, Boxes.dinf(Boxes.dref(Boxes.localUrl())), stblBox))); - - trakBoxes.add(trakBox); - videoDurationUs = max(videoDurationUs, trackDurationUs); - nextTrackId++; + long trackDurationInTrackUnitsVu = 0; + for (int j = 0; j < sampleDurationsVu.size(); j++) { + trackDurationInTrackUnitsVu += sampleDurationsVu.get(j); } + + long trackDurationUs = + Mp4Utils.usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase()); + + @C.TrackType int trackType = MimeTypes.getTrackType(format.sampleMimeType); + ByteBuffer stts = Boxes.stts(sampleDurationsVu); + ByteBuffer stsz = Boxes.stsz(track.writtenSamples()); + ByteBuffer stsc = Boxes.stsc(track.writtenChunkSampleCounts()); + ByteBuffer co64 = Boxes.co64(track.writtenChunkOffsets()); + + String handlerType; + String handlerName; + ByteBuffer mhdBox; + ByteBuffer sampleEntryBox; + ByteBuffer stsdBox; + ByteBuffer stblBox; + + switch (trackType) { + case C.TRACK_TYPE_VIDEO: + handlerType = "vide"; + handlerName = "VideoHandle"; + mhdBox = Boxes.vmhd(); + sampleEntryBox = Boxes.videoSampleEntry(format); + stsdBox = Boxes.stsd(sampleEntryBox); + stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64, Boxes.stss(track.writtenSamples())); + break; + case C.TRACK_TYPE_AUDIO: + handlerType = "soun"; + handlerName = "SoundHandle"; + mhdBox = Boxes.smhd(); + sampleEntryBox = Boxes.audioSampleEntry(format); + stsdBox = Boxes.stsd(sampleEntryBox); + stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64); + break; + case C.TRACK_TYPE_METADATA: + // TODO: (b/280443593) - Check if we can identify a metadata track type from a custom + // mime type. + case C.TRACK_TYPE_UNKNOWN: + handlerType = "meta"; + handlerName = "MetaHandle"; + mhdBox = Boxes.nmhd(); + sampleEntryBox = Boxes.textMetaDataSampleEntry(format); + stsdBox = Boxes.stsd(sampleEntryBox); + stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64); + break; + default: + throw new IllegalArgumentException("Unsupported track type"); + } + + // The below statement is also a description of how a mdat box looks like, with all the + // inner boxes and what they actually store. Although they're technically instance methods, + // everything that is written to a box is visible in the argument list. + ByteBuffer trakBox = + Boxes.trak( + Boxes.tkhd( + nextTrackId, + // Using the time base of the entire file, not that of the track; otherwise, + // Quicktime will stretch the audio accordingly, see b/158120042. + (int) Mp4Utils.vuFromUs(trackDurationUs, MVHD_TIMEBASE), + metadataCollector.modificationTimestampSeconds, + metadataCollector.orientation, + format), + Boxes.mdia( + Boxes.mdhd( + trackDurationInTrackUnitsVu, + track.videoUnitTimebase(), + metadataCollector.modificationTimestampSeconds, + languageCode), + Boxes.hdlr(handlerType, handlerName), + Boxes.minf(mhdBox, Boxes.dinf(Boxes.dref(Boxes.localUrl())), stblBox))); + + trakBoxes.add(trakBox); + videoDurationUs = max(videoDurationUs, trackDurationUs); + trexBoxes.add(Boxes.trex(nextTrackId)); + nextTrackId++; } ByteBuffer mvhdBox = @@ -173,7 +172,12 @@ import org.checkerframework.checker.nullness.qual.PolyNull; ByteBuffer moovBox; moovBox = - Boxes.moov(mvhdBox, udtaBox, metaBox, trakBoxes, /* mvexBox= */ ByteBuffer.allocate(0)); + Boxes.moov( + mvhdBox, + udtaBox, + metaBox, + trakBoxes, + isFragmentedMp4 ? Boxes.mvex(trexBoxes) : ByteBuffer.allocate(0)); // Also add XMP if needed if (metadataCollector.xmpData != null) { 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 2c63d8fd1d..d95785254c 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4Muxer.java @@ -88,8 +88,18 @@ 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; + private boolean fragmentedMp4Enabled; + private int fragmentDurationUs; @Nullable private AnnexBToAvccConverter annexBToAvccConverter; /** @@ -100,6 +110,7 @@ public final class Mp4Muxer { public Builder(FileOutputStream fileOutputStream) { this.fileOutputStream = checkNotNull(fileOutputStream); lastFrameDurationBehavior = LAST_FRAME_DURATION_BEHAVIOR_INSERT_SHORT_FRAME; + fragmentDurationUs = DEFAULT_FRAGMENT_DURATION_US; } /** @@ -127,18 +138,49 @@ public final class Mp4Muxer { 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(); Mp4MoovStructure moovStructure = new Mp4MoovStructure(metadataCollector, lastFrameDurationBehavior); + AnnexBToAvccConverter avccConverter = + annexBToAvccConverter == null ? AnnexBToAvccConverter.DEFAULT : annexBToAvccConverter; Mp4Writer mp4Writer = - new DefaultMp4Writer( - fileOutputStream, - moovStructure, - annexBToAvccConverter == null - ? AnnexBToAvccConverter.DEFAULT - : annexBToAvccConverter); + fragmentedMp4Enabled + ? new FragmentedMp4Writer( + fileOutputStream, moovStructure, avccConverter, fragmentDurationUs) + : new DefaultMp4Writer(fileOutputStream, moovStructure, avccConverter); return new Mp4Muxer(mp4Writer, metadataCollector); } diff --git a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java index 22fa860fd9..b1d7ebb73f 100644 --- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java +++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java @@ -30,6 +30,7 @@ import android.media.MediaCodec; import androidx.media3.common.C; import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; +import androidx.media3.muxer.FragmentedMp4Writer.SampleMetadata; import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.DumpableMp4Box; import androidx.media3.test.utils.TestUtil; @@ -527,6 +528,52 @@ public class BoxesTest { context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("ftyp_box")); } + @Test + public void createMfhdBox_matchesExpected() throws IOException { + ByteBuffer mfhdBox = Boxes.mfhd(/* sequenceNumber= */ 5); + + DumpableMp4Box dumpableBox = new DumpableMp4Box(mfhdBox); + DumpFileAsserts.assertOutput( + context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("mfhd_box")); + } + + @Test + public void createTfhdBox_matchesExpected() throws IOException { + ByteBuffer tfhdBox = Boxes.tfhd(/* trackId= */ 1); + + DumpableMp4Box dumpableBox = new DumpableMp4Box(tfhdBox); + DumpFileAsserts.assertOutput( + context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("tfhd_box")); + } + + @Test + public void createTrunBox_matchesExpected() throws IOException { + int sampleCount = 5; + List samplesMetadata = new ArrayList<>(sampleCount); + for (int i = 0; i < sampleCount; i++) { + samplesMetadata.add( + new SampleMetadata( + /* durationsVu= */ 2_000L, + /* size= */ 5_000, + /* flags= */ i == 0 ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0)); + } + + ByteBuffer trunBox = Boxes.trun(samplesMetadata); + + DumpableMp4Box dumpableBox = new DumpableMp4Box(trunBox); + DumpFileAsserts.assertOutput( + context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("trun_box")); + } + + @Test + public void createTrexBox_matchesExpected() throws IOException { + ByteBuffer trexBox = Boxes.trex(/* trackId= */ 2); + + DumpableMp4Box dumpableBox = new DumpableMp4Box(trexBox); + DumpFileAsserts.assertOutput( + context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("trex_box")); + } + private static List createBufferInfoListWithSamplePresentationTimestamps( long... timestampsUs) { List bufferInfoList = new ArrayList<>(); diff --git a/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump b/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump new file mode 100644 index 0000000000..a91a71a164 --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented.dump @@ -0,0 +1,1325 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=1237]] +numberOfTracks = 2 +track 0: + total output bytes = 7944083 + sample count = 127 + format 0: + id = 1 + sampleMimeType = video/hevc + codecs = hvc1.2.4.L153 + width = 1280 + height = 720 + colorInfo: + colorSpace = 6 + colorRange = 2 + colorTransfer = 6 + lumaBitdepth = 10 + chromaBitdepth = 10 + initializationData: + data = length 99, hash 99842E5A + sample 0: + time = 0 + flags = 1 + data = length 31784, hash F88A0ADA + sample 1: + time = 33322 + flags = 0 + data = length 9972, hash 25C1DE50 + sample 2: + time = 67222 + flags = 0 + data = length 28744, hash 8B1B2A8A + sample 3: + time = 99977 + flags = 0 + data = length 54040, hash 4B286B44 + sample 4: + time = 133300 + flags = 0 + data = length 63348, hash F7F91966 + sample 5: + time = 166633 + flags = 0 + data = length 56969, hash FE4DDEF2 + sample 6: + time = 199955 + flags = 0 + data = length 66511, hash BE7EF815 + sample 7: + time = 233344 + flags = 0 + data = length 72550, hash 8028BAF0 + sample 8: + time = 266755 + flags = 0 + data = length 66999, hash CA2B154E + sample 9: + time = 300177 + flags = 0 + data = length 70365, hash C1E81317 + sample 10: + time = 333622 + flags = 0 + data = length 65657, hash 5196D8EE + sample 11: + time = 367066 + flags = 0 + data = length 56166, hash 3A67C0F2 + sample 12: + time = 400533 + flags = 0 + data = length 54535, hash 8916E14F + sample 13: + time = 434000 + flags = 0 + data = length 60809, hash 30F11073 + sample 14: + time = 467466 + flags = 0 + data = length 73105, hash 2BDB6A1A + sample 15: + time = 500933 + flags = 0 + data = length 74843, hash 885F9563 + sample 16: + time = 534377 + flags = 0 + data = length 75645, hash 37DBCF31 + sample 17: + time = 567822 + flags = 0 + data = length 75659, hash 8E9E5330 + sample 18: + time = 601277 + flags = 0 + data = length 75569, hash DCC63D7C + sample 19: + time = 634755 + flags = 0 + data = length 76181, hash BD6BE82B + sample 20: + time = 668255 + flags = 0 + data = length 88354, hash 73E3B994 + sample 21: + time = 701755 + flags = 0 + data = length 75573, hash 7A99EEEB + sample 22: + time = 735266 + flags = 0 + data = length 80617, hash 4988E1AF + sample 23: + time = 768766 + flags = 0 + data = length 69367, hash 58EE9A3C + sample 24: + time = 802255 + flags = 0 + data = length 74534, hash 3E5FC997 + sample 25: + time = 835733 + flags = 0 + data = length 67594, hash DEAFD3CF + sample 26: + time = 869200 + flags = 0 + data = length 67960, hash C21D9BD2 + sample 27: + time = 902655 + flags = 0 + data = length 62461, hash 2DBFA365 + sample 28: + time = 936111 + flags = 0 + data = length 45621, hash 4B60D3B3 + sample 29: + time = 969533 + flags = 0 + data = length 41886, hash F69594F + sample 30: + time = 1002944 + flags = 1 + data = length 157922, hash 5D2DED48 + sample 31: + time = 1036355 + flags = 0 + data = length 59055, hash 3EC0D10F + sample 32: + time = 1069766 + flags = 0 + data = length 70362, hash 81D6533C + sample 33: + time = 1103155 + flags = 0 + data = length 69276, hash 1A5E3B7F + sample 34: + time = 1136544 + flags = 0 + data = length 66796, hash 6B26749E + sample 35: + time = 1169933 + flags = 0 + data = length 69426, hash 42939F73 + sample 36: + time = 1203300 + flags = 0 + data = length 53495, hash 47917D51 + sample 37: + time = 1236666 + flags = 0 + data = length 65041, hash C82B74C1 + sample 38: + time = 1270033 + flags = 0 + data = length 65539, hash EE34C22 + sample 39: + time = 1303400 + flags = 0 + data = length 70441, hash 61A63884 + sample 40: + time = 1336766 + flags = 0 + data = length 60140, hash C99A10E7 + sample 41: + time = 1370133 + flags = 0 + data = length 56647, hash 4E0D6F5E + sample 42: + time = 1403500 + flags = 0 + data = length 56195, hash FCA1A4FE + sample 43: + time = 1436844 + flags = 0 + data = length 59142, hash 778A6DDD + sample 44: + time = 1470200 + flags = 0 + data = length 59930, hash 3AF0685F + sample 45: + time = 1503544 + flags = 0 + data = length 42885, hash 77BCAE9D + sample 46: + time = 1536922 + flags = 0 + data = length 52167, hash 9FCE82F2 + sample 47: + time = 1570266 + flags = 0 + data = length 47566, hash 9F63D6AD + sample 48: + time = 1603566 + flags = 0 + data = length 50422, hash 83A948C8 + sample 49: + time = 1636911 + flags = 0 + data = length 49230, hash 39307D4 + sample 50: + time = 1670244 + flags = 0 + data = length 49503, hash A882BA5C + sample 51: + time = 1703577 + flags = 0 + data = length 49103, hash C43B39DA + sample 52: + time = 1736911 + flags = 0 + data = length 62007, hash 9B604007 + sample 53: + time = 1770233 + flags = 0 + data = length 60107, hash EF52B8FA + sample 54: + time = 1803566 + flags = 0 + data = length 58932, hash 18B2AB80 + sample 55: + time = 1836900 + flags = 0 + data = length 59795, hash A2DEF758 + sample 56: + time = 1870222 + flags = 0 + data = length 56216, hash 2669E720 + sample 57: + time = 1903555 + flags = 0 + data = length 59850, hash B1B411C + sample 58: + time = 1936877 + flags = 0 + data = length 58022, hash B4F0AA9E + sample 59: + time = 1970211 + flags = 0 + data = length 73111, hash 249DC0F7 + sample 60: + time = 2003555 + flags = 1 + data = length 121871, hash E158BA4C + sample 61: + time = 2036866 + flags = 0 + data = length 58917, hash A4E35D6C + sample 62: + time = 2070200 + flags = 0 + data = length 57435, hash B7E918BC + sample 63: + time = 2103522 + flags = 0 + data = length 60018, hash 79A48F55 + sample 64: + time = 2136855 + flags = 0 + data = length 56421, hash 7B2FBB9 + sample 65: + time = 2170200 + flags = 0 + data = length 57080, hash BF9E1AB4 + sample 66: + time = 2203511 + flags = 0 + data = length 59033, hash 2F75A2D7 + sample 67: + time = 2236844 + flags = 0 + data = length 55988, hash 6FB02EE1 + sample 68: + time = 2270177 + flags = 0 + data = length 59010, hash 7E315397 + sample 69: + time = 2303500 + flags = 0 + data = length 53968, hash 1E69F007 + sample 70: + time = 2336833 + flags = 0 + data = length 54754, hash 29D81885 + sample 71: + time = 2370155 + flags = 0 + data = length 54825, hash 360D7650 + sample 72: + time = 2403500 + flags = 0 + data = length 57683, hash 6B1C8F74 + sample 73: + time = 2436822 + flags = 0 + data = length 58820, hash D640752F + sample 74: + time = 2470144 + flags = 0 + data = length 55979, hash 72AA3662 + sample 75: + time = 2503477 + flags = 0 + data = length 53372, hash 188F88DB + sample 76: + time = 2536811 + flags = 0 + data = length 56082, hash AE587B34 + sample 77: + time = 2570144 + flags = 0 + data = length 55031, hash F2A1D185 + sample 78: + time = 2603477 + flags = 0 + data = length 51234, hash E258D5F0 + sample 79: + time = 2636800 + flags = 0 + data = length 69294, hash 56D53FB7 + sample 80: + time = 2670133 + flags = 0 + data = length 63400, hash D6E7AC33 + sample 81: + time = 2703466 + flags = 0 + data = length 61711, hash 3DD7E6B0 + sample 82: + time = 2736800 + flags = 0 + data = length 60393, hash FF7BDC39 + sample 83: + time = 2770122 + flags = 0 + data = length 74625, hash CB685A54 + sample 84: + time = 2803455 + flags = 0 + data = length 61453, hash A48659E6 + sample 85: + time = 2836800 + flags = 0 + data = length 59662, hash 879501DF + sample 86: + time = 2870122 + flags = 0 + data = length 68564, hash 68EEF64C + sample 87: + time = 2903444 + flags = 0 + data = length 69549, hash 26CA4587 + sample 88: + time = 2936777 + flags = 0 + data = length 71190, hash A64362A6 + sample 89: + time = 2970100 + flags = 0 + data = length 68276, hash 4587AAB6 + sample 90: + time = 3003422 + flags = 1 + data = length 115575, hash 8DDA331B + sample 91: + time = 3036755 + flags = 0 + data = length 56416, hash 3CB6F5AD + sample 92: + time = 3070088 + flags = 0 + data = length 54110, hash 7B27C656 + sample 93: + time = 3103400 + flags = 0 + data = length 68308, hash C7F4AE80 + sample 94: + time = 3136733 + flags = 0 + data = length 67629, hash 48E625B6 + sample 95: + time = 3170088 + flags = 0 + data = length 66968, hash D46F0E01 + sample 96: + time = 3203388 + flags = 0 + data = length 53022, hash 91852F32 + sample 97: + time = 3236711 + flags = 0 + data = length 66729, hash 12CA7617 + sample 98: + time = 3270044 + flags = 0 + data = length 53556, hash 904B00CF + sample 99: + time = 3303366 + flags = 0 + data = length 63459, hash AB813676 + sample 100: + time = 3336700 + flags = 0 + data = length 63637, hash 8B0750F6 + sample 101: + time = 3370022 + flags = 0 + data = length 64700, hash 8922E5BE + sample 102: + time = 3403355 + flags = 0 + data = length 54680, hash 4F49EB3D + sample 103: + time = 3436688 + flags = 0 + data = length 62600, hash 9DF2F9F5 + sample 104: + time = 3470011 + flags = 0 + data = length 69506, hash DB702311 + sample 105: + time = 3503344 + flags = 0 + data = length 50277, hash 1034F0A6 + sample 106: + time = 3536700 + flags = 0 + data = length 52100, hash 33745B51 + sample 107: + time = 3569988 + flags = 0 + data = length 65067, hash F73FE2C7 + sample 108: + time = 3603322 + flags = 0 + data = length 68940, hash 4331DA16 + sample 109: + time = 3636655 + flags = 0 + data = length 55215, hash 68087A40 + sample 110: + time = 3669988 + flags = 0 + data = length 56090, hash 2F483911 + sample 111: + time = 3703311 + flags = 0 + data = length 58356, hash D7D190C6 + sample 112: + time = 3736633 + flags = 0 + data = length 57368, hash 2F6B7918 + sample 113: + time = 3769966 + flags = 0 + data = length 56591, hash 19B696D2 + sample 114: + time = 3803288 + flags = 0 + data = length 54748, hash 7E0CE70E + sample 115: + time = 3836611 + flags = 0 + data = length 73133, hash CFA46EE4 + sample 116: + time = 3869944 + flags = 0 + data = length 52577, hash 5E174B5 + sample 117: + time = 3903288 + flags = 0 + data = length 74756, hash 5A571060 + sample 118: + time = 3936600 + flags = 0 + data = length 67171, hash 6F13FB2C + sample 119: + time = 3969922 + flags = 0 + data = length 42130, hash DAE94DB2 + sample 120: + time = 4003255 + flags = 1 + data = length 120199, hash 32DEA792 + sample 121: + time = 4036588 + flags = 0 + data = length 51028, hash FC7AF64F + sample 122: + time = 4069900 + flags = 0 + data = length 51879, hash 74852E0E + sample 123: + time = 4103233 + flags = 0 + data = length 51443, hash B735512A + sample 124: + time = 4137433 + flags = 0 + data = length 65012, hash DB29A2CD + sample 125: + time = 4169888 + flags = 0 + data = length 59318, hash 24AF2123 + sample 126: + time = 4203222 + flags = 0 + data = length 62411, hash 6CAE0387 +track 1: + total output bytes = 133209 + sample count = 195 + format 0: + averageBitrate = 256000 + peakBitrate = 256000 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 2 + sampleRate = 48000 + language = ``` + initializationData: + data = length 2, hash 560 + sample 0: + time = 0 + flags = 1 + data = length 682, hash 34C58E7C + sample 1: + time = 64312 + flags = 1 + data = length 846, hash 22FB31EF + sample 2: + time = 85645 + flags = 1 + data = length 757, hash F55DC63E + sample 3: + time = 107000 + flags = 1 + data = length 690, hash BD26CD74 + sample 4: + time = 128312 + flags = 1 + data = length 660, hash 5713CCCB + sample 5: + time = 149645 + flags = 1 + data = length 646, hash 74C2D10F + sample 6: + time = 171000 + flags = 1 + data = length 652, hash B49BACA1 + sample 7: + time = 192312 + flags = 1 + data = length 658, hash BF36BC46 + sample 8: + time = 213645 + flags = 1 + data = length 666, hash 69CBADD8 + sample 9: + time = 235000 + flags = 1 + data = length 668, hash 99204158 + sample 10: + time = 256312 + flags = 1 + data = length 671, hash AE181AC8 + sample 11: + time = 277645 + flags = 1 + data = length 678, hash C432ACF0 + sample 12: + time = 299000 + flags = 1 + data = length 682, hash C23E1F17 + sample 13: + time = 320312 + flags = 1 + data = length 685, hash B22AB49 + sample 14: + time = 341645 + flags = 1 + data = length 683, hash B073F447 + sample 15: + time = 363000 + flags = 1 + data = length 675, hash C7614E26 + sample 16: + time = 384312 + flags = 1 + data = length 679, hash 5B075ADF + sample 17: + time = 405645 + flags = 1 + data = length 687, hash E260C35B + sample 18: + time = 427000 + flags = 1 + data = length 687, hash 3C316334 + sample 19: + time = 448312 + flags = 1 + data = length 680, hash BCD8469 + sample 20: + time = 469645 + flags = 1 + data = length 681, hash CBD0BF0F + sample 21: + time = 491000 + flags = 1 + data = length 719, hash F7594265 + sample 22: + time = 512312 + flags = 1 + data = length 699, hash 4975812A + sample 23: + time = 533645 + flags = 1 + data = length 723, hash 11C7327E + sample 24: + time = 555000 + flags = 1 + data = length 765, hash DED05454 + sample 25: + time = 576312 + flags = 1 + data = length 785, hash 9E7C3FDD + sample 26: + time = 597645 + flags = 1 + data = length 728, hash B433E15E + sample 27: + time = 619000 + flags = 1 + data = length 708, hash ED0B3337 + sample 28: + time = 640312 + flags = 1 + data = length 642, hash 6B447435 + sample 29: + time = 661645 + flags = 1 + data = length 628, hash D59CB28F + sample 30: + time = 683000 + flags = 1 + data = length 594, hash BCC990B5 + sample 31: + time = 704312 + flags = 1 + data = length 622, hash C9AE9991 + sample 32: + time = 725625 + flags = 1 + data = length 633, hash 4B7D59B8 + sample 33: + time = 746958 + flags = 1 + data = length 661, hash A98CE814 + sample 34: + time = 768291 + flags = 1 + data = length 662, hash 9A3E0D79 + sample 35: + time = 789625 + flags = 1 + data = length 669, hash 2A0B67AC + sample 36: + time = 810958 + flags = 1 + data = length 691, hash C339C4EE + sample 37: + time = 832291 + flags = 1 + data = length 678, hash CF770B8C + sample 38: + time = 853625 + flags = 1 + data = length 678, hash 685F97BC + sample 39: + time = 874958 + flags = 1 + data = length 688, hash 7DC1FBD3 + sample 40: + time = 896291 + flags = 1 + data = length 684, hash F7D9FE89 + sample 41: + time = 917625 + flags = 1 + data = length 683, hash 5E3EA281 + sample 42: + time = 938958 + flags = 1 + data = length 675, hash F576AE6 + sample 43: + time = 960291 + flags = 1 + data = length 697, hash B0EBE204 + sample 44: + time = 981625 + flags = 1 + data = length 675, hash 3C928CCA + sample 45: + time = 1002958 + flags = 1 + data = length 680, hash 34650DF5 + sample 46: + time = 1024291 + flags = 1 + data = length 685, hash 564F62A + sample 47: + time = 1045625 + flags = 1 + data = length 691, hash 71BBA88D + sample 48: + time = 1066958 + flags = 1 + data = length 736, hash C8F0D575 + sample 49: + time = 1088291 + flags = 1 + data = length 718, hash 2F13561A + sample 50: + time = 1109625 + flags = 1 + data = length 692, hash CF153F84 + sample 51: + time = 1130958 + flags = 1 + data = length 673, hash 30357B83 + sample 52: + time = 1152291 + flags = 1 + data = length 690, hash 5FB65B72 + sample 53: + time = 1173625 + flags = 1 + data = length 769, hash F5E1AAEA + sample 54: + time = 1194958 + flags = 1 + data = length 733, hash 9327D738 + sample 55: + time = 1216291 + flags = 1 + data = length 627, hash 203CFA24 + sample 56: + time = 1237625 + flags = 1 + data = length 697, hash 1FCE39D0 + sample 57: + time = 1258958 + flags = 1 + data = length 585, hash B53A076B + sample 58: + time = 1280291 + flags = 1 + data = length 650, hash FDFCA752 + sample 59: + time = 1301625 + flags = 1 + data = length 725, hash 4D4FA788 + sample 60: + time = 1322958 + flags = 1 + data = length 788, hash 6D883F0B + sample 61: + time = 1344291 + flags = 1 + data = length 649, hash 9125CC1A + sample 62: + time = 1365625 + flags = 1 + data = length 616, hash C02AB7EA + sample 63: + time = 1386958 + flags = 1 + data = length 624, hash D49000E1 + sample 64: + time = 1408291 + flags = 1 + data = length 664, hash 482C9994 + sample 65: + time = 1429625 + flags = 1 + data = length 656, hash A234172A + sample 66: + time = 1450958 + flags = 1 + data = length 649, hash BCCAD04D + sample 67: + time = 1472291 + flags = 1 + data = length 655, hash B961E395 + sample 68: + time = 1493625 + flags = 1 + data = length 673, hash 5BD56013 + sample 69: + time = 1514958 + flags = 1 + data = length 700, hash FE25D834 + sample 70: + time = 1536291 + flags = 1 + data = length 668, hash 45203245 + sample 71: + time = 1557625 + flags = 1 + data = length 672, hash F9269558 + sample 72: + time = 1578958 + flags = 1 + data = length 682, hash C205B4DF + sample 73: + time = 1600291 + flags = 1 + data = length 686, hash A4632474 + sample 74: + time = 1621625 + flags = 1 + data = length 747, hash B2F3AA1D + sample 75: + time = 1642958 + flags = 1 + data = length 711, hash B3A33D80 + sample 76: + time = 1664291 + flags = 1 + data = length 652, hash 37A9B9BF + sample 77: + time = 1685625 + flags = 1 + data = length 675, hash F6BE4CAC + sample 78: + time = 1706958 + flags = 1 + data = length 672, hash 22A12DFC + sample 79: + time = 1728291 + flags = 1 + data = length 674, hash E740F44 + sample 80: + time = 1749625 + flags = 1 + data = length 680, hash A065804 + sample 81: + time = 1770958 + flags = 1 + data = length 663, hash 805CE20 + sample 82: + time = 1792291 + flags = 1 + data = length 688, hash C2E28B22 + sample 83: + time = 1813625 + flags = 1 + data = length 672, hash BF738F27 + sample 84: + time = 1834958 + flags = 1 + data = length 673, hash AFE85361 + sample 85: + time = 1856291 + flags = 1 + data = length 679, hash C9D68F4F + sample 86: + time = 1877625 + flags = 1 + data = length 676, hash 42C67933 + sample 87: + time = 1898958 + flags = 1 + data = length 748, hash 16944018 + sample 88: + time = 1920291 + flags = 1 + data = length 730, hash D592050C + sample 89: + time = 1941625 + flags = 1 + data = length 785, hash DB11A4E8 + sample 90: + time = 1962958 + flags = 1 + data = length 708, hash 445F4443 + sample 91: + time = 1984291 + flags = 1 + data = length 630, hash BD57EF90 + sample 92: + time = 2005625 + flags = 1 + data = length 621, hash FB977F1F + sample 93: + time = 2026958 + flags = 1 + data = length 656, hash 53E25FBE + sample 94: + time = 2048270 + flags = 1 + data = length 664, hash A9D0717 + sample 95: + time = 2069625 + flags = 1 + data = length 672, hash 6F2663EA + sample 96: + time = 2090937 + flags = 1 + data = length 677, hash 6EBB686B + sample 97: + time = 2112270 + flags = 1 + data = length 679, hash BF29A1EC + sample 98: + time = 2133625 + flags = 1 + data = length 683, hash 69F6750D + sample 99: + time = 2154937 + flags = 1 + data = length 691, hash A79A804F + sample 100: + time = 2176270 + flags = 1 + data = length 734, hash 31FB39E8 + sample 101: + time = 2197625 + flags = 1 + data = length 657, hash F99E1ADC + sample 102: + time = 2218937 + flags = 1 + data = length 659, hash FDC16724 + sample 103: + time = 2240270 + flags = 1 + data = length 795, hash 23302539 + sample 104: + time = 2261625 + flags = 1 + data = length 691, hash 5AA01A0 + sample 105: + time = 2282937 + flags = 1 + data = length 640, hash A9A214AB + sample 106: + time = 2304270 + flags = 1 + data = length 651, hash F3253A0E + sample 107: + time = 2325625 + flags = 1 + data = length 652, hash 2D4DE02 + sample 108: + time = 2346937 + flags = 1 + data = length 772, hash 16817D3A + sample 109: + time = 2368270 + flags = 1 + data = length 756, hash 738E4C8D + sample 110: + time = 2389625 + flags = 1 + data = length 781, hash 61372EAE + sample 111: + time = 2410937 + flags = 1 + data = length 658, hash 83B5A955 + sample 112: + time = 2432270 + flags = 1 + data = length 667, hash C3CF8AEF + sample 113: + time = 2453625 + flags = 1 + data = length 768, hash C6534483 + sample 114: + time = 2474937 + flags = 1 + data = length 688, hash 1C14B263 + sample 115: + time = 2496270 + flags = 1 + data = length 599, hash 51CF483 + sample 116: + time = 2517625 + flags = 1 + data = length 594, hash F290D460 + sample 117: + time = 2538937 + flags = 1 + data = length 633, hash 262E26E6 + sample 118: + time = 2560270 + flags = 1 + data = length 656, hash 9158E6A1 + sample 119: + time = 2581625 + flags = 1 + data = length 668, hash 3AC6C8DF + sample 120: + time = 2602937 + flags = 1 + data = length 667, hash DB111C93 + sample 121: + time = 2624270 + flags = 1 + data = length 670, hash 5EA45C5E + sample 122: + time = 2645625 + flags = 1 + data = length 663, hash 1CF1EC34 + sample 123: + time = 2666937 + flags = 1 + data = length 673, hash 9609104 + sample 124: + time = 2688270 + flags = 1 + data = length 704, hash D274E425 + sample 125: + time = 2709625 + flags = 1 + data = length 681, hash 4D720ACE + sample 126: + time = 2730937 + flags = 1 + data = length 682, hash C49E4619 + sample 127: + time = 2752270 + flags = 1 + data = length 680, hash 1AB4733A + sample 128: + time = 2773625 + flags = 1 + data = length 675, hash BA047E60 + sample 129: + time = 2794937 + flags = 1 + data = length 688, hash 9679B8E9 + sample 130: + time = 2816270 + flags = 1 + data = length 687, hash 57DBCD4 + sample 131: + time = 2837625 + flags = 1 + data = length 680, hash 91BA9BF2 + sample 132: + time = 2858937 + flags = 1 + data = length 757, hash 741D6330 + sample 133: + time = 2880270 + flags = 1 + data = length 651, hash 60508D7D + sample 134: + time = 2901625 + flags = 1 + data = length 679, hash 7A32FD22 + sample 135: + time = 2922937 + flags = 1 + data = length 666, hash 98C3F963 + sample 136: + time = 2944270 + flags = 1 + data = length 694, hash 59D9B67B + sample 137: + time = 2965625 + flags = 1 + data = length 680, hash 6FA356DD + sample 138: + time = 2986937 + flags = 1 + data = length 665, hash 3D7E32D9 + sample 139: + time = 3008250 + flags = 1 + data = length 681, hash 2592B0DF + sample 140: + time = 3029604 + flags = 1 + data = length 680, hash 2BA659D7 + sample 141: + time = 3050916 + flags = 1 + data = length 667, hash 1E21B749 + sample 142: + time = 3072250 + flags = 1 + data = length 683, hash 57E1A624 + sample 143: + time = 3093604 + flags = 1 + data = length 673, hash B7216D34 + sample 144: + time = 3114916 + flags = 1 + data = length 684, hash 2FDBEB3A + sample 145: + time = 3136250 + flags = 1 + data = length 707, hash 1D528F18 + sample 146: + time = 3157604 + flags = 1 + data = length 693, hash 24148721 + sample 147: + time = 3178916 + flags = 1 + data = length 660, hash C89F9451 + sample 148: + time = 3200250 + flags = 1 + data = length 679, hash 67C16179 + sample 149: + time = 3221604 + flags = 1 + data = length 685, hash 6EF9DD57 + sample 150: + time = 3242916 + flags = 1 + data = length 672, hash CFF4E296 + sample 151: + time = 3264250 + flags = 1 + data = length 681, hash 994F630 + sample 152: + time = 3285604 + flags = 1 + data = length 684, hash 3118D2E9 + sample 153: + time = 3306916 + flags = 1 + data = length 677, hash 3628592F + sample 154: + time = 3328250 + flags = 1 + data = length 689, hash 309E58A0 + sample 155: + time = 3349604 + flags = 1 + data = length 677, hash D1F3255B + sample 156: + time = 3370916 + flags = 1 + data = length 689, hash B3E864BA + sample 157: + time = 3392229 + flags = 1 + data = length 680, hash 469FA2FF + sample 158: + time = 3413562 + flags = 1 + data = length 688, hash DC9FC31B + sample 159: + time = 3434895 + flags = 1 + data = length 675, hash E2396CC7 + sample 160: + time = 3456229 + flags = 1 + data = length 738, hash C1B7A30A + sample 161: + time = 3477562 + flags = 1 + data = length 723, hash CEABDA70 + sample 162: + time = 3498895 + flags = 1 + data = length 698, hash 59E1B5D8 + sample 163: + time = 3520229 + flags = 1 + data = length 671, hash 71CD7BFA + sample 164: + time = 3541562 + flags = 1 + data = length 652, hash 45894636 + sample 165: + time = 3562895 + flags = 1 + data = length 667, hash E98A528A + sample 166: + time = 3584229 + flags = 1 + data = length 682, hash 8AA9E761 + sample 167: + time = 3605562 + flags = 1 + data = length 670, hash 7D071859 + sample 168: + time = 3626895 + flags = 1 + data = length 672, hash 4FA7BDBB + sample 169: + time = 3648229 + flags = 1 + data = length 779, hash 85D8FF74 + sample 170: + time = 3669562 + flags = 1 + data = length 699, hash CABC0AF6 + sample 171: + time = 3690895 + flags = 1 + data = length 635, hash 35BD0FED + sample 172: + time = 3712229 + flags = 1 + data = length 646, hash D4960FAC + sample 173: + time = 3733562 + flags = 1 + data = length 669, hash 4DAC2897 + sample 174: + time = 3754895 + flags = 1 + data = length 675, hash FD60998A + sample 175: + time = 3776229 + flags = 1 + data = length 677, hash FED0180B + sample 176: + time = 3797562 + flags = 1 + data = length 668, hash C6183862 + sample 177: + time = 3818895 + flags = 1 + data = length 671, hash EBA9EF22 + sample 178: + time = 3840229 + flags = 1 + data = length 668, hash CF88A2FF + sample 179: + time = 3861562 + flags = 1 + data = length 727, hash A9311311 + sample 180: + time = 3882895 + flags = 1 + data = length 701, hash C6351159 + sample 181: + time = 3904229 + flags = 1 + data = length 847, hash 1864F774 + sample 182: + time = 3925562 + flags = 1 + data = length 765, hash 616DD2EA + sample 183: + time = 3946895 + flags = 1 + data = length 674, hash 671D4342 + sample 184: + time = 3968229 + flags = 1 + data = length 723, hash 567566A2 + sample 185: + time = 3989562 + flags = 1 + data = length 580, hash B38C9C63 + sample 186: + time = 4010895 + flags = 1 + data = length 583, hash 5668BFCE + sample 187: + time = 4032229 + flags = 1 + data = length 631, hash 7E86C98E + sample 188: + time = 4053562 + flags = 1 + data = length 656, hash 95A41C9B + sample 189: + time = 4074895 + flags = 1 + data = length 822, hash 2A045560 + sample 190: + time = 4096229 + flags = 1 + data = length 643, hash 551E7C72 + sample 191: + time = 4117562 + flags = 1 + data = length 617, hash 463482D9 + sample 192: + time = 4138895 + flags = 1 + data = length 640, hash E714454F + sample 193: + time = 4160229 + flags = 1 + data = length 646, hash 6DD5E81B + sample 194: + time = 4181562 + flags = 1 + data = length 690, hash 407EC299 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented_box_structure.dump b/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented_box_structure.dump new file mode 100644 index 0000000000..582946734c --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/hdr10-720p.mp4_fragmented_box_structure.dump @@ -0,0 +1,90 @@ +ftyp (28 bytes): + Data = length 20, hash EF896440 +moov (1209 bytes): + mvhd (108 bytes): + Data = length 100, hash 5CF3AC6F + trak (610 bytes): + tkhd (92 bytes): + Data = length 84, hash 112173F4 + mdia (510 bytes): + mdhd (32 bytes): + Data = length 24, hash 42753A93 + hdlr (44 bytes): + Data = length 36, hash A0852FF2 + minf (426 bytes): + vmhd (20 bytes): + Data = length 12, hash EE830681 + dinf (36 bytes): + Data = length 28, hash D535436B + stbl (362 bytes): + stsd (270 bytes): + Data = length 262, hash 58217180 + stts (16 bytes): + Data = length 8, hash 94446F01 + stsz (20 bytes): + Data = length 12, hash EE830681 + stsc (16 bytes): + Data = length 8, hash 94446F01 + co64 (16 bytes): + Data = length 8, hash 94446F01 + stss (16 bytes): + Data = length 8, hash 94446F01 + trak (411 bytes): + tkhd (92 bytes): + Data = length 84, hash 34D7906B + mdia (311 bytes): + mdhd (32 bytes): + Data = length 24, hash EA3D1FE6 + hdlr (44 bytes): + Data = length 36, hash 49FC755F + minf (227 bytes): + smhd (16 bytes): + Data = length 8, hash 94446F01 + dinf (36 bytes): + Data = length 28, hash D535436B + stbl (167 bytes): + stsd (91 bytes): + Data = length 83, hash 895E2DCB + stts (16 bytes): + Data = length 8, hash 94446F01 + stsz (20 bytes): + Data = length 12, hash EE830681 + stsc (16 bytes): + Data = length 8, hash 94446F01 + co64 (16 bytes): + Data = length 8, hash 94446F01 + mvex (72 bytes): + trex (32 bytes): + Data = length 24, hash C35D3183 + trex (32 bytes): + Data = length 24, hash 14070F84 +moof (2852 bytes): + mfhd (16 bytes): + Data = length 8, hash 94446F02 + traf (1120 bytes): + tfhd (16 bytes): + Data = length 8, hash 94446F02 + trun (1096 bytes): + Data = length 1088, hash 1F7B824F + traf (1708 bytes): + tfhd (16 bytes): + Data = length 8, hash 94446F03 + trun (1684 bytes): + Data = length 1676, hash 46E974DC +mdat (5712395 bytes): + Data = length 5712379, hash 86B2819D +moof (1220 bytes): + mfhd (16 bytes): + Data = length 8, hash 94446F03 + traf (484 bytes): + tfhd (16 bytes): + Data = length 8, hash 94446F02 + trun (460 bytes): + Data = length 452, hash 36E6F796 + traf (712 bytes): + tfhd (16 bytes): + Data = length 8, hash 94446F03 + trun (688 bytes): + Data = length 680, hash 4E3D2F16 +mdat (2364929 bytes): + Data = length 2364913, hash D363A845 diff --git a/libraries/test_data/src/test/assets/muxerdumps/mfhd_box.dump b/libraries/test_data/src/test/assets/muxerdumps/mfhd_box.dump new file mode 100644 index 0000000000..821352ae33 --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/mfhd_box.dump @@ -0,0 +1,2 @@ +mfhd (16 bytes): + Data = length 8, hash 94446F06 diff --git a/libraries/test_data/src/test/assets/muxerdumps/tfhd_box.dump b/libraries/test_data/src/test/assets/muxerdumps/tfhd_box.dump new file mode 100644 index 0000000000..3247975e6c --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/tfhd_box.dump @@ -0,0 +1,2 @@ +tfhd (16 bytes): + Data = length 8, hash 94446F02 diff --git a/libraries/test_data/src/test/assets/muxerdumps/trex_box.dump b/libraries/test_data/src/test/assets/muxerdumps/trex_box.dump new file mode 100644 index 0000000000..e8ee197943 --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/trex_box.dump @@ -0,0 +1,2 @@ +trex (32 bytes): + Data = length 24, hash 14070F84 diff --git a/libraries/test_data/src/test/assets/muxerdumps/trun_box.dump b/libraries/test_data/src/test/assets/muxerdumps/trun_box.dump new file mode 100644 index 0000000000..a9272ecaff --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/trun_box.dump @@ -0,0 +1,2 @@ +trun (76 bytes): + Data = length 68, hash 750F9113 diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableMp4Box.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableMp4Box.java index 4105e4a337..6a63ffd839 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableMp4Box.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DumpableMp4Box.java @@ -25,7 +25,8 @@ import java.nio.ByteBuffer; @UnstableApi public final class DumpableMp4Box implements Dumper.Dumpable { private static final ImmutableSet CONTAINER_BOXES = - ImmutableSet.of("moov", "trak", "mdia", "minf", "stbl", "edts", "meta"); + ImmutableSet.of( + "moov", "trak", "mdia", "minf", "stbl", "edts", "meta", "mvex", "moof", "traf"); private final ParsableByteArray box; /***