diff --git a/demo/boxHtmlTable.js b/demo/boxHtmlTable.js index 8bfa2c11..e811c4c2 100644 --- a/demo/boxHtmlTable.js +++ b/demo/boxHtmlTable.js @@ -12,7 +12,19 @@ function generatePropertyValue(prop, value) { if (typeof value?.toString === 'function') { const content = value.toString(); if (value.hasOwnProperty('toString') || content.startsWith('<')) { - return content; + return content.replace( + /[&<>"'\-\n]/g, + m => + ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '-': '‑', + '\n': '
', + })[m], + ); } } diff --git a/entries/all-boxes.ts b/entries/all-boxes.ts index 802d5ece..943af1c3 100644 --- a/entries/all-boxes.ts +++ b/entries/all-boxes.ts @@ -2,6 +2,7 @@ export * from '#/boxes/a1lx'; export * from '#/boxes/a1op'; export * from '#/boxes/auxC'; export * from '#/boxes/av1C'; +export * from '#/boxes/av3c'; export * from '#/boxes/avcC'; export * from '#/boxes/btrt'; export * from '#/boxes/ccst'; @@ -20,6 +21,7 @@ export * from '#/boxes/csch'; export * from '#/boxes/cslg'; export * from '#/boxes/ctts'; export * from '#/boxes/dac3'; +export * from '#/boxes/dca3'; export * from '#/boxes/dec3'; export * from '#/boxes/defaults'; export * from '#/boxes/dfLa'; diff --git a/src/BitStream.ts b/src/BitStream.ts new file mode 100644 index 00000000..eab260a4 --- /dev/null +++ b/src/BitStream.ts @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2026. Paul Higgs + * License: BSD-3-Clause (see LICENSE file) + * + * + * reads bits and bytes from a buffer that may not contain aligned values + * TODO: add writing support + */ + +import { DataStream, Endianness } from '#/DataStream'; +import type { MultiBufferStream } from './buffer'; + +class State { + rbyte: number; + rbit: number; + wbyte: number; + wbit: number; + end: number; + read_error: boolean; + write_error: boolean; + + constructor() { + this.rbyte = this.rbit = this.wbyte = this.wbit = this.end = 0; + this.read_error = this.write_error = false; + } + + readBitOffset(): number { + return 8 * this.rbyte + this.rbit; + } + readByteOffset() { + return this.rbyte; + } + writeBitOffset(): number { + return 8 * this.wbyte + this.wbit; + } + writeByteOffset() { + return this.wbyte; + } + aligned() { + return this.read_error && this.rbit !== 0; + } +} + +export class BitStream { + private buffer: Array; + private stream: MultiBufferStream | DataStream; + private state: State; + private endianness: Endianness; + + constructor(stream: MultiBufferStream | DataStream, endianness?: Endianness) { + this.state = new State(); + this.stream = stream; + this.endianness = endianness ? endianness : Endianness.BIG_ENDIAN; + } + + appendUint8(count = 1): void { + for (let i = 0; i < count; i++) this.buffer.push(this.stream.readUint8()); + this.state.end = this.state.wbyte = this.buffer.length; + } + + extend(bits: number): void { + let count = bits; + while (count > 0) { + this.buffer.push(0); + count -= 8; + } + } + + readBit(): number { + //! Read the next bit and advance the read pointer. + if (this.state.read_error || this.endOfRead()) { + this.state.read_error = true; + return 0; + } + const bit: number = + (this.buffer[this.state.rbyte] >> + (this.endianness === Endianness.BIG_ENDIAN ? 7 - this.state.rbit : this.state.rbit)) & + 0x01; + if (++this.state.rbit > 7) { + this.state.rbyte++; + this.state.rbit = 0; + } + return bit; + } + + peekBit(): number { + //! Read the next bit and but dont advance the read pointer. + if (this.state.read_error || this.endOfRead()) { + this.state.read_error = true; + return 0; + } + const bit: number = + (this.buffer[this.state.rbyte] >> + (this.endianness === Endianness.BIG_ENDIAN ? 7 - this.state.rbit : this.state.rbit)) & + 0x01; + return bit; + } + + endOfRead(): boolean { + return this.state.rbyte === this.state.wbyte && this.state.rbit === this.state.wbit; + } + + getBool(): boolean { + return this.readBit() !== 0; + } + + private rdb = function (bytes: number): number { + let i: number, res: number; + // eslint-disable-next-line no-loss-of-precision + const ff = 0xffffffffffffffff; + if (this.state.read_error) return ff; + if (this.state.rbit === 0) { + // Read buffer is byte aligned. Most common case. + if (this.state.rbyte + bytes > this.state.wbyte) { + // Not enough bytes to read. + this.state.read_error = true; + return ff; + } else { + for (res = 0, i = 0; i < bytes; i++) res = (res << 8) + this.buffer[this.state.rbyte + i]; + this.state.rbyte += bytes; + return res; + } + } else { + // Read buffer is not byte aligned, use an intermediate aligned buffer. + if (this.currentReadBitOffset() + 8 * bytes > this.currentWriteBitOffset()) { + // Not enough bytes to read. + this.state.read_error = true; + return ff; + } else { + for (res = 0, i = 0; i < bytes; i++) { + if (this.endianness === Endianness.BIG_ENDIAN) + res = + (res << 8) + + ((this.buffer[this.state.rbyte] << this.state.rbit) | + (this.buffer[this.state.rbyte + 1] >> (8 - this.state.rbit))); + else + res = + (res << 8) + + ((this.buffer[this.state.rbyte] >> this.state.rbit) | + (this.buffer[this.state.rbyte + 1] << (8 - this.state.rbit))); + this.state.rbyte++; + } + return res; + } + } + return ff; // we should never get here!! + }; + + readUint8() { + return this.rdb(1); + } + + readUint16(endianness?: Endianness) { + return (endianness ?? this.endianness) === Endianness.BIG_ENDIAN + ? this.GetUInt16BE(this.rdb(2)) + : this.GetUInt16LE(this.rdb(2)); + } + private GetUInt16BE = function (val: number) { + return this.OSisLittleEndian() ? this.ByteSwap16(val) : val; + }; + private GetUInt16LE = function (val: number) { + return this.OSisLittleEndian() ? val : this.ByteSwap16(val); + }; + private ByteSwap16 = function (x: number): number { + return (x << 8) | (x >> 8); + }; + + readUint24(endianness?: Endianness) { + return (endianness ?? this.endianness) === Endianness.BIG_ENDIAN + ? this.GetUInt24BE(this.rdb(3)) + : this.GetUInt24LE(this.rdb(3)); + } + + private ByteSwap24 = function (x: number) { + return ((x & 0xff0000) >> 16) | (x & 0xff00) | (x & (0xff << 16)); + }; + private GetUInt24BE = function (val: number) { + return this.OSisLittleEndian() ? this.ByteSwap24(val) : val; + }; + private GetUInt24LE = function (val: number) { + return this.OSisLittleEndian() ? val : this.ByteSwap24(val); + }; + + readUint32(endianness?: Endianness) { + return (endianness ?? this.endianness) === Endianness.BIG_ENDIAN + ? this.GetUInt32BE(this.rdb(4)) + : this.GetUInt32LE(this.rdb(4)); + } + private ByteSwap32 = function (x: number) { + return (x << 24) | ((x << 8) & 0x00ff0000) | ((x >> 8) & 0x0000ff00) | (x >> 24); + }; + private GetUInt32BE = function (val: number) { + return this.OSisLittleEndian() ? this.ByteSwap32(val) : val; + }; + private GetUInt32LE = function (val: number) { + return this.OSisLittleEndian() ? val : this.ByteSwap32(val); + }; + + readBits(bits: number): number { + // No read if read error is already set or not enough bits to read. + if ( + this.state.read_error || + this.currentReadBitOffset() + bits > this.currentWriteBitOffset() + ) { + this.state.read_error = true; + return 0; + } + let val = 0; + if (this.endianness === Endianness.BIG_ENDIAN) { + // Read leading bits up to byte boundary + while (bits > 0 && this.state.rbit !== 0) { + val = (val << 1) | this.readBit(); + --bits; + } + + // Read complete bytes + while (bits > 7) { + val = (val << 8) | this.buffer[this.state.rbyte++]; + bits -= 8; + } + + // Read trailing bits + while (bits > 0) { + val = (val << 1) | this.readBit(); + --bits; + } + } else { + // Little endian decoding + let shift = 0; + + // Read leading bits up to byte boundary + while (bits > 0 && this.state.rbit !== 0) { + val |= this.readBit() << shift; + --bits; + shift++; + } + + // Read complete bytes + while (bits > 7) { + val |= this.buffer[this.state.rbyte++] << shift; + bits -= 8; + shift += 8; + } + + // Read trailing bits + while (bits > 0) { + val |= this.readBit() << shift; + --bits; + shift++; + } + } + return val; + } + + skipBits(bits: number): boolean { + if (this.state.read_error) { + // Can't skip bits and bytes if read error is already set. + return false; + } + const rpos = 8 * this.state.rbyte + this.state.rbit + bits; + const wpos = 8 * this.state.wbyte + this.state.wbit; + if (rpos > wpos) { + this.state.rbyte = this.state.wbyte; + this.state.rbit = this.state.wbit; + this.state.read_error = true; + return false; + } + this.state.rbyte = rpos >> 3; + this.state.rbit = rpos & 7; + return true; + } + + skipBit(): boolean { + return this.skipBits(1); + } + + readUE(): number { + // read in an Unsigned Exp-Golomb code; + if (this.readBit() === 1) return 0; + let zero_count = 1; + while (this.peekBit() === 0) { + this.readBit(); + zero_count++; + } + return this.readBits(zero_count + 1) - 1; + } + + byte_alignment(): void { + while (!this.state.aligned) this.skipBit(); + } + + private OSisLittleEndian = function (): boolean { + return this.stream.endianness === Endianness.LITTLE_ENDIAN; + }; + + currentReadByteOffset(): number { + return this.state.readByteOffset(); + } + currentReadBitOffset(): number { + return this.state.readBitOffset(); + } + currentWriteByteOffset(): number { + return this.state.writeByteOffset(); + } + currentWriteBitOffset(): number { + return this.state.writeBitOffset(); + } + bitsRemaining(): number { + return this.currentWriteBitOffset() - this.currentReadBitOffset(); + } + writeBitsRemaining(): number { + return 8 * this.buffer.length - this.currentWriteBitOffset(); + } + /* + TODO - for near future implementation to support writing AVS3 related boxes that are not byte aligned + writeBit(bit: number): void { + if (this.writeBitsRemaining() < 1) + this.extend(1); + } + */ +} diff --git a/src/boxes/av3c.ts b/src/boxes/av3c.ts new file mode 100644 index 00000000..8a0d6920 --- /dev/null +++ b/src/boxes/av3c.ts @@ -0,0 +1,515 @@ +/* + * Copyright (c) 2025. Paul Higgs + * License: BSD-3-Clause (see LICENSE file) + */ + +import { Box } from '#/box'; +import type { MultiBufferStream } from '#/buffer'; +import { BitStream } from '#/BitStream'; + +import { + DescribedValue, + AVS3data, + HexadecimalValue, + BinaryValue, + BooleanValue, +} from './avs-common'; + +interface ReferencePicture { + library_index_flag?: number; + referenced_library_picture_index?: number; + abs_delta_doi?: number; + sign_delta_doi?: number; +} + +class ReferencePictureSet { + private _list: number; + private _set: number; + private _pics: Array; + private _reference_to_library_enable_flag?: number; + private _library_enable_flag_set: boolean; + + constructor(list: number, set: number) { + this._list = list; + this._set = set; + this._pics = []; + this._library_enable_flag_set = false; + } + set_reference_to_library_enable_flag(flag: number) { + this._library_enable_flag_set = true; + this._reference_to_library_enable_flag = flag; + } + get_reference_to_library_enable_flag(): number { + return this._library_enable_flag_set ? this._reference_to_library_enable_flag : 0; + } + push(pic: ReferencePicture) { + this._pics.push(pic); + } + toString() { + let ret = '{'; + if (this._library_enable_flag_set) + ret += 'reference_to_library_enable_flag: ' + this._reference_to_library_enable_flag; + this._pics.forEach(e => { + ret += (ret.length > 3 ? ', ' : '') + JSON.stringify(e).replace(/"/g, ''); + }); + ret += '}'; + return ret; + } +} + +class ReferencePictureList { + private _list?: number; + private _sets?: Array; + + constructor(list: number) { + this._list = list; + this._sets = []; + } + push(set: ReferencePictureSet) { + this._sets.push(set); + } + toString() { + if (this._sets.length === 0) return '(empty)'; + const l: Array = []; + this._sets.forEach(set => { + l.push(set.toString()); + }); + return l.join(', '); + } +} + +class WeightQuantMatrix { + WeightQuantMatrix4x4: Array>; + WeightQuantMatrix8x8: Array>; + + constructor(reader: BitStream) { + this.WeightQuantMatrix4x4 = []; + this.WeightQuantMatrix8x8 = []; + + for (let sizeId = 0; sizeId < 2; sizeId++) { + const this_size: Array> = []; + const WQMSize = 1 << (sizeId + 2); + for (let i = 0; i < WQMSize; i++) { + const iVal: Array = []; + for (let j = 0; j < WQMSize; j++) iVal.push(reader.readUE()); + this_size.push(iVal); + } + if (sizeId === 0) this.WeightQuantMatrix4x4 = this_size; + else this.WeightQuantMatrix8x8 = this_size; + } + } + toString() { + let str = ''; + if (this.WeightQuantMatrix4x4.length) + str += '4x4: ' + JSON.stringify(this.WeightQuantMatrix4x4); + if (this.WeightQuantMatrix8x8.length) + str += (str.length > 2 ? ',\n' : '') + '8x8: ' + JSON.stringify(this.WeightQuantMatrix8x8); + return str; + } +} + +const MAIN_8 = 0x20, + MAIN_10 = 0x22, + HIGH_8 = 0x30, + HIGH_10 = 0x32; +const RESERVED = 'Reserved', + FORBIDDEN = 'Forbidden'; +const AVS3profiles = [ + // Table B.1 of T/AI 109.2 + { profile: MAIN_8, description: 'Main 8 bit' }, + { profile: MAIN_10, description: 'Main 10 bit' }, + { profile: HIGH_8, description: 'High 8 bit' }, + { profile: HIGH_10, description: 'High 10 bit' }, + { profile: 0x00, description: FORBIDDEN }, +]; +const AVS3levels = [ + // Table B.1 of T/AI 109.2 + { level: 0x50, description: '8.0.30' }, + { level: 0x52, description: '8.2.30' }, + { level: 0x51, description: '8.4.30' }, + { level: 0x53, description: '8.6.30' }, + { level: 0x54, description: '8.0.60' }, + { level: 0x56, description: '8.2.60' }, + { level: 0x55, description: '8.4.60' }, + { level: 0x57, description: '8.6.60' }, + { level: 0x58, description: '8.0.120' }, + { level: 0x5a, description: '8.2.120' }, + { level: 0x59, description: '8.4.120' }, + { level: 0x5b, description: '8.6.120' }, + { level: 0x60, description: '10.0.30' }, + { level: 0x62, description: '10.2.30' }, + { level: 0x61, description: '10.4.30' }, + { level: 0x63, description: '10.6.30' }, + { level: 0x64, description: '10.0.60' }, + { level: 0x66, description: '10.2.60' }, + { level: 0x65, description: '10.4.60' }, + { level: 0x67, description: '10.6.60' }, + { level: 0x68, description: '10.0.120' }, + { level: 0x6a, description: '10.2.120' }, + { level: 0x69, description: '10.4.120' }, + { level: 0x6b, description: '10.6.120' }, + { level: 0x10, description: '2.0.15' }, + { level: 0x12, description: '2.0.30' }, + { level: 0x14, description: '2.0.60' }, + { level: 0x20, description: '4.0.30' }, + { level: 0x22, description: '4.0.60' }, + { level: 0x40, description: '6.0.30' }, + { level: 0x42, description: '6.2.30' }, + { level: 0x41, description: '6.4.30' }, + { level: 0x43, description: '6.6.30' }, + { level: 0x44, description: '6.0.60' }, + { level: 0x46, description: '6.2.60' }, + { level: 0x45, description: '6.4.60' }, + { level: 0x47, description: '6.6.60' }, + { level: 0x48, description: '6.0.120' }, + { level: 0x4a, description: '6.2.120' }, + { level: 0x49, description: '6.4.120' }, + { level: 0x4b, description: '6.6.120' }, + { level: 0x00, description: FORBIDDEN }, +]; +const AVS3precisions = [ + // Table 45 of T/AI 109.2 + { precision: 1, description: '8-bit' }, + { precision: 2, description: '10-bit' }, +]; +const AVS3framerates = [ + '', + '24/1.001', + '24', + '25', + '30/1.001', + '30', + '50', + '60/1.001', + '60', + '100', + '120', + '200', + '240', + '300', + '120/1.001', +]; +const AVS3ratios = ['', '1.0', '4:3', '16:9', '2.21:1']; + +const AVS3Vconfiguration = (version: number) => (version !== 1 ? 'not supported' : ''); + +function AVS3profile(profile: number) { + const t = AVS3profiles.find(e => e.profile === profile); + return t === undefined ? RESERVED : t.description; +} + +function AVS3level(level: number) { + const t = AVS3levels.find(e => e.level === level); + return t === undefined ? RESERVED : t.description; +} + +function AVS3precision(precision: number) { + const t = AVS3precisions.find(e => e.precision === precision); + return t === undefined ? RESERVED : t.description; +} + +const AVS3chroma = (chroma: number) => (chroma === 1 ? '4:2:0' : RESERVED); + +const AVS3aspectratio = (ratio: number) => + ratio === 0 ? FORBIDDEN : ratio >= AVS3ratios.length ? RESERVED : AVS3ratios[ratio]; + +const AVS3framerate = (framerate: number) => + framerate === 0 + ? FORBIDDEN + : framerate >= AVS3framerates.length + ? RESERVED + : AVS3framerates[framerate] + ' fps'; + +interface SequenceHeaderElements { + video_sequence_start_code?: HexadecimalValue; + profile_id?: HexadecimalValue; + level_id?: HexadecimalValue; + progressive_sequence?: BooleanValue; + field_coded_sequence?: BooleanValue; + library_stream_flag?: BooleanValue; + library_picture_enable_flag?: BooleanValue; + duplicate_sequence_number_flag?: BooleanValue; + horizontal_size?: number; + vertical_size?: number; + chroma_format?: BinaryValue; + sample_precision?: BinaryValue; + encoding_precision?: BinaryValue; + aspect_ratio?: BinaryValue; + frame_rate_code?: BinaryValue; + bit_rate_lower?: number; + bit_rate_upper?: number; + low_delay?: number; + temporal_id_enable_flag?: BooleanValue; + max_dpb_minus1?: number; + bbv_buffer_size?: number; + rpl1_index_exist_flag?: BooleanValue; + rpl1_same_as_rpl0_flag?: BooleanValue; + num_ref_pic_list_set0?: number; + rpl0?: ReferencePictureList; + num_ref_pic_list_set1?: number; + rpl1?: ReferencePictureList; + num_ref_default_active_minus1_0?: number; + num_ref_default_active_minus1_1?: number; + log2_lcu_size_minus2?: number; + log2_min_cu_size_minus2?: number; + log2_max_part_ratio_minus2?: number; + max_split_times_minus6?: number; + log2_min_qt_size_minus2?: number; + log2_max_bt_size_minus2?: number; + log2_max_eqt_size_minus3?: number; + weight_quant_enable_flag?: BooleanValue; + load_seq_weight_quant_data_flag?: BooleanValue; + weight_quant_matrix?: WeightQuantMatrix; + st_enable_flag?: BooleanValue; + sao_enable_flag?: BooleanValue; + alf_enable_flag?: BooleanValue; + affine_enable_flag?: BooleanValue; + smvd_enable_flag?: BooleanValue; + ipcm_enable_flag?: BooleanValue; + amvr_enable_flag?: BooleanValue; + num_of_hmvp_cand?: number; + umve_enable_flag?: BooleanValue; + emvr_enable_flag?: BooleanValue; + intra_pf_enable_flag?: BooleanValue; + tscpm_enable_flag?: BooleanValue; + dt_enable_flag?: BooleanValue; + log2_max_dt_size_minus4?: number; + pbt_enable_flag?: BooleanValue; + pmc_enable_flag?: BooleanValue; + iip_enable_flag?: BooleanValue; + sawp_enable_flag?: BooleanValue; + asr_enable_flag?: BooleanValue; + awp_enable_flag?: BooleanValue; + etmvp_mvap_enable_flag?: BooleanValue; + dmvr_enable_flag?: BooleanValue; + bio_enable_flag?: BooleanValue; + bgc_enable_flag?: BooleanValue; + inter_pf_enable_flag?: BooleanValue; + inter_pfc_enable_flag?: BooleanValue; + obmc_enable_flag?: BooleanValue; + sbt_enable_flag?: BooleanValue; + ist_enable_flag?: BooleanValue; + esao_enable_flag?: BooleanValue; + ccsao_enable_flag?: BooleanValue; + ealf_enable_flag?: BooleanValue; + ibc_enable_flag?: BooleanValue; + isc_enable_flag?: BooleanValue; + num_of_intra_hmvp_cand?: number; + fimc_enable_flag?: BooleanValue; + nn_tools_set_hook?: number; + num_of_nn_filter_minus1?: number; + output_reorder_delay?: number; + cross_patch_loop_filter_enable_flag?: BooleanValue; + ref_colocated_patch_flag?: BooleanValue; + stable_patch_flag?: BooleanValue; + uniform_patch_flag?: BooleanValue; + patch_width_minus1?: number; + patch_height_minus1?: number; +} + +class AVS3SequenceHeader extends AVS3data { + data: SequenceHeaderElements; + + constructor(bit_reader: BitStream) { + super(); + this.data = {}; + this.deserialise(bit_reader); + } + deserialise(bit_reader: BitStream) { + this.data.video_sequence_start_code = new HexadecimalValue(bit_reader.readUint32()); + this.data.profile_id = new HexadecimalValue(bit_reader.readUint8(), AVS3profile); + this.data.level_id = new HexadecimalValue(bit_reader.readUint8(), AVS3level); + this.data.progressive_sequence = new BooleanValue(bit_reader.readBit()); + this.data.field_coded_sequence = new BooleanValue(bit_reader.readBit()); + this.data.library_stream_flag = new BooleanValue(bit_reader.readBit()); + if (!this.data.library_stream_flag.value) { + this.data.library_picture_enable_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.library_picture_enable_flag.value) + this.data.duplicate_sequence_number_flag = new BooleanValue(bit_reader.readBit()); + } + bit_reader.skipBit(); // marker_bit + + this.data.horizontal_size = bit_reader.readBits(14); + bit_reader.skipBit(); // marker_bit + + this.data.vertical_size = bit_reader.readBits(14); + this.data.chroma_format = new BinaryValue(bit_reader.readBits(2), 2, AVS3chroma); + this.data.sample_precision = new BinaryValue(bit_reader.readBits(3), 3, AVS3precision); + + if (this.data.profile_id.value === MAIN_10 || this.data.profile_id.value === HIGH_10) + this.data.encoding_precision = new BinaryValue(bit_reader.readBits(3), 3, AVS3precision); + bit_reader.skipBit(); // marker_bit + + this.data.aspect_ratio = new BinaryValue(bit_reader.readBits(4), 4, AVS3aspectratio); + this.data.frame_rate_code = new BinaryValue(bit_reader.readBits(4), 4, AVS3framerate); + bit_reader.skipBit(); // marker_bit + + this.data.bit_rate_lower = bit_reader.readBits(18); + bit_reader.skipBit(); // marker_bit + + this.data.bit_rate_upper = bit_reader.readBits(12); + this.data.low_delay = bit_reader.readBit(); + this.data.temporal_id_enable_flag = new BooleanValue(bit_reader.readBit()); + bit_reader.skipBit(); // marker_bit + + this.data.bbv_buffer_size = bit_reader.readBits(18); + bit_reader.skipBit(); // marker_bit + + this.data.max_dpb_minus1 = bit_reader.readBits(4); + this.data.rpl1_index_exist_flag = new BooleanValue(bit_reader.readBit()); + this.data.rpl1_same_as_rpl0_flag = new BooleanValue(bit_reader.readBit()); + bit_reader.skipBit(); // marker_bit + + const reference_picture_list = function ( + list: number, + rpls: number, + library_picture_enable_flag: boolean, + ) { + const this_set = new ReferencePictureSet(list, rpls); + if (library_picture_enable_flag) + this_set.set_reference_to_library_enable_flag(bit_reader.readBit()); + const num_of_ref_pic = bit_reader.readUE(); + for (let i = 0; i < num_of_ref_pic; i++) { + const this_pic: ReferencePicture = {}; + let LibraryIndexFlag = 0; + if (this_set.get_reference_to_library_enable_flag()) + LibraryIndexFlag = this_pic.library_index_flag = bit_reader.readBit(); + if (LibraryIndexFlag !== 0) this_pic.referenced_library_picture_index = bit_reader.readUE(); + else { + this_pic.abs_delta_doi = bit_reader.readUE(); + if (this_pic.abs_delta_doi > 0) this_pic.sign_delta_doi = bit_reader.readBit(); + } + this_set.push(this_pic); + } + return this_set; + }; + + this.data.num_ref_pic_list_set0 = bit_reader.readUE(); + this.data.rpl0 = new ReferencePictureList(0); + for (let j = 0; j < this.data.num_ref_pic_list_set0; j++) + this.data.rpl0.push( + reference_picture_list(0, j, this.data.library_picture_enable_flag.value), + ); + + if (!this.data.rpl1_same_as_rpl0_flag) { + this.data.num_ref_pic_list_set1 = bit_reader.readUE(); + this.data.rpl1 = new ReferencePictureList(1); + for (let j = 0; j < this.data.num_ref_pic_list_set1; j++) + this.data.rpl1.push( + reference_picture_list(1, j, this.data.library_picture_enable_flag.value), + ); + } + + this.data.num_ref_default_active_minus1_0 = bit_reader.readUE(); + this.data.num_ref_default_active_minus1_1 = bit_reader.readUE(); + this.data.log2_lcu_size_minus2 = bit_reader.readBits(3); + this.data.log2_min_cu_size_minus2 = bit_reader.readBits(2); + this.data.log2_max_part_ratio_minus2 = bit_reader.readBits(2); + this.data.max_split_times_minus6 = bit_reader.readBits(3); + this.data.log2_min_qt_size_minus2 = bit_reader.readBits(3); + this.data.log2_max_bt_size_minus2 = bit_reader.readBits(3); + this.data.log2_max_eqt_size_minus3 = bit_reader.readBits(2); + bit_reader.skipBit(); // marker_bit + + this.data.weight_quant_enable_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.weight_quant_enable_flag.value) { + this.data.load_seq_weight_quant_data_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.load_seq_weight_quant_data_flag.value) + this.data.weight_quant_matrix = new WeightQuantMatrix(bit_reader); + } + + this.data.st_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.sao_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.alf_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.affine_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.smvd_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.ipcm_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.amvr_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.num_of_hmvp_cand = bit_reader.readBits(4); + this.data.umve_enable_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.num_of_hmvp_cand !== 0 && this.data.amvr_enable_flag.value) + this.data.emvr_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.intra_pf_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.tscpm_enable_flag = new BooleanValue(bit_reader.readBit()); + bit_reader.skipBit(); // marker_bit + + this.data.dt_enable_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.dt_enable_flag.value) this.data.log2_max_dt_size_minus4 = bit_reader.readBits(2); + this.data.pbt_enable_flag = new BooleanValue(bit_reader.readBit()); + + if (this.data.profile_id.value === MAIN_10 || this.data.profile_id.value === HIGH_10) { + this.data.pmc_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.iip_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.sawp_enable_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.affine_enable_flag.value) + this.data.asr_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.awp_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.etmvp_mvap_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.dmvr_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.bio_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.bgc_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.inter_pf_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.inter_pfc_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.obmc_enable_flag = new BooleanValue(bit_reader.readBit()); + + this.data.sbt_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.ist_enable_flag = new BooleanValue(bit_reader.readBit()); + + this.data.esao_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.ccsao_enable_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.alf_enable_flag.value) + this.data.ealf_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.ibc_enable_flag = new BooleanValue(bit_reader.readBit()); + bit_reader.skipBit(); // marker_bit + + this.data.isc_enable_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.ibc_enable_flag.value || this.data.isc_enable_flag.value) + this.data.num_of_intra_hmvp_cand = bit_reader.readBits(4); + this.data.fimc_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.nn_tools_set_hook = bit_reader.readBits(8); + if (this.data.nn_tools_set_hook & 0x01) + this.data.num_of_nn_filter_minus1 = bit_reader.readUE(); + bit_reader.skipBit(); // marker_bit + } + if (this.data.low_delay === 0) this.data.output_reorder_delay = bit_reader.readBits(5); + this.data.cross_patch_loop_filter_enable_flag = new BooleanValue(bit_reader.readBit()); + this.data.ref_colocated_patch_flag = new BooleanValue(bit_reader.readBit()); + this.data.stable_patch_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.stable_patch_flag.value) { + this.data.uniform_patch_flag = new BooleanValue(bit_reader.readBit()); + if (this.data.uniform_patch_flag.value) { + bit_reader.skipBit(); // marker_bit + this.data.patch_width_minus1 = bit_reader.readUE(); + this.data.patch_height_minus1 = bit_reader.readUE(); + } + } + bit_reader.skipBits(2); // reserved bits + } + toString() { + return super.toString(this.data); + } +} + +export class av3cBox extends Box { + static override readonly fourcc = 'av3c' as const; + box_name = 'AVS3ConfigurationBox' as const; + + configurationVersion: DescribedValue; + sequence_header_length?: number; + sequence_header?: AVS3SequenceHeader; + library_dependency_idc?: BinaryValue; + + parse(stream: MultiBufferStream) { + const bit_reader = new BitStream(stream); + this.configurationVersion = new DescribedValue(stream.readUint8(), AVS3Vconfiguration); + if (this.configurationVersion.value === 1) { + this.sequence_header_length = stream.readUint16(); + bit_reader.appendUint8(this.sequence_header_length); + + this.sequence_header = new AVS3SequenceHeader(bit_reader); + + // library_dependency_idc is in the AVS3DecoderConfigurationRecord + this.library_dependency_idc = new BinaryValue(stream.readUint8(), 2); + } + } +} diff --git a/src/boxes/avs-common.ts b/src/boxes/avs-common.ts new file mode 100644 index 00000000..35cd9e74 --- /dev/null +++ b/src/boxes/avs-common.ts @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025. Paul Higgs + * License: BSD-3-Clause (see LICENSE file) + */ + +export type DescriberFunction = (n: number) => string; + +export class DescribedValue { + private _value: number; + private _description: string; + + constructor(value: number, descriptionFn?: DescriberFunction) { + this._value = value; + this._description = descriptionFn ? descriptionFn(value) : undefined; + } + hasOwnProperty(name: string) { + // we need to 'trick' the check done in generatePropertyValue() og boxHTMLTable.js + return name === 'toString'; + } + toString() { + return `${this.value}${this._description && this._description.length > 0 ? ' (' + this._description + ')' : ''}`; + } + get value(): number { + return this._value; + } +} + +export class HexadecimalValue { + private _value: number; + private _description: string; + + constructor(value: number, descriptionFn?: DescriberFunction) { + this._value = value; + this._description = descriptionFn ? descriptionFn(value) : undefined; + } + hasOwnProperty(name: string) { + // we need to 'trick' the check done in generatePropertyValue() og boxHTMLTable.js + return name === 'toString'; + } + toString() { + return `0x${this._value.toString(16)}${this._description && this._description.length > 0 ? ' (' + this._description + ')' : ''}`; + } + get value(): number { + return this._value; + } +} + +export class BinaryValue { + private _value: number; + private _bits: number; + private _description: string; + + constructor(value: number, bits: number, descriptionFn?: DescriberFunction) { + this._value = value; + this._bits = bits; + this._description = descriptionFn ? descriptionFn(value) : undefined; + } + hasOwnProperty(name: string) { + // we need to 'trick' the check done in generatePropertyValue() og boxHTMLTable.js + return name === 'toString'; + } + toString() { + let res = 'b'; + for (let i = this._bits; i > 0; i--) res += this._value & (1 << (i - 1)) ? '1' : '0'; + return ( + res + + (this._description && this._description.length > 0 ? ' (' + this._description + ')' : '') + ); + } + get value(): number { + return this._value; + } +} + +export class BooleanValue { + private _value: boolean; + + constructor(value: boolean | number) { + this._value = typeof value === 'number' ? value > 0 : value; + } + hasOwnProperty(name: string) { + // we need to 'trick' the check done in generatePropertyValue() og boxHTMLTable.js + return name === 'toString'; + } + toString() { + return `${this._value ? 1 : 0} (${this._value ? 'true' : 'false'})`; + } + get value(): boolean { + return this._value; + } +} + +export class AVS3data { + /* not currently used to beautify output + toHTML(data: object): string { + let res = ''; + const props = Object.getOwnPropertyNames(data); + if (props) + props.forEach(function (val) { + let fmt_val = ''; + if (Array.isArray(data[val])) { + for (let i = 0; i < data[val].length; i++) { + const hex = data[val][i].toString(16); + fmt_val += hex.length === 1 ? '0' + hex : hex; + if (i % 4 === 3) fmt_val += ' '; + } + } else fmt_val = data[val]; + res += `${val}${fmt_val}`; + }); + return `${res}
`; + } */ + + hasOwnProperty(name: string) { + // we need to 'trick' the check done in generatePropertyValue() og boxHTMLTable.js + return name === 'toString'; + } + toString(data: object): string { + let res = ''; + const props = Object.getOwnPropertyNames(data); + props.forEach(function (val) { + let fmt_val = ''; + if (Array.isArray(data[val])) { + for (let i = 0; i < data[val].length; i++) { + const hex = data[val][i].toString(16); + fmt_val += hex.length === 1 ? '0' + hex : hex; + if (i % 4 === 3) fmt_val += ' '; + } + } else fmt_val = data[val]; + res += `${val}: ${fmt_val}\n`; + }); + return res; + } +} diff --git a/src/boxes/dca3.ts b/src/boxes/dca3.ts new file mode 100644 index 00000000..68d2c233 --- /dev/null +++ b/src/boxes/dca3.ts @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2025. Paul Higgs + * License: BSD-3-Clause (see LICENSE file) + */ + +import { Box } from '#/box'; +import type { MultiBufferStream } from '#/buffer'; +import { BitStream } from '#/BitStream'; + +import { DescribedValue, AVS3data } from './avs-common'; + +// values for audio_codec_id +const HIGH_RATE_CODING = 0, + LOSSLESS_CODING = 1, + FULL_RATE_CODING = 2; + +// values for content_type +const CHANNEL_BASED = 0, + OBJECT_BASED = 1, + CHANNEL_AND_OBJECT = 2, + HOA = 3; + +function AVS3Acodec(codec_id: number) { + const codecs = ['General High Rate', 'Lossless', 'General Full Rate']; + return codec_id < codecs.length ? codecs[codec_id] : 'undefined'; +} + +function AVS3Achannel_number(channel_number_index: number) { + const configs = [ + 'Mono', + 'Stereo', + '5.1', + '7.1', + '10.2', + '22.2', + '4.0/FOA', + '5.1.2', + '5.1.4', + '7.1.2', + '7.1.4', + '3rd HOA', + '2nd HOA', + ]; + return channel_number_index < configs.length ? configs[channel_number_index] : 'undefined'; +} + +function AVS3Asampling_frequency(sampling_frequency_index: number) { + const frequencies = [192000, 96000, 48000, 44100, 32000, 24000, 22050, 16000, 8000]; + return sampling_frequency_index < frequencies.length + ? frequencies[sampling_frequency_index] + 'Hz' + : 'reserved'; +} + +function AVS3Aresolution(resolution: number) { + switch (resolution) { + case 0: + return '8 bits/sample'; + case 1: + return '16 bits/sample'; + case 2: + return '24 bits/sample'; + } + return 'reserved'; +} + +function AVS3Anntype(nn_type: number) { + switch (nn_type) { + case 0: + return 'basic neural network'; + case 1: + return 'low-complexity neural network'; + } + return 'reserved'; +} + +function AVS3Acodingprofile(conding_profile: number) { + switch (conding_profile) { + case 0: + return 'basic framework'; + case 1: + return 'object metadata framework'; + case 2: + return 'HOA data coding framework'; + } + return 'reserved'; +} + +interface GAconfig { + sampling_frequency_index?: DescribedValue; + nn_type?: DescribedValue; + content_type?: number; + channel_number_index?: DescribedValue; + number_objects?: number; + hoa_order?: number; + total_bitrate?: number; + resolution?: DescribedValue; +} +class AVS3GAConfig extends AVS3data { + data: GAconfig; + constructor(bit_reader: BitStream) { + super(); + this.data = {}; + this.deserialise(bit_reader); + } + deserialise(bit_reader: BitStream) { + this.data.sampling_frequency_index = new DescribedValue( + bit_reader.readBits(4), + AVS3Asampling_frequency, + ); + this.data.nn_type = new DescribedValue(bit_reader.readBits(3), AVS3Anntype); + bit_reader.skipBits(1); + this.data.content_type = bit_reader.readBits(4); + if (this.data.content_type === CHANNEL_BASED) { + this.data.channel_number_index = new DescribedValue( + bit_reader.readBits(7), + AVS3Achannel_number, + ); + bit_reader.skipBits(1); + } else if (this.data.content_type === OBJECT_BASED) { + this.data.number_objects = bit_reader.readBits(7); + bit_reader.skipBits(1); + } else if (this.data.content_type === CHANNEL_AND_OBJECT) { + this.data.channel_number_index = new DescribedValue( + bit_reader.readBits(7), + AVS3Achannel_number, + ); + bit_reader.skipBits(1); + this.data.number_objects = bit_reader.readBits(7); + bit_reader.skipBits(1); + } else if (this.data.content_type === HOA) { + this.data.hoa_order = bit_reader.readBits(4); + } + this.data.total_bitrate = bit_reader.readUint16(); + this.data.resolution = new DescribedValue(bit_reader.readBits(2), AVS3Aresolution); + } + toString(): string { + return super.toString(this.data); + } +} + +interface GHconfig { + sampling_frequency_index?: number; + anc_data_index?: number; + coding_profile?: DescribedValue; + bitstream_type?: number; + channel_number_index?: number; + bitrate_index?: number; + raw_frame_length?: number; + resolution?: DescribedValue; + addition_info?: Array; +} +class AVS3GHConfig extends AVS3data { + data: GHconfig; + constructor(bit_reader: BitStream) { + super(); + this.data = {}; + this.deserialise(bit_reader); + } + deserialise(bit_reader: BitStream) { + this.data.sampling_frequency_index = bit_reader.readBits(4); + this.data.anc_data_index = bit_reader.readBit(); + this.data.coding_profile = new DescribedValue(bit_reader.readBits(3), AVS3Acodingprofile); + this.data.bitstream_type = bit_reader.readBits(1); + this.data.channel_number_index = bit_reader.readBits(7); + this.data.bitrate_index = bit_reader.readBits(4); + this.data.raw_frame_length = bit_reader.readUint16(); + this.data.resolution = new DescribedValue(bit_reader.readBits(2), AVS3Aresolution); + const addition_info_length = bit_reader.readUint16(); + if (addition_info_length > 0) { + this.data.addition_info = []; + for (let i = 0; i < addition_info_length; i++) + this.data.addition_info.push(bit_reader.readUint8()); + } + } + toString(): string { + return super.toString(this.data); + } +} + +interface LLconfig { + sampling_frequency_index?: number; + sampling_frequency?: number; + anc_data_index?: number; + coding_profile?: DescribedValue; + channel_number?: number; + resolution?: DescribedValue; + addition_info?: Array; +} +class AVS3LLConfig extends AVS3data { + data: LLconfig; + constructor(bit_reader: BitStream) { + super(); + this.data = {}; + this.deserialise(bit_reader); + } + deserialise(bit_reader: BitStream) { + this.data.sampling_frequency_index = bit_reader.readBits(4); + if (this.data.sampling_frequency_index === 0xf) + this.data.sampling_frequency = bit_reader.readUint24(); + this.data.anc_data_index = bit_reader.readBit(); + this.data.coding_profile = new DescribedValue(bit_reader.readBits(3), AVS3Acodingprofile); + this.data.channel_number = bit_reader.readUint8(); + this.data.resolution = new DescribedValue(bit_reader.readBits(2), AVS3Aresolution); + const addition_info_length = bit_reader.readUint16(); + if (addition_info_length > 0) { + this.data.addition_info = []; + for (let i = 0; i < addition_info_length; i++) + this.data.addition_info.push(bit_reader.readUint8()); + } + bit_reader.skipBits(2); // reserved + } + toString(): string { + return super.toString(this.data); + } +} + +export class dca3Box extends Box { + static override readonly fourcc = 'dca3' as const; + box_name = 'AVS3AConfigurationBox' as const; + + private audio_codec_id: DescribedValue; + private Avs3AudioGAConfig?: AVS3GAConfig; + private Avs3AudioGHConfig?: AVS3GHConfig; + private Avs3AudioLLConfig?: AVS3LLConfig; + + parse(stream: MultiBufferStream) { + const bit_reader = new BitStream(stream); + bit_reader.appendUint8(this.size - this.hdr_size); + this.audio_codec_id = new DescribedValue(bit_reader.readBits(4), AVS3Acodec); + + switch (this.audio_codec_id.value) { + case FULL_RATE_CODING: + this.Avs3AudioGAConfig = new AVS3GAConfig(bit_reader); + break; + case HIGH_RATE_CODING: + this.Avs3AudioGHConfig = new AVS3GHConfig(bit_reader); + break; + case LOSSLESS_CODING: + this.Avs3AudioLLConfig = new AVS3LLConfig(bit_reader); + break; + } + bit_reader.byte_alignment(); + } + + get_audio_codec_id_str() { + return this.audio_codec_id.value.toString(10).padStart(2, '0'); + } +} diff --git a/src/boxes/sampleentries/sampleentry.ts b/src/boxes/sampleentries/sampleentry.ts index 2d8c1bc8..2d1a354e 100644 --- a/src/boxes/sampleentries/sampleentry.ts +++ b/src/boxes/sampleentries/sampleentry.ts @@ -1,5 +1,7 @@ import { av1CBox } from '#/boxes/av1C'; import { avcCBox } from '#/boxes/avcC'; +import { av3cBox } from '#/boxes/av3c'; +import { dca3Box } from '#/boxes/dca3'; import { sinfBox } from '#/boxes/defaults'; import { esdsBox } from '#/boxes/esds'; import { hvcCBox } from '#/boxes/hvcC'; @@ -104,6 +106,23 @@ export class av01SampleEntry extends VisualSampleEntry { } } +export class avs3SampleEntry extends VisualSampleEntry { + static override readonly fourcc = 'avs3' as const; + box_name = 'AVS3VideoSampleEntry' as const; + + av3c: av3cBox; + + getCodec(): string { + const profile = this.av3c.sequence_header?.data?.profile_id + ? this.av3c.sequence_header.data?.profile_id.value.toString(16) + : 'XX'; + const level = this.av3c.sequence_header?.data?.level_id + ? this.av3c.sequence_header.data?.level_id.value.toString(16) + : 'XX'; + return `${super.getCodec()}.${profile}.${level}`; + } +} + export class dav1SampleEntry extends VisualSampleEntry { static override readonly fourcc = 'dav1' as const; } @@ -347,10 +366,6 @@ export class vp09SampleEntry extends vpcCSampleEntryBase { static override readonly fourcc = 'vp09' as const; } -export class avs3SampleEntry extends VisualSampleEntry { - static override readonly fourcc = 'avs3' as const; -} - export class j2kiSampleEntry extends VisualSampleEntry { static override readonly fourcc = 'j2ki' as const; // ISO/IEC 15444-16:2021 Section 7.3 @@ -437,6 +452,22 @@ export class fLaCSampleEntry extends AudioSampleEntry { static override readonly fourcc = 'fLaC' as const; } +export class av3aSampleEntry extends AudioSampleEntry { + // AATF (AVS3 Audio Transport Format) audio sample entry + static override readonly fourcc = 'av3a' as const; + box_name = 'AVS3AudioSampleEntry' as const; + + dca3: dca3Box; + + getCodec() { + return `${super.getCodec()}.${this.dca3.get_audio_codec_id_str()}`; + } +} + +export class a3asSampleEntry extends AudioSampleEntry { + static override readonly fourcc = 'a3as' as const; +} + // Encrypted sample entries export class encvSampleEntry extends VisualSampleEntry { static override readonly fourcc = 'encv' as const;