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
This commit is contained in:
sheenachhabra 2024-01-24 09:37:56 -08:00 committed by Copybara-Service
parent 966b710897
commit 0acf6902e5
5 changed files with 50 additions and 12 deletions

View File

@ -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<Long> writtenChunkOffsets) {
/** Returns the stco (32-bit chunk offset) box. */
public static ByteBuffer stco(List<Long> 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<Long> 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();

View File

@ -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");

View File

@ -497,6 +497,17 @@ public class BoxesTest {
context, dumpableBox, MuxerTestUtil.getExpectedDumpFilePath("stsc_box"));
}
@Test
public void createStcoBox_matchesExpected() throws IOException {
ImmutableList<Long> 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<Long> chunkOffsets = ImmutableList.of(1_000L, 5_000L, 7_000L, 10_000L);

View File

@ -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):

View File

@ -0,0 +1,2 @@
stco (32 bytes):
Data = length 24, hash E472D485