/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.msf.LOCMAFParser');
goog.require('shaka.log');
goog.require('shaka.media.SegmentUtils');
goog.require('shaka.util.Mp4Generator');
/**
* Parser for LOCMAF media objects, which turns each one back into the CMAF
* chunk it was made from.
*
* LOCMAF is CMAF with the `moof` taken apart. The sample data travels
* untouched as the object's trailing bytes, and everything the `moof` said
* about it -- sample sizes, durations, flags, composition offsets, the decode
* time, and the CENC per-sample metadata -- travels ahead of it as a handful
* of tagged fields. Consecutive chunks of a group carry only what changed,
* so the steady-state object is a two-byte header plus one frame.
*
* ## What has to be remembered
*
* Unlike a CMAF chunk, a LOCMAF object is not self-describing. Three kinds of
* state make it readable:
*
* 1. **The initialization segment.** The track's `trex` defaults, its
* `track_ID`, and, for protected tracks, its `tenc` defaults are what
* every omitted field falls back to. They are read once, from the same
* CMAF Header a plain `cmaf` track would carry, and handed in as
* constructor parameters.
*
* 2. **The previous chunk of the group.** A delta header is a set of
* differences against the chunk before it, so a chunk cannot be read in
* isolation. A full header re-anchors that state; so does a rawBoxes
* object, after which the next header must be a full one.
*
* 3. **The decode time.** `baseMediaDecodeTime` never travels in a delta
* header at all. It is derived as the previous chunk's decode time plus
* the sum of the previous chunk's effective sample durations, which
* makes the running state load-bearing for timing and not merely for
* compression.
*
* Because of (2) and (3), a lost object poisons every delta that follows it.
* The spec requires the receiver to notice -- a gap in object IDs -- and stop
* applying deltas until the next full header or the next group, which is what
* `checkContinuity_` does.
*
* ## What comes out
*
* The reconstruction follows the canonical form of the spec's Section 15
* rather than any convenient equivalent. Playback does not need canonical
* bytes, but conformance vectors are expressed in them, so producing them is
* what makes this parser testable against the reference implementation.
*
* Note that the canonical chunk is a function of the chunk's *effective*
* values alone -- what each sample's duration, size, flags and offset work
* out to -- and not of how those values happened to be split between
* per-sample lists and `tfhd`/`trex` defaults on the wire. This parser
* therefore expands every field to a per-sample array first and re-derives
* the box layout from the arrays, which is both simpler than tracking wire
* provenance and the only way two encoders' output can be compared.
*
* @see https://datatracker.ietf.org/doc/draft-einarsson-moq-locmaf/
*
* @final
*/
shaka.msf.LOCMAFParser = class {
/**
* @param {!shaka.extern.MsfCodec} codec The negotiated draft's primitive
* codec, used to read the variable-length integers the spec calls vi64.
* It defines them by reference to MOQT's own encoding, which changed in
* draft-17, so the same header bytes mean different numbers depending on
* which draft delivered them.
* @param {!shaka.msf.LOCMAFParser.TrackParams} params Everything read out
* of the track's CMAF Header.
*/
constructor(codec, params) {
/** @private {!shaka.extern.MsfCodec} */
this.codec_ = codec;
/** @private {!shaka.msf.LOCMAFParser.TrackParams} */
this.params_ = params;
/**
* The in-group reference state: the represented field values of the
* previous chunk, keyed by LOCMAF field ID. Not the effective values --
* a delta applies to what the previous chunk carried, so a field the
* previous chunk left to a default has a previous value of zero.
* @private {!Map<number, (number|!Array<number>|!Uint8Array)>}
*/
this.fields_ = new Map();
/**
* True once a full header has established a reference the deltas in this
* group can be applied to.
* @private {boolean}
*/
this.hasReference_ = false;
/**
* The sum of the previous chunk's effective sample durations, which is
* the only thing a delta chunk's decode time can be derived from.
* @private {number}
*/
this.previousDurationSum_ = 0;
/** @private {?bigint} */
this.group_ = null;
/** @private {?bigint} */
this.previousObjectId_ = null;
}
/**
* Reconstructs the CMAF chunk one LOCMAF object carries, or returns null
* when the object carries no appendable media -- an object status, a
* rawBoxes object holding initialization bytes -- or when it cannot be
* read.
*
* @param {!shaka.extern.MsfObject} obj
* @return {?shaka.msf.LOCMAFParser.Chunk}
*/
parse(obj) {
this.checkContinuity_(obj);
if (!obj.data.byteLength) {
return null;
}
try {
return this.parseObject_(obj.data);
} catch (error) {
// A malformed object is not fatal, but it does break the chain: the
// fields it would have contributed are unknown, so every delta that
// follows would be applied to the wrong reference.
shaka.log.warning('Discarding malformed LOCMAF object', error);
this.reset_();
return null;
}
}
/**
* Notices the two things that invalidate the delta chain: a new group, and
* a hole in the object IDs of the current one.
*
* @param {!shaka.extern.MsfObject} obj
* @private
*/
checkContinuity_(obj) {
const location = obj.location;
if (this.group_ === null || location.group !== this.group_) {
this.group_ = location.group;
this.reset_();
} else if (this.previousObjectId_ !== null &&
location.object !== this.previousObjectId_ + BigInt(1)) {
shaka.log.debug('Gap in LOCMAF object IDs; waiting for a full header',
this.previousObjectId_, location.object);
this.reset_();
}
this.previousObjectId_ = location.object;
}
/**
* Drops the in-group reference, so that nothing is decoded until the next
* full header or rawBoxes object.
*
* @private
*/
reset_() {
this.fields_.clear();
this.hasReference_ = false;
this.previousDurationSum_ = 0;
}
/**
* @param {!Uint8Array} data
* @return {?shaka.msf.LOCMAFParser.Chunk}
* @private
*/
parseObject_(data) {
const ElementType = shaka.msf.LOCMAFParser.ElementType_;
/** @type {!Array<!Uint8Array>} */
const genBoxes = [];
let offset = 0;
while (true) {
const element = this.readVarInt_(data, offset);
offset += element.bytesRead;
if (element.value === ElementType.RAW_BOXES) {
if (genBoxes.length) {
throw new Error('rawBoxes is not the first element');
}
return this.parseRawBoxes_(data.subarray(offset));
}
if (element.value === ElementType.GEN_BOX) {
const genBox = this.readGenBox_(data, offset);
genBoxes.push(genBox.box);
offset = genBox.offset;
continue;
}
const isDelta = element.value === ElementType.DELTA_HEADER;
if (!isDelta && element.value !== ElementType.FULL_HEADER) {
// Element types are not self-delimiting, so an unrecognized one
// cannot be skipped past; the spec makes this a hard failure rather
// than let two receivers reconstruct different chunks.
throw new Error(`Unknown LOCMAF element type ${element.value}`);
}
const length = this.readVarInt_(data, offset);
offset += length.bytesRead;
if (offset + length.value > data.byteLength) {
throw new Error('LOCMAF property block overruns the object');
}
const block = data.subarray(offset, offset + length.value);
offset += length.value;
const properties = this.decodeProperties_(block, isDelta);
if (isDelta) {
this.applyDelta_(properties);
} else {
this.applyFull_(properties);
}
return this.buildChunk_(genBoxes, data.subarray(offset));
}
}
/**
* A rawBoxes object is reconstructed verbatim, but it resets the delta
* chain: deriving reference state from it would mean parsing a `moof` back
* out of the bytes, which reconstruction never otherwise needs.
*
* @param {!Uint8Array} boxes
* @return {?shaka.msf.LOCMAFParser.Chunk}
* @private
*/
parseRawBoxes_(boxes) {
this.reset_();
if (!boxes.byteLength) {
throw new Error('Empty rawBoxes element');
}
// What the boxes are is not signaled: in-band initialization bytes
// (`ftyp` + `moov`) and a verbatim chunk are both legal. Reading the
// timing tells the two apart -- only a chunk has a `tfdt` and a `trun` --
// and a caller with no timing has nothing to append.
const info = shaka.media.SegmentUtils.getStartTimeAndDurationFromMp4(
boxes, this.params_.timescale);
if (!info.duration) {
shaka.log.debug('Ignoring rawBoxes object with no media timing');
return null;
}
return {
startTime: info.startTime,
duration: info.duration,
data: boxes,
};
}
/**
* Reads one genBox element and rebuilds the ISO box it carries. The wire
* form drops the 4-byte size, so `box_size` covers the FourCC and the
* contents, and the reconstructed box is four bytes longer.
*
* @param {!Uint8Array} data
* @param {number} offset
* @return {!{box: !Uint8Array, offset: number}}
* @private
*/
readGenBox_(data, offset) {
const size = this.readVarInt_(data, offset);
offset += size.bytesRead;
if (size.value < 4) {
throw new Error(`genBox size ${size.value} is below the FourCC`);
}
if (size.value > 0xfffffffb) {
throw new Error(`genBox size ${size.value} overflows a 32-bit box`);
}
if (offset + size.value > data.byteLength) {
throw new Error('genBox overruns the object');
}
const box = new Uint8Array(4 + size.value);
this.writeUint32_(box, 0, box.byteLength);
box.set(data.subarray(offset, offset + size.value), 4);
return {box, offset: offset + size.value};
}
/**
* Decodes a property block into field values.
*
* The parity of a field ID decides how its value is framed -- an even ID is
* one bare vi64, an odd ID is length-prefixed bytes -- which is what lets a
* receiver step over a field it does not know. That framing rule is fixed;
* how the value bytes are read is per-field, and three fields do not follow
* the plain absolute-in-full, delta-in-delta pattern.
*
* @param {!Uint8Array} block
* @param {boolean} isDelta
* @return {!Map<number, (number|!Array<number>|!Uint8Array)>}
* @private
*/
decodeProperties_(block, isDelta) {
const Field = shaka.msf.LOCMAFParser.Field;
/** @type {!Map<number, (number|!Array<number>|!Uint8Array)>} */
const properties = new Map();
let offset = 0;
while (offset < block.byteLength) {
const id = this.readVarInt_(block, offset);
offset += id.bytesRead;
if (properties.has(id.value)) {
throw new Error(`Repeated LOCMAF field ${id.value}`);
}
if (id.value % 2 === 0) {
const value = this.readVarInt_(block, offset);
offset += value.bytesRead;
properties.set(id.value, isDelta ?
shaka.msf.LOCMAFParser.zigzagToSigned_(value.raw) : value.value);
continue;
}
const length = this.readVarInt_(block, offset);
offset += length.bytesRead;
if (offset + length.value > block.byteLength) {
throw new Error(`LOCMAF field ${id.value} overruns the block`);
}
const bytes = block.subarray(offset, offset + length.value);
offset += length.value;
if (id.value === Field.SENC_INITIALIZATION_VECTOR) {
// Opaque bytes, overwritten rather than differenced. Copied because
// it outlives the object it came from as reference state.
properties.set(id.value, bytes.slice());
} else {
// Composition-time offsets are signed in both contexts, because
// B-frames make them negative; the deletion list is a control field
// whose elements are plain field IDs.
const signed = isDelta ?
id.value !== Field.DELTA_DELETED_LOCMAF_IDS :
id.value === Field.TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS;
properties.set(id.value, this.decodeList_(bytes, signed));
}
}
return properties;
}
/**
* Decodes a vi64 list. The element count is never needed: a list always
* carries exactly as many values as the chunk's list has entries, so the
* byte-length prefix delimits it on its own. That holds through length
* changes too -- a list that grows carries absolute values for the new
* tail, and one that shrinks simply emits fewer values.
*
* @param {!Uint8Array} bytes
* @param {boolean} signed
* @return {!Array<number>}
* @private
*/
decodeList_(bytes, signed) {
/** @type {!Array<number>} */
const values = [];
let offset = 0;
while (offset < bytes.byteLength) {
const value = this.readVarInt_(bytes, offset);
offset += value.bytesRead;
values.push(signed ?
shaka.msf.LOCMAFParser.zigzagToSigned_(value.raw) : value.value);
}
return values;
}
/**
* Replaces the reference state with a full header's absolute values.
*
* @param {!Map<number, (number|!Array<number>|!Uint8Array)>} properties
* @private
*/
applyFull_(properties) {
const Field = shaka.msf.LOCMAFParser.Field;
if (properties.has(Field.DELTA_DELETED_LOCMAF_IDS)) {
throw new Error('A full LOCMAF header carries a deletion list');
}
if (!properties.has(Field.TRUN_SAMPLE_COUNT)) {
throw new Error('A full LOCMAF header carries no sample count');
}
if (!properties.has(Field.TFDT_BASE_MEDIA_DECODE_TIME)) {
throw new Error('A full LOCMAF header carries no decode time');
}
this.fields_.clear();
for (const id of properties.keys()) {
if (shaka.msf.LOCMAFParser.KNOWN_FIELDS_.has(id)) {
this.fields_.set(id, properties.get(id));
}
}
this.hasReference_ = true;
}
/**
* Folds a delta header into the reference state. Deletions are applied
* first, so that a field the current chunk drops falls back to its default
* instead of being differenced against a value that no longer applies.
*
* @param {!Map<number, (number|!Array<number>|!Uint8Array)>} properties
* @private
*/
applyDelta_(properties) {
const Field = shaka.msf.LOCMAFParser.Field;
if (!this.hasReference_) {
throw new Error('A LOCMAF delta header has no reference to apply to');
}
if (properties.has(Field.TFDT_BASE_MEDIA_DECODE_TIME)) {
throw new Error('A LOCMAF delta header carries a decode time');
}
const deleted = properties.get(Field.DELTA_DELETED_LOCMAF_IDS);
if (deleted) {
for (const id of /** @type {!Array<number>} */ (deleted)) {
this.fields_.delete(id);
}
}
for (const id of properties.keys()) {
if (id === Field.DELTA_DELETED_LOCMAF_IDS ||
!shaka.msf.LOCMAFParser.KNOWN_FIELDS_.has(id)) {
continue;
}
const value = properties.get(id);
if (id === Field.SENC_INITIALIZATION_VECTOR) {
this.fields_.set(id, value);
} else if (id % 2 === 0) {
const previous =
/** @type {number|undefined} */ (this.fields_.get(id)) || 0;
this.fields_.set(id, previous + /** @type {number} */ (value));
} else {
const previous =
/** @type {!Array<number>|undefined} */ (this.fields_.get(id)) ||
[];
// A list that grew carries the absolute value for each new entry,
// which is the same arithmetic against a previous value of zero.
this.fields_.set(id, /** @type {!Array<number>} */ (value).map(
(delta, i) => (previous[i] || 0) + delta));
}
}
// The decode time is the one field a delta chunk never carries. CMAF
// requires a contiguous decode timeline, so it is exactly the previous
// chunk's time plus the samples the previous chunk held; an encoder that
// breaks the timeline has to emit a full header instead.
this.fields_.set(Field.TFDT_BASE_MEDIA_DECODE_TIME,
this.baseMediaDecodeTime_() + this.previousDurationSum_);
}
/**
* @return {number}
* @private
*/
baseMediaDecodeTime_() {
return /** @type {number} */ (
this.fields_.get(shaka.msf.LOCMAFParser.Field
.TFDT_BASE_MEDIA_DECODE_TIME));
}
/**
* @param {!Array<!Uint8Array>} genBoxes
* @param {!Uint8Array} payload
* @return {!shaka.msf.LOCMAFParser.Chunk}
* @private
*/
buildChunk_(genBoxes, payload) {
const effective = this.computeEffective_(payload.byteLength);
let durationSum = 0;
for (const duration of effective.durations) {
durationSum += duration;
}
this.previousDurationSum_ = durationSum;
// The mdat header is always the 8-byte form: the ISO 64-bit largesize
// escape is not allowed, so a payload this large cannot be packaged at
// all and would otherwise be written as a silently truncated size.
if (payload.byteLength > 0xfffffff7) {
throw new Error('LOCMAF payload overflows a 32-bit mdat');
}
const moof = this.buildMoof_(effective);
let total = moof.byteLength + 8 + payload.byteLength;
for (const genBox of genBoxes) {
total += genBox.byteLength;
}
const data = new Uint8Array(total);
let offset = 0;
for (const genBox of genBoxes) {
data.set(genBox, offset);
offset += genBox.byteLength;
}
data.set(moof, offset);
offset += moof.byteLength;
this.writeUint32_(data, offset, 8 + payload.byteLength);
data.set(shaka.msf.LOCMAFParser.MDAT_, offset + 4);
data.set(payload, offset + 8);
return {
startTime: effective.baseMediaDecodeTime / this.params_.timescale,
duration: durationSum / this.params_.timescale,
data,
};
}
/**
* Expands the reference state into the chunk's effective values: what each
* sample's duration, size, flags and composition offset actually are, once
* the `tfhd` and `trex` defaults have been resolved. Everything downstream
* reads these arrays and nothing reads the fields, which is what makes the
* output independent of how the encoder chose to distribute the values.
*
* @param {number} payloadLength
* @return {!shaka.msf.LOCMAFParser.Effective_}
* @private
*/
computeEffective_(payloadLength) {
const Field = shaka.msf.LOCMAFParser.Field;
const params = this.params_;
const fields = this.fields_;
const sampleCount =
/** @type {number|undefined} */ (fields.get(Field.TRUN_SAMPLE_COUNT));
if (sampleCount === undefined) {
throw new Error('LOCMAF chunk has no sample count');
}
const listLength = (id) => {
const list = /** @type {!Array<number>|undefined} */ (fields.get(id));
return list ? list.length : -1;
};
for (const id of [Field.TRUN_SAMPLE_DURATIONS,
Field.TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS, Field.TRUN_SAMPLE_FLAGS,
Field.SENC_SUBSAMPLE_COUNT]) {
const length = listLength(id);
if (length !== -1 && length !== sampleCount) {
throw new Error(`LOCMAF field ${id} has ${length} of ${sampleCount}` +
' entries');
}
}
const sizes = this.deriveSizes_(sampleCount, payloadLength);
const durations = [];
const flags = [];
const compositionTimeOffsets = [];
const perSampleDurations =
/** @type {!Array<number>|undefined} */ (
fields.get(Field.TRUN_SAMPLE_DURATIONS));
const defaultDuration =
/** @type {number|undefined} */ (
fields.get(Field.TFHD_DEFAULT_SAMPLE_DURATION));
const perSampleFlags =
/** @type {!Array<number>|undefined} */ (
fields.get(Field.TRUN_SAMPLE_FLAGS));
const firstSampleFlags =
/** @type {number|undefined} */ (
fields.get(Field.TRUN_FIRST_SAMPLE_FLAGS));
const defaultFlags =
/** @type {number|undefined} */ (
fields.get(Field.TFHD_DEFAULT_SAMPLE_FLAGS));
const perSampleOffsets =
/** @type {!Array<number>|undefined} */ (
fields.get(Field.TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS));
for (let i = 0; i < sampleCount; i++) {
durations.push(perSampleDurations ? perSampleDurations[i] :
(defaultDuration !== undefined ? defaultDuration :
params.trexSampleDuration));
if (perSampleFlags) {
flags.push(perSampleFlags[i]);
} else if (i === 0 && firstSampleFlags !== undefined) {
flags.push(firstSampleFlags);
} else if (defaultFlags !== undefined) {
flags.push(defaultFlags);
} else {
flags.push(params.trexSampleFlags);
}
compositionTimeOffsets.push(perSampleOffsets ? perSampleOffsets[i] : 0);
}
const sampleDescriptionIndex =
/** @type {number|undefined} */ (
fields.get(Field.TFHD_SAMPLE_DESCRIPTION_INDEX));
return {
sampleCount,
baseMediaDecodeTime: this.baseMediaDecodeTime_(),
sampleDescriptionIndex: sampleDescriptionIndex !== undefined ?
sampleDescriptionIndex : params.trexSampleDescriptionIndex,
durations,
sizes,
flags,
compositionTimeOffsets,
cenc: this.computeCenc_(sampleCount),
};
}
/**
* Derives every sample's size. The last one is never on the wire: it is
* whatever is left of the payload, which saves a vi64 on every chunk and
* makes the payload length authoritative.
*
* @param {number} sampleCount
* @param {number} payloadLength
* @return {!Array<number>}
* @private
*/
deriveSizes_(sampleCount, payloadLength) {
const Field = shaka.msf.LOCMAFParser.Field;
if (sampleCount === 0) {
if (payloadLength) {
throw new Error('LOCMAF chunk has no samples but a non-empty payload');
}
return [];
}
const listed = /** @type {!Array<number>|undefined} */ (
this.fields_.get(Field.TRUN_SAMPLE_SIZES));
if (listed) {
if (listed.length !== sampleCount - 1) {
throw new Error(`LOCMAF sample sizes hold ${listed.length} of ` +
`${sampleCount - 1} entries`);
}
let sum = 0;
for (const size of listed) {
sum += size;
}
if (sum > payloadLength) {
throw new Error('LOCMAF sample sizes exceed the payload');
}
return listed.concat([payloadLength - sum]);
}
const uniform = () => {
const explicit = /** @type {number|undefined} */ (
this.fields_.get(Field.TFHD_DEFAULT_SAMPLE_SIZE));
if (explicit !== undefined) {
return explicit;
}
if (sampleCount === 1) {
return payloadLength;
}
if (this.params_.trexSampleSize) {
return this.params_.trexSampleSize;
}
if (payloadLength === 0) {
return 0;
}
throw new Error('LOCMAF chunk has no derivable sample sizes');
};
const size = uniform();
if (sampleCount * size !== payloadLength) {
throw new Error(`LOCMAF sample size ${size} does not fill the payload`);
}
return new Array(sampleCount).fill(size);
}
/**
* Gathers the per-sample encryption metadata, or returns null when the
* chunk carries none.
*
* @param {number} sampleCount
* @return {?shaka.msf.LOCMAFParser.Cenc_}
* @private
*/
computeCenc_(sampleCount) {
const Field = shaka.msf.LOCMAFParser.Field;
const fields = this.fields_;
const carriesCenc = shaka.msf.LOCMAFParser.CENC_FIELDS_.some(
(id) => fields.has(id));
if (!this.params_.isProtected) {
if (carriesCenc) {
// The reconstruction below is defined only for protected tracks, so
// there is no agreed answer to what such a chunk expands to.
throw new Error('LOCMAF chunk carries CENC fields on a clear track');
}
return null;
}
const explicitIvSize = /** @type {number|undefined} */ (
fields.get(Field.SENC_PER_SAMPLE_IV_SIZE));
const perSampleIvSize = explicitIvSize !== undefined ? explicitIvSize :
this.params_.defaultPerSampleIvSize;
const subsampleCounts = /** @type {!Array<number>|undefined} */ (
fields.get(Field.SENC_SUBSAMPLE_COUNT)) || null;
// Under cbcs full-sample encryption the IV is constant and lives in the
// initialization segment, so a protected chunk can legitimately have no
// per-sample auxiliary information at all -- and then none of the three
// boxes is emitted.
if (!sampleCount || (!perSampleIvSize && !subsampleCounts)) {
return null;
}
const ivs = /** @type {!Uint8Array} */ (
fields.get(Field.SENC_INITIALIZATION_VECTOR) ||
new Uint8Array(0));
if (ivs.byteLength !== sampleCount * perSampleIvSize) {
throw new Error(`LOCMAF holds ${ivs.byteLength} IV bytes for ` +
`${sampleCount} samples of ${perSampleIvSize}`);
}
let subsampleTotal = 0;
for (const count of subsampleCounts || []) {
subsampleTotal += count;
}
const clearBytes = /** @type {!Array<number>|undefined} */ (
fields.get(Field.SENC_BYTES_OF_CLEAR_DATA)) || [];
const protectedBytes = /** @type {!Array<number>|undefined} */ (
fields.get(Field.SENC_BYTES_OF_PROTECTED_DATA)) || [];
if (subsampleCounts && (clearBytes.length !== subsampleTotal ||
protectedBytes.length !== subsampleTotal)) {
throw new Error('LOCMAF subsample map does not match its counts');
}
return {perSampleIvSize, ivs, subsampleCounts, clearBytes, protectedBytes};
}
/**
* Builds the `moof`. The two offsets it has to carry -- `trun.data_offset`
* and `saio.offset` -- both depend on the finished size of the box that
* holds them, so the boxes are built with the offsets left at zero and
* patched once every size is known.
*
* @param {!shaka.msf.LOCMAFParser.Effective_} effective
* @return {!Uint8Array}
* @private
*/
buildMoof_(effective) {
const Mp4Generator = shaka.util.Mp4Generator;
const layout = shaka.msf.LOCMAFParser.chooseLayout_(
effective, this.params_);
const tfhd = this.buildTfhd_(layout);
const tfdt = this.buildTfdt_(effective);
const trun = this.buildTrun_(effective, layout);
const cencBoxes = this.buildCencBoxes_(effective);
const boxes = [tfhd, tfdt, trun].concat(cencBoxes);
// The moof is its own header, the mfhd box, the traf header, and the
// boxes above, so its size is known before anything is assembled.
let moofSize = 8 + 16 + 8;
for (const box of boxes) {
moofSize += box.byteLength;
}
// With default-base-is-moof set, both of the offsets below are measured
// from the first byte of the moof.
this.writeUint32_(trun, 16, moofSize + 8);
if (cencBoxes.length) {
const saio = cencBoxes[1];
const senc = cencBoxes[2];
// senc is the last box of the traf, so it ends where the moof does.
// Sixteen bytes past its start are its box header, its version and
// flags, and its sample count, leaving the first sample's auxiliary
// information.
this.writeUint32_(saio, 16, moofSize - senc.byteLength + 16);
}
return Mp4Generator.box('moof',
Mp4Generator.box('mfhd', new Uint8Array(8)),
Mp4Generator.box('traf', ...boxes));
}
/**
* Recomputes the three CENC boxes, in the order the canonical form fixes
* them in, or returns nothing when the chunk carries no per-sample
* encryption metadata. Only senc travels; saiz and saio follow from it.
*
* @param {!shaka.msf.LOCMAFParser.Effective_} effective
* @return {!Array<!Uint8Array>}
* @private
*/
buildCencBoxes_(effective) {
if (!effective.cenc) {
return [];
}
return [
this.buildSaiz_(effective),
this.buildSaio_(),
this.buildSenc_(effective),
];
}
/**
* @param {!shaka.msf.LOCMAFParser.Layout_} layout
* @return {!Uint8Array}
* @private
*/
buildTfhd_(layout) {
const Flags = shaka.msf.LOCMAFParser.TfhdFlags_;
/** @type {!Array<number>} */
const values = [];
// Sample data offsets are relative to the moof, so no base data offset
// is present.
let flags = Flags.DEFAULT_BASE_IS_MOOF;
if (layout.sampleDescriptionIndex !== null) {
flags |= Flags.SAMPLE_DESCRIPTION_INDEX_PRESENT;
values.push(layout.sampleDescriptionIndex);
}
if (layout.defaultSampleDuration !== null) {
flags |= Flags.DEFAULT_SAMPLE_DURATION_PRESENT;
values.push(layout.defaultSampleDuration);
}
if (layout.defaultSampleSize !== null) {
flags |= Flags.DEFAULT_SAMPLE_SIZE_PRESENT;
values.push(layout.defaultSampleSize);
}
if (layout.defaultSampleFlags !== null) {
flags |= Flags.DEFAULT_SAMPLE_FLAGS_PRESENT;
values.push(layout.defaultSampleFlags);
}
const payload = new Uint8Array(8 + 4 * values.length);
this.writeUint32_(payload, 0, flags);
this.writeUint32_(payload, 4, this.params_.trackId);
for (let i = 0; i < values.length; i++) {
this.writeUint32_(payload, 8 + 4 * i, values[i]);
}
return shaka.util.Mp4Generator.box('tfhd', payload);
}
/**
* @param {!shaka.msf.LOCMAFParser.Effective_} effective
* @return {!Uint8Array}
* @private
*/
buildTfdt_(effective) {
// Version 1 unconditionally: a live decode timeline passes 32 bits within
// hours at a 90 kHz timescale, and the four extra bytes never travel.
const payload = new Uint8Array(12);
payload[0] = 1;
this.writeUint64_(
payload, 4, effective.baseMediaDecodeTime);
return shaka.util.Mp4Generator.box('tfdt', payload);
}
/**
* @param {!shaka.msf.LOCMAFParser.Effective_} effective
* @param {!shaka.msf.LOCMAFParser.Layout_} layout
* @return {!Uint8Array}
* @private
*/
buildTrun_(effective, layout) {
const Flags = shaka.msf.LOCMAFParser.TrunFlags_;
const n = effective.sampleCount;
let flags = Flags.DATA_OFFSET_PRESENT;
if (layout.firstSampleFlags !== null) {
flags |= Flags.FIRST_SAMPLE_FLAGS_PRESENT;
}
if (layout.perSampleDurations) {
flags |= Flags.SAMPLE_DURATION_PRESENT;
}
if (layout.perSampleSizes) {
flags |= Flags.SAMPLE_SIZE_PRESENT;
}
if (layout.perSampleFlags) {
flags |= Flags.SAMPLE_FLAGS_PRESENT;
}
if (layout.perSampleOffsets) {
flags |= Flags.SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT;
}
const perSample = (layout.perSampleDurations ? 1 : 0) +
(layout.perSampleSizes ? 1 : 0) + (layout.perSampleFlags ? 1 : 0) +
(layout.perSampleOffsets ? 1 : 0);
const head = 12 + (layout.firstSampleFlags !== null ? 4 : 0);
const payload = new Uint8Array(head + 4 * perSample * n);
// Version 1 makes the composition offset signed, which is what B-frames
// need; version 0 is reached only when no offset is negative.
const version = layout.signedOffsets ? 1 : 0;
this.writeUint32_(payload, 0, (version << 24) | flags);
this.writeUint32_(payload, 4, n);
// The data offset is patched in once the moof size is known.
if (layout.firstSampleFlags !== null) {
this.writeUint32_(payload, 12, layout.firstSampleFlags);
}
let offset = head;
for (let i = 0; i < n; i++) {
if (layout.perSampleDurations) {
this.writeUint32_(payload, offset, effective.durations[i]);
offset += 4;
}
if (layout.perSampleSizes) {
this.writeUint32_(payload, offset, effective.sizes[i]);
offset += 4;
}
if (layout.perSampleFlags) {
this.writeUint32_(payload, offset, effective.flags[i]);
offset += 4;
}
if (layout.perSampleOffsets) {
this.writeUint32_(payload, offset, effective.compositionTimeOffsets[i]);
offset += 4;
}
}
return shaka.util.Mp4Generator.box('trun', payload);
}
/**
* Recomputes `saiz`, which LOCMAF never carries: every sample's auxiliary
* information size follows from the IV size and the subsample count.
*
* @param {!shaka.msf.LOCMAFParser.Effective_} effective
* @return {!Uint8Array}
* @private
*/
buildSaiz_(effective) {
const cenc = /** @type {!shaka.msf.LOCMAFParser.Cenc_} */ (effective.cenc);
const n = effective.sampleCount;
const sizes = [];
for (let i = 0; i < n; i++) {
// When the subsample flag is set every sample carries its two-byte
// count, even a sample that has no subsamples.
const size = cenc.perSampleIvSize + (cenc.subsampleCounts ?
2 + 6 * cenc.subsampleCounts[i] : 0);
if (size > 0xff) {
throw new Error(`LOCMAF auxiliary info size ${size} exceeds a byte`);
}
sizes.push(size);
}
const uniform = sizes.every((size) => size === sizes[0]);
const payload = new Uint8Array(9 + (uniform ? 0 : n));
payload[4] = uniform && n ? sizes[0] : 0;
this.writeUint32_(payload, 5, n);
if (!uniform) {
payload.set(sizes, 9);
}
return shaka.util.Mp4Generator.box('saiz', payload);
}
/**
* @return {!Uint8Array}
* @private
*/
buildSaio_() {
const payload = new Uint8Array(12);
this.writeUint32_(payload, 4, 1);
// The offset is patched in once the size of everything before senc is
// known.
return shaka.util.Mp4Generator.box('saio', payload);
}
/**
* @param {!shaka.msf.LOCMAFParser.Effective_} effective
* @return {!Uint8Array}
* @private
*/
buildSenc_(effective) {
const cenc = /** @type {!shaka.msf.LOCMAFParser.Cenc_} */ (effective.cenc);
const n = effective.sampleCount;
const useSubsamples = !!cenc.subsampleCounts;
let size = 8;
for (let i = 0; i < n; i++) {
size += cenc.perSampleIvSize +
(useSubsamples ? 2 + 6 * cenc.subsampleCounts[i] : 0);
}
const payload = new Uint8Array(size);
if (useSubsamples) {
// senc_use_subsamples
payload[3] = 0x02;
}
this.writeUint32_(payload, 4, n);
let offset = 8;
let subsample = 0;
for (let i = 0; i < n; i++) {
payload.set(
cenc.ivs.subarray(i * cenc.perSampleIvSize,
(i + 1) * cenc.perSampleIvSize),
offset);
offset += cenc.perSampleIvSize;
if (!useSubsamples) {
continue;
}
const count = cenc.subsampleCounts[i];
this.writeUint16_(payload, offset, count);
offset += 2;
for (let j = 0; j < count; j++) {
this.writeUint16_(
payload, offset, cenc.clearBytes[subsample]);
this.writeUint32_(
payload, offset + 2, cenc.protectedBytes[subsample]);
offset += 6;
subsample++;
}
}
return shaka.util.Mp4Generator.box('senc', payload);
}
/**
* Decides which values become `tfhd` defaults and which become per-sample
* `trun` entries. This is where the canonical form is pinned down: the
* decision is made from the effective values alone, so a chunk that carried
* redundant defaults on the wire reconstructs to the same bytes as its
* minimal counterpart.
*
* @param {!shaka.msf.LOCMAFParser.Effective_} effective
* @param {!shaka.msf.LOCMAFParser.TrackParams} params
* @return {!shaka.msf.LOCMAFParser.Layout_}
* @private
*/
static chooseLayout_(effective, params) {
const n = effective.sampleCount;
const {durations, sizes, flags, compositionTimeOffsets} = effective;
const allEqual = (values) => values.every((value) => value === values[0]);
const uniformDurations = n > 0 && allEqual(durations);
const uniformSizes = n > 0 && allEqual(sizes);
const uniformFlags = n > 0 && allEqual(flags);
// A random-access chunk flags its first sample as a sync sample and
// leaves the rest alone, which is worth a dedicated field rather than a
// per-sample table.
const firstFlagsDiffer = n > 1 && !uniformFlags &&
allEqual(flags.slice(1));
let defaultFlags = null;
if (uniformFlags) {
defaultFlags = flags[0];
} else if (firstFlagsDiffer) {
defaultFlags = flags[1];
}
const sdi = effective.sampleDescriptionIndex;
return {
sampleDescriptionIndex:
sdi !== params.trexSampleDescriptionIndex ? sdi : null,
defaultSampleDuration:
uniformDurations && durations[0] !== params.trexSampleDuration ?
durations[0] : null,
defaultSampleSize:
uniformSizes && sizes[0] !== params.trexSampleSize ? sizes[0] : null,
defaultSampleFlags:
defaultFlags !== null && defaultFlags !== params.trexSampleFlags ?
defaultFlags : null,
firstSampleFlags: firstFlagsDiffer ? flags[0] : null,
perSampleDurations: n > 0 && !uniformDurations,
perSampleSizes: n > 0 && !uniformSizes,
perSampleFlags: n > 0 && !uniformFlags && !firstFlagsDiffer,
perSampleOffsets:
compositionTimeOffsets.some((offset) => offset !== 0),
signedOffsets: compositionTimeOffsets.some((offset) => offset < 0),
};
}
/**
* Reads one vi64, reporting both its value as a number and the raw unsigned
* value the zigzag decoding needs.
*
* @param {!Uint8Array} bytes
* @param {number} offset
* @return {!{value: number, raw: bigint, bytesRead: number}}
* @private
*/
readVarInt_(bytes, offset) {
if (offset >= bytes.byteLength) {
throw new Error('LOCMAF object ended inside a variable-length integer');
}
const decoded = this.codec_.decodeVarIntAt(bytes, offset);
return {
value: Number(decoded.value),
raw: decoded.value,
bytesRead: decoded.bytesRead,
};
}
/**
* Undoes the zigzag mapping, which interleaves the signs so that a small
* negative delta encodes as compactly as a small positive one.
*
* @param {bigint} value
* @return {number}
* @private
*/
static zigzagToSigned_(value) {
const one = BigInt(1);
return Number((value >> one) ^ -(value & one));
}
/**
* @param {!Uint8Array} bytes
* @param {number} offset
* @param {number} value
* @private
*/
writeUint16_(bytes, offset, value) {
bytes[offset] = (value >>> 8) & 0xff;
bytes[offset + 1] = value & 0xff;
}
/**
* @param {!Uint8Array} bytes
* @param {number} offset
* @param {number} value
* @private
*/
writeUint32_(bytes, offset, value) {
bytes[offset] = (value >>> 24) & 0xff;
bytes[offset + 1] = (value >>> 16) & 0xff;
bytes[offset + 2] = (value >>> 8) & 0xff;
bytes[offset + 3] = value & 0xff;
}
/**
* @param {!Uint8Array} bytes
* @param {number} offset
* @param {number} value
* @private
*/
writeUint64_(bytes, offset, value) {
const high = Math.floor(value / 0x100000000);
this.writeUint32_(bytes, offset, high);
this.writeUint32_(bytes, offset + 4, value - high * 0x100000000);
}
};
/**
* Everything the track's CMAF Header contributes to reconstruction. The
* `trex` defaults and the `tenc` per-sample IV size are what every omitted
* field falls back to, so they are read once per track rather than per chunk.
*
* @typedef {{
* trackId: number,
* timescale: number,
* trexSampleDescriptionIndex: number,
* trexSampleDuration: number,
* trexSampleSize: number,
* trexSampleFlags: number,
* isProtected: boolean,
* defaultPerSampleIvSize: number,
* }}
*/
shaka.msf.LOCMAFParser.TrackParams;
/**
* One reconstructed CMAF chunk.
*
* @typedef {{
* startTime: number,
* duration: number,
* data: !Uint8Array,
* }}
*/
shaka.msf.LOCMAFParser.Chunk;
/**
* The per-sample encryption metadata of one chunk.
*
* @typedef {{
* perSampleIvSize: number,
* ivs: !Uint8Array,
* subsampleCounts: ?Array<number>,
* clearBytes: !Array<number>,
* protectedBytes: !Array<number>,
* }}
* @private
*/
shaka.msf.LOCMAFParser.Cenc_;
/**
* What one chunk works out to, per sample, once every default is resolved.
*
* @typedef {{
* sampleCount: number,
* baseMediaDecodeTime: number,
* sampleDescriptionIndex: number,
* durations: !Array<number>,
* sizes: !Array<number>,
* flags: !Array<number>,
* compositionTimeOffsets: !Array<number>,
* cenc: ?shaka.msf.LOCMAFParser.Cenc_,
* }}
* @private
*/
shaka.msf.LOCMAFParser.Effective_;
/**
* Which values go in the `tfhd` and which are written per sample.
*
* @typedef {{
* sampleDescriptionIndex: ?number,
* defaultSampleDuration: ?number,
* defaultSampleSize: ?number,
* defaultSampleFlags: ?number,
* firstSampleFlags: ?number,
* perSampleDurations: boolean,
* perSampleSizes: boolean,
* perSampleFlags: boolean,
* perSampleOffsets: boolean,
* signedOffsets: boolean,
* }}
* @private
*/
shaka.msf.LOCMAFParser.Layout_;
/**
* The LOCMAF element types, which tag each element of an object payload.
*
* @enum {number}
* @private
*/
shaka.msf.LOCMAFParser.ElementType_ = {
GEN_BOX: 1,
FULL_HEADER: 2,
DELTA_HEADER: 3,
RAW_BOXES: 4,
};
/**
* The LOCMAF header field IDs. Every scalar has an even ID and every list or
* byte field an odd one, which is what makes the parity rule a framing rule.
*
* @enum {number}
*/
shaka.msf.LOCMAFParser.Field = {
TRUN_SAMPLE_SIZES: 1,
TFHD_SAMPLE_DESCRIPTION_INDEX: 2,
TRUN_SAMPLE_DURATIONS: 3,
TFHD_DEFAULT_SAMPLE_DURATION: 4,
TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS: 5,
TFHD_DEFAULT_SAMPLE_SIZE: 6,
TRUN_SAMPLE_FLAGS: 7,
TFHD_DEFAULT_SAMPLE_FLAGS: 8,
SENC_INITIALIZATION_VECTOR: 9,
TFDT_BASE_MEDIA_DECODE_TIME: 10,
SENC_SUBSAMPLE_COUNT: 11,
TRUN_FIRST_SAMPLE_FLAGS: 12,
SENC_BYTES_OF_CLEAR_DATA: 13,
TRUN_SAMPLE_COUNT: 14,
SENC_BYTES_OF_PROTECTED_DATA: 15,
SENC_PER_SAMPLE_IV_SIZE: 16,
DELTA_DELETED_LOCMAF_IDS: 27,
};
/**
* The fields this parser stores as reference state. An unknown field is
* skipped by the parity rule and otherwise ignored, so that new ones can be
* added without breaking older receivers.
*
* @const {!Set<number>}
* @private
*/
shaka.msf.LOCMAFParser.KNOWN_FIELDS_ = new Set(
/** @type {!Array<number>} */ (
Object.values(shaka.msf.LOCMAFParser.Field)));
/**
* The fields that only a protected track may carry.
*
* @const {!Array<number>}
* @private
*/
shaka.msf.LOCMAFParser.CENC_FIELDS_ = [
shaka.msf.LOCMAFParser.Field.SENC_INITIALIZATION_VECTOR,
shaka.msf.LOCMAFParser.Field.SENC_SUBSAMPLE_COUNT,
shaka.msf.LOCMAFParser.Field.SENC_BYTES_OF_CLEAR_DATA,
shaka.msf.LOCMAFParser.Field.SENC_BYTES_OF_PROTECTED_DATA,
shaka.msf.LOCMAFParser.Field.SENC_PER_SAMPLE_IV_SIZE,
];
/**
* @const {!Uint8Array}
* @private
*/
shaka.msf.LOCMAFParser.MDAT_ = new Uint8Array([0x6d, 0x64, 0x61, 0x74]);
/**
* @enum {number}
* @private
*/
shaka.msf.LOCMAFParser.TfhdFlags_ = {
SAMPLE_DESCRIPTION_INDEX_PRESENT: 0x000002,
DEFAULT_SAMPLE_DURATION_PRESENT: 0x000008,
DEFAULT_SAMPLE_SIZE_PRESENT: 0x000010,
DEFAULT_SAMPLE_FLAGS_PRESENT: 0x000020,
DEFAULT_BASE_IS_MOOF: 0x020000,
};
/**
* @enum {number}
* @private
*/
shaka.msf.LOCMAFParser.TrunFlags_ = {
DATA_OFFSET_PRESENT: 0x000001,
FIRST_SAMPLE_FLAGS_PRESENT: 0x000004,
SAMPLE_DURATION_PRESENT: 0x000100,
SAMPLE_SIZE_PRESENT: 0x000200,
SAMPLE_FLAGS_PRESENT: 0x000400,
SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT: 0x000800,
};