UNPKG

isobmff-inspector

Version:

Simple ISOBMFF parser, compatible with JavaScript and Node.JS

1,717 lines (1,694 loc) 126 kB
#!/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