From 0acf6902e57678dc9e6ea8de3a4b3b88b9ec0519 Mon Sep 17 00:00:00 2001 From: sheenachhabra Date: Wed, 24 Jan 2024 09:37:56 -0800 Subject: [PATCH] Write "stco" box instead of "co64" for fragmented MP4 As per MP4 spec ISO 14496-12: 8.7.5 Chunk Offset Box, Both "stco" and "co64" can be used to store chunk offsets. While "stco" supports 32-bit offsets, "co64" supports 64-bit offsets. In non fragmented MP4, the mdat box can be extremely large, hence muxer uses "co64" box. But for fragmented MP4, muxer does not write any data in this chunk offset box (present in "moov" box) because all sample related info is present in "moof" box. Technically, "co64" box should also work in fragmented MP4because its empty only but QuickTime player fails to play video if "co64" box is present in fragmented MP4 output file. Testing: Verified that QuickTime player does not play video when "co64" box is present but is able to play when "stco" box is present. #minor-release PiperOrigin-RevId: 601147046 --- .../java/androidx/media3/muxer/Boxes.java | 32 +++++++++++++++---- .../media3/muxer/Mp4MoovStructure.java | 13 +++++--- .../java/androidx/media3/muxer/BoxesTest.java | 11 +++++++ ...r10-720p.mp4_fragmented_box_structure.dump | 4 +-- .../src/test/assets/muxerdumps/stco_box.dump | 2 ++ 5 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 libraries/test_data/src/test/assets/muxerdumps/stco_box.dump 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 1862952e9c..de393a5849 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java @@ -22,6 +22,7 @@ import static androidx.media3.muxer.ColorUtils.MEDIAFORMAT_STANDARD_TO_PRIMARIES import static androidx.media3.muxer.ColorUtils.MEDIAFORMAT_TRANSFER_TO_MP4_TRANSFER; import static androidx.media3.muxer.Mp4Utils.BYTES_PER_INTEGER; import static androidx.media3.muxer.Mp4Utils.MVHD_TIMEBASE; +import static androidx.media3.muxer.Mp4Utils.UNSIGNED_INT_MAX_VALUE; import android.media.MediaCodec; import android.media.MediaCodec.BufferInfo; @@ -736,16 +737,35 @@ import java.util.Locale; return BoxUtils.wrapIntoBox("stsc", contents); } - /** Returns the co64 (chunk offset) box. */ - public static ByteBuffer co64(List writtenChunkOffsets) { + /** Returns the stco (32-bit chunk offset) box. */ + public static ByteBuffer stco(List writtenChunkOffsets) { ByteBuffer contents = - ByteBuffer.allocate(writtenChunkOffsets.size() * 8 + Mp4Utils.MAX_FIXED_LEAF_BOX_SIZE); + ByteBuffer.allocate(2 * BYTES_PER_INTEGER + writtenChunkOffsets.size() * BYTES_PER_INTEGER); - contents.putInt(0x0); // version. - contents.putInt(writtenChunkOffsets.size()); // entry_count. + contents.putInt(0x0); // version and flags + contents.putInt(writtenChunkOffsets.size()); // entry_count; unsigned int(32) for (int i = 0; i < writtenChunkOffsets.size(); i++) { - contents.putLong(writtenChunkOffsets.get(i)); // chunk_offset. + long chunkOffset = writtenChunkOffsets.get(i); + checkState(chunkOffset <= UNSIGNED_INT_MAX_VALUE, "Only 32-bit offset is allowed"); + contents.putInt((int) chunkOffset); // chunk_offset; unsigned int(32) + } + + contents.flip(); + return BoxUtils.wrapIntoBox("stco", contents); + } + + /** Returns the co64 (64-bit chunk offset) box. */ + public static ByteBuffer co64(List writtenChunkOffsets) { + ByteBuffer contents = + ByteBuffer.allocate( + 2 * BYTES_PER_INTEGER + 2 * writtenChunkOffsets.size() * BYTES_PER_INTEGER); + + contents.putInt(0x0); // version and flags + contents.putInt(writtenChunkOffsets.size()); // entry_count; unsigned int(32) + + for (int i = 0; i < writtenChunkOffsets.size(); i++) { + contents.putLong(writtenChunkOffsets.get(i)); // chunk_offset; unsigned int(64) } contents.flip(); 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 978a62600c..ffacb22326 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Mp4MoovStructure.java @@ -93,7 +93,10 @@ import org.checkerframework.checker.nullness.qual.PolyNull; ByteBuffer stts = Boxes.stts(sampleDurationsVu); ByteBuffer stsz = Boxes.stsz(track.writtenSamples()); ByteBuffer stsc = Boxes.stsc(track.writtenChunkSampleCounts()); - ByteBuffer co64 = Boxes.co64(track.writtenChunkOffsets()); + ByteBuffer chunkOffsetBox = + isFragmentedMp4 + ? Boxes.stco(track.writtenChunkOffsets()) + : Boxes.co64(track.writtenChunkOffsets()); String handlerType; String handlerName; @@ -109,7 +112,9 @@ import org.checkerframework.checker.nullness.qual.PolyNull; mhdBox = Boxes.vmhd(); sampleEntryBox = Boxes.videoSampleEntry(format); stsdBox = Boxes.stsd(sampleEntryBox); - stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64, Boxes.stss(track.writtenSamples())); + stblBox = + Boxes.stbl( + stsdBox, stts, stsz, stsc, chunkOffsetBox, Boxes.stss(track.writtenSamples())); break; case C.TRACK_TYPE_AUDIO: handlerType = "soun"; @@ -117,7 +122,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull; mhdBox = Boxes.smhd(); sampleEntryBox = Boxes.audioSampleEntry(format); stsdBox = Boxes.stsd(sampleEntryBox); - stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64); + stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, chunkOffsetBox); break; case C.TRACK_TYPE_METADATA: // TODO: (b/280443593) - Check if we can identify a metadata track type from a custom @@ -128,7 +133,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull; mhdBox = Boxes.nmhd(); sampleEntryBox = Boxes.textMetaDataSampleEntry(format); stsdBox = Boxes.stsd(sampleEntryBox); - stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, co64); + stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, chunkOffsetBox); break; default: throw new IllegalArgumentException("Unsupported track type"); 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 8204898860..2f38b53878 100644 --- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java +++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java @@ -497,6 +497,17 @@ public class BoxesTest { context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("stsc_box")); } + @Test + public void createStcoBox_matchesExpected() throws IOException { + ImmutableList chunkOffsets = ImmutableList.of(1_000L, 5_000L, 7_000L, 10_000L); + + ByteBuffer stcoBox = Boxes.stco(chunkOffsets); + + DumpableMp4Box dumpableBox = new DumpableMp4Box(stcoBox); + DumpFileAsserts.assertOutput( + context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("stco_box")); + } + @Test public void createCo64Box_matchesExpected() throws IOException { ImmutableList chunkOffsets = ImmutableList.of(1_000L, 5_000L, 7_000L, 10_000L); 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 index c64995fb65..737a6b591a 100644 --- 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 @@ -25,7 +25,7 @@ moov (1209 bytes): Data = length 12, hash EE830681 stsc (16 bytes): Data = length 8, hash 94446F01 - co64 (16 bytes): + stco (16 bytes): Data = length 8, hash 94446F01 stss (16 bytes): Data = length 8, hash 94446F01 @@ -51,7 +51,7 @@ moov (1209 bytes): Data = length 12, hash EE830681 stsc (16 bytes): Data = length 8, hash 94446F01 - co64 (16 bytes): + stco (16 bytes): Data = length 8, hash 94446F01 mvex (72 bytes): trex (32 bytes): diff --git a/libraries/test_data/src/test/assets/muxerdumps/stco_box.dump b/libraries/test_data/src/test/assets/muxerdumps/stco_box.dump new file mode 100644 index 0000000000..f1d13fecf3 --- /dev/null +++ b/libraries/test_data/src/test/assets/muxerdumps/stco_box.dump @@ -0,0 +1,2 @@ +stco (32 bytes): + Data = length 24, hash E472D485