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; /***