isobmff-inspector
Version:
Simple ISOBMFF parser, compatible with JavaScript and Node.JS
1,717 lines (1,694 loc) • 126 kB
JavaScript
#!/usr/bin/env node
"use strict";
// cli/cli.js
var import_node_events = require("node:events");
var import_node_fs = require("node:fs");
var import_promises = require("node:fs/promises");
var import_node_path = require("node:path");
// src/utils/bytes.js
var textDecoder = new TextDecoder();
function parseBoxType(bytes, offset) {
return String.fromCharCode(
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3]
);
}
function be2toi(bytes, off) {
return (bytes[0 + off] << 8) + bytes[1 + off];
}
function be3toi(bytes, off) {
return bytes[0 + off] * 65536 + bytes[1 + off] * 256 + bytes[2 + off];
}
function be4toi(bytes, off) {
return bytes[0 + off] * 16777216 + bytes[1 + off] * 65536 + bytes[2 + off] * 256 + bytes[3 + off];
}
function be5toi(bytes, off) {
return bytes[0 + off] * 4294967296 + bytes[1 + off] * 16777216 + bytes[2 + off] * 65536 + bytes[3 + off] * 256 + bytes[4 + off];
}
function be8toi(bytes, off) {
return (bytes[0 + off] * 16777216 + bytes[1 + off] * 65536 + bytes[2 + off] * 256 + bytes[3 + off]) * 4294967296 + bytes[4 + off] * 16777216 + bytes[5 + off] * 65536 + bytes[6 + off] * 256 + bytes[7 + off];
}
function bytesToHex(uint8arr, off, nbBytes) {
if (!uint8arr) {
return "";
}
const arr = uint8arr.slice(off, nbBytes + off);
let hexStr = "";
for (let i = 0; i < arr.length; i++) {
let hex = (arr[i] & 255).toString(16);
hex = hex.length === 1 ? `0${hex}` : hex;
hexStr += hex;
}
return hexStr.toUpperCase();
}
function utf8ToStr(uint8arr, off = 0, nbBytes) {
if (!uint8arr) {
return "";
}
if (nbBytes === void 0) {
if (off === 0) {
return textDecoder.decode(uint8arr);
}
return textDecoder.decode(uint8arr.slice(off));
}
const arr = uint8arr.slice(off, nbBytes + off);
return textDecoder.decode(arr);
}
function viewToUint8Array(view) {
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
}
function isBufferSource(value) {
return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
}
function bufferSourceToUint8Array(arr) {
if (arr instanceof Uint8Array) {
return arr;
}
if (arr instanceof ArrayBuffer) {
return new Uint8Array(arr);
}
return viewToUint8Array(arr);
}
function byteChunkToUint8Array(chunk) {
if (chunk instanceof Uint8Array) {
return chunk;
}
if (chunk instanceof ArrayBuffer) {
return new Uint8Array(chunk);
}
if (ArrayBuffer.isView(chunk)) {
return viewToUint8Array(chunk);
}
throw new Error(
"Progressive ISOBMFF inputs must yield ArrayBuffer or TypedArray chunks."
);
}
function asyncByteIterable(iterable) {
return {
async *[Symbol.asyncIterator]() {
for await (const chunk of iterable) {
yield byteChunkToUint8Array(chunk);
}
}
};
}
function getProgressiveSource(input) {
if (input === null || input === void 0) {
return void 0;
}
if (typeof input === "object" && "body" in input) {
const body = (
/** @type {{ body?: unknown }} */
input.body
);
if (body !== null && body !== void 0) {
return getProgressiveSource(body);
}
}
if (typeof input === "object" && "getReader" in input && typeof input.getReader === "function") {
return {
async *[Symbol.asyncIterator]() {
const reader = (
/** @type {ReadableStream} */
input.getReader()
);
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
yield byteChunkToUint8Array(value);
}
} finally {
reader.releaseLock();
}
}
};
}
if (typeof input === "object" && Symbol.asyncIterator in input && typeof input[Symbol.asyncIterator] === "function") {
return asyncByteIterable(
/** @type {AsyncIterable<unknown>} */
input
);
}
if (typeof input === "object" && Symbol.iterator in input && typeof input[Symbol.iterator] === "function") {
return asyncByteIterable(
/** @type {Iterable<unknown>} */
input
);
}
if (typeof input === "object" && "stream" in input && typeof input.stream === "function") {
return getProgressiveSource(input.stream());
}
if (typeof input === "object" && "arrayBuffer" in input && typeof input.arrayBuffer === "function") {
const arrayBuffer = (
/** @type {{ arrayBuffer: () => Promise<ArrayBuffer> }} */
input.arrayBuffer
);
return asyncByteIterable({
async *[Symbol.asyncIterator]() {
yield await arrayBuffer.call(input);
}
});
}
return void 0;
}
// src/utils/ProgressiveByteReader.js
var ProgressiveByteReader = class {
/**
* @param {AsyncIterator<Uint8Array>} iterator
*/
constructor(iterator) {
this._iterator = iterator;
this._buffers = [];
this._bufferedLength = 0;
this._done = false;
}
/**
* @param {number} nbBytes
* @returns {Promise<void>}
*/
async ensure(nbBytes) {
while (!this._done && this._bufferedLength < nbBytes) {
const next = await this._iterator.next();
if (next.done) {
this._done = true;
break;
}
if (next.value.length > 0) {
this._buffers.push(next.value);
this._bufferedLength += next.value.length;
}
}
}
/**
* @returns {number}
*/
getBufferedLength() {
return this._bufferedLength;
}
/**
* @returns {boolean}
*/
isDone() {
return this._done && this._bufferedLength === 0;
}
/**
* @param {number} nbBytes
* @returns {Uint8Array}
*/
takeAvailable(nbBytes) {
const size = Math.min(nbBytes, this._bufferedLength);
const result = new Uint8Array(size);
let resultOffset = 0;
while (resultOffset < size) {
const buffer = this._buffers[0];
const copiedLength = Math.min(buffer.length, size - resultOffset);
result.set(buffer.subarray(0, copiedLength), resultOffset);
resultOffset += copiedLength;
if (copiedLength === buffer.length) {
this._buffers.shift();
} else {
this._buffers[0] = buffer.subarray(copiedLength);
}
this._bufferedLength -= copiedLength;
}
return result;
}
/**
* @param {number} nbBytes
* @returns {Promise<Uint8Array>}
*/
async read(nbBytes) {
await this.ensure(nbBytes);
return this.takeAvailable(nbBytes);
}
/**
* @param {number} nbBytes
* @param {(chunk: Uint8Array) => void | Promise<void>} onChunk
* @returns {Promise<Uint8Array>}
*/
async readWithCallback(nbBytes, onChunk) {
return this._readConsumed(nbBytes, onChunk);
}
/**
* @param {number} nbBytes
* @returns {Promise<number>}
*/
async skip(nbBytes) {
return this._skipConsumed(nbBytes);
}
/**
* @param {number} nbBytes
* @param {(chunk: Uint8Array) => void | Promise<void>} onChunk
* @returns {Promise<number>}
*/
async skipWithCallback(nbBytes, onChunk) {
return this._skipConsumed(nbBytes, onChunk);
}
/**
* @returns {Promise<number>}
*/
async skipUntilEnd() {
return this._skipConsumed(void 0);
}
/**
* @param {(chunk: Uint8Array) => void | Promise<void>} onChunk
* @returns {Promise<number>}
*/
async skipUntilEndWithCallback(onChunk) {
return this._skipConsumed(void 0, onChunk);
}
/**
* @returns {Promise<Uint8Array>}
*/
async readUntilEnd() {
return this._readConsumed(void 0);
}
/**
* @param {(chunk: Uint8Array) => void | Promise<void>} onChunk
* @returns {Promise<Uint8Array>}
*/
async readUntilEndWithCallback(onChunk) {
return this._readConsumed(void 0, onChunk);
}
/**
* @param {number | undefined} nbBytes
* @param {((chunk: Uint8Array) => void | Promise<void>)=} onChunk
* @returns {Promise<number>}
*/
async _skipConsumed(nbBytes, onChunk) {
return this._consume(nbBytes, onChunk, false);
}
/**
* @param {number | undefined} nbBytes
* @param {((chunk: Uint8Array) => void | Promise<void>)=} onChunk
* @returns {Promise<Uint8Array>}
*/
async _readConsumed(nbBytes, onChunk) {
return this._consume(nbBytes, onChunk, true);
}
/**
* @param {number | undefined} nbBytes
* @param {((chunk: Uint8Array) => void | Promise<void>) | undefined} onChunk
* @param {boolean} collect
* @returns {Promise<any>}
*/
async _consume(nbBytes, onChunk, collect) {
let remaining = nbBytes;
let consumed = 0;
const chunks = [];
while (remaining === void 0 || remaining > 0) {
await this.ensure(1);
if (this._bufferedLength === 0) {
break;
}
const chunkLength = remaining === void 0 ? this._bufferedLength : Math.min(remaining, this._bufferedLength);
const chunk = this.takeAvailable(chunkLength);
consumed += chunk.length;
if (remaining !== void 0) {
remaining -= chunk.length;
}
if (collect) {
chunks.push(chunk);
}
await onChunk?.(chunk);
}
if (!collect) {
return consumed;
}
const result = new Uint8Array(consumed);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
};
// src/fields.js
var MAC_EPOCH_TO_UNIX_EPOCH_SECONDS = 2082844800;
function decodeFixedPoint(value, fractionalBits) {
return value / 2 ** fractionalBits;
}
function toSignedInt(value, bits) {
const maxUnsigned = 2 ** bits;
const signedBoundary = 2 ** (bits - 1);
return value >= signedBoundary ? value - maxUnsigned : value;
}
function decodeSignedFixedPoint(value, bits, fractionalBits) {
return decodeFixedPoint(toSignedInt(value, bits), fractionalBits);
}
function bytesField(value, offset, nbBytes) {
return { kind: "bytes", value: bytesToHex(value, offset, nbBytes) };
}
function fixedPointField(raw, bits, fractionalBits, format) {
return {
kind: "fixed-point",
value: decodeFixedPoint(raw, fractionalBits),
raw,
format,
signed: false,
bits
};
}
function signedFixedPointField(raw, bits, fractionalBits, format) {
return {
kind: "fixed-point",
value: decodeSignedFixedPoint(raw, bits, fractionalBits),
raw,
format,
signed: true,
bits
};
}
function bitsField(raw, totalBits, parts) {
let remainingBits = totalBits;
const fields = parts.map((part) => {
remainingBits -= part.bits;
const value = Math.floor(raw / 2 ** remainingBits) & 2 ** part.bits - 1;
return {
key: part.key,
value,
bits: part.bits,
shift: remainingBits,
mask: (2 ** part.bits - 1) * 2 ** remainingBits
};
});
return {
kind: "bits",
value: fields.find((field) => field.key === "value")?.value ?? raw,
raw,
bits: totalBits,
fields
};
}
function flagsField(raw, totalBits, flags) {
return {
kind: "flags",
value: raw,
raw,
bits: totalBits,
flags: Object.entries(flags).map(([key, mask]) => ({
key,
value: (raw & mask) !== 0,
mask
}))
};
}
function unixSecondsToIsoString(unixSeconds) {
if (typeof unixSeconds === "bigint") {
if (unixSeconds < BigInt(Number.MIN_SAFE_INTEGER) || unixSeconds > BigInt(Number.MAX_SAFE_INTEGER)) {
return null;
}
return unixSecondsToIsoString(Number(unixSeconds));
}
const unixMilliseconds = unixSeconds * 1e3;
if (!Number.isFinite(unixMilliseconds)) {
return null;
}
const date = new Date(unixMilliseconds);
return Number.isNaN(date.getTime()) ? null : date.toISOString();
}
function macDateField(value) {
const unixSeconds = typeof value === "bigint" ? value - BigInt(MAC_EPOCH_TO_UNIX_EPOCH_SECONDS) : value - MAC_EPOCH_TO_UNIX_EPOCH_SECONDS;
return {
kind: "date",
value,
date: unixSecondsToIsoString(unixSeconds),
epoch: "1904-01-01T00:00:00.000Z",
unit: "seconds"
};
}
function parsedBoxValue(key, value, meta) {
const metadata = typeof meta === "string" ? { description: meta } : meta ?? {};
const ret = {
key,
...normalizeField(value)
};
if (metadata.offset !== void 0) {
ret.offset = metadata.offset;
}
if (metadata.byteLength !== void 0) {
ret.byteLength = metadata.byteLength;
}
if (metadata.description !== void 0) {
ret.description = metadata.description;
}
return ret;
}
function structField(fields, layout) {
const ret = {
kind: "struct",
fields
};
if (layout !== void 0) {
ret.layout = layout;
}
return ret;
}
function isParsedField(value) {
return typeof value === "object" && value !== null && "kind" in value && typeof value.kind === "string";
}
function normalizeField(value) {
if (isParsedField(value)) {
return value;
}
if (typeof value === "number") {
return { kind: "number", value };
}
if (typeof value === "bigint") {
return { kind: "bigint", value };
}
if (typeof value === "string") {
return { kind: "string", value };
}
if (typeof value === "boolean") {
return { kind: "boolean", value };
}
if (Array.isArray(value)) {
return {
kind: "array",
items: value.map((item) => normalizeField(item))
};
}
if (value && typeof value === "object") {
if (value instanceof Uint8Array) {
return {
kind: "bytes",
value: bytesToHex(value, 0, value.byteLength)
};
}
return structField(
Object.entries(value).map(
([key, fieldValue]) => parsedBoxValue(key, fieldValue)
)
);
}
if (value === null) {
return { kind: "null", value: null };
}
throw new TypeError(`Unsupported parsed field value: ${typeof value}`);
}
// src/BoxReader.js
var BoxReader = class {
/** @type {Uint8Array} */
#buffer;
/** @type {number} */
#baseOffset;
/** @type {import("./types.js").ParsedBoxValue[]} */
#values = [];
/** @type {import("./types.js").ParsedBoxIssue[]} */
#issues = [];
/**
* Current byte position in #buffer, starting at 0 and ending at
* `#buffer.length`.
*
* Each read operation will advance this cursor in #buffer.
*/
#currentOffset = 0;
/**
* @param {Uint8Array} buffer
* @param {number=} baseOffset
*/
constructor(buffer, baseOffset = 0) {
this.#buffer = buffer;
this.#baseOffset = baseOffset;
}
/**
* Get the number of bytes that are not yet read.
* @returns {number}
*/
getRemainingLength() {
return Math.max(0, this.#buffer.length - this.#currentOffset);
}
/**
* If `true`, the current box is already fully parsed.
* @returns {boolean}
*/
isFinished() {
return this.#buffer.length <= this.#currentOffset;
}
/**
* Returns the total length of the current box in bytes.
* @returns {number}
*/
getTotalLength() {
return this.#buffer.length;
}
/**
* Returns the current byte position in the box payload.
* @returns {number}
*/
getCurrentOffset() {
return this.#currentOffset;
}
/**
* Read the next `nbBytes` bytes, convert it into the corresponding
* unsigned integer and store it as a field named `key` for the current box.
*
* Throws if less that `nbBytes` bytes remain in the current box.
*
* Throws if 8 bytes or more is read. If you need to read 8 bytes, use
* `fieldUint64` (which creates a bigint).
*
* @template {NumberKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {number}
*/
fieldUint(key, nbBytes, meta) {
const baseOffset = this.#currentOffset;
return this.#pushField(key, this.#bytesToInt(nbBytes), {
...this.#withSpan(baseOffset, meta)
});
}
/**
* Read the next 8 bytes, convert it into the corresponding
* unsigned bigint and store it as a field named `key` for the current box.
*
* Throws if less that 8 bytes remain in the current box.
*
* @template {BigIntKeys<T>} K
* @param {K} key
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {bigint}
*/
fieldUint64(key, meta) {
const baseOffset = this.#currentOffset;
return this.#pushField(key, this.#bytesToUint64BigInt(), {
...this.#withSpan(baseOffset, meta)
});
}
/**
* Read the next 8 bytes, convert it into the corresponding
* **signed** bigint and store it as a field named `key` for the current box.
*
* Throws if less that 8 bytes remain in the current box.
*
* @template {BigIntKeys<T>} K
* @param {K} key
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {bigint}
*/
fieldInt64(key, meta) {
const baseOffset = this.#currentOffset;
return this.#pushField(key, this.#bytesToInt64BigInt(), {
...this.#withSpan(baseOffset, meta)
});
}
/**
* @template {NumberKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {number}
*/
fieldSignedInt(key, nbBytes, meta) {
const baseOffset = this.#currentOffset;
return this.#pushField(
key,
toSignedInt(this.#bytesToInt(nbBytes), nbBytes * 8),
{ ...this.#withSpan(baseOffset, meta) }
);
}
/**
* @template {BytesKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {Uint8Array}
*/
fieldBytes(key, nbBytes, meta) {
this.#ensureAvailable(nbBytes);
const baseOffset = this.#currentOffset;
const value = this.#buffer.slice(baseOffset, baseOffset + nbBytes);
this.#currentOffset += nbBytes;
this.#values.push(
parsedBoxValue(
key,
bytesField(this.#buffer, baseOffset, nbBytes),
this.#withSpan(baseOffset, meta)
)
);
return value;
}
/**
* @template {StringKeys<T>} K
* @param {K} key
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {string}
*/
fieldNullTerminatedAscii(key, meta) {
const baseOffset = this.#currentOffset;
return this.#pushField(key, this.#parseNullTerminatedAscii(), {
...this.#withSpan(baseOffset, meta)
});
}
/**
* @template {StringKeys<T>} K
* @param {K} key
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {string}
*/
fieldNullTerminatedUtf8(key, meta) {
const baseOffset = this.#currentOffset;
return this.#pushField(key, this.#parseNullTerminatedUtf8(), {
...this.#withSpan(baseOffset, meta)
});
}
/**
* Decode the next 4 bytes into a string if printable ASCII, or the
* corresponding 32 bit integer if not, and set it as a field named
* `key` on the current box.
*
* Throws if less than 4 are remaining in the buffer.
*
* @template {StringKeys<T>} K
* @param {K} key
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {string|number}
*/
fieldFourCc(key, meta) {
const baseOffset = this.#currentOffset;
return this.#pushField(key, this.#readFourCc(), {
...this.#withSpan(baseOffset, meta)
});
}
/**
* @template {FixedPointKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {number} fractionalBits
* @param {string} format
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {import("./types.js").ParsedFixedPointField}
*/
fieldFixedPoint(key, nbBytes, fractionalBits, format, meta) {
const baseOffset = this.#currentOffset;
const value = fixedPointField(
this.#bytesToInt(nbBytes),
nbBytes * 8,
fractionalBits,
format
);
this.#pushField(key, value, { ...this.#withSpan(baseOffset, meta) });
return value;
}
/**
* @template {FixedPointKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {number} bits
* @param {number} fractionalBits
* @param {string} format
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {import("./types.js").ParsedFixedPointField}
*/
fieldSignedFixedPoint(key, nbBytes, bits, fractionalBits, format, meta) {
const baseOffset = this.#currentOffset;
const value = signedFixedPointField(
this.#bytesToInt(nbBytes),
bits,
fractionalBits,
format
);
this.#pushField(key, value, { ...this.#withSpan(baseOffset, meta) });
return value;
}
/**
* @template {DateKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {import("./types.js").ParsedDateField}
*/
fieldMacDate(key, nbBytes, meta) {
const baseOffset = this.#currentOffset;
const raw = nbBytes === 8 ? this.#bytesToUint64BigInt() : this.#bytesToInt(nbBytes);
const value = macDateField(raw);
this.#pushField(key, value, { ...this.#withSpan(baseOffset, meta) });
return value;
}
/**
* @template {BitsKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {import("./types.js").ParsedBitsFieldPartDefinition[]} parts
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {number}
*/
fieldBits(key, nbBytes, parts, meta) {
const baseOffset = this.#currentOffset;
const value = bitsField(this.#bytesToInt(nbBytes), nbBytes * 8, parts);
this.#pushField(key, value, { ...this.#withSpan(baseOffset, meta) });
return value.value;
}
/**
* @template {FlagsKeys<T>} K
* @param {K} key
* @param {number} nbBytes
* @param {Record<string, number>} flags
* @param {string|ParsedBoxFieldMetadata} [meta]
* @returns {number}
*/
fieldFlags(key, nbBytes, flags, meta) {
const baseOffset = this.#currentOffset;
const value = flagsField(this.#bytesToInt(nbBytes), nbBytes * 8, flags);
this.#pushField(key, value, { ...this.#withSpan(baseOffset, meta) });
return value.value;
}
/**
* @template V
* @template {KeysForValue<T, V>} K
* @param {K} key
* @param {V} value
* @param {string | ParsedBoxFieldMetadata=} [meta]
* @returns {V}
*/
addField(key, value, meta) {
return this.#pushField(
key,
value,
typeof meta === "string" || meta?.offset === void 0 ? meta : { ...meta, offset: this.#baseOffset + meta.offset }
);
}
/**
* @param {"warning" | "error"} severity
* @param {string} message
* @returns {void}
*/
addIssue(severity, message) {
this.#issues.push({ severity, message });
}
/**
* Read the next `nbBytes` bytes and returns the corresponding
* unsigned integer.
*
* Throws if less that `nbBytes` bytes remain in the current box.
*
* Throws if 8 bytes or more is read. If you need to read 8 bytes, use
* `fieldUint64` (which creates a bigint).
* @param {number} nbBytes
* @returns {number}
*/
readUint(nbBytes) {
return this.#bytesToInt(nbBytes);
}
/**
* Read the next 8 bytes and returns the corresponding bigint.
*
* Throws if less that 8 bytes remain in the current box.
*
* @returns {bigint}
*/
readUint64() {
return this.#bytesToUint64BigInt();
}
/**
* Parse the next 4 bytes as a **signed** (two's complement) 64-bit integer
* into a bigint.
*
* Throws if less than 8 bytes are remaining in the buffer.
*
* @returns {bigint}
*/
readInt64() {
return this.#bytesToInt64BigInt();
}
/**
* Read the next bytes and return it as an Uint8Array.
*
* Throws if less than `nbBytes` bytes are remaining in the buffer.
*
* @param {number} nbBytes
* @returns {Uint8Array}
*/
readBytes(nbBytes) {
this.#ensureAvailable(nbBytes);
const res = this.#buffer.slice(
this.#currentOffset,
this.#currentOffset + nbBytes
);
this.#currentOffset += nbBytes;
return res;
}
/**
* Decode the next `nbBytes` as UTF-8 text.
*
* Throws if less than `nbBytes` bytes are remaining in the buffer.
*
* @param {number} nbBytes
* @returns {string}
*/
readAsUtf8(nbBytes) {
return this.#readAsUtf8(nbBytes);
}
/**
* Decode the next 4 bytes into a string if printable ASCII, or the
* corresponding 32 bit integer if not.
*
* Throws if less than 4 are remaining in the buffer.
*
* @returns {string|number}
*/
readFourCc() {
return this.#readFourCc();
}
/** @returns {import("./types.js").ParsedBoxValue[]} */
getValues() {
return this.#values.slice();
}
/** @returns {import("./types.js").ParsedBoxIssue[]} */
getIssues() {
return this.#issues.slice();
}
// Now, private methods implementing the logic
/**
* @param {number} nbBytes
* @returns {void}
*/
#ensureAvailable(nbBytes) {
if (!Number.isInteger(nbBytes) || nbBytes < 0) {
throw new Error(`Cannot read an invalid byte length: ${nbBytes}.`);
}
const remaining = this.#buffer.length - this.#currentOffset;
if (remaining < nbBytes) {
throw new Error(
`Cannot read ${nbBytes} byte(s) at offset ${this.#currentOffset}: only ${Math.max(0, remaining)} byte(s) remaining.`
);
}
}
/**
* @param {number} baseOffset
* @param {string | ParsedBoxFieldMetadata | undefined} meta
* @returns {ParsedBoxFieldMetadata}
*/
#withSpan(baseOffset, meta) {
const normalized = typeof meta === "string" ? { description: meta } : meta ?? {};
return {
...normalized,
offset: normalized.offset === void 0 ? this.#baseOffset + baseOffset : this.#baseOffset + normalized.offset,
byteLength: normalized.byteLength ?? this.#currentOffset - baseOffset
};
}
/**
* @template V
* @template {KeysForValue<T, V>} K
* @param {K} key
* @param {V} value
* @param {string | ParsedBoxFieldMetadata=} [meta]
* @returns {V}
*/
#pushField(key, value, meta) {
this.#values.push(parsedBoxValue(key, value, meta));
return value;
}
/**
* Returns the N next bytes, as a single number.
*
* /!\ only work for now for 1, 2, 3, 4, 5 or 8 bytes.
*
* /!\ Depending on the size of the number, it may be larger than JS'
* limit.
*
* @param {number} nbBytes
* @returns {number}
*/
#bytesToInt(nbBytes) {
this.#ensureAvailable(nbBytes);
let res;
switch (nbBytes) {
case 1:
res = this.#buffer[this.#currentOffset];
break;
case 2:
res = be2toi(this.#buffer, this.#currentOffset);
break;
case 3:
res = be3toi(this.#buffer, this.#currentOffset);
break;
case 4:
res = be4toi(this.#buffer, this.#currentOffset);
break;
case 5:
res = be5toi(this.#buffer, this.#currentOffset);
break;
default:
throw new Error("not implemented yet.");
}
this.#currentOffset += nbBytes;
return res;
}
/**
* Returns the next 8 bytes as an exact unsigned 64-bit bigint.
* @returns {bigint}
*/
#bytesToUint64BigInt() {
this.#ensureAvailable(8);
const hex = bytesToHex(this.#buffer, this.#currentOffset, 8);
const toBigint = hexToBigInt(hex);
this.#currentOffset += 8;
return toBigint;
}
/**
* Returns the next 8 bytes as an exact signed 64-bit bigint.
* @returns {bigint}
*/
#bytesToInt64BigInt() {
this.#ensureAvailable(8);
const hex = bytesToHex(this.#buffer, this.#currentOffset, 8);
const toBigInt = BigInt.asIntN(64, hexToBigInt(hex));
this.#currentOffset += 8;
return toBigInt;
}
/**
* Returns the N next bytes into a string.
* @param {number} nbBytes
* @returns {string}
*/
#readAsUtf8(nbBytes) {
this.#ensureAvailable(nbBytes);
const res = utf8ToStr(this.#buffer, this.#currentOffset, nbBytes);
this.#currentOffset += nbBytes;
return res;
}
#readFourCc() {
this.#ensureAvailable(4);
let isPrintable = true;
for (let i = this.#currentOffset; i < this.#currentOffset + 4; i++) {
const b = this.#buffer[i];
if (b < 32 || b > 126) {
isPrintable = false;
break;
}
}
const res = isPrintable ? (
// Convert to string, same codes as UTF-16's lower byte
String.fromCharCode(
this.#buffer[this.#currentOffset],
this.#buffer[this.#currentOffset + 1],
this.#buffer[this.#currentOffset + 2],
this.#buffer[this.#currentOffset + 3]
)
) : (
// Fallback: return unsigned 32-bit number (big-endian)
be4toi(this.#buffer, this.#currentOffset)
);
this.#currentOffset += 4;
return res;
}
/**
* @returns {string}
*/
#parseNullTerminatedAscii() {
const bytes = [];
while (!this.isFinished()) {
const value = this.readUint(1);
if (value === 0) {
break;
}
bytes.push(value);
}
return String.fromCharCode.apply(null, bytes);
}
/**
* @returns {string}
*/
#parseNullTerminatedUtf8() {
const bytes = [];
while (!this.isFinished()) {
const value = this.readUint(1);
if (value === 0) {
break;
}
bytes.push(value);
}
return utf8ToStr(new Uint8Array(bytes));
}
};
function hexToBigInt(hex) {
return BigInt(`0x${hex}`);
}
// src/boxes/helpers.js
function createTrackReferenceTypeBox(name, description) {
return {
name,
description,
parser(reader) {
const track_IDs = [];
const trackIdsOffset = reader.getCurrentOffset();
while (reader.getRemainingLength() >= 4) {
track_IDs.push(reader.readUint(4));
}
reader.addField("track_IDs", track_IDs, {
offset: trackIdsOffset,
byteLength: reader.getCurrentOffset() - trackIdsOffset
});
if (!reader.isFinished()) {
reader.fieldBytes("trailing_bytes", reader.getRemainingLength());
}
}
};
}
function parseTransformationMatrix(r) {
return structField(
[
parsedBoxValue(
"a",
signedFixedPointField(r.readUint(4), 32, 16, "16.16")
),
parsedBoxValue(
"b",
signedFixedPointField(r.readUint(4), 32, 16, "16.16")
),
parsedBoxValue("u", signedFixedPointField(r.readUint(4), 32, 30, "2.30")),
parsedBoxValue(
"c",
signedFixedPointField(r.readUint(4), 32, 16, "16.16")
),
parsedBoxValue(
"d",
signedFixedPointField(r.readUint(4), 32, 16, "16.16")
),
parsedBoxValue("v", signedFixedPointField(r.readUint(4), 32, 30, "2.30")),
parsedBoxValue(
"x",
signedFixedPointField(r.readUint(4), 32, 16, "16.16")
),
parsedBoxValue(
"y",
signedFixedPointField(r.readUint(4), 32, 16, "16.16")
),
parsedBoxValue("w", signedFixedPointField(r.readUint(4), 32, 30, "2.30"))
],
"matrix-3x3"
);
}
function parsePascalAsciiString(r, length) {
const stringLength = Math.min(r.readUint(1), length - 1);
let value = "";
for (let i = 0; i < stringLength; i++) {
const byte = r.readUint(1);
if (byte < 32 || byte > 126) {
throw new Error(
`Non-printable ASCII character found: 0x${byte.toString(16).toUpperCase()}`
);
}
value += String.fromCharCode(byte);
}
const paddingLength = length - 1 - stringLength;
if (paddingLength > 0) {
r.readBytes(paddingLength);
}
return value;
}
function readVisualSampleEntry(reader) {
const reserved = [];
const reservedOffset = reader.getCurrentOffset();
for (let i = 0; i < 6; i++) {
reserved.push(reader.readUint(1));
}
reader.addField("reserved", reserved, {
offset: reservedOffset,
byteLength: reader.getCurrentOffset() - reservedOffset
});
reader.fieldUint("data_reference_index", 2);
reader.fieldUint("pre_defined", 2);
reader.fieldUint("reserved_1", 2);
const preDefined1Offset = reader.getCurrentOffset();
const preDefined1 = [
reader.readUint(4),
reader.readUint(4),
reader.readUint(4)
];
reader.addField("pre_defined_1", preDefined1, {
offset: preDefined1Offset,
byteLength: reader.getCurrentOffset() - preDefined1Offset
});
reader.fieldUint("width", 2);
reader.fieldUint("height", 2);
reader.fieldFixedPoint("horizresolution", 4, 16, "16.16");
reader.fieldFixedPoint("vertresolution", 4, 16, "16.16");
reader.fieldUint("reserved_2", 4);
reader.fieldUint("frame_count", 2);
const compressorNameOffset = reader.getCurrentOffset();
reader.addField("compressorname", parsePascalAsciiString(reader, 32), {
offset: compressorNameOffset,
byteLength: reader.getCurrentOffset() - compressorNameOffset
});
reader.fieldUint("depth", 2);
reader.fieldUint("pre_defined", 2);
}
function parseAudioSampleEntry(r) {
const reserved = [];
const reservedOffset = r.getCurrentOffset();
for (let i = 0; i < 6; i++) {
reserved.push(r.readUint(1));
}
r.addField("reserved", reserved, {
offset: reservedOffset,
byteLength: r.getCurrentOffset() - reservedOffset
});
r.fieldUint("data_reference_index", 2);
const version = r.fieldUint("version", 2);
r.fieldUint("revision_level", 2);
r.fieldUint("vendor", 4);
r.fieldUint("channelcount", 2);
r.fieldUint("samplesize", 2);
r.fieldUint("compression_id", 2);
r.fieldUint("packet_size", 2);
r.fieldFixedPoint("samplerate", 4, 16, "16.16");
if (version === 1) {
r.fieldUint("samples_per_packet", 4);
r.fieldUint("bytes_per_packet", 4);
r.fieldUint("bytes_per_frame", 4);
r.fieldUint("bytes_per_sample", 4);
} else if (version === 2) {
r.fieldUint("struct_size", 4);
r.fieldFixedPoint("sample_rate", 4, 16, "16.16");
r.fieldUint("channel_count", 4);
r.fieldUint("reserved_1", 4);
r.fieldUint("bits_per_channel", 4);
r.fieldUint("format_specific_flags", 4);
r.fieldUint("bytes_per_audio_packet", 4);
r.fieldUint("LPCM_frames_per_audio_packet", 4);
}
}
function parseDescriptorLength(r) {
let length = 0;
let size = 0;
while (size < 4) {
const currentByte = r.readUint(1);
size += 1;
length = length << 7 | currentByte & 127;
if ((currentByte & 128) === 0) {
return {
length,
size
};
}
}
throw new Error("invalid descriptor length");
}
function parseNestedDescriptors(r, size) {
const descriptors = [];
let remaining = size;
while (remaining > 0) {
const before = r.getRemainingLength();
const descriptor = parseDescriptor(r);
const consumed = before - r.getRemainingLength();
remaining -= consumed;
descriptors.push(descriptor);
}
if (remaining !== 0) {
throw new Error("descriptor size mismatch");
}
return descriptors;
}
function parseDescriptorPayload(r, tag, size) {
if (tag === 3) {
const es_id = r.readUint(2);
const flags = r.readUint(1);
const ret = {
es_id,
stream_dependence_flag: !!(flags & 128),
URL_flag: !!(flags & 64),
OCRstream_flag: !!(flags & 32),
stream_priority: flags & 31
};
let consumed = 3;
if (ret.stream_dependence_flag) {
ret.depends_on_es_id = r.readUint(2);
consumed += 2;
}
if (ret.URL_flag) {
const urlLength = r.readUint(1);
ret.URL_length = urlLength;
ret.URL_string = urlLength > 0 ? r.readAsUtf8(urlLength) : "";
consumed += 1 + urlLength;
}
if (ret.OCRstream_flag) {
ret.ocr_es_id = r.readUint(2);
consumed += 2;
}
if (size > consumed) {
ret.descriptors = parseNestedDescriptors(r, size - consumed);
}
return ret;
}
if (tag === 4) {
const objectTypeIndication = r.readUint(1);
const streamByte = r.readUint(1);
const ret = {
object_type_indication: objectTypeIndication,
stream_type: streamByte >> 2 & 63,
up_stream: !!(streamByte >> 1 & 1),
reserved: streamByte & 1,
buffer_size_db: r.readUint(3),
max_bitrate: r.readUint(4),
avg_bitrate: r.readUint(4)
};
if (size > 13) {
ret.descriptors = parseNestedDescriptors(r, size - 13);
}
return ret;
}
if (tag === 5) {
return {
decoder_specific_info: size > 0 ? r.readBytes(size) : ""
};
}
if (tag === 6) {
return {
predefined: r.readUint(1),
remaining_payload: size > 1 ? r.readBytes(size - 1) : ""
};
}
return {
data: size > 0 ? r.readBytes(size) : ""
};
}
function parseDescriptor(r) {
const tag = r.readUint(1);
const { length, size } = parseDescriptorLength(r);
return {
tag,
size: length,
header_size: size + 1,
payload: parseDescriptorPayload(r, tag, length)
};
}
// src/boxes/ac-3.js
var ac_3_default = {
name: "AC-3 Audio Sample Entry",
description: "Describes AC-3 audio samples and their dac3 decoder-specific box.",
container: true,
parser(reader) {
parseAudioSampleEntry(reader);
}
};
// src/boxes/av01.js
var av01_default = {
name: "AV1 Sample Entry",
description: "Describes AV1 video samples and carries child decoder configuration boxes such as av1C.",
container: true,
parser(reader) {
readVisualSampleEntry(reader);
}
};
// src/boxes/avc1.js
var avc1_default = {
name: "AVC Sample Entry",
description: "Describes AVC video samples whose parameter sets are stored in this entry.",
container: true,
parser(reader) {
readVisualSampleEntry(reader);
}
};
// src/boxes/avc3.js
var avc3_default = {
name: "AVC3 Sample Entry",
description: "Describes AVC video samples whose parameter sets may be carried in-band.",
container: true,
parser(r) {
readVisualSampleEntry(r);
}
};
// src/boxes/avcC.js
var avcC_default = {
name: "AVC Decoder Configuration Record",
description: "Stores AVC decoder configuration, including profile data and parameter sets.",
parser(reader) {
reader.fieldUint("configurationVersion", 1);
reader.fieldUint("AVCProfileIndication", 1);
reader.fieldUint("profile_compatibility", 1);
reader.fieldUint("AVCLevelIndication", 1);
reader.fieldBits("lengthSizeMinusOne", 1, [
{ key: "reserved", bits: 6 },
{ key: "value", bits: 2 }
]);
const numOfSequenceParameterSets = reader.fieldBits(
"numOfSequenceParameterSets",
1,
[
{ key: "reserved", bits: 3 },
{ key: "value", bits: 5 }
]
);
const sequenceParameterSets = [];
const sequenceParameterSetsOffset = reader.getCurrentOffset();
for (let i = 0; i < numOfSequenceParameterSets; i++) {
const sequenceParameterSetLength = reader.readUint(2);
sequenceParameterSets.push({
length: sequenceParameterSetLength,
data: reader.readBytes(sequenceParameterSetLength)
});
}
reader.addField("sequenceParameterSets", sequenceParameterSets, {
offset: sequenceParameterSetsOffset,
byteLength: reader.getCurrentOffset() - sequenceParameterSetsOffset
});
const numOfPictureParameterSets = reader.fieldUint(
"numOfPictureParameterSets",
1
);
const pictureParameterSets = [];
const pictureParameterSetsOffset = reader.getCurrentOffset();
for (let i = 0; i < numOfPictureParameterSets; i++) {
const pictureParameterSetLength = reader.readUint(2);
pictureParameterSets.push({
length: pictureParameterSetLength,
data: reader.readBytes(pictureParameterSetLength)
});
}
reader.addField("pictureParameterSets", pictureParameterSets, {
offset: pictureParameterSetsOffset,
byteLength: reader.getCurrentOffset() - pictureParameterSetsOffset
});
if (!reader.isFinished()) {
reader.fieldBytes("ext", reader.getRemainingLength());
}
}
};
// src/boxes/btrt.js
var btrt_default = {
name: "Bit Rate Box",
description: "Provides buffer size and bitrate limits for a sample entry.",
parser(reader) {
reader.fieldUint("bufferSizeDB", 4);
reader.fieldUint("maxBitrate", 4);
reader.fieldUint("avgBitrate", 4);
}
};
// src/boxes/cdsc.js
var cdsc_default = createTrackReferenceTypeBox(
"Content Description Track Reference Type Box",
"Lists track IDs described by the containing metadata or descriptive track."
);
// src/boxes/co64.js
var co64_default = {
name: "Chunk Large Offset Box",
description: "Maps each media chunk to its 64-bit byte offset in the file.",
parser(reader) {
const version = reader.fieldUint(
"version",
1,
"This box's version. Should be `0` for this box."
);
if (version !== 0) {
throw new Error("invalid version");
}
reader.fieldUint("flags", 3);
const entry_count = reader.fieldUint(
"entry_count",
4,
"Number of chunk offsets declared"
);
const chunk_offsets = [];
const chunkOffsetsOffset = reader.getCurrentOffset();
for (let i = 0; i < entry_count; i++) {
chunk_offsets.push(reader.readUint64());
}
reader.addField("chunk_offsets", chunk_offsets, {
offset: chunkOffsetsOffset,
byteLength: reader.getCurrentOffset() - chunkOffsetsOffset
});
}
};
// src/boxes/colr.js
var colr_default = {
name: "Colour Information Box",
description: "Signals the colour representation used by visual samples.",
parser(reader) {
const colour_type = reader.fieldFourCc("colour_type");
if (colour_type === "nclx") {
reader.fieldUint("colour_primaries", 2);
reader.fieldUint("transfer_characteristics", 2);
reader.fieldUint("matrix_coefficients", 2);
if (!reader.isFinished()) {
reader.fieldBits("full_range_flag", 1, [
{ key: "value", bits: 1 },
{ key: "reserved", bits: 7 }
]);
}
} else if (colour_type === "nclc") {
reader.fieldUint("colour_primaries", 2);
reader.fieldUint("transfer_characteristics", 2);
reader.fieldUint("matrix_coefficients", 2);
} else if ((colour_type === "rICC" || colour_type === "prof") && !reader.isFinished()) {
reader.fieldBytes("ICC_profile", reader.getRemainingLength());
} else if (!reader.isFinished()) {
reader.fieldBytes("ICC_profile", reader.getRemainingLength());
}
}
};
// src/boxes/cslg.js
var cslg_default = {
name: "Composition To Decode Box",
description: "Provides composition-to-decode timeline offsets used to reconstruct presentation timestamps for re-ordered samples.",
parser(reader) {
const version = reader.fieldUint("version", 1, "cslg version");
reader.fieldUint("flags", 3, "cslg flags, generally at 0");
if (version === 0) {
reader.fieldSignedInt("compositionToDTSShift", 4);
reader.fieldSignedInt("leastDecodeToDisplayDelta", 4);
reader.fieldSignedInt("greatestDecodeToDisplayDelta", 4);
reader.fieldSignedInt("compositionStartTime", 4);
reader.fieldSignedInt("compositionEndTime", 4);
} else if (version === 1) {
reader.fieldInt64("compositionToDTSShift");
reader.fieldInt64("leastDecodeToDisplayDelta");
reader.fieldInt64("greatestDecodeToDisplayDelta");
reader.fieldInt64("compositionStartTime");
reader.fieldInt64("compositionEndTime");
} else {
throw new Error("invalid version");
}
}
};
// src/boxes/ctts.js
var ctts_default = {
name: "Composition Time to Sample Box",
description: "Maps samples to composition-time offsets for presentation order.",
parser(reader) {
const version = reader.fieldUint("version", 1, "This box's version.");
if (version > 1) {
throw new Error("invalid version");
}
reader.fieldUint("flags", 3);
const entry_count = reader.fieldUint(
"entry_count",
4,
"Number of entries in that box"
);
const entries = [];
const entriesOffset = reader.getCurrentOffset();
for (let i = 0; i < entry_count; i++) {
entries.push({
sample_count: reader.readUint(4),
sample_offset: version === 0 ? reader.readUint(4) : ~~reader.readUint(4)
});
}
reader.addField("entries", entries, {
offset: entriesOffset,
byteLength: reader.getCurrentOffset() - entriesOffset
});
}
};
// src/boxes/dac3.js
var dac3_default = {
name: "AC-3 Specific Box",
description: "Packed AC-3 decoder configuration carrying stream identification, channel mode, LFE presence and bitrate code.",
parser(reader) {
reader.fieldBits("ac3_config", 3, [
{ key: "fscod", bits: 2 },
{ key: "bsid", bits: 5 },
{ key: "bsmod", bits: 3 },
{ key: "acmod", bits: 3 },
{ key: "lfeon", bits: 1 },
{ key: "bit_rate_code", bits: 5 },
{ key: "reserved", bits: 5 }
]);
}
};
// src/boxes/data.js
function keepInvalidIntegerPayloadAsBytes(reader, typeCode, length) {
reader.addIssue(
"warning",
`Metadata integer type ${typeCode} is defined for 1 to 4 byte payloads, got ${length} byte(s); keeping raw payload bytes.`
);
reader.fieldBytes("value", length);
}
var data_default = {
name: "Metadata Value Box",
description: "Carries a typed metadata value for an Apple metadata item.",
parser(reader) {
const type_set = reader.fieldUint("type_set", 1);
const type_code = reader.fieldUint("type_code", 3);
reader.fieldUint("locale", 4);
const remaining = reader.getRemainingLength();
if (type_set !== 0) {
reader.addIssue(
"warning",
`Unsupported metadata type set ${type_set}; keeping raw payload bytes.`
);
reader.fieldBytes("value", remaining);
return;
}
switch (type_code) {
case 1: {
const baseOffset = reader.getCurrentOffset();
reader.addField(
"value",
// TODO: Add `reader.fieldUtf8(key, nbBytes)` API?
new TextDecoder().decode(reader.readBytes(remaining)),
{
description: "UTF-8 metadata value.",
offset: baseOffset,
byteLength: remaining
}
);
return;
}
case 21:
if (remaining < 1 || remaining > 4) {
keepInvalidIntegerPayloadAsBytes(reader, type_code, remaining);
return;
}
reader.fieldSignedInt(
"value",
remaining,
"Signed big-endian integer metadata value."
);
return;
case 22:
if (remaining < 1 || remaining > 4) {
keepInvalidIntegerPayloadAsBytes(reader, type_code, remaining);
return;
}
reader.fieldUint(
"value",
remaining,
"Unsigned big-endian integer metadata value."
);
return;
default:
reader.fieldBytes("value", remaining);
}
}
};
// src/boxes/dec3.js
var dec3_default = {
name: "EC-3 Specific Box",
description: "Packed EC-3 decoder configuration listing independent substreams and optional Atmos/JOC extension signalling.",
parser(reader) {
const header = reader.fieldBits("header", 2, [
{ key: "data_rate", bits: 13 },
{ key: "num_ind_sub", bits: 3 }
]);
const substreamCount = (header & 7) + 1;
const substreams = [];
const substreamsOffset = reader.getCurrentOffset();
for (let i = 0; i < substreamCount && !reader.isFinished(); i++) {
const entryHeader = bitsField(reader.readUint(3), 24, [
{ key: "fscod", bits: 2 },
{ key: "bsid", bits: 5 },
{ key: "bsmod", bits: 5 },
{ key: "acmod", bits: 3 },
{ key: "lfeon", bits: 1 },
{ key: "reserved", bits: 3 },
{ key: "num_dep_sub", bits: 4 },
{ key: "__tail", bits: 1 }
]);
let numDepSub = 0;
let tail = 0;
const fields = [];
for (const field of entryHeader.fields) {
if (field.key === "__tail") {
tail = field.value;
continue;
}
if (field.key === "num_dep_sub") {
numDepSub = field.value;
}
fields.push(parsedBoxValue(field.key, field.value));
}
if (numDepSub > 0 && !reader.isFinished()) {
fields.push(
parsedBoxValue("chan_loc", tail << 8 | reader.readUint(1))
);
} else {
fields.push(parsedBoxValue("reserved_2", tail));
}
substreams.push(structField(fields));
}
reader.addField("substreams", substreams, {
offset: substreamsOffset,
byteLength: reader.getCurrentOffset() - substreamsOffset
});
if (reader.getRemainingLength() >= 2) {
const extensionOffset = reader.getCurrentOffset();
const extension = bitsField(reader.readUint(1), 8, [
{ key: "reserved", bits: 7 },
{ key: "flag_ec3_extension_type_a", bits: 1 }
]);
reader.addField(
"ec3_extension",
structField([
...extension.fields.map(
(field) => parsedBoxValue(field.key, field.value)
),
parsedBoxValue("complexity_index_type_a", reader.readUint(1))
]),
{
offset: extensionOffset,
byteLength: reader.getCurrentOffset() - extensionOffset
}
);
}
if (!reader.isFinished()) {
reader.fieldBytes("trailing_bytes", reader.getRemainingLength());
}
}
};
// src/boxes/dinf.js
var dinf_default = {
name: "Data Information Box",
description: "Objects that declare the location of the media information in a track.",
container: true
};
// src/boxes/dOps.js
var dOps_default = {
name: "Opus Specific Box",
description: "Stores the Opus decoder configuration equivalent to the Ogg OpusHead payload.",
parser(reader) {
reader.fieldUint("Version", 1);
const cCount = reader.fieldUint("OutputChannelCount", 1);
reader.fieldUint("PreSkip", 2);
reader.fieldUint("InputSampleRate", 4);
reader.fieldSignedInt("OutputGain", 2);
const channelMappingFamily = reader.fieldUint("ChannelMappingFamily", 1);
if (channelMappingFamily !== 0 && !reader.isFinished()) {
reader.fieldUint("StreamCount", 1);
reader.fieldUint("CoupledCount", 1);
const mapping = [];
const mappingOffset = reader.getCurrentOffset();
for (let i = 0; i < cCount && !reader.isFinished(); i++) {
mapping.push(reader.readUint(1));
}
reader.addField("ChannelMapping", mapping, {
offset: mappingOffset,
byteLength: reader.getCurrentOffset() - mappingOffset
});
}
}
};
// src/boxes/dref.js
var dref_default = {
name: "Data Reference Box",
descr