Clean up comments in Boxes.java

The CL aims to

1. Shorten unnecessary lengthy chatty comments.
2. Remove dead TODOs.
3. nit fixes for comment style consistency.
4. Remove usage of "we" in the comments.
5. Media3 muxer does not need to mention the behaviour of framework muxer
unless its required for some purpose, so remove them.

PiperOrigin-RevId: 668985875
This commit is contained in:
sheenachhabra 2024-08-29 10:33:26 -07:00 committed by Copybara-Service
parent 39ed9cf88d
commit 388d1f17b9

View File

@ -50,12 +50,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.checkerframework.checker.nullness.qual.PolyNull; import org.checkerframework.checker.nullness.qual.PolyNull;
/** /** Writes out various types of boxes as per MP4 (ISO/IEC 14496-12) standards. */
* Writes out various types of boxes as per MP4 (ISO/IEC 14496-12) standards.
*
* <p>Boxes do not construct their sub-boxes but take them as input {@linkplain ByteBuffer byte
* buffers}.
*/
/* package */ final class Boxes { /* package */ final class Boxes {
/** Provides track's metadata like media format, written samples. */ /** Provides track's metadata like media format, written samples. */
public interface TrackMetadataProvider { public interface TrackMetadataProvider {
@ -70,25 +65,25 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
ImmutableList<Integer> writtenChunkSampleCounts(); ImmutableList<Integer> writtenChunkSampleCounts();
} }
/* Total number of bytes in an integer. */ /** Total number of bytes in an integer. */
private static final int BYTES_PER_INTEGER = 4; private static final int BYTES_PER_INTEGER = 4;
// Box size (4 bytes) + Box name (4 bytes) /** Box size (4 bytes) + Box name (4 bytes) */
public static final int BOX_HEADER_SIZE = 8; public static final int BOX_HEADER_SIZE = 8;
// Box size = 1 to indicate 64-bit box size (4 bytes) + Box name (4 bytes) + actual size (8 bytes) /**
* Box size = 1 to indicate 64-bit box size (4 bytes) + Box name (4 bytes) + actual box size (8
* bytes)
*/
public static final int LARGE_SIZE_BOX_HEADER_SIZE = 16; public static final int LARGE_SIZE_BOX_HEADER_SIZE = 16;
/** The size (in bytes) of the mfhd box content. */
public static final int MFHD_BOX_CONTENT_SIZE = 2 * BYTES_PER_INTEGER; public static final int MFHD_BOX_CONTENT_SIZE = 2 * BYTES_PER_INTEGER;
/** The size (in bytes) of the tfhd box content. */
public static final int TFHD_BOX_CONTENT_SIZE = 4 * BYTES_PER_INTEGER; public static final int TFHD_BOX_CONTENT_SIZE = 4 * BYTES_PER_INTEGER;
/** /** The maximum size (in bytes) of boxes that have fixed sizes. */
* The maximum length of boxes which have fixed sizes.
*
* <p>Technically, we'd know how long they actually are; this upper bound is much simpler to
* produce though and we'll throw if we overflow anyway.
*/
private static final int MAX_FIXED_LEAF_BOX_SIZE = 200; private static final int MAX_FIXED_LEAF_BOX_SIZE = 200;
/** /**
@ -97,10 +92,13 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
*/ */
private static final long MVHD_TIMEBASE = 10_000L; private static final long MVHD_TIMEBASE = 10_000L;
// unsigned int(2) sample_depends_on = 2 (bit index 25 and 24) /** 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; 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) /**
* 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 static final int TRUN_BOX_NON_SYNC_SAMPLE_FLAGS = 0b00000001_00000001_00000000_00000000;
private Boxes() {} private Boxes() {}
@ -153,7 +151,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
// Generate the sample durations to calculate the total duration for tkhd box. // Generate the sample durations to calculate the total duration for tkhd box.
List<Integer> sampleDurationsVu = List<Integer> sampleDurationsVu =
Boxes.convertPresentationTimestampsToDurationsVu( convertPresentationTimestampsToDurationsVu(
track.writtenSamples(), track.writtenSamples(),
minInputPtsUs, minInputPtsUs,
track.videoUnitTimebase(), track.videoUnitTimebase(),
@ -167,17 +165,15 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
long trackDurationUs = usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase()); long trackDurationUs = usFromVu(trackDurationInTrackUnitsVu, track.videoUnitTimebase());
@C.TrackType int trackType = MimeTypes.getTrackType(format.sampleMimeType); @C.TrackType int trackType = MimeTypes.getTrackType(format.sampleMimeType);
ByteBuffer stts = Boxes.stts(sampleDurationsVu); ByteBuffer stts = stts(sampleDurationsVu);
ByteBuffer ctts = ByteBuffer ctts =
MimeTypes.isVideo(format.sampleMimeType) MimeTypes.isVideo(format.sampleMimeType)
? Boxes.ctts(track.writtenSamples(), sampleDurationsVu, track.videoUnitTimebase()) ? ctts(track.writtenSamples(), sampleDurationsVu, track.videoUnitTimebase())
: ByteBuffer.allocate(0); : ByteBuffer.allocate(0);
ByteBuffer stsz = Boxes.stsz(track.writtenSamples()); ByteBuffer stsz = stsz(track.writtenSamples());
ByteBuffer stsc = Boxes.stsc(track.writtenChunkSampleCounts()); ByteBuffer stsc = stsc(track.writtenChunkSampleCounts());
ByteBuffer chunkOffsetBox = ByteBuffer chunkOffsetBox =
isFragmentedMp4 isFragmentedMp4 ? stco(track.writtenChunkOffsets()) : co64(track.writtenChunkOffsets());
? Boxes.stco(track.writtenChunkOffsets())
: Boxes.co64(track.writtenChunkOffsets());
String handlerType; String handlerType;
String handlerName; String handlerName;
@ -190,81 +186,68 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
case C.TRACK_TYPE_VIDEO: case C.TRACK_TYPE_VIDEO:
handlerType = "vide"; handlerType = "vide";
handlerName = "VideoHandle"; handlerName = "VideoHandle";
mhdBox = Boxes.vmhd(); mhdBox = vmhd();
sampleEntryBox = Boxes.videoSampleEntry(format); sampleEntryBox = videoSampleEntry(format);
stsdBox = Boxes.stsd(sampleEntryBox); stsdBox = stsd(sampleEntryBox);
stblBox = stblBox =
Boxes.stbl( stbl(stsdBox, stts, ctts, stsz, stsc, chunkOffsetBox, stss(track.writtenSamples()));
stsdBox,
stts,
ctts,
stsz,
stsc,
chunkOffsetBox,
Boxes.stss(track.writtenSamples()));
break; break;
case C.TRACK_TYPE_AUDIO: case C.TRACK_TYPE_AUDIO:
handlerType = "soun"; handlerType = "soun";
handlerName = "SoundHandle"; handlerName = "SoundHandle";
mhdBox = Boxes.smhd(); mhdBox = smhd();
sampleEntryBox = Boxes.audioSampleEntry(format); sampleEntryBox = audioSampleEntry(format);
stsdBox = Boxes.stsd(sampleEntryBox); stsdBox = stsd(sampleEntryBox);
stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, chunkOffsetBox); stblBox = stbl(stsdBox, stts, stsz, stsc, chunkOffsetBox);
break; break;
case C.TRACK_TYPE_METADATA: 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: case C.TRACK_TYPE_UNKNOWN:
handlerType = "meta"; handlerType = "meta";
handlerName = "MetaHandle"; handlerName = "MetaHandle";
mhdBox = Boxes.nmhd(); mhdBox = nmhd();
sampleEntryBox = Boxes.textMetaDataSampleEntry(format); sampleEntryBox = textMetaDataSampleEntry(format);
stsdBox = Boxes.stsd(sampleEntryBox); stsdBox = stsd(sampleEntryBox);
stblBox = Boxes.stbl(stsdBox, stts, stsz, stsc, chunkOffsetBox); stblBox = stbl(stsdBox, stts, stsz, stsc, chunkOffsetBox);
break; break;
default: default:
throw new IllegalArgumentException("Unsupported track type"); 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 = ByteBuffer trakBox =
Boxes.trak( trak(
Boxes.tkhd( tkhd(
nextTrackId, nextTrackId,
trackDurationUs, trackDurationUs,
creationTimestampSeconds, creationTimestampSeconds,
modificationTimestampSeconds, modificationTimestampSeconds,
metadataCollector.orientationData.orientation, metadataCollector.orientationData.orientation,
format), format),
Boxes.mdia( mdia(
Boxes.mdhd( mdhd(
trackDurationInTrackUnitsVu, trackDurationInTrackUnitsVu,
track.videoUnitTimebase(), track.videoUnitTimebase(),
creationTimestampSeconds, creationTimestampSeconds,
modificationTimestampSeconds, modificationTimestampSeconds,
languageCode), languageCode),
Boxes.hdlr(handlerType, handlerName), hdlr(handlerType, handlerName),
Boxes.minf(mhdBox, Boxes.dinf(Boxes.dref(Boxes.localUrl())), stblBox))); minf(mhdBox, dinf(dref(localUrl())), stblBox)));
trakBoxes.add(trakBox); trakBoxes.add(trakBox);
videoDurationUs = max(videoDurationUs, trackDurationUs); videoDurationUs = max(videoDurationUs, trackDurationUs);
trexBoxes.add(Boxes.trex(nextTrackId)); trexBoxes.add(trex(nextTrackId));
nextTrackId++; nextTrackId++;
} }
ByteBuffer mvhdBox = ByteBuffer mvhdBox =
Boxes.mvhd( mvhd(nextTrackId, creationTimestampSeconds, modificationTimestampSeconds, videoDurationUs);
nextTrackId, creationTimestampSeconds, modificationTimestampSeconds, videoDurationUs); ByteBuffer udtaBox = udta(metadataCollector.locationData);
ByteBuffer udtaBox = Boxes.udta(metadataCollector.locationData);
ByteBuffer metaBox = ByteBuffer metaBox =
metadataCollector.metadataEntries.isEmpty() metadataCollector.metadataEntries.isEmpty()
? ByteBuffer.allocate(0) ? ByteBuffer.allocate(0)
: Boxes.meta( : meta(
Boxes.hdlr(/* handlerType= */ "mdta", /* handlerName= */ ""), hdlr(/* handlerType= */ "mdta", /* handlerName= */ ""),
Boxes.keys(Lists.newArrayList(metadataCollector.metadataEntries)), keys(Lists.newArrayList(metadataCollector.metadataEntries)),
Boxes.ilst(Lists.newArrayList(metadataCollector.metadataEntries))); ilst(Lists.newArrayList(metadataCollector.metadataEntries)));
List<ByteBuffer> subBoxes = new ArrayList<>(); List<ByteBuffer> subBoxes = new ArrayList<>();
subBoxes.add(mvhdBox); subBoxes.add(mvhdBox);
@ -272,15 +255,14 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
subBoxes.add(metaBox); subBoxes.add(metaBox);
subBoxes.addAll(trakBoxes); subBoxes.addAll(trakBoxes);
if (isFragmentedMp4) { if (isFragmentedMp4) {
subBoxes.add(Boxes.mvex(trexBoxes)); subBoxes.add(mvex(trexBoxes));
} }
ByteBuffer moovBox = BoxUtils.wrapBoxesIntoBox("moov", subBoxes); ByteBuffer moovBox = BoxUtils.wrapBoxesIntoBox("moov", subBoxes);
// Also add XMP if needed
if (metadataCollector.xmpData != null) { if (metadataCollector.xmpData != null) {
return BoxUtils.concatenateBuffers( return BoxUtils.concatenateBuffers(
moovBox, Boxes.uuid(Boxes.XMP_UUID, ByteBuffer.wrap(metadataCollector.xmpData.data))); moovBox, uuid(XMP_UUID, ByteBuffer.wrap(metadataCollector.xmpData.data)));
} else { } else {
// No need for another copy if there is no XMP to be appended. // No need for another copy if there is no XMP to be appended.
return moovBox; return moovBox;
@ -300,10 +282,10 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
int orientation, int orientation,
Format format) { Format format) {
ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x00000007); // version and flags; allow presentation, etc. contents.putInt(0x00000007); // version and flags: allow presentation, etc.
contents.putInt(creationTimestampSeconds); // creation_time; unsigned int(32) contents.putInt(creationTimestampSeconds); // creation_time: unsigned int(32)
contents.putInt(modificationTimestampSeconds); // modification_time; unsigned int(32) contents.putInt(modificationTimestampSeconds); // modification_time: unsigned int(32)
contents.putInt(trackId); contents.putInt(trackId);
contents.putInt(0); // reserved contents.putInt(0); // reserved
@ -345,8 +327,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0); // version and flags contents.putInt(0); // version and flags
contents.putInt(creationTimestampSeconds); // creation_time; unsigned int(32) contents.putInt(creationTimestampSeconds); // creation_time: unsigned int(32)
contents.putInt(modificationTimestampSeconds); // modification_time; unsigned int(32) contents.putInt(modificationTimestampSeconds); // modification_time: unsigned int(32)
contents.putInt((int) MVHD_TIMEBASE); // The per-track timescales might be different. contents.putInt((int) MVHD_TIMEBASE); // The per-track timescales might be different.
contents.putInt( contents.putInt(
(int) vuFromUs(videoDurationUs, MVHD_TIMEBASE)); // Duration of the entire video. (int) vuFromUs(videoDurationUs, MVHD_TIMEBASE)); // Duration of the entire video.
@ -390,8 +372,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x0); // version and flags contents.putInt(0x0); // version and flags
contents.putInt(creationTimestampSeconds); // creation_time; unsigned int(32) contents.putInt(creationTimestampSeconds); // creation_time: unsigned int(32)
contents.putInt(modificationTimestampSeconds); // modification_time; unsigned int(32) contents.putInt(modificationTimestampSeconds); // modification_time: unsigned int(32)
contents.putInt(videoUnitTimebase); contents.putInt(videoUnitTimebase);
@ -463,9 +445,9 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
String mimeType = checkNotNull(format.sampleMimeType); String mimeType = checkNotNull(format.sampleMimeType);
byte[] mimeBytes = Util.getUtf8Bytes(mimeType); byte[] mimeBytes = Util.getUtf8Bytes(mimeType);
contents.put(mimeBytes); // content_encoding contents.put(mimeBytes); // content_encoding
contents.put((byte) 0x00); contents.put((byte) 0x0);
contents.put(mimeBytes); // mime_format contents.put(mimeBytes); // mime_format
contents.put((byte) 0x00); contents.put((byte) 0x0);
contents.flip(); contents.flip();
return BoxUtils.wrapIntoBox("mett", contents); return BoxUtils.wrapIntoBox("mett", contents);
@ -478,8 +460,6 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
/** Returns the dref (data references) box. */ /** Returns the dref (data references) box. */
public static ByteBuffer dref(ByteBuffer... dataLocationBoxes) { public static ByteBuffer dref(ByteBuffer... dataLocationBoxes) {
// We have a "number of contained boxes" field; let's pretend this is also a box so that
// wrapBoxesIntoBoxes() can concatenate it with the rest.
ByteBuffer header = ByteBuffer.allocate(8); ByteBuffer header = ByteBuffer.allocate(8);
header.putInt(0); header.putInt(0);
header.putInt(dataLocationBoxes.length); header.putInt(dataLocationBoxes.length);
@ -506,12 +486,9 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
public static ByteBuffer localUrl() { public static ByteBuffer localUrl() {
ByteBuffer contents = ByteBuffer.allocate(4); ByteBuffer contents = ByteBuffer.allocate(4);
// Flag indicating that the data is in fact in this very file instead of a remote // Indicates that the data is in this file instead of in a remote URL. Hence no URL is written.
// URL. Accordingly, no actual URL string is present.
contents.putInt(1); contents.putInt(1);
// Since we set the flag to 1, no actual URL needs to follow.
contents.flip(); contents.flip();
return BoxUtils.wrapIntoBox("url ", contents); return BoxUtils.wrapIntoBox("url ", contents);
} }
@ -529,14 +506,14 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
*/ */
public static ByteBuffer hdlr(String handlerType, String handlerName) { public static ByteBuffer hdlr(String handlerType, String handlerName) {
ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x0); // version and flags. contents.putInt(0x0); // version and flags
contents.putInt(0); // pre_defined. contents.putInt(0); // pre_defined
contents.put(Util.getUtf8Bytes(handlerType)); // handler_type. contents.put(Util.getUtf8Bytes(handlerType)); // handler_type
contents.putInt(0); // reserved. contents.putInt(0); // reserved
contents.putInt(0); // reserved. contents.putInt(0); // reserved
contents.putInt(0); // reserved. contents.putInt(0); // reserved
contents.put(Util.getUtf8Bytes(handlerName)); // name. contents.put(Util.getUtf8Bytes(handlerName)); // name
contents.put((byte) 0); // The null terminator for name. contents.put((byte) 0); // The null terminator for name
contents.flip(); contents.flip();
return BoxUtils.wrapIntoBox("hdlr", contents); return BoxUtils.wrapIntoBox("hdlr", contents);
@ -566,7 +543,6 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
* <p>This box contains user data like location info. * <p>This box contains user data like location info.
*/ */
public static ByteBuffer udta(@Nullable Mp4LocationData location) { public static ByteBuffer udta(@Nullable Mp4LocationData location) {
// We can just omit the entire box if there is no location info available.
if (location == null) { if (location == null) {
return ByteBuffer.allocate(0); return ByteBuffer.allocate(0);
} }
@ -576,7 +552,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
ByteBuffer xyzBoxContents = ByteBuffer.allocate(locationString.length() + 2 + 2); ByteBuffer xyzBoxContents = ByteBuffer.allocate(locationString.length() + 2 + 2);
xyzBoxContents.putShort((short) (xyzBoxContents.capacity() - 4)); xyzBoxContents.putShort((short) (xyzBoxContents.capacity() - 4));
xyzBoxContents.putShort((short) 0x15C7); // language code? xyzBoxContents.putShort((short) 0x15C7); // language code
xyzBoxContents.put(Util.getUtf8Bytes(locationString)); xyzBoxContents.put(Util.getUtf8Bytes(locationString));
checkState(xyzBoxContents.limit() == xyzBoxContents.capacity()); checkState(xyzBoxContents.limit() == xyzBoxContents.capacity());
@ -679,11 +655,11 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
ByteBuffer contents = ByteBuffer contents =
ByteBuffer.allocate(codecSpecificBox.remaining() + MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer.allocate(codecSpecificBox.remaining() + MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x00); // reserved contents.putInt(0x0); // reserved
contents.putShort((short) 0x0); // reserved contents.putShort((short) 0x0); // reserved
contents.putShort((short) 0x1); // data ref index contents.putShort((short) 0x1); // data ref index
contents.putInt(0x00); // reserved contents.putInt(0x0); // reserved
contents.putInt(0x00); // reserved contents.putInt(0x0); // reserved
int channelCount = format.channelCount; int channelCount = format.channelCount;
contents.putShort((short) channelCount); contents.putShort((short) channelCount);
@ -863,19 +839,17 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
public static ByteBuffer stts(List<Integer> durationsVu) { public static ByteBuffer stts(List<Integer> durationsVu) {
ByteBuffer contents = ByteBuffer.allocate(durationsVu.size() * 8 + MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(durationsVu.size() * 8 + MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x0); // version and flags. contents.putInt(0x0); // version and flags
// We will know total entry count only after processing all the sample durations, so put in a // Total entry count is known only after processing all sample durations, so put in a
// placeholder for total entry count and store its index. // placeholder for total entry count and store its index.
int totalEntryCountIndex = contents.position(); int totalEntryCountIndex = contents.position();
contents.putInt(0x0); // entry_count. contents.putInt(0x0); // entry_count
int totalEntryCount = 0; int totalEntryCount = 0;
long lastDurationVu = -1L; long lastDurationVu = -1L;
int lastSampleCountIndex = -1; int lastSampleCountIndex = -1;
// Note that the framework MediaMuxer adjust time deltas within plus-minus 100 us, so that
// samples have repeating duration values. It saves few entries in the table.
for (int i = 0; i < durationsVu.size(); i++) { for (int i = 0; i < durationsVu.size(); i++) {
int durationVu = durationsVu.get(i); int durationVu = durationsVu.get(i);
if (lastDurationVu != durationVu) { if (lastDurationVu != durationVu) {
@ -903,7 +877,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
List<BufferInfo> samplesInfo, List<Integer> durationVu, int videoUnitTimescale) { List<BufferInfo> samplesInfo, List<Integer> durationVu, int videoUnitTimescale) {
// Generate the sample composition offsets list to create ctts box. // Generate the sample composition offsets list to create ctts box.
List<Integer> compositionOffsets = List<Integer> compositionOffsets =
Boxes.calculateSampleCompositionTimeOffsets(samplesInfo, durationVu, videoUnitTimescale); calculateSampleCompositionTimeOffsets(samplesInfo, durationVu, videoUnitTimescale);
if (compositionOffsets.isEmpty()) { if (compositionOffsets.isEmpty()) {
return ByteBuffer.allocate(0); return ByteBuffer.allocate(0);
@ -915,10 +889,10 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
contents.putInt(1); // version and flags. contents.putInt(1); // version and flags.
// We will know total entry count only after processing all the composition offsets, so put in // Total entry count is known only after processing all the composition offsets, so put in
// a placeholder for total entry count and store its index. // a placeholder for total entry count and store its index.
int totalEntryCountIndex = contents.position(); int totalEntryCountIndex = contents.position();
contents.putInt(0x0); // entry_count. contents.putInt(0x0); // entry_count
int totalEntryCount = 0; int totalEntryCount = 0;
int lastCompositionOffset = -1; int lastCompositionOffset = -1;
@ -996,14 +970,14 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
public static ByteBuffer stsz(List<MediaCodec.BufferInfo> writtenSamples) { public static ByteBuffer stsz(List<MediaCodec.BufferInfo> writtenSamples) {
ByteBuffer contents = ByteBuffer.allocate(writtenSamples.size() * 4 + MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(writtenSamples.size() * 4 + MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x0); // version and flags. contents.putInt(0x0); // version and flags
// TODO: b/270583563 - Consider optimizing for identically-sized samples. // TODO: b/270583563 - Consider optimizing for identically-sized samples.
// sample_size; specifying the default sample size. Set to zero to indicate that the samples // sample_size: specifying the default sample size. Set to zero to indicate that the samples
// have different sizes and they are stored in the sample size table. // have different sizes and they are stored in the sample size table.
contents.putInt(0); contents.putInt(0);
contents.putInt(writtenSamples.size()); // sample_count. contents.putInt(writtenSamples.size()); // sample_count
for (int i = 0; i < writtenSamples.size(); i++) { for (int i = 0; i < writtenSamples.size(); i++) {
contents.putInt(writtenSamples.get(i).size); contents.putInt(writtenSamples.get(i).size);
@ -1018,17 +992,17 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
ByteBuffer contents = ByteBuffer contents =
ByteBuffer.allocate(writtenChunkSampleCounts.size() * 12 + MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer.allocate(writtenChunkSampleCounts.size() * 12 + MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x0); // version and flags. contents.putInt(0x0); // version and flags
contents.putInt(writtenChunkSampleCounts.size()); // entry_count. contents.putInt(writtenChunkSampleCounts.size()); // entry_count
int currentChunk = 1; int currentChunk = 1;
// TODO: b/270583563 - Consider optimizing for consecutive chunks having same number of samples. // TODO: b/270583563 - Consider optimizing for consecutive chunks having same number of samples.
for (int i = 0; i < writtenChunkSampleCounts.size(); i++) { for (int i = 0; i < writtenChunkSampleCounts.size(); i++) {
int samplesInChunk = writtenChunkSampleCounts.get(i); int samplesInChunk = writtenChunkSampleCounts.get(i);
contents.putInt(currentChunk); // first_chunk. contents.putInt(currentChunk); // first_chunk
contents.putInt(samplesInChunk); // samples_per_chunk. contents.putInt(samplesInChunk); // samples_per_chunk
// sample_description_index; we have only one sample description in each track. // sample_description_index: there is only one sample description in each track.
contents.putInt(1); contents.putInt(1);
currentChunk += 1; currentChunk += 1;
@ -1044,12 +1018,12 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
ByteBuffer.allocate(2 * BYTES_PER_INTEGER + writtenChunkOffsets.size() * BYTES_PER_INTEGER); ByteBuffer.allocate(2 * BYTES_PER_INTEGER + writtenChunkOffsets.size() * BYTES_PER_INTEGER);
contents.putInt(0x0); // version and flags contents.putInt(0x0); // version and flags
contents.putInt(writtenChunkOffsets.size()); // entry_count; unsigned int(32) contents.putInt(writtenChunkOffsets.size()); // entry_count: unsigned int(32)
for (int i = 0; i < writtenChunkOffsets.size(); i++) { for (int i = 0; i < writtenChunkOffsets.size(); i++) {
long chunkOffset = writtenChunkOffsets.get(i); long chunkOffset = writtenChunkOffsets.get(i);
checkState(chunkOffset <= UNSIGNED_INT_MAX_VALUE, "Only 32-bit chunk offset is allowed"); checkState(chunkOffset <= UNSIGNED_INT_MAX_VALUE, "Only 32-bit chunk offset is allowed");
contents.putInt((int) chunkOffset); // chunk_offset; unsigned int(32) contents.putInt((int) chunkOffset); // chunk_offset: unsigned int(32)
} }
contents.flip(); contents.flip();
@ -1063,10 +1037,10 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
2 * BYTES_PER_INTEGER + 2 * writtenChunkOffsets.size() * BYTES_PER_INTEGER); 2 * BYTES_PER_INTEGER + 2 * writtenChunkOffsets.size() * BYTES_PER_INTEGER);
contents.putInt(0x0); // version and flags contents.putInt(0x0); // version and flags
contents.putInt(writtenChunkOffsets.size()); // entry_count; unsigned int(32) contents.putInt(writtenChunkOffsets.size()); // entry_count: unsigned int(32)
for (int i = 0; i < writtenChunkOffsets.size(); i++) { for (int i = 0; i < writtenChunkOffsets.size(); i++) {
contents.putLong(writtenChunkOffsets.get(i)); // chunk_offset; unsigned int(64) contents.putLong(writtenChunkOffsets.get(i)); // chunk_offset: unsigned int(64)
} }
contents.flip(); contents.flip();
@ -1077,19 +1051,19 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
public static ByteBuffer stss(List<MediaCodec.BufferInfo> writtenSamples) { public static ByteBuffer stss(List<MediaCodec.BufferInfo> writtenSamples) {
ByteBuffer contents = ByteBuffer.allocate(writtenSamples.size() * 4 + MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(writtenSamples.size() * 4 + MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x0); // version and flags. contents.putInt(0x0); // version and flags
// We will know total entry count only after processing all the sample, so put in a placeholder // Total entry count is known only after processing all sample, so put in a placeholder
// for total entry count and store its index. // for total entry count and store its index.
int totalEntryCountIndex = contents.position(); int totalEntryCountIndex = contents.position();
contents.putInt(writtenSamples.size()); // entry_count. contents.putInt(writtenSamples.size()); // entry_count
int currentSampleNumber = 1; int currentSampleNumber = 1;
int totalKeyFrames = 0; int totalKeyFrames = 0;
for (int i = 0; i < writtenSamples.size(); i++) { for (int i = 0; i < writtenSamples.size(); i++) {
MediaCodec.BufferInfo info = writtenSamples.get(i); MediaCodec.BufferInfo info = writtenSamples.get(i);
if ((info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) > 0) { if ((info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) > 0) {
contents.putInt(currentSampleNumber); // sample_number. contents.putInt(currentSampleNumber);
totalKeyFrames++; totalKeyFrames++;
} }
@ -1106,8 +1080,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
public static ByteBuffer stsd(ByteBuffer sampleEntryBox) { public static ByteBuffer stsd(ByteBuffer sampleEntryBox) {
ByteBuffer contents = ByteBuffer.allocate(sampleEntryBox.limit() + MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(sampleEntryBox.limit() + MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x0); // version and flags. contents.putInt(0x0); // version and flags
contents.putInt(1); // entry_count, We have only one sample description in each track. contents.putInt(1); // entry_count: there is only one sample description in each track.
contents.put(sampleEntryBox); contents.put(sampleEntryBox);
contents.flip(); contents.flip();
@ -1265,22 +1239,14 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
/** Adjusts the duration of the very last sample if needed. */ /** Adjusts the duration of the very last sample if needed. */
private static void adjustLastSampleDuration( private static void adjustLastSampleDuration(
List<Integer> durationsToBeAdjustedVu, @Mp4Muxer.LastSampleDurationBehavior int behavior) { List<Integer> durationsToBeAdjustedVu, @Mp4Muxer.LastSampleDurationBehavior int behavior) {
// Technically, MP4 file stores frame durations, not timestamps. If a frame starts at a // For a track having less than 3 samples, duplicating the last frame duration will
// given timestamp then the duration of the last frame is not obvious. If samples follow each // significantly increase the overall track duration, so avoid that.
// other in roughly regular intervals (e.g. in a normal, 30 fps video), it can be safely assumed
// that the last sample will have same duration (~33ms) as other samples. On the other hand, if
// there are just a few, irregularly spaced frames, with duplication, the entire duration of the
// video will increase, creating abnormal gaps.
if (durationsToBeAdjustedVu.size() <= 2) { if (durationsToBeAdjustedVu.size() <= 2) {
// Nothing to duplicate if there are 0 or 1 entries.
return; return;
} }
switch (behavior) { switch (behavior) {
case Mp4Muxer.LAST_SAMPLE_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION: case Mp4Muxer.LAST_SAMPLE_DURATION_BEHAVIOR_DUPLICATE_PREV_DURATION:
// This is the default MediaMuxer behavior: the last sample duration is a copy of the
// previous sample duration.
durationsToBeAdjustedVu.set( durationsToBeAdjustedVu.set(
durationsToBeAdjustedVu.size() - 1, durationsToBeAdjustedVu.size() - 1,
durationsToBeAdjustedVu.get(durationsToBeAdjustedVu.size() - 2)); durationsToBeAdjustedVu.get(durationsToBeAdjustedVu.size() - 2));
@ -1299,10 +1265,10 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
private static ByteBuffer d263Box() { private static ByteBuffer d263Box() {
ByteBuffer d263Box = ByteBuffer.allocate(7); ByteBuffer d263Box = ByteBuffer.allocate(7);
d263Box.put(" ".getBytes(UTF_8)); // 4 spaces (vendor) d263Box.put(" ".getBytes(UTF_8)); // 4 spaces (vendor)
d263Box.put((byte) 0x00); // decoder version d263Box.put((byte) 0x0); // decoder version
// TODO: b/352000778 - Get profile and level from format. // TODO: b/352000778 - Get profile and level from format.
d263Box.put((byte) 0x10); // level d263Box.put((byte) 0x10); // level
d263Box.put((byte) 0x00); // profile d263Box.put((byte) 0x0); // profile
d263Box.flip(); d263Box.flip();
return BoxUtils.wrapIntoBox("d263", d263Box); return BoxUtils.wrapIntoBox("d263", d263Box);
@ -1429,7 +1395,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
contents.put(bitDepthLumaMinus8); contents.put(bitDepthLumaMinus8);
contents.put(bitDepthChromaMinus8); contents.put(bitDepthChromaMinus8);
// avgFrameRate; value 0 indicates an unspecified average frame rate. // avgFrameRate: value 0 indicates an unspecified average frame rate.
contents.putShort((short) 0); contents.putShort((short) 0);
// constantFrameRate (2 bits) + numTemporalLayers (3 bits) + temporalIdNested (1 bit) + // constantFrameRate (2 bits) + numTemporalLayers (3 bits) + temporalIdNested (1 bit) +
@ -1560,7 +1526,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
byte[] hdrStaticInfo = colorInfo.hdrStaticInfo; byte[] hdrStaticInfo = colorInfo.hdrStaticInfo;
if (hdrStaticInfo != null) { if (hdrStaticInfo != null) {
ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x00); // version and flag. contents.putInt(0x0); // version and flag
contents.put(hdrStaticInfo); contents.put(hdrStaticInfo);
contents.flip(); contents.flip();
return BoxUtils.wrapIntoBox("SmDm", contents); return BoxUtils.wrapIntoBox("SmDm", contents);
@ -1590,7 +1556,6 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
contents.put((byte) 'l'); contents.put((byte) 'l');
contents.put((byte) 'x'); contents.put((byte) 'x');
// Parameters going into the file.
short primaries = 0; short primaries = 0;
short transfer = 0; short transfer = 0;
short matrix = 0; short matrix = 0;
@ -1688,7 +1653,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
getSizeBuffer(csdSize + dsiSizeBuffer.remaining() + dcdSizeBuffer.remaining() + 21); getSizeBuffer(csdSize + dsiSizeBuffer.remaining() + dcdSizeBuffer.remaining() + 21);
ByteBuffer contents = ByteBuffer.allocate(csdSize + MAX_FIXED_LEAF_BOX_SIZE); ByteBuffer contents = ByteBuffer.allocate(csdSize + MAX_FIXED_LEAF_BOX_SIZE);
contents.putInt(0x00); // Version and flags. contents.putInt(0x0); // version and flags
contents.put((byte) 0x03); // ES_DescrTag contents.put((byte) 0x03); // ES_DescrTag
contents.put(esdSizeBuffer); contents.put(esdSizeBuffer);
@ -1696,7 +1661,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
contents.putShort((short) 0x0000); // ES_ID contents.putShort((short) 0x0000); // ES_ID
// streamDependenceFlag (1 bit) + URL_Flag (1 bit) + OCRstreamFlag (1 bit) + streamPriority (5 // streamDependenceFlag (1 bit) + URL_Flag (1 bit) + OCRstreamFlag (1 bit) + streamPriority (5
// bits) // bits)
contents.put(isVideo ? (byte) 0x1f : (byte) 0x00); contents.put(isVideo ? (byte) 0x1f : (byte) 0x0);
contents.put((byte) 0x04); // DecoderConfigDescrTag contents.put((byte) 0x04); // DecoderConfigDescrTag
contents.put(dcdSizeBuffer); contents.put(dcdSizeBuffer);
@ -1709,7 +1674,7 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
int size = isVideo ? 0x017700 : 0x000300; int size = isVideo ? 0x017700 : 0x000300;
contents.putShort((short) ((size >> 8) & 0xFFFF)); // First 16 bits of buffer size. contents.putShort((short) ((size >> 8) & 0xFFFF)); // First 16 bits of buffer size.
contents.put((byte) 0x00); // Last 8 bits of buffer size. contents.put((byte) 0x0); // Last 8 bits of buffer size.
contents.putInt(peakBitrate != Format.NO_VALUE ? peakBitrate : 0); contents.putInt(peakBitrate != Format.NO_VALUE ? peakBitrate : 0);
contents.putInt(averageBitrate != Format.NO_VALUE ? averageBitrate : 0); contents.putInt(averageBitrate != Format.NO_VALUE ? averageBitrate : 0);
@ -1818,16 +1783,13 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
throw new IllegalArgumentException("Non-length-3 language code: " + code); throw new IllegalArgumentException("Non-length-3 language code: " + code);
} }
// Use an int so that we don't bump into the issue of Java not having unsigned types. We take // Take only last 5 bits of each letter.
// the last 5 bits of each letter to supply 5 bits each of the eventual code.
int value = (bytes[2] & 0x1F); int value = (bytes[2] & 0x1F);
value += (bytes[1] & 0x1F) << 5; value += (bytes[1] & 0x1F) << 5;
value += (bytes[0] & 0x1F) << 10; value += (bytes[0] & 0x1F) << 10;
// This adds up to 15 bits; the 16th one is really supposed to be 0. // Total 15 bits for the language code and the 16th bit should be 0.
checkState((value & 0x8000) == 0); return (short) (value & 0x7FFF);
return (short) (value & 0xFFFF);
} }
/** /**
@ -1859,6 +1821,6 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
/** Converts microseconds to video units, using the provided timebase. */ /** Converts microseconds to video units, using the provided timebase. */
private static long vuFromUs(long timestampUs, long videoUnitTimebase) { private static long vuFromUs(long timestampUs, long videoUnitTimebase) {
return timestampUs * videoUnitTimebase / 1_000_000L; // (division for us to s conversion) return timestampUs * videoUnitTimebase / 1_000_000L; // Division for microsecond to second.
} }
} }