@blockcast/mmt-container 2.1.0 → 2.2.0-main.02321582f1ca
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -17
- package/dist/browser-runtime.js +1302 -249
- package/dist/browser-runtime.js.map +4 -4
- package/dist/carp-catalog.js.map +1 -1
- package/dist/catalog-fec-params.d.ts +17 -1
- package/dist/catalog-fec-params.d.ts.map +1 -1
- package/dist/catalog-fec-params.js +15 -0
- package/dist/catalog-fec-params.js.map +2 -2
- package/dist/catalog-network-source.js.map +1 -1
- package/dist/cmcd-producer.d.ts +65 -47
- package/dist/cmcd-producer.d.ts.map +1 -1
- package/dist/cmcd-producer.js +138 -109
- package/dist/cmcd-producer.js.map +2 -2
- package/dist/delivery-catalog.js +26 -16
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/delta-fallback-config.js.map +1 -1
- package/dist/fec-manager.d.ts +33 -1
- package/dist/fec-manager.d.ts.map +1 -1
- package/dist/fec-manager.js +213 -6
- package/dist/fec-manager.js.map +2 -2
- package/dist/fec-state.js.map +1 -1
- package/dist/fec-worker-setup.d.ts +1 -1
- package/dist/fec-worker-setup.js +1 -1
- package/dist/fec-worker-setup.js.map +1 -1
- package/dist/held-layer-admission.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +2 -2
- package/dist/iso-block-locator.d.ts +9 -0
- package/dist/iso-block-locator.d.ts.map +1 -1
- package/dist/iso-block-locator.js +17 -0
- package/dist/iso-block-locator.js.map +2 -2
- package/dist/iso-bmff-timing.d.ts +10 -1
- package/dist/iso-bmff-timing.d.ts.map +1 -1
- package/dist/iso-bmff-timing.js +36 -2
- package/dist/iso-bmff-timing.js.map +2 -2
- package/dist/media-router.js.map +1 -1
- package/dist/mfu-reassembler.js +12 -19
- package/dist/mfu-reassembler.js.map +2 -2
- package/dist/mmtp-container-parser.d.ts +37 -2
- package/dist/mmtp-container-parser.d.ts.map +1 -1
- package/dist/mmtp-container-parser.js +98 -28
- package/dist/mmtp-container-parser.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +222 -45
- package/dist/mmtp-container-parser.no-modules.js.map +3 -3
- package/dist/mmtp-decode-pipeline.js +97 -28
- package/dist/mmtp-decode-pipeline.js.map +2 -2
- package/dist/mmtp-fec-session.d.ts +41 -3
- package/dist/mmtp-fec-session.d.ts.map +1 -1
- package/dist/mmtp-fec-session.js +118 -13
- package/dist/mmtp-fec-session.js.map +3 -3
- package/dist/mmtp-router.d.ts +25 -2
- package/dist/mmtp-router.d.ts.map +1 -1
- package/dist/mmtp-router.js +1749 -56
- package/dist/mmtp-router.js.map +4 -4
- package/dist/multicast-auth-catalog.js.map +1 -1
- package/dist/multicast-input.js.map +1 -1
- package/dist/multicast-manager.d.ts +15 -2
- package/dist/multicast-manager.d.ts.map +1 -1
- package/dist/multicast-manager.js +15683 -14941
- package/dist/multicast-manager.js.map +4 -4
- package/dist/multicast-transport.d.ts +11 -3
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +281 -35
- package/dist/multicast-transport.js.map +4 -4
- package/dist/multicast-unwrap.js +12 -19
- package/dist/multicast-unwrap.js.map +2 -2
- package/dist/no-modules.js +215 -42
- package/dist/no-modules.js.map +3 -3
- package/dist/repair-fetch.js.map +1 -1
- package/dist/repair-layer-subscriptions.d.ts +82 -0
- package/dist/repair-layer-subscriptions.d.ts.map +1 -0
- package/dist/repair-layer-subscriptions.js +192 -0
- package/dist/repair-layer-subscriptions.js.map +7 -0
- package/dist/signaling.js.map +2 -2
- package/dist/timestamp-codec.js +12 -19
- package/dist/timestamp-codec.js.map +2 -2
- package/dist/track-session.d.ts +1 -1
- package/dist/track-session.js.map +1 -1
- package/dist/wasm-mfu-reassembler.js.map +1 -1
- package/dist/wasm-mmtp-container-parser.js +97 -28
- package/dist/wasm-mmtp-container-parser.js.map +2 -2
- package/package.json +8 -5
- package/schemas/catalog.schema.json +4 -0
- package/src/catalog-fec-params.ts +36 -1
- package/src/cmcd-producer.ts +194 -179
- package/src/fec-manager.ts +256 -6
- package/src/fec-worker-setup.ts +2 -2
- package/src/index.ts +18 -2
- package/src/iso-block-locator.ts +19 -0
- package/src/iso-bmff-timing.ts +40 -1
- package/src/mmtp-container-parser.ts +96 -8
- package/src/mmtp-fec-session.ts +115 -15
- package/src/mmtp-router.ts +414 -29
- package/src/multicast-manager.ts +26 -5
- package/src/multicast-transport.ts +44 -20
- package/src/repair-layer-subscriptions.ts +261 -0
- package/src/track-session.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/serial-number.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/fec-bindings.ts", "../src/mmtp-fec-session.ts", "../src/held-layer-admission.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nexport const SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// fec_code_id_for_repair_flow code points (ISO/IEC 23008-10 Table 1;\n// draft-ramadan-moq-mmt-01 Section 8.3).\nexport const FEC_CODE_REED_SOLOMON = 1\nexport const FEC_CODE_RAPTORQ = 3\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n hasOverlayFecId: boolean\n}\n\n/** Options for extractMFUPayload and explainMFURejection. */\nexport interface MFUExtractOptions {\n /**\n * When true, do NOT subtract the Source FEC Payload ID from the payload\n * boundary, even if fecType >= 1. For FEC-recovered symbols whose FEC PID\n * was already stripped before WASM decoding.\n */\n skipFecId?: boolean\n /**\n * The catalog declares a keyframe overlay (`scope: \"keyframe\"` repair track)\n * for this packet's source. A RAP packet then carries two Source FEC Payload\n * IDs, base first and overlay last (BLO-32719 Clarification 7). This is a\n * catalog fact, never inferred: the header has no overlay bit, and a payload\n * four bytes shorter is byte-identical to a missing overlay. Mirrors\n * mmt-core `split_payload_and_source_fec_trailers(overlay_declared)`.\n */\n overlayDeclared?: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: MFUExtractOptions,\n rapFlag = false,\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false, hasOverlayFecId: false }\n : null\n }\n\n const overlay = options?.overlayDeclared === true && rapFlag\n if (fecType === 1) {\n const fecStart = payload.length - sourceFecTrailerSize(overlay)\n if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true, hasOverlayFecId: overlay }\n }\n // An overlay ID rides behind a base ID the header says is absent.\n if (fecType !== 0 || overlay) return null\n\n if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false, hasOverlayFecId: false }\n }\n\n return null\n}\n\nfunction sourceFecTrailerSize(overlay: boolean): number {\n return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n /** Keyframe-overlay Source FEC Payload ID: RAP packets with `overlayDeclared` only. */\n overlayFecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. The legacy\n * authenticator suffix and the Source FEC trailer are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-01 \u00A73.1).\n * @param options.overlayDeclared The catalog declares a keyframe overlay for this\n * source, so a RAP packet carries an 8-byte trailer (see MFUExtractOptions).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n header.rapFlag,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag)\n if (observed !== expected) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${expected} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact legacy authenticator trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const trailerStart =\n mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId)\n const ssIdAt = (offset: number) => ({\n ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false),\n })\n const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null\n const overlayFecPayloadId = trailerLayout.hasOverlayFecId\n ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE)\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n checkRsId = true,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.ssbLength === 0 ||\n (checkRsId && repairId.rsId >= repairId.rsbLength) ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n/**\n * Why a repair object of an mmtp track is discarded on admission:\n * `repairLength` -- the object is not exactly 12 + 13 + T bytes with a\n * 12-byte MMTP header (C = 0, X = 0; draft-ramadan-moq-mmt-02 7.3, judged\n * before any Repair FEC Payload ID field is read); `repairRsId` -- RS_ID is not\n * below RSB_length, under every scheme; `repairSchemeBound` -- the object\n * breaks its scheme's bound: reed-solomon SSB_length + RSB_length > 255, which\n * also caps RS_ID at 253, or raptorq SSB_length + RSB_length - 1 > 2^24 - 1\n * (draft-ramadan-moq-fec-core-00 7.5 and 10.3, -block-codes-00 7).\n */\nexport type RepairObjectReject = 'repairLength' | 'repairRsId' | 'repairSchemeBound'\n\n/**\n * Admission of one mmtp repair object under its instance's T and scheme. The\n * caller names the instance (by the repair track it subscribed); nothing here\n * infers it from the fields. `repairRsId` and `repairSchemeBound` carry the\n * frame so the caller can first route an RSB_length below a held layer's\n * O_i + P_i to the 7.3.2 stop, which takes precedence over the discard.\n * `null`: not an iso repair packet, or SSB_length 0.\n */\nexport function admitMmtpRepairObject(\n packet: Uint8Array,\n header: MMTPHeader,\n symbolSize: number,\n algorithm: string | undefined,\n): { frame: MoqRepairFrame; reject?: Exclude<RepairObjectReject, 'repairLength'> } | { reject: 'repairLength' } | null {\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n if (header.headerLength !== MMTP_HEADER_SIZE || packet.length !== MMTP_HEADER_SIZE + REPAIR_FEC_PID_SIZE + symbolSize) {\n return { reject: 'repairLength' }\n }\n const frame = moqRepairFrameFrom(packet, readRepairFecPayloadIdFields(packet, header), false)\n if (!frame) return null\n if (frame.rsId >= frame.rsbLength) return { frame, reject: 'repairRsId' }\n const blockSymbols = frame.ssbLength + frame.rsbLength\n if (algorithm === 'reed-solomon' ? blockSymbols > 255 : algorithm === 'raptorq' && blockSymbols - 1 > 0xffffff) {\n return { frame, reject: 'repairSchemeBound' }\n }\n return { frame }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * RFC 1982 serial-number arithmetic for the 32-bit MMTP AL-FEC symbol\n * counters (ISO/IEC 23008-1:2023 C.5.2 SS_ID, C.5.3 SS_Start).\n *\n * SS_ID is a 32-bit field, so a stream that runs long enough wraps it to 0.\n * Ordering two SS_IDs (or two blocks by their first SS_ID) with `<` reads\n * every post-wrap value as older than every pre-wrap one. Serial order\n * agrees with `<` whenever the two values are less than 2^31 apart, so for\n * every stream that has not reached the wrap the result is unchanged; only\n * the order across the wrap point differs (BLO-35597 iso:5).\n */\n\n/** 2^SERIAL_BITS for SERIAL_BITS = 32. */\nexport const SERIAL32_SPAN = 0x1_0000_0000\n\n/** RFC 1982 \u00A73.2's 2^(SERIAL_BITS - 1). */\nexport const SERIAL32_HALF = 0x8000_0000\n\nfunction assertUint32(value: number, name: string): void {\n if (!Number.isInteger(value) || value < 0 || value >= SERIAL32_SPAN) {\n throw new RangeError(`${name} must be a uint32 (got ${value})`)\n }\n}\n\n/**\n * RFC 1982 \u00A73.1 addition, extended to negative `n` for the matching\n * subtraction: `(s + n) mod 2^32`. `n` must lie in (-2^31, 2^31).\n */\nexport function serial32Add(s: number, n: number): number {\n assertUint32(s, 's')\n if (!Number.isInteger(n) || n <= -SERIAL32_HALF || n >= SERIAL32_HALF) {\n throw new RangeError(`serial addend must lie in (-2^31, 2^31) (got ${n})`)\n }\n return (s + n + SERIAL32_SPAN) % SERIAL32_SPAN\n}\n\n/** Forward distance from `from` to `to`: `(to - from) mod 2^32`. */\nexport function serial32Distance(from: number, to: number): number {\n return (to - from) >>> 0\n}\n\n/**\n * RFC 1982 \u00A73.2: `a` precedes `b`. Values exactly 2^31 apart are unordered\n * (neither precedes the other).\n */\nexport function serial32Precedes(a: number, b: number): boolean {\n const distance = serial32Distance(a, b)\n return distance !== 0 && distance < SERIAL32_HALF\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "/**\n * Runtime FEC bindings (BLO-50171): what each source track's FEC resolves to\n * for this library's receiver, read from `validateFecCatalog`'s per-instance\n * result and never re-derived.\n *\n * Every runtime reader (the container's `resolveCatalogTrackFec`,\n * `resolveRepairLayers`, `MulticastManager.configureMultiTrack`, transport's\n * `catalogRepairTracks`) goes through `resolveFecBindings`, so they cannot\n * disagree with the validator or with each other about which tracks have FEC\n * and which repair tracks belong to them.\n *\n * The draft-ramadan-moq-fec-catalog-00 array is the only shape. Both catalog\n * dialects bind through it: an MSF `tracks[]` entry directly, and a hang\n * `video`/`audio` rendition (`fec` the same array) with its `repair.renditions`\n * entries (`source` + `instanceId`), mapped onto a `tracks[]` view so the same\n * validator runs. A hang rendition is authoritative over a `tracks[]` entry of\n * the same name.\n *\n * Fail closed per instance: a source whose base instance is not `ok` resolves\n * to `unavailable` with the validator's code and path, and costs no other\n * track anything. A draft-01 object `fec` is `fec_member` at that track's\n * `fec`: the track plays without FEC.\n */\nimport { validateFecCatalog, type FecCatalogCode, type FecCatalogReceiver } from './fec-catalog.js'\n\n/** The receiver this runtime is: it decodes `iso-ssbg1-v1` only. */\nexport const RUNTIME_FEC_RECEIVER: FecCatalogReceiver = Object.freeze({\n schemes: Object.freeze(['raptorq', 'reed-solomon']),\n blockProfiles: Object.freeze(['iso-ssbg1-v1']),\n overlays: true,\n})\n\n/** Repair track priority when the track states none (MoQT scale, 192..255). */\nexport const DEFAULT_FEC_REPAIR_PRIORITY = 240\n\n/** One `ok` instance's geometry. */\nexport interface FecInstanceGeometry {\n readonly instanceId: number\n readonly scheme: 'raptorq' | 'reed-solomon'\n /** `iso-ssbg1-v1` under `RUNTIME_FEC_RECEIVER`. */\n readonly blockProfile: string\n /** K. */\n readonly sourceSymbols: number\n /** P_0 for a base instance; the overlay's own P for an overlay. */\n readonly repairSymbols: number\n /** T. */\n readonly symbolSize: number\n readonly interleaveDepthMs?: number\n readonly reorderToleranceMs?: number\n readonly repairTrack: string\n readonly symbolTrack?: string\n}\n\nexport interface FecLayerBinding {\n readonly layer: number\n readonly repairTrackName: string\n readonly repairSymbols: number\n /** O_i = sum(P_j, j < i). */\n readonly rsIdOffset: number\n readonly priority: number\n}\n\nexport interface FecOverlayBinding {\n readonly repairTrackName: string\n readonly scope: 'keyframe'\n readonly sourceSymbols: number\n readonly repairSymbols: number\n readonly priority: number\n readonly geometry: FecInstanceGeometry\n}\n\nexport interface FecBindingUnavailable {\n /** The validator's code, or `fec_base_instances` for a track with no base\n * instance or more than one `ok` one (the MMTP wire carries one base Source FEC Payload\n * ID, mmt-02 7.4.1). */\n readonly code: FecCatalogCode | 'fec_base_instances'\n /** JSON Pointer into the catalog (a hang path starts `/video` or `/audio`). */\n readonly path: string\n readonly outcome: 'unavailable' | 'violation'\n}\n\nexport type FecSourceBinding =\n | {\n readonly status: 'ok'\n readonly track: string\n /** JSON Pointer of the source track (`/tracks/<i>`, `/video/renditions/<name>`). */\n readonly pointer: string\n readonly base: FecInstanceGeometry\n /** Base layers in order, layer 0 first. */\n readonly layers: readonly FecLayerBinding[]\n /** Sum of P over the base layers: the wire RSB_length. */\n readonly totalBaseRepairSymbols: number\n readonly overlay?: FecOverlayBinding\n /** Why a declared overlay is not used, when one is not. */\n readonly overlayUnavailable?: FecBindingUnavailable\n }\n | {\n readonly status: 'unavailable'\n readonly track: string\n readonly pointer: string\n readonly reason: FecBindingUnavailable\n }\n\ntype Obj = Record<string, unknown>\nconst recordOf = (v: unknown): Obj | undefined =>\n typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as Obj) : undefined\nconst escape = (s: string) => s.replace(/~/g, '~0').replace(/\\//g, '~1')\n\n/**\n * The `tracks[]` view of a catalog in either dialect, with each view track's\n * JSON Pointer in the original catalog.\n */\nexport function fecCatalogTracks(catalog: unknown): { tracks: unknown[]; pointers: string[] } {\n const root = recordOf(catalog) ?? {}\n const tracks: unknown[] = []\n const pointers: string[] = []\n const hangNames = new Set<string>()\n for (const role of ['video', 'audio'] as const) {\n for (const [name, r] of Object.entries(recordOf(recordOf(root[role])?.['renditions']) ?? {})) {\n const entry = recordOf(r)\n if (!entry) continue\n hangNames.add(name)\n const kind = recordOf(entry['container'])?.['kind']\n // A hang rendition carries no `packaging`; its container kind is it.\n // `mmtp-fec` is hang's kind for MMTP that carries FEC (the FFmpeg\n // publisher's `moq_publish_set_video_packaging`): MMTP packaging.\n const view: Obj = { name, packaging: kind === undefined || kind === 'mmtp-fec' ? 'mmtp' : kind }\n if (entry['fec'] !== undefined) view['fec'] = entry['fec']\n tracks.push(view)\n pointers.push(`/${role}/renditions/${escape(name)}`)\n }\n }\n const hangRenditions = tracks.length\n for (const [name, r] of Object.entries(recordOf(recordOf(root['repair'])?.['renditions']) ?? {})) {\n hangNames.add(name)\n tracks.push({ packaging: 'fec-repair', ...recordOf(r), name })\n pointers.push(`/repair/renditions/${escape(name)}`)\n }\n const raw = Array.isArray(root['tracks']) ? root['tracks'] : []\n const named = new Set(hangNames)\n raw.forEach((t, i) => {\n const name = recordOf(t)?.['name']\n if (typeof name === 'string') named.add(name)\n if (typeof name === 'string' && hangNames.has(name)) return\n tracks.push(t)\n pointers.push(`/tracks/${i}`)\n })\n // hang layer 0 needs no `repair.renditions` entry: the rendition's instance\n // names it, and a hang Root (hang's toHang) carries no repair block. Such a\n // name stands for a repair track of that instance.\n tracks.slice(0, hangRenditions).forEach((t, ti) => {\n const view = t as Obj\n if (!Array.isArray(view['fec'])) return\n view['fec'].forEach((o, ii) => {\n const repairTrack = recordOf(o)?.['repairTrack']\n if (typeof repairTrack !== 'string' || named.has(repairTrack)) return\n named.add(repairTrack)\n const instanceId = recordOf(o)?.['instanceId']\n tracks.push({ name: repairTrack, packaging: 'fec-repair', source: view['name'], ...(instanceId === undefined ? {} : { instanceId }) })\n pointers.push(`${pointers[ti]}/fec/${ii}/repairTrack`)\n })\n })\n return { tracks, pointers }\n}\n\n/**\n * Every source track's runtime FEC binding, by track name. A track with no\n * `fec`, or an empty `fec` array, declares no FEC and has no entry.\n */\nexport function resolveFecBindings(\n catalog: unknown,\n receiver: FecCatalogReceiver = RUNTIME_FEC_RECEIVER,\n): ReadonlyMap<string, FecSourceBinding> {\n const { tracks, pointers } = fecCatalogTracks(catalog)\n const report = validateFecCatalog({ tracks }, receiver)\n const at = (path: string) => {\n const m = /^\\/tracks\\/(\\d+)(.*)$/.exec(path)\n return m ? pointers[Number(m[1])] + m[2] : path\n }\n const trackAt = (i: number) => recordOf(tracks[i]) ?? {}\n const nameAt = (i: number | null) => (i === null ? undefined : (trackAt(i)['name'] as string))\n const why = (i: { outcome: string; issues: readonly { code: FecCatalogCode; path: string }[]; path: string }) =>\n Object.freeze({\n code: i.issues[0]?.code ?? 'fec_member',\n path: at(i.issues[0]?.path ?? i.path),\n outcome: i.outcome === 'unavailable' ? ('unavailable' as const) : ('violation' as const),\n })\n\n const out = new Map<string, FecSourceBinding>()\n tracks.forEach((t, ti) => {\n const track = recordOf(t)\n const name = track?.['name']\n if (!track || typeof name !== 'string' || out.has(name)) return\n if (track['packaging'] === 'fec-repair' || track['packaging'] === 'fec-source') return\n const fec = track['fec']\n if (fec === undefined) return\n if (!Array.isArray(fec)) {\n const reason = { code: 'fec_member' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const }\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason: Object.freeze(reason) }))\n return\n }\n if (fec.length === 0) return\n const insts = report.instances.filter((i) => i.trackIndex === ti)\n const isOverlay = (i: (typeof insts)[number]) => Object.hasOwn(recordOf(fec[Number(i.path.split('/').pop())]) ?? {}, 'scope')\n const bases = insts.filter((i) => !isOverlay(i))\n const okBases = bases.filter((i) => i.outcome === 'ok')\n if (okBases.length !== 1) {\n const reason =\n // No base instance at all (overlay-only `fec`) is a count of 0, not\n // the overlay's own issue.\n okBases.length > 1 || bases.length === 0\n ? Object.freeze({ code: 'fec_base_instances' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const })\n : why(bases[0] ?? insts[0]!)\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason }))\n return\n }\n const b = okBases[0]!\n const base = geometry(recordOf(fec[Number(b.path.split('/').pop())])!, b.instanceId!, nameAt(b.symbolTrack))\n const priorityOf = (i: number | null) => {\n const p = i === null ? undefined : trackAt(i)['priority']\n return typeof p === 'number' ? p : DEFAULT_FEC_REPAIR_PRIORITY\n }\n const layers: FecLayerBinding[] = [\n { layer: 0, repairTrackName: base.repairTrack, repairSymbols: base.repairSymbols, rsIdOffset: 0, priority: priorityOf(b.repairTrack) },\n ]\n const more = b.boundRepairTracks\n .map((tj) => ({ tj, layer: trackAt(tj)['repairLayer'] as number }))\n .sort((x, y) => x.layer - y.layer)\n let offset = base.repairSymbols\n for (const { tj, layer } of more) {\n const repairSymbols = trackAt(tj)['repairSymbols'] as number\n layers.push({ layer, repairTrackName: nameAt(tj)!, repairSymbols, rsIdOffset: offset, priority: priorityOf(tj) })\n offset += repairSymbols\n }\n let overlay: FecOverlayBinding | undefined\n let overlayUnavailable: FecBindingUnavailable | undefined\n const ov = insts.find(isOverlay)\n if (ov?.outcome === 'ok') {\n const g = geometry(recordOf(fec[Number(ov.path.split('/').pop())])!, ov.instanceId!, nameAt(ov.symbolTrack))\n overlay = Object.freeze({\n repairTrackName: g.repairTrack,\n scope: 'keyframe' as const,\n sourceSymbols: g.sourceSymbols,\n repairSymbols: g.repairSymbols,\n priority: priorityOf(ov.repairTrack),\n geometry: g,\n })\n } else if (ov) overlayUnavailable = why(ov)\n out.set(\n name,\n Object.freeze({\n status: 'ok' as const,\n track: name,\n pointer: pointers[ti]!,\n base,\n layers: Object.freeze(layers.map((l) => Object.freeze(l))),\n totalBaseRepairSymbols: offset,\n ...(overlay ? { overlay } : {}),\n ...(overlayUnavailable ? { overlayUnavailable } : {}),\n }),\n )\n })\n return out\n}\n\n// The validator passed this instance for this receiver, so every member read\n// here is present and in range.\nfunction geometry(o: Obj, instanceId: number, symbolTrack: string | undefined): FecInstanceGeometry {\n return Object.freeze({\n instanceId,\n scheme: o['scheme'] as 'raptorq' | 'reed-solomon',\n blockProfile: o['blockProfile'] as string,\n sourceSymbols: o['sourceSymbols'] as number,\n repairSymbols: o['repairSymbols'] as number,\n symbolSize: o['symbolSize'] as number,\n ...(typeof o['interleaveDepthMs'] === 'number' ? { interleaveDepthMs: o['interleaveDepthMs'] } : {}),\n ...(typeof o['reorderToleranceMs'] === 'number' ? { reorderToleranceMs: o['reorderToleranceMs'] } : {}),\n repairTrack: o['repairTrack'] as string,\n ...(symbolTrack !== undefined ? { symbolTrack } : {}),\n })\n}\n", "/**\n * In-band AL-FEC for one MMTP asset received over a MoQT subscription: the\n * asset's source packets on its own track, its repair packets on the repair\n * track the catalog names (`fec.repairTrack`). The same asset's packets can also\n * arrive on the multicast group (native SSM or AMT) carrying it: the caller\n * states each symbol's path, and a block fed on both -- multicast source\n * symbols repaired from the MoQT repair track, say -- is counted as mixed-path\n * under `stats.mixedPath`, never inferred. Built for a player that owns\n * its own MMTP parser and wants nothing else of the multicast stack\n * (MulticastManager, MmtpRouter, FECManager): the Shaka MSF MMTP packaging.\n *\n * The session is clock-free, like the reliable-MoQ reassembly policy it sits\n * beside. A block is settled only by wire evidence: clean once every source\n * the repair's SSB_length states has arrived, recovered when the decoder\n * completes it, failed once the newest SS_ID has moved it past the decoder's\n * retention. No timer and no arrival time decides a block's fate, so a repair\n * that is merely late on its own subscription is never counted as a loss.\n * The retention is derived from the catalog's `fecBlockBudgetMs`, and a\n * catalog missing either input throws rather than defaulting.\n *\n * Every repair packet runs the fec-01 6.3.9 held-layer check (layer 0, B_0 =\n * catalog `fec.repairSymbols`) through the same HeldLayerAdmission FECManager\n * uses. A packet below B_0 at or after the anchor fails the session closed:\n * `stats.failClosed` reports why, and every later repair is refused.\n *\n * Every repair packet that cannot be used is counted under the reason it was\n * refused, and every block ends under exactly one outcome, so a stream whose\n * FEC is doing nothing can be told apart from one with nothing to repair.\n */\n\nimport {\n admitMmtpRepairObject,\n isoBlockContains,\n isoBlocksBehindLowerBound,\n parseFecBlockProfile,\n parseMMTPHeader,\n serial32Add,\n serial32Distance,\n serial32Precedes,\n ssbg1FitsSymbol,\n trimSsbg1SourceSymbol,\n type FecBlockProfile,\n} from '@blockcast/mmt-base'\nimport { deriveFecRetentionBlocks, fecBlockBudgetMs } from '@blockcast/mmt-fec'\nimport {\n HeldLayerAdmission,\n heldLayerAnchorHorizon,\n type FecFailClosedReason,\n type HeldLayerInput,\n} from './held-layer-admission.js'\n\nexport type { FecFailClosedReason, HeldLayerInput } from './held-layer-admission.js'\n\n/** The mmt-wasm `MmtFecDecoder` members the session drives. */\nexport interface MmtpFecSessionDecoder {\n configure_params(k: number, symbolSize: number): void\n add_source(ssId: number, data: Uint8Array, timestamp: number): Uint8Array | null | undefined\n add_repair(\n ssStart: number,\n ssbLength: number,\n rsId: number,\n data: Uint8Array,\n timestamp: number,\n ): Uint8Array | null | undefined\n cleanup_by_ss_start(current: number): Uint32Array\n cleanup_blocks_detailed(ssStarts: Uint32Array): unknown\n free(): void\n}\n\n/** `wasm_bindgen.MmtFecDecoder`, or a constructor of the same shape. */\nexport type MmtpFecSessionDecoderConstructor = new (\n interleaveDepth: number,\n blockProfile: string,\n algorithm?: string,\n) => MmtpFecSessionDecoder\n\n/** The fields of one `ok` draft-ramadan-moq-fec-catalog-00 instance object\n * the session reads (the source track's base instance). */\nexport interface MmtpFecSessionCatalogFec {\n /** `raptorq` or `reed-solomon`; required. */\n scheme?: unknown\n blockProfile?: unknown\n sourceSymbols?: unknown\n /** P_0: layer 0's repair symbols per block, the fec-01 6.3.9 total B_0. */\n repairSymbols?: unknown\n /** Layer 0's repair track; names it for addRepair()'s `repairTrackName`. */\n repairTrack?: unknown\n symbolSize?: unknown\n interleaveDepthMs?: unknown\n reorderToleranceMs?: unknown\n}\n\nexport interface MmtpFecSessionConfig {\n /** The source track's catalog `fec` block. */\n fec: MmtpFecSessionCatalogFec\n /** Builds the RaptorQ decoder, normally `wasm_bindgen.MmtFecDecoder`. */\n Decoder: MmtpFecSessionDecoderConstructor\n /**\n * The source's repair layers as the catalog resolves them\n * (`TrackFecParams.repairLayers`), for a caller that subscribes more than\n * layer 0. Absent: layer 0 alone, B_0 = `fec.repairSymbols`.\n */\n repairLayers?: readonly HeldLayerInput[]\n /** The layers the caller holds (fec-01 6.3.9). Absent: [0]. */\n heldLayers?: readonly number[]\n}\n\n/**\n * The path a symbol arrived on: its MoQT subscription, or the multicast\n * group (native SSM or AMT) carrying the same asset. The caller states it;\n * the session never infers it.\n */\nexport type MmtpFecSessionPath = 'moq' | 'multicast'\n\n/** A count per path. */\nexport interface MmtpFecSessionPathCounts {\n moq: number\n multicast: number\n}\n\n/** How the settled blocks that were fed on both paths ended. */\nexport interface MmtpFecSessionMixedPathStats {\n clean: number\n recovered: number\n failed: number\n released: number\n}\n\n/** How the session's blocks and packets ended so far. */\nexport interface MmtpFecSessionStats {\n /** Source symbols fed, counting each SS_ID once. */\n sourceSymbols: number\n /** Repair symbols accepted for decoding. */\n repairSymbols: number\n /** Blocks whose every source arrived. */\n blocksClean: number\n /** Blocks with loss that the decoder completed. */\n blocksRecovered: number\n /** Blocks with loss the decoder could not complete before retiring them. */\n blocksFailed: number\n /** Blocks still unsettled when the session was released. */\n blocksReleased: number\n /** Source packets the session recovered and handed back. */\n packetsRecovered: number\n /** Source packets of failed blocks that never arrived and were not recovered. */\n packetsLost: number\n /**\n * Packets the session could not use, keyed by reason. Two reasons are not\n * loss and belong in no loss tally. `repair:block-settled` counts the\n * repair symbols of a block that had already settled, which on a healthy\n * stream is every repair symbol of a block after the first.\n * `source:decoder-refused` counts source packets the decoder threw on:\n * they were delivered to the parser, only unprotected.\n */\n dropped: Record<string, number>\n /** `sourceSymbols`, by the path each arrived on. */\n sourceSymbolsByPath: MmtpFecSessionPathCounts\n /** `repairSymbols`, by the path each arrived on. */\n repairSymbolsByPath: MmtpFecSessionPathCounts\n /**\n * Blocks whose symbols the session used arrived on both paths -- for\n * instance multicast source symbols and MoQT repair -- by how each ended.\n * Each is also counted under its outcome above. A symbol the session\n * refused, and a repair that arrived after its block was already clean,\n * contributed nothing to its block and do not make it mixed.\n */\n mixedPath: MmtpFecSessionMixedPathStats\n /**\n * fec-01 6.3.9: null until a repair packet at or after the anchor (the\n * earliest block, in SS_Start serial order, whose repair carried\n * RSB_length >= B_0) states an RSB_length below B_0; then why. Terminal:\n * every later repair is refused under `repair:fail-closed` (the packet that\n * failed it is counted there too), and block state stays as it was. Sources\n * are still taken and can still complete a block from repair admitted\n * before the failure.\n */\n failClosed: FecFailClosedReason | null\n}\n\ninterface BlockState {\n ssbLength: number\n received: number\n /** The paths of the symbols used for this block, as PATH_BITS. */\n paths: number\n /** RS_IDs of the repair symbols the decoder accepted for this block. */\n rsIds: Set<number>\n}\n\nconst PATH_BITS: Record<MmtpFecSessionPath, number> = { moq: 1, multicast: 2 }\nconst MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast\n\n/**\n * One asset's in-band FEC state. See the file comment.\n */\nexport class MmtpFecSession {\n readonly blockProfile: FecBlockProfile\n readonly algorithm: 'raptorq' | 'reed-solomon'\n readonly sourceSymbols: number\n /** P_0 = B_0, the held total every repair packet is checked against. */\n readonly repairSymbols: number\n readonly symbolSize: number\n /** The decoder's retention, in blocks, derived from the catalog budget. */\n readonly retentionBlocks: number\n\n #decoder: MmtpFecSessionDecoder | null\n /** Blocks a repair has defined and that are not settled yet, by SS_Start. */\n #blocks = new Map<number, BlockState>()\n /** SS_Starts of settled blocks, so a later repair for one is refused. */\n #settled = new Set<number>()\n /**\n * SS_IDs whose packet the parser has, with the PATH_BITS of the path it was\n * received on, or 0 for one the session recovered.\n */\n #have = new Map<number, number>()\n #newest: number | undefined\n /** The newest SS_ID when the remembered SS_IDs were last pruned. */\n #prunedAt: number | undefined\n #repairPacketId: number | undefined\n #newestSource: number | undefined\n #heldLayers: HeldLayerAdmission\n #anchorHorizon: number\n #stats: MmtpFecSessionStats = {\n sourceSymbols: 0,\n repairSymbols: 0,\n blocksClean: 0,\n blocksRecovered: 0,\n blocksFailed: 0,\n blocksReleased: 0,\n packetsRecovered: 0,\n packetsLost: 0,\n dropped: {},\n sourceSymbolsByPath: { moq: 0, multicast: 0 },\n repairSymbolsByPath: { moq: 0, multicast: 0 },\n mixedPath: { clean: 0, recovered: 0, failed: 0, released: 0 },\n failClosed: null,\n }\n\n /**\n * @throws when the instance lacks or misstates a field the session needs:\n * the scheme, the block profile, K, P_0 (`repairSymbols`), T,\n * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.\n */\n constructor(config: MmtpFecSessionConfig) {\n const fec = config?.fec\n if (!fec || typeof fec !== 'object') {\n throw new Error('MmtpFecSession: the catalog fec instance is required')\n }\n this.blockProfile = parseFecBlockProfile(fec.blockProfile, 'fec.blockProfile')\n if (fec.scheme !== 'raptorq' && fec.scheme !== 'reed-solomon') {\n throw new Error(`MmtpFecSession: fec.scheme ${JSON.stringify(fec.scheme)} is not implemented`)\n }\n this.algorithm = fec.scheme\n this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, 'fec.sourceSymbols')\n this.repairSymbols = requirePositiveInteger(fec.repairSymbols, 'fec.repairSymbols')\n this.symbolSize = requirePositiveInteger(fec.symbolSize, 'fec.symbolSize')\n const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, 'fec.interleaveDepthMs')\n const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, 'fec.reorderToleranceMs')\n this.retentionBlocks = deriveFecRetentionBlocks(\n fecBlockBudgetMs(interleaveDepthMs, reorderToleranceMs),\n interleaveDepthMs,\n )\n this.#heldLayers = new HeldLayerAdmission(\n config.repairLayers ?? [\n {\n layer: 0,\n repairTrackName: typeof fec.repairTrack === 'string' ? fec.repairTrack : '',\n rsIdOffset: 0,\n repairSymbols: this.repairSymbols,\n },\n ],\n config.heldLayers,\n )\n this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)\n if (typeof config.Decoder !== 'function') {\n throw new Error('MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required')\n }\n const decoder = new config.Decoder(this.retentionBlocks, this.blockProfile, this.algorithm)\n try {\n decoder.configure_params(this.sourceSymbols, this.symbolSize)\n } catch (error) {\n decoder.free()\n throw error\n }\n this.#decoder = decoder\n }\n\n /** True once release() ran. */\n get released(): boolean {\n return this.#decoder === null\n }\n\n get stats(): MmtpFecSessionStats {\n return {\n ...this.#stats,\n dropped: { ...this.#stats.dropped },\n sourceSymbolsByPath: { ...this.#stats.sourceSymbolsByPath },\n repairSymbolsByPath: { ...this.#stats.repairSymbolsByPath },\n mixedPath: { ...this.#stats.mixedPath },\n failClosed: this.#stats.failClosed && { ...this.#stats.failClosed },\n }\n }\n\n /**\n * Feeds one received source packet: the MMTP packet without its four-byte\n * Source FEC Payload ID, under the SS_ID that ID stated. This is what\n * MmtpContainerParser's onSourceSymbol() hands over. `path` is where it\n * was received; a caller that only subscribes over MoQT can leave it out.\n *\n * @returns the protected packets of this packet's block that it recovered,\n * in SS_ID order, for the caller to feed to its parser with\n * `{skipFecPayloadId: true, recovered: true}`. Usually empty.\n */\n addSource(ssId: number, packet: Uint8Array, path: MmtpFecSessionPath = 'moq'): Uint8Array[] {\n const decoder = this.#decoder\n if (!decoder) return this.#drop('source:released')\n const pathBit = PATH_BITS[path]\n if (!pathBit) return this.#drop('source:unknown-path')\n if (!Number.isInteger(ssId) || ssId < 0 || ssId > 0xffff_ffff) return this.#drop('source:bad-ss-id')\n if (!ssbg1FitsSymbol(packet.byteLength, this.symbolSize)) return this.#drop('source:exceeds-symbol')\n if (this.#have.has(ssId)) return this.#drop('source:duplicate')\n this.#have.set(ssId, pathBit)\n this.#stats.sourceSymbols++\n this.#stats.sourceSymbolsByPath[path]++\n this.#noteNewest(ssId)\n if (this.#newestSource === undefined || serial32Precedes(this.#newestSource, ssId)) this.#newestSource = ssId\n\n const start = this.#blockStartOf(ssId)\n const block = start === undefined ? undefined : this.#blocks.get(start)\n if (block) {\n block.received++\n block.paths |= pathBit\n }\n\n let output: Uint8Array | null | undefined\n try {\n output = decoder.add_source(ssId, packet, 0)\n } catch {\n // The packet itself reached the parser; only its protection is lost.\n this.#drop('source:decoder-refused')\n output = undefined\n }\n const recovered: Uint8Array[] = []\n if (start !== undefined && block) {\n if (output) {\n recovered.push(...this.#settleDecoded(start, block, output))\n } else if (block.received >= block.ssbLength) {\n this.#settleClean(start)\n }\n }\n this.#retireBehindHorizon()\n return recovered\n }\n\n /**\n * Feeds one object of the repair track: a whole MMTP repair packet (ISO/IEC\n * 23008-1 C.5.3). A repair track carries one repair flow, so the first\n * repair packet fixes its packet ID and a packet of any other is refused.\n * `path` is where it was received, as for addSource(). `repairTrackName`\n * is the repair track it was received on, for the 6.3.9 own-track rule; a\n * track the layers do not list is refused as `repair:unlisted-track`.\n *\n * @returns the protected packets this repair symbol recovered, as for\n * addSource().\n */\n addRepair(packet: Uint8Array, path: MmtpFecSessionPath = 'moq', repairTrackName?: string): Uint8Array[] {\n const decoder = this.#decoder\n if (!decoder) return this.#drop('repair:released')\n if (this.#stats.failClosed) return this.#drop('repair:fail-closed')\n const pathBit = PATH_BITS[path]\n if (!pathBit) return this.#drop('repair:unknown-path')\n let header\n try {\n header = parseMMTPHeader(packet)\n } catch {\n return this.#drop('repair:malformed')\n }\n // core-00 7.5 / mmt-02 7.3: exactly 12 + 13 + T, judged before any field.\n const admission = admitMmtpRepairObject(packet, header, this.symbolSize, this.algorithm)\n if (!admission) return this.#drop('repair:malformed')\n if (!('frame' in admission)) return this.#drop('repair:length')\n const frame = admission.frame\n if (this.#repairPacketId !== undefined && frame.packetId !== this.#repairPacketId) {\n return this.#drop('repair:foreign-packet-id')\n }\n if (admission.reject) {\n // An RSB_length below a held layer's total goes to the 7.3.2 stop, not\n // to the discard; the block state is unchanged either way.\n const verdict = this.#heldLayers.failsClosed(\n repairTrackName, frame.ssStart, frame.rsbLength, this.#anchorHorizon, this.#newestSource,\n )\n if (verdict) {\n this.#stats.failClosed = verdict\n return this.#drop('repair:fail-closed')\n }\n return this.#drop(admission.reject === 'repairRsId' ? 'repair:rs-id' : 'repair:scheme-bound')\n }\n this.#repairPacketId ??= frame.packetId\n if (frame.ssbLength > this.sourceSymbols) return this.#drop('repair:ssb-length')\n // fec-01 6.3.9, after the 4.5 discards and the size check, before a block\n // is placed. Without a track name only the anchored rule (b) applies.\n const verdict = this.#heldLayers.admit(\n repairTrackName,\n frame.ssStart,\n frame.rsbLength,\n this.#anchorHorizon,\n this.#newestSource,\n )\n if (verdict === 'discard') return this.#drop('repair:unlisted-track')\n if (verdict !== undefined) {\n this.#stats.failClosed = verdict\n return this.#drop('repair:fail-closed')\n }\n\n const start = frame.ssStart\n let block = this.#blocks.get(start)\n if (block && block.ssbLength !== frame.ssbLength) return this.#drop('repair:conflicting-ssb-length')\n // One repair symbol per (SS_Start, RS_ID), whichever path delivers it\n // first: a second copy never reaches the decoder, the counts or the paths.\n if (block?.rsIds.has(frame.rsId)) return this.#drop('repair:duplicate')\n if (!block) {\n if (this.#settled.has(start)) return this.#drop('repair:block-settled')\n if (this.#behindHorizon(start)) return this.#drop('repair:behind-horizon')\n block = { ssbLength: frame.ssbLength, received: 0, paths: 0, rsIds: new Set() }\n this.#countHeld(start, block)\n this.#blocks.set(start, block)\n }\n this.#noteNewest(serial32Add(start, frame.ssbLength - 1))\n if (block.received >= block.ssbLength) {\n this.#settleClean(start)\n this.#retireBehindHorizon()\n return []\n }\n\n let output: Uint8Array | null | undefined\n try {\n output = decoder.add_repair(start, frame.ssbLength, frame.rsId, frame.symbolData, 0)\n } catch {\n return this.#drop('repair:decoder-refused')\n }\n block.rsIds.add(frame.rsId)\n this.#stats.repairSymbols++\n this.#stats.repairSymbolsByPath[path]++\n block.paths |= pathBit\n const recovered = output ? this.#settleDecoded(start, block, output) : []\n this.#retireBehindHorizon()\n return recovered\n }\n\n /**\n * Frees the decoder and every block. Blocks still unsettled are counted as\n * released, not failed: nothing proved them unrecoverable. Idempotent;\n * returns whether this call released anything.\n */\n release(): boolean {\n const decoder = this.#decoder\n if (!decoder) return false\n this.#decoder = null\n this.#stats.blocksReleased += this.#blocks.size\n for (const block of this.#blocks.values()) this.#noteMixed(block, 'released')\n this.#blocks.clear()\n this.#settled.clear()\n this.#have.clear()\n decoder.free()\n return true\n }\n\n #drop(reason: string): Uint8Array[] {\n this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1\n return []\n }\n\n #noteNewest(ssId: number): void {\n if (this.#newest === undefined || serial32Precedes(this.#newest, ssId)) this.#newest = ssId\n }\n\n #blockStartOf(ssId: number): number | undefined {\n for (const [start, block] of this.#blocks) {\n if (isoBlockContains(start, block.ssbLength, ssId)) return start\n }\n return undefined\n }\n\n /** Counts the sources of a new block the parser already has, and their paths. */\n #countHeld(start: number, block: BlockState): void {\n for (let i = 0; i < block.ssbLength; i++) {\n const pathBit = this.#have.get(serial32Add(start, i))\n if (pathBit === undefined) continue\n block.received++\n block.paths |= pathBit\n }\n }\n\n #noteMixed(block: BlockState, outcome: keyof MmtpFecSessionMixedPathStats): void {\n if ((block.paths & MIXED_PATHS) === MIXED_PATHS) this.#stats.mixedPath[outcome]++\n }\n\n /** The decoder's own rule: more than its retention behind, counted from below. */\n #behindHorizon(start: number): boolean {\n const newest = this.#newest\n if (newest === undefined) return false\n return isoBlocksBehindLowerBound(start, newest, this.sourceSymbols) > this.retentionBlocks\n }\n\n #settle(start: number): void {\n this.#blocks.delete(start)\n this.#settled.add(start)\n }\n\n #settleClean(start: number): void {\n const block = this.#blocks.get(start)\n if (!block) return\n this.#settle(start)\n this.#stats.blocksClean++\n this.#noteMixed(block, 'clean')\n try {\n this.#decoder?.cleanup_blocks_detailed(Uint32Array.of(start))\n } catch {\n // The decoder retires the block by its own horizon instead.\n }\n }\n\n /**\n * The decoder returns a completed block as its K' ssbg_mode1 symbols,\n * concatenated: split by T, trim by each symbol's length field, and hand\n * back the packets the parser does not have yet.\n */\n #settleDecoded(start: number, block: BlockState, output: Uint8Array): Uint8Array[] {\n this.#settle(start)\n if (output.byteLength !== block.ssbLength * this.symbolSize) {\n this.#stats.blocksFailed++\n this.#stats.packetsLost += block.ssbLength - block.received\n this.#noteMixed(block, 'failed')\n this.#drop('block:decoded-length')\n return []\n }\n const recovered: Uint8Array[] = []\n let refused = 0\n for (let i = 0; i < block.ssbLength; i++) {\n const ssId = serial32Add(start, i)\n if (this.#have.has(ssId)) continue\n const symbol = output.subarray(i * this.symbolSize, (i + 1) * this.symbolSize)\n const packet = trimSsbg1SourceSymbol(symbol)\n if (!packet) {\n refused++\n continue\n }\n this.#have.set(ssId, 0)\n recovered.push(packet.slice())\n }\n if (refused > 0) {\n this.#stats.blocksFailed++\n this.#stats.packetsLost += refused\n this.#noteMixed(block, 'failed')\n this.#drop('block:bad-ssbg1-symbol')\n } else if (recovered.length === 0) {\n this.#stats.blocksClean++\n this.#noteMixed(block, 'clean')\n } else {\n this.#stats.blocksRecovered++\n this.#noteMixed(block, 'recovered')\n }\n this.#stats.packetsRecovered += recovered.length\n return recovered\n }\n\n /**\n * Retires every block, and every SS_ID the session remembers, that the\n * newest SS_ID has moved past the retention. An unsettled block retired\n * here is a failure: more of its sources were lost than its repair could\n * restore, or its repair never arrived.\n */\n #retireBehindHorizon(): void {\n const decoder = this.#decoder\n const newest = this.#newest\n if (!decoder || newest === undefined) return\n try {\n decoder.cleanup_by_ss_start(newest)\n } catch {\n // Block fate below does not depend on the decoder's own sweep.\n }\n for (const [start, block] of this.#blocks) {\n if (!this.#behindHorizon(start)) continue\n this.#settle(start)\n this.#stats.blocksFailed++\n this.#stats.packetsLost += block.ssbLength - block.received\n this.#noteMixed(block, 'failed')\n }\n // What is remembered only needs pruning once a block's worth of SS_IDs\n // has gone by, not on every packet.\n const prunedAt = this.#prunedAt\n // #newest only moves forward, so the distance is how far it has moved.\n if (prunedAt !== undefined && serial32Distance(prunedAt, newest) < this.sourceSymbols) return\n this.#prunedAt = newest\n for (const ssId of this.#have.keys()) {\n if (this.#behindHorizon(ssId)) this.#have.delete(ssId)\n }\n for (const start of this.#settled) {\n if (this.#behindHorizon(start)) this.#settled.delete(start)\n }\n }\n}\n\nfunction requirePositiveInteger(value: unknown, path: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {\n throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`)\n }\n return value\n}\n\nfunction requireNumber(value: unknown, path: string): number {\n if (typeof value !== 'number') {\n throw new Error(`MmtpFecSession: ${path} is required (got ${String(value)}); it is never defaulted`)\n }\n return value\n}\n", "/**\n * The fec-01 6.3.9 receiver check (held-layer anchored RSB_length), in one\n * place. FECManager and MmtpFecSession both run their repair packets through\n * HeldLayerAdmission; neither restates the rule.\n */\n\nimport { serial32Distance, serial32Precedes } from '@blockcast/mmt-base'\n\n/**\n * Why a source track's FEC failed closed at runtime (fec-01 6.3.9): a repair\n * packet stated an RSB_length below a held layer's total B_i on that layer's\n * own repair track (`below_own_track_total`), or at or after the anchor, the\n * earliest block in SS_Start serial order whose repair carried >= B_i\n * (`below_anchored_total`). Repair for the track is refused from then on.\n */\nexport interface FecFailClosedReason {\n code: 'below_own_track_total' | 'below_anchored_total'\n layer: number\n total: number\n rsbLength: number\n ssStart: number\n anchor?: number\n}\n\n/** One repair layer as the catalog states it: B_i = rsIdOffset + repairSymbols. */\nexport interface HeldLayerInput {\n layer: number\n repairTrackName: string\n rsIdOffset: number\n repairSymbols: number\n}\n\ninterface HeldLayerState {\n layer: number\n repairTrackName: string\n total: number\n /** Earliest (serial order) block start whose repair carried >= total. */\n anchor?: number\n /** Latest (serial order) block start whose repair carried < total. */\n latestBelow?: number\n /** RSB_length that latestBelow's repair carried, for the fail-closed reason. */\n latestBelowRsb?: number\n /** Newest block start seen; anchor and latestBelow are retired behind it. */\n newest?: number\n /**\n * The anchor fell behind the horizon, so no block that can still arrive\n * precedes it: every later packet must carry >= total. Its serial is no\n * longer compared, so SS_Start wrapping past it cannot misorder it.\n */\n settled?: boolean\n}\n\n/** The largest horizon compared, so every compared serial stays in one half-window. */\nconst HORIZON_CAP = 0x40000000\n\n/**\n * How far (in SS_Start symbols) behind the newest block a 6.3.9 anchor or\n * below-total block can still matter: the iso retention in blocks times K,\n * capped at 2^30. Unknown inputs give the cap.\n */\nexport function heldLayerAnchorHorizon(retentionBlocks: number | undefined, k: number): number {\n return retentionBlocks === undefined || k <= 0 ? HORIZON_CAP : Math.min(HORIZON_CAP, retentionBlocks * k)\n}\n\n/**\n * fec-01 6.3.9 anchored state for one FEC instance's held layers. For each\n * held layer i with total B_i = O_i + P_i: the anchor is the earliest block,\n * in SS_Start serial order, whose repair carried RSB_length >= B_i. The\n * instance fails closed on (a) a packet on layer i's own repair track (i > 0)\n * below B_i, or (b) any packet of the instance at or after the anchor below\n * B_i. Blocks before the anchor (pre-append) stay valid at the previous total.\n * The result is not latched here: the caller owns what failing closed means.\n */\nexport class HeldLayerAdmission {\n readonly #layerNames: ReadonlySet<string>\n readonly #states: HeldLayerState[]\n\n constructor(layers: readonly HeldLayerInput[], heldLayers: readonly number[] = [0]) {\n const held = new Set(heldLayers)\n this.#layerNames = new Set(layers.map((l) => l.repairTrackName))\n this.#states = layers\n .filter((l) => held.has(l.layer))\n .map((l) => ({ layer: l.layer, repairTrackName: l.repairTrackName, total: l.rsIdOffset + l.repairSymbols }))\n }\n\n /**\n * Judges one repair packet, after the 4.5 discards and the size check and\n * before it places a block. `repairTrackName` is the subscribed repair\n * track, never inferred from RS_ID; without it only (b) runs. A packet on a\n * repair track the catalog does not list is `'discard'` and anchors nothing.\n * `sourceFrontier` is the newest source SS_ID, if any: a repair more than\n * `horizon` past it is judged without moving the anchored state.\n *\n * @returns undefined to admit, `'discard'`, or the fail-closed reason.\n */\n admit(\n repairTrackName: string | undefined,\n ssStart: number,\n rsbLength: number,\n horizon: number,\n sourceFrontier?: number,\n ): FecFailClosedReason | 'discard' | undefined {\n return this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, true)\n }\n\n #admit(\n repairTrackName: string | undefined,\n ssStart: number,\n rsbLength: number,\n horizon: number,\n sourceFrontier: number | undefined,\n retroactive: boolean,\n ): FecFailClosedReason | 'discard' | undefined {\n if (repairTrackName !== undefined && !this.#layerNames.has(repairTrackName)) return 'discard'\n // A repair may run ahead of the media, but one more than `horizon` past the\n // source frontier must not move the held-layer state: advancing `newest` on\n // it would retire every real anchor and skip this check until traffic caught\n // up. Such a repair is still judged below the total: it is on its own track,\n // or after any anchor by definition. With no anchor yet it says nothing.\n // It is still admitted: in iso the frontier only moves when a repair places\n // held sources, so dropping it would stall recovery after a long outage.\n // IsoBlockLocator.placeRepair may still refuse the same repair as `ahead`:\n // it measures from `newest`, which held sources move, not from this frontier.\n const farAhead =\n sourceFrontier !== undefined &&\n serial32Precedes(sourceFrontier, ssStart) &&\n serial32Distance(sourceFrontier, ssStart) > horizon\n for (const state of this.#states) {\n let code: FecFailClosedReason['code'] | undefined\n if (farAhead) {\n if (rsbLength < state.total) {\n if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'\n else if (state.anchor !== undefined || state.settled) code = 'below_anchored_total'\n }\n } else {\n // Serials more than `horizon` behind the newest block are retired, so\n // every serial still compared sits inside one half-window and SS_Start\n // wrapping on a long-running stream cannot reorder them.\n if (state.newest === undefined || serial32Precedes(state.newest, ssStart)) state.newest = ssStart\n const stale = (s: number) => serial32Distance(s, state.newest!) > horizon\n if (state.latestBelow !== undefined && stale(state.latestBelow)) {\n delete state.latestBelow\n delete state.latestBelowRsb\n }\n if (state.anchor !== undefined && stale(state.anchor)) state.settled = true\n if (rsbLength < state.total) {\n if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'\n else if (stale(ssStart)) continue // past retention: undecodable, anchors nothing\n else if (state.settled || (state.anchor !== undefined && !serial32Precedes(ssStart, state.anchor))) code = 'below_anchored_total'\n else if (state.latestBelow === undefined || serial32Precedes(state.latestBelow, ssStart)) {\n state.latestBelow = ssStart\n state.latestBelowRsb = rsbLength\n }\n } else if (!state.settled && !stale(ssStart)) {\n if (state.anchor === undefined || serial32Precedes(ssStart, state.anchor)) state.anchor = ssStart\n if (retroactive && state.latestBelow !== undefined && !serial32Precedes(state.latestBelow, state.anchor)) {\n code = 'below_anchored_total'\n ssStart = state.latestBelow\n rsbLength = state.latestBelowRsb!\n }\n }\n }\n if (code !== undefined) {\n return {\n code, layer: state.layer, total: state.total, rsbLength, ssStart,\n ...(state.anchor === undefined ? {} : { anchor: state.anchor }),\n }\n }\n }\n return undefined\n }\n\n /**\n * The core-00 7.5 carve-out for a repair object about to be discarded\n * (RS_ID or scheme bound): whether its own RSB_length, below a held layer's\n * total on that layer's track or on an anchored block, fails the instance\n * closed under 7.3.2. Leaves the anchored state as it was: a discarded\n * object anchors nothing.\n */\n failsClosed(\n repairTrackName: string | undefined,\n ssStart: number,\n rsbLength: number,\n horizon: number,\n sourceFrontier?: number,\n ): FecFailClosedReason | undefined {\n const saved = this.#states.map((state) => ({ ...state }))\n try {\n // No retroactive verdict: a discarded object anchors nothing, so it\n // cannot fail an earlier below-total block, and every held layer is\n // still judged on the object's own RSB_length.\n const verdict = this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, false)\n return typeof verdict === 'object' ? verdict : undefined\n } finally {\n saved.forEach((state, i) => { this.#states[i] = state })\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
3
|
+
"sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/signalling.ts", "../../mmt-base/src/serial-number.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/fec-bindings.ts", "../src/mmtp-fec-session.ts", "../src/held-layer-admission.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nexport const SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// fec_code_id_for_repair_flow code points (ISO/IEC 23008-10 Table 1;\n// draft-ramadan-moq-mmt-01 Section 8.3).\nexport const FEC_CODE_REED_SOLOMON = 1\nexport const FEC_CODE_RAPTORQ = 3\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n}\n\n/** Options for extractMFUPayload and explainMFURejection. */\nexport interface MFUExtractOptions {\n /**\n * When true, do NOT subtract the Source FEC Payload ID from the payload\n * boundary, even if fecType >= 1. For FEC-recovered symbols whose FEC PID\n * was already stripped before WASM decoding.\n */\n skipFecId?: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: MFUExtractOptions,\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false }\n : null\n }\n\n if (fecType === 1) {\n const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false }\n }\n\n return null\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. The legacy\n * authenticator suffix and the Source FEC trailer are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-01 \u00A73.1).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n const expected = SOURCE_FEC_PAYLOAD_ID_SIZE\n if (observed !== expected) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${expected} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact legacy authenticator trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const fecPayloadId = trailerLayout.hasFecId\n ? {\n ssId: new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE,\n 4,\n ).getUint32(0, false),\n }\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n checkRsId = true,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.ssbLength === 0 ||\n (checkRsId && repairId.rsId >= repairId.rsbLength) ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n/**\n * Why a repair object of an mmtp track is discarded on admission:\n * `repairLength` -- the object is not exactly 12 + 13 + T bytes with a\n * 12-byte MMTP header (C = 0, X = 0; draft-ramadan-moq-mmt-02 7.3, judged\n * before any Repair FEC Payload ID field is read); `repairRsId` -- RS_ID is not\n * below RSB_length, under every scheme; `repairSchemeBound` -- the object\n * breaks its scheme's bound: reed-solomon SSB_length + RSB_length > 255, which\n * also caps RS_ID at 253, or raptorq SSB_length + RSB_length - 1 > 2^24 - 1\n * (draft-ramadan-moq-fec-core-00 7.5 and 10.3, -block-codes-00 7).\n */\nexport type RepairObjectReject = 'repairLength' | 'repairRsId' | 'repairSchemeBound'\n\n/**\n * Admission of one mmtp repair object under its instance's T and scheme. The\n * caller names the instance (by the repair track it subscribed); nothing here\n * infers it from the fields. `repairRsId` and `repairSchemeBound` carry the\n * frame so the caller can first route an RSB_length below a held layer's\n * O_i + P_i to the 7.3.2 stop, which takes precedence over the discard.\n * `null`: not an iso repair packet, or SSB_length 0.\n */\nexport function admitMmtpRepairObject(\n packet: Uint8Array,\n header: MMTPHeader,\n symbolSize: number,\n algorithm: string | undefined,\n): { frame: MoqRepairFrame; reject?: Exclude<RepairObjectReject, 'repairLength'> } | { reject: 'repairLength' } | null {\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n if (header.headerLength !== MMTP_HEADER_SIZE || packet.length !== MMTP_HEADER_SIZE + REPAIR_FEC_PID_SIZE + symbolSize) {\n return { reject: 'repairLength' }\n }\n const frame = moqRepairFrameFrom(packet, readRepairFecPayloadIdFields(packet, header), false)\n if (!frame) return null\n if (frame.rsId >= frame.rsbLength) return { frame, reject: 'repairRsId' }\n const blockSymbols = frame.ssbLength + frame.rsbLength\n if (algorithm === 'reed-solomon' ? blockSymbols > 255 : algorithm === 'raptorq' && blockSymbols - 1 > 0xffffff) {\n return { frame, reject: 'repairSchemeBound' }\n }\n return { frame }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * Receiver-side parsers for MMTP signalling message mode.\n *\n * - The MMTP payload header for signalling message mode\n * (ISO/IEC 23008-1:2023 9.3.4.2, Figure 15).\n * - The general signalling message format (10.2, Table 20).\n * - The AL-FEC message (Annex C Table C.3 as replaced by\n * ISO/IEC 23008-1:2023/Amd 1:2025; semantics C.6.3).\n * - The PA message (10.3.2, Table 21) and MPT message (10.3.4, Table 23)\n * with the MP table (10.3.9, Table 29), Identifier_mapping (10.6.3,\n * Table 98), asset_id (10.6.2, Table 97) and MMT_general_location_info\n * (10.6.1, Table 95).\n *\n * Every parser here is total: malformed, truncated, ambiguous or unsupported\n * input yields null, never an exception. Nothing is inferred: a field the\n * receiver cannot place exactly makes the whole message unparseable, because\n * every later field of a sequential syntax would be misread after it.\n *\n * Reserved bits are ignored on receipt. The senders write them, and a\n * receiver that refuses a reserved value cannot accept a conformant revision\n * of the syntax that assigns it.\n */\nimport {\n ALFEC_MESSAGE_ID,\n FEC_CODE_RAPTORQ,\n FEC_CODE_REED_SOLOMON,\n MMTP_SIGNALING_PACKET_ID,\n PACKET_TYPE_SIGNALING,\n parseMMTPHeader,\n type MMTPHeader,\n} from './mmtp.js'\n\n// ---------------------------------------------------------------------------\n// Wire reader\n// ---------------------------------------------------------------------------\n\n/** Internal: raised by WireReader, caught at every exported parser. */\nclass MalformedSignalling extends Error {}\n\nclass WireReader {\n readonly #bytes: Uint8Array\n readonly #end: number\n #offset: number\n\n constructor(bytes: Uint8Array, start = 0, end = bytes.length) {\n this.#bytes = bytes\n this.#offset = start\n this.#end = end\n }\n\n get offset(): number {\n return this.#offset\n }\n\n get remaining(): number {\n return this.#end - this.#offset\n }\n\n #take(length: number): number {\n if (length < 0 || length > this.remaining) throw new MalformedSignalling('truncated')\n const at = this.#offset\n this.#offset += length\n return at\n }\n\n u8(): number {\n return this.#bytes[this.#take(1)]!\n }\n\n u16(): number {\n const at = this.#take(2)\n return (this.#bytes[at]! << 8) | this.#bytes[at + 1]!\n }\n\n u24(): number {\n const at = this.#take(3)\n return (this.#bytes[at]! << 16) | (this.#bytes[at + 1]! << 8) | this.#bytes[at + 2]!\n }\n\n u32(): number {\n const at = this.#take(4)\n return ((this.#bytes[at]! << 24) | (this.#bytes[at + 1]! << 16) |\n (this.#bytes[at + 2]! << 8) | this.#bytes[at + 3]!) >>> 0\n }\n\n /** A copy, so a parsed message never aliases a reused packet buffer. */\n bytes(length: number): Uint8Array {\n const at = this.#take(length)\n return this.#bytes.slice(at, at + length)\n }\n\n /** Throw unless every byte up to the end bound was consumed. */\n expectEnd(): void {\n if (this.remaining !== 0) throw new MalformedSignalling('trailing bytes')\n }\n}\n\nfunction fail(reason: string): never {\n throw new MalformedSignalling(reason)\n}\n\n/** Run a parser, turning only our own malformed-input signal into null. */\nfunction total<T>(parse: () => T | null): T | null {\n try {\n return parse()\n } catch (error) {\n if (error instanceof MalformedSignalling) return null\n throw error\n }\n}\n\n// ---------------------------------------------------------------------------\n// 9.3.4.2 \u2014 MMTP payload header for signalling message mode\n// ---------------------------------------------------------------------------\n\n/**\n * How the signalling payload of an MMTP packet was framed: always\n * `iso-9.3.4.2`, the 2-byte `f_i(2) RES(4) H(1) A(1) | frag_counter(8)`\n * header of ISO/IEC 23008-1:2023 9.3.4.2 ahead of the message(s).\n *\n * The pre-remediation Blockcast form, an AL-FEC message directly after the\n * MMTP header, is not accepted (BLO-35597, Phase L). Its first two bytes,\n * `0x02 0x03`, read as a 9.3.4.2 header claiming three further fragments\n * of a complete message, which the header parser refuses: such a packet\n * parses to null, never to a guess.\n */\nexport type SignallingPayloadForm = 'iso-9.3.4.2'\n\nexport interface SignallingPayload {\n form: SignallingPayloadForm\n /** Each complete signalling message, starting at its message_id. */\n messages: Uint8Array[]\n}\n\n/** f_i (Table 19): payload contains one or more complete signalling messages. */\nexport const SIGNALLING_FRAGMENTATION_COMPLETE = 0b00\n\n/**\n * Split the payload of an MMTP signalling packet into its complete\n * signalling messages.\n *\n * Returns null when the packet is not a well-formed, unprotected (FEC_type 0)\n * signalling packet, or when it carries a fragment (f_i != 00): reassembly\n * across packets is not implemented, and a fragment is not a message.\n */\nexport function parseSignallingPayload(mmtpPacket: Uint8Array): SignallingPayload | null {\n let header: MMTPHeader\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n if (header.packetType !== PACKET_TYPE_SIGNALING || header.fecType !== 0) return null\n const payload = mmtpPacket.subarray(header.headerLength)\n return total(() => splitSignallingPayload(payload))\n}\n\nfunction splitSignallingPayload(payload: Uint8Array): SignallingPayload {\n if (payload.length < 2) fail('short signalling payload')\n const reader = new WireReader(payload)\n const flags = reader.u8()\n const fragmentation = flags >> 6\n const lengthIs32Bits = ((flags >> 1) & 1) === 1 // H\n const aggregated = (flags & 1) === 1 // A\n const fragmentCounter = reader.u8()\n if (fragmentation !== SIGNALLING_FRAGMENTATION_COMPLETE) fail('fragmented signalling message')\n // frag_counter counts the fragments that follow this one; a complete\n // message has none, and an aggregated payload \"shall\" carry 0.\n if (fragmentCounter !== 0) fail('frag_counter on a complete message')\n\n if (!aggregated) {\n // A=0: exactly one message and no MSG_length field (H is meaningless).\n if (reader.remaining === 0) fail('empty signalling payload')\n return { form: 'iso-9.3.4.2', messages: [payload.subarray(reader.offset)] }\n }\n\n const messages: Uint8Array[] = []\n while (reader.remaining > 0) {\n const length = lengthIs32Bits ? reader.u32() : reader.u16()\n if (length === 0) fail('zero MSG_length')\n const start = reader.offset\n reader.bytes(length) // bounds check\n messages.push(payload.subarray(start, start + length))\n }\n if (messages.length === 0) fail('aggregated payload with no message')\n return { form: 'iso-9.3.4.2', messages }\n}\n\n// ---------------------------------------------------------------------------\n// 10.2 \u2014 general signalling message format\n// ---------------------------------------------------------------------------\n\nexport const PA_MESSAGE_ID = 0x0000\nexport const MPI_MESSAGE_ID_FIRST = 0x0001\nexport const MPI_MESSAGE_ID_LAST = 0x0010\nexport const MPT_MESSAGE_ID_FIRST = 0x0011\n/** MPT message carrying the complete MP table (Table 101). */\nexport const MPT_MESSAGE_ID_COMPLETE = 0x0020\n\n/**\n * Whether a message's `length` field is 32 bits (Table 20: PA and MPI\n * messages; Table 101: the 0x7000-0x7FFF range \"32-bit length message\").\n */\nexport function signallingMessageHas32BitLength(messageId: number): boolean {\n return messageId === PA_MESSAGE_ID ||\n (messageId >= MPI_MESSAGE_ID_FIRST && messageId <= MPI_MESSAGE_ID_LAST) ||\n (messageId >= 0x7000 && messageId <= 0x7fff)\n}\n\n/**\n * Read the general header (Table 20) of a complete message and check that\n * `length` covers exactly the rest of the message.\n */\nfunction readMessageHeader(message: Uint8Array): { reader: WireReader; messageId: number; version: number } {\n const reader = new WireReader(message)\n const messageId = reader.u16()\n const version = reader.u8()\n const length = signallingMessageHas32BitLength(messageId) ? reader.u32() : reader.u16()\n // \"The value 0 is not valid for this field\" (C.6.3, 10.3.2.3, 10.3.4.3).\n if (length === 0 || length !== reader.remaining) fail('message length mismatch')\n return { reader, messageId, version }\n}\n\n// ---------------------------------------------------------------------------\n// Annex C Table C.3 (Amd 1:2025) \u2014 AL-FEC message\n// ---------------------------------------------------------------------------\n\n/** fec_coding_structure values (Table C.8 as replaced by Amd 1:2025). */\nexport const FEC_CODING_STRUCTURE_NONE = 0b0000\nexport const FEC_CODING_STRUCTURE_ONE_STAGE = 0b0001\nexport const FEC_CODING_STRUCTURE_TWO_STAGE = 0b0010\nexport const FEC_CODING_STRUCTURE_LAYER_AWARE = 0b0011\nexport const FEC_CODING_STRUCTURE_ADAPTIVE = 0b0100\n\n/** ssbg_mode values (Table C.5). b11 is reserved. */\nexport const SSBG_MODE_0 = 0b00\nexport const SSBG_MODE_1 = 0b01\nexport const SSBG_MODE_2 = 0b10\n\nexport interface AlFecPrivateField {\n privateFlag: 0 | 1\n data: Uint8Array\n}\n\nexport interface AlFecRepairFlow {\n /**\n * repair_flow_id (16 bits under Amd 1:2025 for one-stage, two-stage and\n * LA-FEC). Blockcast senders set it to the repair flow's MMTP packet_id.\n */\n repairFlowId: number\n /**\n * fec_code_id_for_repair_flow. Under LA-FEC (0011) one value is signalled\n * per FEC flow and is repeated here on every layer.\n */\n fecCodeId: number\n /**\n * The private_flag/private_field_length/private_field triple, present\n * exactly when the message's private_fec_flag is 1. Shared by every layer\n * under LA-FEC, like fecCodeId.\n */\n privateField?: AlFecPrivateField\n maximumK: number\n maximumP: number\n protectionWindowTime: number\n protectionWindowSize: number\n}\n\nexport interface AlFecFlow {\n fecFlowId: number\n sourceFlowId: number\n /** Every asset packet_id of the FEC source flow, in signalled order. */\n packetIds: number[]\n fecCodingStructure: number\n ssbgMode: number\n ffsrptsFlag: 0 | 1\n /**\n * The low bit of the coding byte. Amd 1:2025 Table C.3 names it\n * fec_payload_id_mode (Table C.7: 0 = mode 0, Source FEC Payload ID added\n * to each packet; 1 = mode 1, C.7, packets unmodified). The unamended 2023\n * Table C.3 lists the same bit as reserved \"1\". The parse never depends on\n * it \u2014 no later field is conditional on it \u2014 so both readings are\n * accepted and the value is reported, not interpreted (BLO-35597 N1).\n */\n fecPayloadIdMode: 0 | 1\n /** length_of_repair_symbol, in bytes. */\n repairSymbolSize: number\n /** Present exactly when ssbg_mode is b10. */\n ssbgMode2?: {\n numRepairSymbolPerPacket: number\n numSymbolElementPerSourceSymbol: number\n }\n /** num_of_sub_block_per_source_block, two-stage (0010) only. */\n numSubBlockPerSourceBlock?: number\n /** One entry for one-stage; N3 parity flows; N4 LA-FEC layers; none for 0000. */\n repairFlows: AlFecRepairFlow[]\n}\n\nexport interface AlFecMessageBody {\n /** The message's version counter (C.6.3). Any value is accepted. */\n version: number\n fecFlag: 0 | 1\n privateFecFlag: 0 | 1\n /** Empty exactly when fec_flag is 0 (no FEC source flow). */\n flows: AlFecFlow[]\n}\n\nexport interface AlFecMessage extends AlFecMessageBody {\n payloadForm: SignallingPayloadForm\n}\n\nfunction readPrivateField(reader: WireReader): AlFecPrivateField {\n const head = reader.u8()\n return { privateFlag: (head >> 7) as 0 | 1, data: reader.bytes(head & 0x7f) }\n}\n\nfunction readRepairFlowTail(\n reader: WireReader,\n repairFlowId: number,\n fecCodeId: number,\n privateField: AlFecPrivateField | undefined,\n): AlFecRepairFlow {\n const maximumK = reader.u24()\n const maximumP = reader.u24()\n const protectionWindowTime = reader.u32()\n const protectionWindowSize = reader.u32()\n return {\n repairFlowId,\n fecCodeId,\n ...(privateField ? { privateField } : {}),\n maximumK,\n maximumP,\n protectionWindowTime,\n protectionWindowSize,\n }\n}\n\nfunction readRepairFlow(reader: WireReader, privateFecFlag: boolean): AlFecRepairFlow {\n const repairFlowId = reader.u16()\n const fecCodeId = reader.u8()\n const privateField = privateFecFlag ? readPrivateField(reader) : undefined\n return readRepairFlowTail(reader, repairFlowId, fecCodeId, privateField)\n}\n\nfunction readFecFlow(reader: WireReader, privateFecFlag: boolean): AlFecFlow {\n const fecFlowId = reader.u8()\n const sourceFlowId = reader.u8()\n const numberOfAssets = reader.u8()\n if (numberOfAssets === 0) fail('FEC flow with no asset')\n const packetIds: number[] = []\n for (let i = 0; i < numberOfAssets; i++) packetIds.push(reader.u16())\n\n const coding = reader.u8()\n const fecCodingStructure = coding >> 4\n const ssbgMode = (coding >> 2) & 0b11\n const ffsrptsFlag = ((coding >> 1) & 1) as 0 | 1\n const fecPayloadIdMode = (coding & 1) as 0 | 1\n if (ssbgMode === 0b11) fail('reserved ssbg_mode')\n const repairSymbolSize = reader.u16()\n\n const flow: AlFecFlow = {\n fecFlowId,\n sourceFlowId,\n packetIds,\n fecCodingStructure,\n ssbgMode,\n ffsrptsFlag,\n fecPayloadIdMode,\n repairSymbolSize,\n repairFlows: [],\n }\n if (ssbgMode === SSBG_MODE_2) {\n flow.ssbgMode2 = {\n numRepairSymbolPerPacket: reader.u16(),\n numSymbolElementPerSourceSymbol: reader.u16(),\n }\n }\n\n switch (fecCodingStructure) {\n case FEC_CODING_STRUCTURE_NONE:\n break\n case FEC_CODING_STRUCTURE_ONE_STAGE:\n flow.repairFlows.push(readRepairFlow(reader, privateFecFlag))\n break\n case FEC_CODING_STRUCTURE_TWO_STAGE: {\n flow.numSubBlockPerSourceBlock = reader.u8()\n const numberOfParityFlows = reader.u8()\n for (let k = 0; k < numberOfParityFlows; k++) {\n flow.repairFlows.push(readRepairFlow(reader, privateFecFlag))\n }\n break\n }\n case FEC_CODING_STRUCTURE_LAYER_AWARE: {\n const numberOfLayers = reader.u8()\n const fecCodeId = reader.u8()\n const privateField = privateFecFlag ? readPrivateField(reader) : undefined\n for (let l = 0; l < numberOfLayers; l++) {\n flow.repairFlows.push(readRepairFlowTail(reader, reader.u16(), fecCodeId, privateField))\n }\n break\n }\n default:\n // 0100 (adaptive FEC, Amd 1:2025): the published table leaves the\n // width of number_of__class unstated, so its flows cannot be placed\n // exactly. 0101-1111 are reserved. Either way every later flow would\n // be misread, so the whole message is refused.\n fail(`unsupported fec_coding_structure ${fecCodingStructure}`)\n }\n return flow\n}\n\n/**\n * Parse a complete AL-FEC message (Table C.3, Amd 1:2025), starting at its\n * message_id. Every FEC flow, every asset packet_id and every\n * coding-structure-conditional field is returned.\n */\nexport function parseAlFecMessageBody(message: Uint8Array): AlFecMessageBody | null {\n return total(() => {\n const { reader, messageId, version } = readMessageHeader(message)\n if (messageId !== ALFEC_MESSAGE_ID) return null\n const flags = reader.u8()\n const fecFlag = (flags >> 7) as 0 | 1\n const privateFecFlag = ((flags >> 6) & 1) as 0 | 1\n if (fecFlag === 0) {\n // \"If every source flows are delivered without AL-FEC protection, all\n // the remaining message is not to be delivered\" (C.6.3).\n reader.expectEnd()\n return { version, fecFlag, privateFecFlag, flows: [] }\n }\n const descriptorLength = reader.u16()\n if (descriptorLength === 0 || descriptorLength !== reader.remaining) {\n fail('length_of_fec_flow_descriptor mismatch')\n }\n const numberOfFecFlows = reader.u8()\n if (numberOfFecFlows === 0) fail('fec_flag=1 with no FEC flow')\n const flows: AlFecFlow[] = []\n for (let i = 0; i < numberOfFecFlows; i++) flows.push(readFecFlow(reader, privateFecFlag === 1))\n reader.expectEnd()\n return { version, fecFlag, privateFecFlag, flows }\n })\n}\n\n/**\n * Parse the AL-FEC message carried by a full MMTP signalling packet (a\n * 9.3.4.2 payload, see SignallingPayloadForm). Returns null when the packet\n * carries no AL-FEC message, more than one, or a malformed one.\n */\nexport function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecMessage | null {\n const packet = parseSignallingPacket(mmtpPacket)\n if (!packet) return null\n const alFec = packet.messages.filter(\n (message): message is Extract<MmtSignallingMessage, { kind: 'al-fec' }> => message.kind === 'al-fec',\n )\n if (alFec.length !== 1) return null\n return { ...alFec[0]!.message, payloadForm: packet.payloadForm }\n}\n\n/**\n * Decoder geometry of one FEC flow's AL-FEC message entry under\n * draft-ramadan-moq-mmt-02 Section 8.3 (\"iso-ssbg1-v1\"): one Asset, coding\n * byte 0x14, fec_code_id_for_repair_flow 3 (RaptorQ) or 1 (Reed-Solomon),\n * and no OTI (draft-ramadan-moq-mmt-02 Section 7.1). K, T and P come from\n * the entry's own fields; a private field, if present, is ignored.\n */\nexport interface AlFecRaptorQFlowConfig {\n /** The flow's one Asset packet_id (number_of_assets is 1). */\n sourcePacketIds: number[]\n repairFlowId: number\n /** `FEC_CODE_RAPTORQ` (3) or `FEC_CODE_REED_SOLOMON` (1). */\n fecCodeId: number\n /** Always `SSBG_MODE_1` (`iso-ssbg1-v1`). */\n ssbgMode: number\n /** Always 0: the profile's coding byte is exactly 0x14. */\n fecPayloadIdMode: 0 | 1\n /** T, from length_of_repair_symbol. */\n repairSymbolSize: number\n /** K, from maximum_k_for_repair_flow. */\n maximumK: number\n /** P, from maximum_p_for_repair_flow: the RSB_length total over every published layer. */\n maximumP: number\n protectionWindowTime: number\n protectionWindowSize: number\n}\n\nconst RAPTORQ_MAX_SOURCE_SYMBOLS = 56_403\nconst REPAIR_SYMBOL_ID_MAX = 0x00ff_ffff\n/** The 12-octet MMTP header plus the 2-octet length of the T - 2 construction (draft-ramadan-moq-mmt-02 Section 7.1). */\nconst MIN_REPAIR_SYMBOL_SIZE = 14\nconst REED_SOLOMON_MAX_BLOCK = 255\n\n/**\n * Validate one parsed FEC flow against draft-ramadan-moq-mmt-02 Sections 8.3\n * and 13.2 and return its decoder geometry, or null when the entry is\n * outside them. Fail closed on everything else:\n *\n * - coding byte other than 0x14 (one-stage, ssbg_mode1, ffsrpts_flag 0,\n * fec_payload_id_mode 0). 0x15 is fec_payload_id_mode 1 under Amd 1:2025,\n * which \"iso-ssbg1-v1\" does not describe (draft-ramadan-moq-mmt-02 Section 7.1);\n * - number_of_assets other than 1;\n * - fec_code_id_for_repair_flow other than 3 or 1, the retired Blockcast\n * code 2 included (ISO/IEC 23008-10 Table 1);\n * - T < 14, K = 0, P = 0, K + P - 1 > 2^24 - 1;\n * - RaptorQ: T not a multiple of 8, or K > 56403;\n * - Reed-Solomon: K + P > 255;\n * - repair_flow_id 0 (the signalling flow) or the flow's own packet_id.\n *\n * Checks that span entries (another entry's packet_id, a duplicate\n * repair_flow_id or Asset) are `alFecMessageFlowConfigs`'.\n */\nexport function alFecRaptorQFlowConfig(flow: AlFecFlow): AlFecRaptorQFlowConfig | null {\n if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1) return null\n if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0) return null\n if (flow.packetIds.length !== 1) return null\n const repair = flow.repairFlows[0]!\n const { fecCodeId, maximumK: k, maximumP: p } = repair\n const t = flow.repairSymbolSize\n if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX) return null\n if (fecCodeId === FEC_CODE_RAPTORQ) {\n if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS) return null\n } else if (fecCodeId === FEC_CODE_REED_SOLOMON) {\n if (k + p > REED_SOLOMON_MAX_BLOCK) return null\n } else {\n return null\n }\n if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0]) return null\n return {\n sourcePacketIds: [...flow.packetIds],\n repairFlowId: repair.repairFlowId,\n fecCodeId,\n ssbgMode: flow.ssbgMode,\n fecPayloadIdMode: flow.fecPayloadIdMode,\n repairSymbolSize: t,\n maximumK: k,\n maximumP: p,\n protectionWindowTime: repair.protectionWindowTime,\n protectionWindowSize: repair.protectionWindowSize,\n }\n}\n\n/**\n * `alFecRaptorQFlowConfig` for every entry of a message, index for index,\n * plus the draft-ramadan-moq-mmt-02 Section 13.2 checks that span entries: an\n * entry whose repair_flow_id is another entry's Asset packet_id, and every\n * entry of two that name one repair_flow_id or list one Asset, is null.\n * A repair_flow_id equal to a packet_id this message does not list (another\n * track in the MP table) is the caller's check: it holds the MP table.\n */\nexport function alFecMessageFlowConfigs(message: AlFecMessageBody): (AlFecRaptorQFlowConfig | null)[] {\n const assetCount = new Map<number, number>()\n const repairCount = new Map<number, number>()\n const bump = (map: Map<number, number>, key: number) => map.set(key, (map.get(key) ?? 0) + 1)\n for (const flow of message.flows) {\n for (const id of flow.packetIds) bump(assetCount, id)\n for (const repair of flow.repairFlows) bump(repairCount, repair.repairFlowId)\n }\n return message.flows.map(flow => {\n const config = alFecRaptorQFlowConfig(flow)\n if (!config) return null\n const [asset] = config.sourcePacketIds\n if (assetCount.get(asset!) !== 1 || repairCount.get(config.repairFlowId) !== 1) return null\n // Its own packet_id is already refused, so any count here is another entry's.\n if (assetCount.has(config.repairFlowId)) return null\n return config\n })\n}\n\n// ---------------------------------------------------------------------------\n// 10.6 \u2014 location, asset_id and identifier mapping\n// ---------------------------------------------------------------------------\n\n/** location_type values (Table 96) a receiver may meet in an asset location. */\nexport type MmtGeneralLocation =\n | { locationType: 0x00; packetId: number }\n | { locationType: 0x01; srcAddress: Uint8Array; dstAddress: Uint8Array; dstPort: number; packetId: number }\n | { locationType: 0x02; srcAddress: Uint8Array; dstAddress: Uint8Array; dstPort: number; packetId: number }\n | { locationType: 0x03; networkId: number; transportStreamId: number; mpeg2Pid: number }\n | { locationType: 0x04; srcAddress: Uint8Array; dstAddress: Uint8Array; dstPort: number; mpeg2Pid: number }\n | { locationType: 0x05; url: Uint8Array }\n | { locationType: 0x06; data: Uint8Array }\n\n/**\n * MMT_general_location_info() for an Asset location. \"Only the value of\n * location_type between 0x00 and 0x06 shall be used for an Asset location\"\n * (10.3.9.3), so any other value is refused rather than skipped: the\n * remaining types have their own lengths, and a reserved one has none.\n */\nfunction readAssetLocation(reader: WireReader): MmtGeneralLocation {\n const locationType = reader.u8()\n switch (locationType) {\n case 0x00:\n return { locationType, packetId: reader.u16() }\n case 0x01:\n case 0x02: {\n const addressLength = locationType === 0x01 ? 4 : 16\n const srcAddress = reader.bytes(addressLength)\n const dstAddress = reader.bytes(addressLength)\n const dstPort = reader.u16()\n return { locationType, srcAddress, dstAddress, dstPort, packetId: reader.u16() }\n }\n case 0x03: {\n const networkId = reader.u16()\n const transportStreamId = reader.u16()\n return { locationType, networkId, transportStreamId, mpeg2Pid: reader.u16() & 0x1fff }\n }\n case 0x04: {\n const srcAddress = reader.bytes(16)\n const dstAddress = reader.bytes(16)\n const dstPort = reader.u16()\n return { locationType, srcAddress, dstAddress, dstPort, mpeg2Pid: reader.u16() & 0x1fff }\n }\n case 0x05:\n return { locationType, url: reader.bytes(reader.u8()) }\n case 0x06:\n return { locationType, data: reader.bytes(reader.u16()) }\n default:\n fail(`location_type ${locationType} is not an Asset location`)\n }\n}\n\n/** Identifier_mapping() (Table 98). */\nexport type MmtIdentifierMapping =\n | { identifierType: 0x00; assetIdScheme: number; assetId: Uint8Array }\n | { identifierType: 0x01; urls: Uint8Array[] }\n | { identifierType: 0x02; regex: Uint8Array }\n | { identifierType: 0x03; representationId: Uint8Array }\n | { identifierType: number; privateIdentifier: Uint8Array }\n\nfunction readIdentifierMapping(reader: WireReader): MmtIdentifierMapping {\n const identifierType = reader.u8()\n switch (identifierType) {\n case 0x00: {\n const assetIdScheme = reader.u32()\n return { identifierType, assetIdScheme, assetId: reader.bytes(reader.u32()) }\n }\n case 0x01: {\n const urls: Uint8Array[] = []\n const urlCount = reader.u16()\n for (let i = 0; i < urlCount; i++) urls.push(reader.bytes(reader.u16()))\n return { identifierType, urls }\n }\n case 0x02:\n return { identifierType, regex: reader.bytes(reader.u16()) }\n case 0x03:\n return { identifierType, representationId: reader.bytes(reader.u16()) }\n default:\n return { identifierType, privateIdentifier: reader.bytes(reader.u16()) }\n }\n}\n\n// ---------------------------------------------------------------------------\n// 10.3.9 \u2014 MP table; 10.3.2 PA message; 10.3.4 MPT message\n// ---------------------------------------------------------------------------\n\n/** table_id range of the MP table (Table 102): subset 0..14, then complete. */\nexport const MP_TABLE_ID_SUBSET_0 = 0x11\nexport const MP_TABLE_ID_COMPLETE = 0x20\nexport const MPI_TABLE_ID_FIRST = 0x01\nexport const MPI_TABLE_ID_LAST = 0x10\n\nexport interface MpAsset {\n identifier: MmtIdentifierMapping\n /** asset_type, a 4CC (for example 'hvc1', 'mp4a'). */\n assetType: string\n assetModificationFlag: boolean\n defaultAssetFlag: boolean\n /** Present exactly when asset_clock_relation_flag is 1. */\n assetClockRelationId?: number\n /** Present exactly when asset_timescale_flag is 1. */\n assetTimescale?: number\n locations: MmtGeneralLocation[]\n descriptors: Uint8Array\n}\n\nexport interface MpTable {\n tableId: number\n version: number\n /** MP_table_mode (Table 30); b11 is reserved and reported as read. */\n mpTableMode: number\n /** Present exactly for the complete table (0x20) and subset 0 (0x11). */\n packageId?: Uint8Array\n /** Present exactly for the complete table (0x20) and subset 0 (0x11). */\n descriptors?: Uint8Array\n assets: MpAsset[]\n}\n\nfunction readMpTable(reader: WireReader): MpTable {\n const tableId = reader.u8()\n if (tableId < MP_TABLE_ID_SUBSET_0 || tableId > MP_TABLE_ID_COMPLETE) fail('not an MP table')\n const version = reader.u8()\n const length = reader.u16()\n if (length === 0) fail('zero MP table length')\n const table = new WireReader(reader.bytes(length))\n\n const mpTableMode = table.u8() & 0b11\n const result: MpTable = { tableId, version, mpTableMode, assets: [] }\n if (tableId === MP_TABLE_ID_COMPLETE || tableId === MP_TABLE_ID_SUBSET_0) {\n result.packageId = table.bytes(table.u8())\n result.descriptors = table.bytes(table.u16())\n }\n const numberOfAssets = table.u8()\n for (let i = 0; i < numberOfAssets; i++) {\n const identifier = readIdentifierMapping(table)\n const assetType = String.fromCharCode(...table.bytes(4))\n const flags = table.u8()\n const asset: MpAsset = {\n identifier,\n assetType,\n assetModificationFlag: ((flags >> 2) & 1) === 1,\n defaultAssetFlag: ((flags >> 1) & 1) === 1,\n locations: [],\n descriptors: new Uint8Array(0),\n }\n if ((flags & 1) === 1) { // asset_clock_relation_flag\n asset.assetClockRelationId = table.u8()\n if ((table.u8() & 1) === 1) asset.assetTimescale = table.u32() // asset_timescale_flag\n }\n const locationCount = table.u8()\n for (let j = 0; j < locationCount; j++) asset.locations.push(readAssetLocation(table))\n asset.descriptors = table.bytes(table.u16())\n result.assets.push(asset)\n }\n table.expectEnd()\n return result\n}\n\n/** Parse a complete MP table (Table 29), starting at its table_id. */\nexport function parseMpTable(bytes: Uint8Array): MpTable | null {\n return total(() => {\n const reader = new WireReader(bytes)\n const table = readMpTable(reader)\n reader.expectEnd()\n return table\n })\n}\n\n/**\n * The packet_ids an Asset is delivered on: every location of type 0x00\n * (same MMTP packet flow), 0x01 (UDP/IPv4) or 0x02 (UDP/IPv6), in\n * signalled order.\n */\nexport function mpAssetPacketIds(asset: MpAsset): number[] {\n const packetIds: number[] = []\n for (const location of asset.locations) {\n if (location.locationType === 0x00 || location.locationType === 0x01 || location.locationType === 0x02) {\n packetIds.push(location.packetId)\n }\n }\n return packetIds\n}\n\nexport interface MmtSignallingTable {\n tableId: number\n version: number\n /** The complete table, starting at its table_id. */\n bytes: Uint8Array\n}\n\nexport interface PaMessage {\n version: number\n /** Every table of the message payload, in order. */\n tables: MmtSignallingTable[]\n /** The MP tables among `tables`, decoded. */\n mpTables: MpTable[]\n}\n\nexport interface MptMessage {\n /** 0x0011-0x001F carry subset 0-14, 0x0020 the complete MP table. */\n messageId: number\n version: number\n table: MpTable\n}\n\n/**\n * Parse a complete PA message (Table 21), starting at its message_id.\n *\n * Every table in the payload is delimited by its own 16-bit `length` and\n * must match the extension's table_id and table_version. For non-MPI tables\n * table_length is \"a copy of the length field\" (10.3.2.3) and must match it\n * too; for an MPI table it is derived differently, so it is not compared.\n */\nexport function parsePaMessageBody(message: Uint8Array): PaMessage | null {\n return total(() => {\n const { reader, messageId, version } = readMessageHeader(message)\n if (messageId !== PA_MESSAGE_ID) return null\n const numberOfTables = reader.u8()\n const entries: { tableId: number; tableVersion: number; tableLength: number }[] = []\n for (let i = 0; i < numberOfTables; i++) {\n entries.push({ tableId: reader.u8(), tableVersion: reader.u8(), tableLength: reader.u16() })\n }\n const tables: MmtSignallingTable[] = []\n const mpTables: MpTable[] = []\n for (const entry of entries) {\n const start = reader.offset\n const tableId = reader.u8()\n const tableVersion = reader.u8()\n const length = reader.u16()\n if (length === 0) fail('zero table length')\n reader.bytes(length) // bounds check\n const isMpi = tableId >= MPI_TABLE_ID_FIRST && tableId <= MPI_TABLE_ID_LAST\n if (tableId !== entry.tableId || tableVersion !== entry.tableVersion ||\n (!isMpi && length !== entry.tableLength)) {\n fail('PA extension does not describe its table')\n }\n const bytes = message.subarray(start, reader.offset)\n tables.push({ tableId, version: tableVersion, bytes })\n if (tableId >= MP_TABLE_ID_SUBSET_0 && tableId <= MP_TABLE_ID_COMPLETE) {\n const table = parseMpTable(bytes)\n if (!table) fail('malformed MP table in PA message')\n mpTables.push(table)\n }\n }\n reader.expectEnd()\n return { version, tables, mpTables }\n })\n}\n\n/** Parse a complete MPT message (Table 23), starting at its message_id. */\nexport function parseMptMessageBody(message: Uint8Array): MptMessage | null {\n return total(() => {\n const { reader, messageId, version } = readMessageHeader(message)\n if (messageId < MPT_MESSAGE_ID_FIRST || messageId > MPT_MESSAGE_ID_COMPLETE) return null\n const table = readMpTable(reader)\n reader.expectEnd()\n return { messageId, version, table }\n })\n}\n\n// ---------------------------------------------------------------------------\n// Packet-level dispatch (the packet_id 0 signalling handler)\n// ---------------------------------------------------------------------------\n\nexport type MmtSignallingMessage =\n | { kind: 'al-fec'; message: AlFecMessageBody }\n | { kind: 'pa'; message: PaMessage }\n | { kind: 'mpt'; message: MptMessage }\n /** A message this module does not decode, delimited and length-checked. */\n | { kind: 'other'; messageId: number; version: number; bytes: Uint8Array }\n\nexport interface MmtSignallingPacket {\n payloadForm: SignallingPayloadForm\n messages: MmtSignallingMessage[]\n}\n\nfunction decodeSignallingMessage(bytes: Uint8Array): MmtSignallingMessage {\n const { messageId, version } = readMessageHeader(bytes)\n if (messageId === ALFEC_MESSAGE_ID) {\n const message = parseAlFecMessageBody(bytes)\n if (!message) fail('malformed AL-FEC message')\n return { kind: 'al-fec', message }\n }\n if (messageId === PA_MESSAGE_ID) {\n const message = parsePaMessageBody(bytes)\n if (!message) fail('malformed PA message')\n return { kind: 'pa', message }\n }\n if (messageId >= MPT_MESSAGE_ID_FIRST && messageId <= MPT_MESSAGE_ID_COMPLETE) {\n const message = parseMptMessageBody(bytes)\n if (!message) fail('malformed MPT message')\n return { kind: 'mpt', message }\n }\n return { kind: 'other', messageId, version, bytes }\n}\n\n/**\n * Parse every signalling message of a full MMTP signalling packet (the\n * packet_id 0 flow). Returns null when the packet or any message in it is\n * malformed: one unparseable message leaves the receiver unable to say what\n * the packet signalled. Only the 9.3.4.2 payload form is accepted.\n */\nexport function parseSignallingPacket(mmtpPacket: Uint8Array): MmtSignallingPacket | null {\n const payload = parseSignallingPayload(mmtpPacket)\n if (!payload) return null\n return total(() => ({\n payloadForm: payload.form,\n messages: payload.messages.map(decodeSignallingMessage),\n }))\n}\n", "/**\n * RFC 1982 serial-number arithmetic for the 32-bit MMTP AL-FEC symbol\n * counters (ISO/IEC 23008-1:2023 C.5.2 SS_ID, C.5.3 SS_Start).\n *\n * SS_ID is a 32-bit field, so a stream that runs long enough wraps it to 0.\n * Ordering two SS_IDs (or two blocks by their first SS_ID) with `<` reads\n * every post-wrap value as older than every pre-wrap one. Serial order\n * agrees with `<` whenever the two values are less than 2^31 apart, so for\n * every stream that has not reached the wrap the result is unchanged; only\n * the order across the wrap point differs (BLO-35597 iso:5).\n */\n\n/** 2^SERIAL_BITS for SERIAL_BITS = 32. */\nexport const SERIAL32_SPAN = 0x1_0000_0000\n\n/** RFC 1982 \u00A73.2's 2^(SERIAL_BITS - 1). */\nexport const SERIAL32_HALF = 0x8000_0000\n\nfunction assertUint32(value: number, name: string): void {\n if (!Number.isInteger(value) || value < 0 || value >= SERIAL32_SPAN) {\n throw new RangeError(`${name} must be a uint32 (got ${value})`)\n }\n}\n\n/**\n * RFC 1982 \u00A73.1 addition, extended to negative `n` for the matching\n * subtraction: `(s + n) mod 2^32`. `n` must lie in (-2^31, 2^31).\n */\nexport function serial32Add(s: number, n: number): number {\n assertUint32(s, 's')\n if (!Number.isInteger(n) || n <= -SERIAL32_HALF || n >= SERIAL32_HALF) {\n throw new RangeError(`serial addend must lie in (-2^31, 2^31) (got ${n})`)\n }\n return (s + n + SERIAL32_SPAN) % SERIAL32_SPAN\n}\n\n/** Forward distance from `from` to `to`: `(to - from) mod 2^32`. */\nexport function serial32Distance(from: number, to: number): number {\n return (to - from) >>> 0\n}\n\n/**\n * RFC 1982 \u00A73.2: `a` precedes `b`. Values exactly 2^31 apart are unordered\n * (neither precedes the other).\n */\nexport function serial32Precedes(a: number, b: number): boolean {\n const distance = serial32Distance(a, b)\n return distance !== 0 && distance < SERIAL32_HALF\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "/**\n * Runtime FEC bindings (BLO-50171): what each source track's FEC resolves to\n * for this library's receiver, read from `validateFecCatalog`'s per-instance\n * result and never re-derived.\n *\n * Every runtime reader (the container's `resolveCatalogTrackFec`,\n * `resolveRepairLayers`, `MulticastManager.configureMultiTrack`, transport's\n * `catalogRepairTracks`) goes through `resolveFecBindings`, so they cannot\n * disagree with the validator or with each other about which tracks have FEC\n * and which repair tracks belong to them.\n *\n * The draft-ramadan-moq-fec-catalog-00 array is the only shape. Both catalog\n * dialects bind through it: an MSF `tracks[]` entry directly, and a hang\n * `video`/`audio` rendition (`fec` the same array) with its `repair.renditions`\n * entries (`source` + `instanceId`), mapped onto a `tracks[]` view so the same\n * validator runs. A hang rendition is authoritative over a `tracks[]` entry of\n * the same name.\n *\n * Fail closed per instance: a source whose base instance is not `ok` resolves\n * to `unavailable` with the validator's code and path, and costs no other\n * track anything. A draft-01 object `fec` is `fec_member` at that track's\n * `fec`: the track plays without FEC.\n */\nimport { validateFecCatalog, type FecCatalogCode, type FecCatalogReceiver } from './fec-catalog.js'\n\n/** The receiver this runtime is: it decodes `iso-ssbg1-v1` only. */\nexport const RUNTIME_FEC_RECEIVER: FecCatalogReceiver = Object.freeze({\n schemes: Object.freeze(['raptorq', 'reed-solomon']),\n blockProfiles: Object.freeze(['iso-ssbg1-v1']),\n overlays: true,\n})\n\n/** Repair track priority when the track states none (MoQT scale, 192..255). */\nexport const DEFAULT_FEC_REPAIR_PRIORITY = 240\n\n/** One `ok` instance's geometry. */\nexport interface FecInstanceGeometry {\n readonly instanceId: number\n readonly scheme: 'raptorq' | 'reed-solomon'\n /** `iso-ssbg1-v1` under `RUNTIME_FEC_RECEIVER`. */\n readonly blockProfile: string\n /** K. */\n readonly sourceSymbols: number\n /** P_0 for a base instance; the overlay's own P for an overlay. */\n readonly repairSymbols: number\n /** T. */\n readonly symbolSize: number\n readonly interleaveDepthMs?: number\n readonly reorderToleranceMs?: number\n readonly repairTrack: string\n readonly symbolTrack?: string\n}\n\nexport interface FecLayerBinding {\n readonly layer: number\n readonly repairTrackName: string\n readonly repairSymbols: number\n /** O_i = sum(P_j, j < i). */\n readonly rsIdOffset: number\n readonly priority: number\n}\n\nexport interface FecOverlayBinding {\n readonly repairTrackName: string\n readonly scope: 'keyframe'\n readonly sourceSymbols: number\n readonly repairSymbols: number\n readonly priority: number\n readonly geometry: FecInstanceGeometry\n}\n\nexport interface FecBindingUnavailable {\n /** The validator's code, or `fec_base_instances` for a track with no base\n * instance or more than one `ok` one (the MMTP wire carries one base Source FEC Payload\n * ID, mmt-02 7.4.1). */\n readonly code: FecCatalogCode | 'fec_base_instances'\n /** JSON Pointer into the catalog (a hang path starts `/video` or `/audio`). */\n readonly path: string\n readonly outcome: 'unavailable' | 'violation'\n}\n\nexport type FecSourceBinding =\n | {\n readonly status: 'ok'\n readonly track: string\n /** JSON Pointer of the source track (`/tracks/<i>`, `/video/renditions/<name>`). */\n readonly pointer: string\n readonly base: FecInstanceGeometry\n /** Base layers in order, layer 0 first. */\n readonly layers: readonly FecLayerBinding[]\n /** Sum of P over the base layers: the wire RSB_length. */\n readonly totalBaseRepairSymbols: number\n readonly overlay?: FecOverlayBinding\n /** Why a declared overlay is not used, when one is not. */\n readonly overlayUnavailable?: FecBindingUnavailable\n }\n | {\n readonly status: 'unavailable'\n readonly track: string\n readonly pointer: string\n readonly reason: FecBindingUnavailable\n }\n\ntype Obj = Record<string, unknown>\nconst recordOf = (v: unknown): Obj | undefined =>\n typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as Obj) : undefined\nconst escape = (s: string) => s.replace(/~/g, '~0').replace(/\\//g, '~1')\n\n/**\n * The `tracks[]` view of a catalog in either dialect, with each view track's\n * JSON Pointer in the original catalog.\n */\nexport function fecCatalogTracks(catalog: unknown): { tracks: unknown[]; pointers: string[] } {\n const root = recordOf(catalog) ?? {}\n const tracks: unknown[] = []\n const pointers: string[] = []\n const hangNames = new Set<string>()\n for (const role of ['video', 'audio'] as const) {\n for (const [name, r] of Object.entries(recordOf(recordOf(root[role])?.['renditions']) ?? {})) {\n const entry = recordOf(r)\n if (!entry) continue\n hangNames.add(name)\n const kind = recordOf(entry['container'])?.['kind']\n // A hang rendition carries no `packaging`; its container kind is it.\n // `mmtp-fec` is hang's kind for MMTP that carries FEC (the FFmpeg\n // publisher's `moq_publish_set_video_packaging`): MMTP packaging.\n const view: Obj = { name, packaging: kind === undefined || kind === 'mmtp-fec' ? 'mmtp' : kind }\n if (entry['fec'] !== undefined) view['fec'] = entry['fec']\n tracks.push(view)\n pointers.push(`/${role}/renditions/${escape(name)}`)\n }\n }\n const hangRenditions = tracks.length\n for (const [name, r] of Object.entries(recordOf(recordOf(root['repair'])?.['renditions']) ?? {})) {\n hangNames.add(name)\n tracks.push({ packaging: 'fec-repair', ...recordOf(r), name })\n pointers.push(`/repair/renditions/${escape(name)}`)\n }\n const raw = Array.isArray(root['tracks']) ? root['tracks'] : []\n const named = new Set(hangNames)\n raw.forEach((t, i) => {\n const name = recordOf(t)?.['name']\n if (typeof name === 'string') named.add(name)\n if (typeof name === 'string' && hangNames.has(name)) return\n tracks.push(t)\n pointers.push(`/tracks/${i}`)\n })\n // hang layer 0 needs no `repair.renditions` entry: the rendition's instance\n // names it, and a hang Root (hang's toHang) carries no repair block. Such a\n // name stands for a repair track of that instance.\n tracks.slice(0, hangRenditions).forEach((t, ti) => {\n const view = t as Obj\n if (!Array.isArray(view['fec'])) return\n view['fec'].forEach((o, ii) => {\n const repairTrack = recordOf(o)?.['repairTrack']\n if (typeof repairTrack !== 'string' || named.has(repairTrack)) return\n named.add(repairTrack)\n const instanceId = recordOf(o)?.['instanceId']\n tracks.push({ name: repairTrack, packaging: 'fec-repair', source: view['name'], ...(instanceId === undefined ? {} : { instanceId }) })\n pointers.push(`${pointers[ti]}/fec/${ii}/repairTrack`)\n })\n })\n return { tracks, pointers }\n}\n\n/**\n * Every source track's runtime FEC binding, by track name. A track with no\n * `fec`, or an empty `fec` array, declares no FEC and has no entry.\n */\nexport function resolveFecBindings(\n catalog: unknown,\n receiver: FecCatalogReceiver = RUNTIME_FEC_RECEIVER,\n): ReadonlyMap<string, FecSourceBinding> {\n const { tracks, pointers } = fecCatalogTracks(catalog)\n const report = validateFecCatalog({ tracks }, receiver)\n const at = (path: string) => {\n const m = /^\\/tracks\\/(\\d+)(.*)$/.exec(path)\n return m ? pointers[Number(m[1])] + m[2] : path\n }\n const trackAt = (i: number) => recordOf(tracks[i]) ?? {}\n const nameAt = (i: number | null) => (i === null ? undefined : (trackAt(i)['name'] as string))\n const why = (i: { outcome: string; issues: readonly { code: FecCatalogCode; path: string }[]; path: string }) =>\n Object.freeze({\n code: i.issues[0]?.code ?? 'fec_member',\n path: at(i.issues[0]?.path ?? i.path),\n outcome: i.outcome === 'unavailable' ? ('unavailable' as const) : ('violation' as const),\n })\n\n const out = new Map<string, FecSourceBinding>()\n tracks.forEach((t, ti) => {\n const track = recordOf(t)\n const name = track?.['name']\n if (!track || typeof name !== 'string' || out.has(name)) return\n if (track['packaging'] === 'fec-repair' || track['packaging'] === 'fec-source') return\n const fec = track['fec']\n if (fec === undefined) return\n if (!Array.isArray(fec)) {\n const reason = { code: 'fec_member' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const }\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason: Object.freeze(reason) }))\n return\n }\n if (fec.length === 0) return\n const insts = report.instances.filter((i) => i.trackIndex === ti)\n const isOverlay = (i: (typeof insts)[number]) => Object.hasOwn(recordOf(fec[Number(i.path.split('/').pop())]) ?? {}, 'scope')\n const bases = insts.filter((i) => !isOverlay(i))\n const okBases = bases.filter((i) => i.outcome === 'ok')\n if (okBases.length !== 1) {\n const reason =\n // No base instance at all (overlay-only `fec`) is a count of 0, not\n // the overlay's own issue.\n okBases.length > 1 || bases.length === 0\n ? Object.freeze({ code: 'fec_base_instances' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const })\n : why(bases[0] ?? insts[0]!)\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason }))\n return\n }\n const b = okBases[0]!\n const base = geometry(recordOf(fec[Number(b.path.split('/').pop())])!, b.instanceId!, nameAt(b.symbolTrack))\n const priorityOf = (i: number | null) => {\n const p = i === null ? undefined : trackAt(i)['priority']\n return typeof p === 'number' ? p : DEFAULT_FEC_REPAIR_PRIORITY\n }\n const layers: FecLayerBinding[] = [\n { layer: 0, repairTrackName: base.repairTrack, repairSymbols: base.repairSymbols, rsIdOffset: 0, priority: priorityOf(b.repairTrack) },\n ]\n const more = b.boundRepairTracks\n .map((tj) => ({ tj, layer: trackAt(tj)['repairLayer'] as number }))\n .sort((x, y) => x.layer - y.layer)\n let offset = base.repairSymbols\n for (const { tj, layer } of more) {\n const repairSymbols = trackAt(tj)['repairSymbols'] as number\n layers.push({ layer, repairTrackName: nameAt(tj)!, repairSymbols, rsIdOffset: offset, priority: priorityOf(tj) })\n offset += repairSymbols\n }\n let overlay: FecOverlayBinding | undefined\n let overlayUnavailable: FecBindingUnavailable | undefined\n const ov = insts.find(isOverlay)\n if (ov?.outcome === 'ok') {\n const g = geometry(recordOf(fec[Number(ov.path.split('/').pop())])!, ov.instanceId!, nameAt(ov.symbolTrack))\n overlay = Object.freeze({\n repairTrackName: g.repairTrack,\n scope: 'keyframe' as const,\n sourceSymbols: g.sourceSymbols,\n repairSymbols: g.repairSymbols,\n priority: priorityOf(ov.repairTrack),\n geometry: g,\n })\n } else if (ov) overlayUnavailable = why(ov)\n out.set(\n name,\n Object.freeze({\n status: 'ok' as const,\n track: name,\n pointer: pointers[ti]!,\n base,\n layers: Object.freeze(layers.map((l) => Object.freeze(l))),\n totalBaseRepairSymbols: offset,\n ...(overlay ? { overlay } : {}),\n ...(overlayUnavailable ? { overlayUnavailable } : {}),\n }),\n )\n })\n return out\n}\n\n// The validator passed this instance for this receiver, so every member read\n// here is present and in range.\nfunction geometry(o: Obj, instanceId: number, symbolTrack: string | undefined): FecInstanceGeometry {\n return Object.freeze({\n instanceId,\n scheme: o['scheme'] as 'raptorq' | 'reed-solomon',\n blockProfile: o['blockProfile'] as string,\n sourceSymbols: o['sourceSymbols'] as number,\n repairSymbols: o['repairSymbols'] as number,\n symbolSize: o['symbolSize'] as number,\n ...(typeof o['interleaveDepthMs'] === 'number' ? { interleaveDepthMs: o['interleaveDepthMs'] } : {}),\n ...(typeof o['reorderToleranceMs'] === 'number' ? { reorderToleranceMs: o['reorderToleranceMs'] } : {}),\n repairTrack: o['repairTrack'] as string,\n ...(symbolTrack !== undefined ? { symbolTrack } : {}),\n })\n}\n", "/**\n * In-band AL-FEC for one MMTP asset received over a MoQT subscription: the\n * asset's source packets on its own track, its repair packets on the repair\n * track the catalog names (`fec.repairTrack`). The same asset's packets can also\n * arrive on the multicast group (native SSM or AMT) carrying it: the caller\n * states each symbol's path, and a block fed on both -- multicast source\n * symbols repaired from the MoQT repair track, say -- is counted as mixed-path\n * under `stats.mixedPath`, never inferred. Built for a player that owns\n * its own MMTP parser and wants nothing else of the multicast stack\n * (MulticastManager, MmtpRouter, FECManager): the Shaka MSF MMTP packaging.\n *\n * The session is clock-free, like the reliable-MoQ reassembly policy it sits\n * beside. A block is settled only by wire evidence: clean once every source\n * the repair's SSB_length states has arrived, recovered when the decoder\n * completes it, failed once the newest SS_ID has moved it past the decoder's\n * retention. No timer and no arrival time decides a block's fate, so a repair\n * that is merely late on its own subscription is never counted as a loss.\n * The retention is derived from the catalog's `fecBlockBudgetMs`, and a\n * catalog missing either input throws rather than defaulting.\n *\n * Every repair packet runs the fec-01 6.3.9 held-layer check (layer 0, B_0 =\n * catalog `fec.repairSymbols`) through the same HeldLayerAdmission FECManager\n * uses. A packet below B_0 at or after the anchor fails the session closed:\n * `stats.failClosed` reports why, and every later repair is refused.\n *\n * Catalog-less (draft-ramadan-moq-mmt-02 8.3, 13.2): a receiver holding\n * layer 0 under the AL-FEC message alone builds the session from the message's\n * fec_flow entry (`alFecFlow`) instead. K, T, the code point and the repair\n * packet_id come from that entry, and layer 0 is held at\n * maximum_p_for_repair_flow, standing for O_0 + P_0, by the anchored clause of\n * draft-ramadan-moq-fec-core-00 7.3.2 alone. A later version that raises the\n * field raises the held total (updateAlFecFlow()); the message carries no\n * millisecond budget, so the caller states the retention (fec-core-00 9.4).\n *\n * Every repair packet that cannot be used is counted under the reason it was\n * refused, and every block ends under exactly one outcome, so a stream whose\n * FEC is doing nothing can be told apart from one with nothing to repair.\n */\n\nimport {\n admitMmtpRepairObject,\n alFecRaptorQFlowConfig,\n FEC_CODE_RAPTORQ,\n isoBlockContains,\n isoBlocksBehindLowerBound,\n parseFecBlockProfile,\n parseMMTPHeader,\n serial32Add,\n serial32Distance,\n serial32Precedes,\n ssbg1FitsSymbol,\n trimSsbg1SourceSymbol,\n type AlFecFlow,\n type AlFecRaptorQFlowConfig,\n type FecBlockProfile,\n} from '@blockcast/mmt-base'\nimport { deriveFecRetentionBlocks, fecBlockBudgetMs } from '@blockcast/mmt-fec'\nimport {\n HeldLayerAdmission,\n heldLayerAnchorHorizon,\n type FecFailClosedReason,\n type HeldLayerInput,\n} from './held-layer-admission.js'\n\nexport type { FecFailClosedReason, HeldLayerInput } from './held-layer-admission.js'\n\n/** The mmt-wasm `MmtFecDecoder` members the session drives. */\nexport interface MmtpFecSessionDecoder {\n configure_params(k: number, symbolSize: number): void\n add_source(ssId: number, data: Uint8Array, timestamp: number): Uint8Array | null | undefined\n add_repair(\n ssStart: number,\n ssbLength: number,\n rsId: number,\n data: Uint8Array,\n timestamp: number,\n ): Uint8Array | null | undefined\n cleanup_by_ss_start(current: number): Uint32Array\n cleanup_blocks_detailed(ssStarts: Uint32Array): unknown\n free(): void\n}\n\n/** `wasm_bindgen.MmtFecDecoder`, or a constructor of the same shape. */\nexport type MmtpFecSessionDecoderConstructor = new (\n interleaveDepth: number,\n blockProfile: string,\n algorithm?: string,\n) => MmtpFecSessionDecoder\n\n/** The fields of one `ok` draft-ramadan-moq-fec-catalog-00 instance object\n * the session reads (the source track's base instance). */\nexport interface MmtpFecSessionCatalogFec {\n /** `raptorq` or `reed-solomon`; required. */\n scheme?: unknown\n blockProfile?: unknown\n sourceSymbols?: unknown\n /** P_0: layer 0's repair symbols per block, the fec-01 6.3.9 total B_0. */\n repairSymbols?: unknown\n /** Layer 0's repair track; names it for addRepair()'s `repairTrackName`. */\n repairTrack?: unknown\n symbolSize?: unknown\n interleaveDepthMs?: unknown\n reorderToleranceMs?: unknown\n}\n\nexport interface MmtpFecSessionConfig {\n /** The source track's catalog `fec` block. */\n fec: MmtpFecSessionCatalogFec\n /** Builds the RaptorQ decoder, normally `wasm_bindgen.MmtFecDecoder`. */\n Decoder: MmtpFecSessionDecoderConstructor\n /**\n * The source's repair layers as the catalog resolves them\n * (`TrackFecParams.repairLayers`), for a caller that subscribes more than\n * layer 0. Absent: layer 0 alone, B_0 = `fec.repairSymbols`.\n */\n repairLayers?: readonly HeldLayerInput[]\n /** The layers the caller holds (fec-01 6.3.9). Absent: [0]. */\n heldLayers?: readonly number[]\n}\n\n/**\n * A catalog-less session: one fec_flow entry of the AL-FEC message\n * (`parseAlFecMessage(...).flows[i]`), draft-ramadan-moq-mmt-02 8.3.\n */\nexport interface MmtpFecSessionMessageConfig {\n /** The source track's entry; it must pass `alFecRaptorQFlowConfig`. */\n alFecFlow: AlFecFlow\n /**\n * The decoder retention in blocks. The message carries no elapsed-time\n * budget (mmt-02 7.5), so open-block state is bounded by local policy\n * (fec-core-00 9.4); required, never defaulted.\n */\n retentionBlocks: number\n Decoder: MmtpFecSessionDecoderConstructor\n}\n\n/** What updateAlFecFlow() did with a later version of the message. */\nexport type MmtpFecSessionMessageUpdate = 'raised' | 'unchanged' | 'refused'\n\n/**\n * The path a symbol arrived on: its MoQT subscription, or the multicast\n * group (native SSM or AMT) carrying the same asset. The caller states it;\n * the session never infers it.\n */\nexport type MmtpFecSessionPath = 'moq' | 'multicast'\n\n/** A count per path. */\nexport interface MmtpFecSessionPathCounts {\n moq: number\n multicast: number\n}\n\n/** How the settled blocks that were fed on both paths ended. */\nexport interface MmtpFecSessionMixedPathStats {\n clean: number\n recovered: number\n failed: number\n released: number\n}\n\n/** How the session's blocks and packets ended so far. */\nexport interface MmtpFecSessionStats {\n /** Source symbols fed, counting each SS_ID once. */\n sourceSymbols: number\n /** Repair symbols accepted for decoding. */\n repairSymbols: number\n /** Blocks whose every source arrived. */\n blocksClean: number\n /** Blocks with loss that the decoder completed. */\n blocksRecovered: number\n /** Blocks with loss the decoder could not complete before retiring them. */\n blocksFailed: number\n /** Blocks still unsettled when the session was released. */\n blocksReleased: number\n /** Source packets the session recovered and handed back. */\n packetsRecovered: number\n /** Source packets of failed blocks that never arrived and were not recovered. */\n packetsLost: number\n /**\n * Packets the session could not use, keyed by reason. Two reasons are not\n * loss and belong in no loss tally. `repair:block-settled` counts the\n * repair symbols of a block that had already settled, which on a healthy\n * stream is every repair symbol of a block after the first.\n * `source:decoder-refused` counts source packets the decoder threw on:\n * they were delivered to the parser, only unprotected.\n */\n dropped: Record<string, number>\n /** `sourceSymbols`, by the path each arrived on. */\n sourceSymbolsByPath: MmtpFecSessionPathCounts\n /** `repairSymbols`, by the path each arrived on. */\n repairSymbolsByPath: MmtpFecSessionPathCounts\n /**\n * Blocks whose symbols the session used arrived on both paths -- for\n * instance multicast source symbols and MoQT repair -- by how each ended.\n * Each is also counted under its outcome above. A symbol the session\n * refused, and a repair that arrived after its block was already clean,\n * contributed nothing to its block and do not make it mixed.\n */\n mixedPath: MmtpFecSessionMixedPathStats\n /**\n * fec-01 6.3.9: null until a repair packet at or after the anchor (the\n * earliest block, in SS_Start serial order, whose repair carried\n * RSB_length >= B_0) states an RSB_length below B_0; then why. Terminal:\n * every later repair is refused under `repair:fail-closed` (the packet that\n * failed it is counted there too), and block state stays as it was. Sources\n * are still taken and can still complete a block from repair admitted\n * before the failure.\n */\n failClosed: FecFailClosedReason | null\n}\n\ninterface BlockState {\n ssbLength: number\n received: number\n /** The paths of the symbols used for this block, as PATH_BITS. */\n paths: number\n /** RS_IDs of the repair symbols the decoder accepted for this block. */\n rsIds: Set<number>\n}\n\nconst PATH_BITS: Record<MmtpFecSessionPath, number> = { moq: 1, multicast: 2 }\nconst MIXED_PATHS = PATH_BITS.moq | PATH_BITS.multicast\n\n/**\n * One asset's in-band FEC state. See the file comment.\n */\nexport class MmtpFecSession {\n readonly blockProfile: FecBlockProfile\n readonly algorithm: 'raptorq' | 'reed-solomon'\n readonly sourceSymbols: number\n /** P_0 = B_0, the held total every repair packet is checked against. */\n get repairSymbols(): number {\n return this.#repairSymbols\n }\n readonly symbolSize: number\n /** The decoder's retention, in blocks, derived from the catalog budget. */\n readonly retentionBlocks: number\n\n #decoder: MmtpFecSessionDecoder | null\n #repairSymbols: number\n /** The message entry a catalog-less session was built from; undefined under a catalog. */\n #message: AlFecRaptorQFlowConfig | undefined\n /** Blocks a repair has defined and that are not settled yet, by SS_Start. */\n #blocks = new Map<number, BlockState>()\n /** SS_Starts of settled blocks, so a later repair for one is refused. */\n #settled = new Set<number>()\n /**\n * SS_IDs whose packet the parser has, with the PATH_BITS of the path it was\n * received on, or 0 for one the session recovered.\n */\n #have = new Map<number, number>()\n #newest: number | undefined\n /** The newest SS_ID when the remembered SS_IDs were last pruned. */\n #prunedAt: number | undefined\n #repairPacketId: number | undefined\n #newestSource: number | undefined\n #heldLayers: HeldLayerAdmission\n #anchorHorizon: number\n #stats: MmtpFecSessionStats = {\n sourceSymbols: 0,\n repairSymbols: 0,\n blocksClean: 0,\n blocksRecovered: 0,\n blocksFailed: 0,\n blocksReleased: 0,\n packetsRecovered: 0,\n packetsLost: 0,\n dropped: {},\n sourceSymbolsByPath: { moq: 0, multicast: 0 },\n repairSymbolsByPath: { moq: 0, multicast: 0 },\n mixedPath: { clean: 0, recovered: 0, failed: 0, released: 0 },\n failClosed: null,\n }\n\n /**\n * @throws when the instance lacks or misstates a field the session needs:\n * the scheme, the block profile, K, P_0 (`repairSymbols`), T,\n * `interleaveDepthMs` or `reorderToleranceMs`. None is defaulted.\n */\n constructor(config: MmtpFecSessionConfig | MmtpFecSessionMessageConfig) {\n if (config && 'alFecFlow' in config) {\n const message = alFecRaptorQFlowConfig(config.alFecFlow)\n if (!message) {\n throw new Error('MmtpFecSession: the AL-FEC entry is outside draft-ramadan-moq-mmt-02 8.3 (iso-ssbg1-v1)')\n }\n this.#message = message\n // Coding byte 0x14 is the iso-ssbg1-v1 profile (mmt-02 8.3).\n this.blockProfile = parseFecBlockProfile('iso-ssbg1-v1', 'alFecFlow')\n this.algorithm = message.fecCodeId === FEC_CODE_RAPTORQ ? 'raptorq' : 'reed-solomon'\n this.sourceSymbols = message.maximumK\n this.#repairSymbols = message.maximumP\n this.symbolSize = message.repairSymbolSize\n this.retentionBlocks = requirePositiveInteger(config.retentionBlocks, 'retentionBlocks')\n this.#repairPacketId = message.repairFlowId\n this.#heldLayers = layerZero(message.maximumP)\n this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)\n this.#decoder = buildDecoder(config.Decoder, this)\n return\n }\n const fec = config?.fec\n if (!fec || typeof fec !== 'object') {\n throw new Error('MmtpFecSession: the catalog fec instance is required')\n }\n this.blockProfile = parseFecBlockProfile(fec.blockProfile, 'fec.blockProfile')\n if (fec.scheme !== 'raptorq' && fec.scheme !== 'reed-solomon') {\n throw new Error(`MmtpFecSession: fec.scheme ${JSON.stringify(fec.scheme)} is not implemented`)\n }\n this.algorithm = fec.scheme\n this.sourceSymbols = requirePositiveInteger(fec.sourceSymbols, 'fec.sourceSymbols')\n this.#repairSymbols = requirePositiveInteger(fec.repairSymbols, 'fec.repairSymbols')\n this.symbolSize = requirePositiveInteger(fec.symbolSize, 'fec.symbolSize')\n const interleaveDepthMs = requireNumber(fec.interleaveDepthMs, 'fec.interleaveDepthMs')\n const reorderToleranceMs = requireNumber(fec.reorderToleranceMs, 'fec.reorderToleranceMs')\n this.retentionBlocks = deriveFecRetentionBlocks(\n fecBlockBudgetMs(interleaveDepthMs, reorderToleranceMs),\n interleaveDepthMs,\n )\n this.#heldLayers = new HeldLayerAdmission(\n config.repairLayers ?? [\n {\n layer: 0,\n repairTrackName: typeof fec.repairTrack === 'string' ? fec.repairTrack : '',\n rsIdOffset: 0,\n repairSymbols: this.repairSymbols,\n },\n ],\n config.heldLayers,\n )\n this.#anchorHorizon = heldLayerAnchorHorizon(this.retentionBlocks, this.sourceSymbols)\n this.#decoder = buildDecoder(config.Decoder, this)\n }\n\n /**\n * A later version of the AL-FEC message, catalog-less sessions only. A\n * maximum_p_for_repair_flow above the held total raises it (`raised`): the\n * anchored check restarts at the new total, so a block generated under the\n * previous one stays valid before the first block carrying the new total\n * (fec-core-00 7.3.2, mmt-02 8.3). A field at or below it changes nothing\n * (`unchanged`): no publisher lowers it, and the check catches a wire that\n * does. An entry with another asset, repair flow, code point, K or T, or\n * one outside 8.3, is `refused`: that is a new source encoding, which needs\n * a new session. A latched fail-closed stays latched either way.\n */\n updateAlFecFlow(flow: AlFecFlow): MmtpFecSessionMessageUpdate {\n const held = this.#message\n if (!held) throw new Error('MmtpFecSession: updateAlFecFlow() needs a session built from an AL-FEC entry')\n const next = alFecRaptorQFlowConfig(flow)\n if (\n !next ||\n next.sourcePacketIds[0] !== held.sourcePacketIds[0] ||\n next.repairFlowId !== held.repairFlowId ||\n next.fecCodeId !== held.fecCodeId ||\n next.maximumK !== held.maximumK ||\n next.repairSymbolSize !== held.repairSymbolSize\n ) {\n return 'refused'\n }\n if (next.maximumP <= this.#repairSymbols) return 'unchanged'\n this.#repairSymbols = next.maximumP\n this.#heldLayers = layerZero(next.maximumP)\n return 'raised'\n }\n\n /** True once release() ran. */\n get released(): boolean {\n return this.#decoder === null\n }\n\n get stats(): MmtpFecSessionStats {\n return {\n ...this.#stats,\n dropped: { ...this.#stats.dropped },\n sourceSymbolsByPath: { ...this.#stats.sourceSymbolsByPath },\n repairSymbolsByPath: { ...this.#stats.repairSymbolsByPath },\n mixedPath: { ...this.#stats.mixedPath },\n failClosed: this.#stats.failClosed && { ...this.#stats.failClosed },\n }\n }\n\n /**\n * Feeds one received source packet: the MMTP packet without its four-byte\n * Source FEC Payload ID, under the SS_ID that ID stated. This is what\n * MmtpContainerParser's onSourceSymbol() hands over. `path` is where it\n * was received; a caller that only subscribes over MoQT can leave it out.\n *\n * @returns the protected packets of this packet's block that it recovered,\n * in SS_ID order, for the caller to feed to its parser with\n * `{skipFecPayloadId: true, recovered: true}`. Usually empty.\n */\n addSource(ssId: number, packet: Uint8Array, path: MmtpFecSessionPath = 'moq'): Uint8Array[] {\n const decoder = this.#decoder\n if (!decoder) return this.#drop('source:released')\n const pathBit = PATH_BITS[path]\n if (!pathBit) return this.#drop('source:unknown-path')\n if (!Number.isInteger(ssId) || ssId < 0 || ssId > 0xffff_ffff) return this.#drop('source:bad-ss-id')\n if (!ssbg1FitsSymbol(packet.byteLength, this.symbolSize)) return this.#drop('source:exceeds-symbol')\n if (this.#have.has(ssId)) return this.#drop('source:duplicate')\n this.#have.set(ssId, pathBit)\n this.#stats.sourceSymbols++\n this.#stats.sourceSymbolsByPath[path]++\n this.#noteNewest(ssId)\n if (this.#newestSource === undefined || serial32Precedes(this.#newestSource, ssId)) this.#newestSource = ssId\n\n const start = this.#blockStartOf(ssId)\n const block = start === undefined ? undefined : this.#blocks.get(start)\n if (block) {\n block.received++\n block.paths |= pathBit\n }\n\n let output: Uint8Array | null | undefined\n try {\n output = decoder.add_source(ssId, packet, 0)\n } catch {\n // The packet itself reached the parser; only its protection is lost.\n this.#drop('source:decoder-refused')\n output = undefined\n }\n const recovered: Uint8Array[] = []\n if (start !== undefined && block) {\n if (output) {\n recovered.push(...this.#settleDecoded(start, block, output))\n } else if (block.received >= block.ssbLength) {\n this.#settleClean(start)\n }\n }\n this.#retireBehindHorizon()\n return recovered\n }\n\n /**\n * Feeds one object of the repair track: a whole MMTP repair packet (ISO/IEC\n * 23008-1 C.5.3). A repair track carries one repair flow, so the first\n * repair packet fixes its packet ID and a packet of any other is refused.\n * `path` is where it was received, as for addSource(). `repairTrackName`\n * is the repair track it was received on, for the 6.3.9 own-track rule; a\n * track the layers do not list is refused as `repair:unlisted-track`.\n *\n * @returns the protected packets this repair symbol recovered, as for\n * addSource().\n */\n addRepair(packet: Uint8Array, path: MmtpFecSessionPath = 'moq', repairTrackName?: string): Uint8Array[] {\n const decoder = this.#decoder\n if (!decoder) return this.#drop('repair:released')\n if (this.#stats.failClosed) return this.#drop('repair:fail-closed')\n const pathBit = PATH_BITS[path]\n if (!pathBit) return this.#drop('repair:unknown-path')\n let header\n try {\n header = parseMMTPHeader(packet)\n } catch {\n return this.#drop('repair:malformed')\n }\n // core-00 7.5 / mmt-02 7.3: exactly 12 + 13 + T, judged before any field.\n const admission = admitMmtpRepairObject(packet, header, this.symbolSize, this.algorithm)\n if (!admission) return this.#drop('repair:malformed')\n if (!('frame' in admission)) return this.#drop('repair:length')\n const frame = admission.frame\n if (this.#repairPacketId !== undefined && frame.packetId !== this.#repairPacketId) {\n return this.#drop('repair:foreign-packet-id')\n }\n if (admission.reject) {\n // An RSB_length below a held layer's total goes to the 7.3.2 stop, not\n // to the discard; the block state is unchanged either way.\n const verdict = this.#heldLayers.failsClosed(\n this.#trackFor(repairTrackName), frame.ssStart, frame.rsbLength, this.#anchorHorizon, this.#newestSource,\n )\n if (verdict) {\n this.#stats.failClosed = verdict\n return this.#drop('repair:fail-closed')\n }\n return this.#drop(admission.reject === 'repairRsId' ? 'repair:rs-id' : 'repair:scheme-bound')\n }\n this.#repairPacketId ??= frame.packetId\n if (frame.ssbLength > this.sourceSymbols) return this.#drop('repair:ssb-length')\n // fec-01 6.3.9, after the 4.5 discards and the size check, before a block\n // is placed. Without a track name only the anchored rule (b) applies.\n const verdict = this.#heldLayers.admit(\n this.#trackFor(repairTrackName),\n frame.ssStart,\n frame.rsbLength,\n this.#anchorHorizon,\n this.#newestSource,\n )\n if (verdict === 'discard') return this.#drop('repair:unlisted-track')\n if (verdict !== undefined) {\n this.#stats.failClosed = verdict\n return this.#drop('repair:fail-closed')\n }\n\n const start = frame.ssStart\n let block = this.#blocks.get(start)\n if (block && block.ssbLength !== frame.ssbLength) return this.#drop('repair:conflicting-ssb-length')\n // One repair symbol per (SS_Start, RS_ID), whichever path delivers it\n // first: a second copy never reaches the decoder, the counts or the paths.\n if (block?.rsIds.has(frame.rsId)) return this.#drop('repair:duplicate')\n if (!block) {\n if (this.#settled.has(start)) return this.#drop('repair:block-settled')\n if (this.#behindHorizon(start)) return this.#drop('repair:behind-horizon')\n block = { ssbLength: frame.ssbLength, received: 0, paths: 0, rsIds: new Set() }\n this.#countHeld(start, block)\n this.#blocks.set(start, block)\n }\n this.#noteNewest(serial32Add(start, frame.ssbLength - 1))\n if (block.received >= block.ssbLength) {\n this.#settleClean(start)\n this.#retireBehindHorizon()\n return []\n }\n\n let output: Uint8Array | null | undefined\n try {\n output = decoder.add_repair(start, frame.ssbLength, frame.rsId, frame.symbolData, 0)\n } catch {\n return this.#drop('repair:decoder-refused')\n }\n block.rsIds.add(frame.rsId)\n this.#stats.repairSymbols++\n this.#stats.repairSymbolsByPath[path]++\n block.paths |= pathBit\n const recovered = output ? this.#settleDecoded(start, block, output) : []\n this.#retireBehindHorizon()\n return recovered\n }\n\n /**\n * Frees the decoder and every block. Blocks still unsettled are counted as\n * released, not failed: nothing proved them unrecoverable. Idempotent;\n * returns whether this call released anything.\n */\n release(): boolean {\n const decoder = this.#decoder\n if (!decoder) return false\n this.#decoder = null\n this.#stats.blocksReleased += this.#blocks.size\n for (const block of this.#blocks.values()) this.#noteMixed(block, 'released')\n this.#blocks.clear()\n this.#settled.clear()\n this.#have.clear()\n decoder.free()\n return true\n }\n\n /** Catalog-less: layer 0 has no own-track clause (mmt-02 8.3), so no track name is judged. */\n #trackFor(repairTrackName: string | undefined): string | undefined {\n return this.#message ? undefined : repairTrackName\n }\n\n #drop(reason: string): Uint8Array[] {\n this.#stats.dropped[reason] = (this.#stats.dropped[reason] ?? 0) + 1\n return []\n }\n\n #noteNewest(ssId: number): void {\n if (this.#newest === undefined || serial32Precedes(this.#newest, ssId)) this.#newest = ssId\n }\n\n #blockStartOf(ssId: number): number | undefined {\n for (const [start, block] of this.#blocks) {\n if (isoBlockContains(start, block.ssbLength, ssId)) return start\n }\n return undefined\n }\n\n /** Counts the sources of a new block the parser already has, and their paths. */\n #countHeld(start: number, block: BlockState): void {\n for (let i = 0; i < block.ssbLength; i++) {\n const pathBit = this.#have.get(serial32Add(start, i))\n if (pathBit === undefined) continue\n block.received++\n block.paths |= pathBit\n }\n }\n\n #noteMixed(block: BlockState, outcome: keyof MmtpFecSessionMixedPathStats): void {\n if ((block.paths & MIXED_PATHS) === MIXED_PATHS) this.#stats.mixedPath[outcome]++\n }\n\n /** The decoder's own rule: more than its retention behind, counted from below. */\n #behindHorizon(start: number): boolean {\n const newest = this.#newest\n if (newest === undefined) return false\n return isoBlocksBehindLowerBound(start, newest, this.sourceSymbols) > this.retentionBlocks\n }\n\n #settle(start: number): void {\n this.#blocks.delete(start)\n this.#settled.add(start)\n }\n\n #settleClean(start: number): void {\n const block = this.#blocks.get(start)\n if (!block) return\n this.#settle(start)\n this.#stats.blocksClean++\n this.#noteMixed(block, 'clean')\n try {\n this.#decoder?.cleanup_blocks_detailed(Uint32Array.of(start))\n } catch {\n // The decoder retires the block by its own horizon instead.\n }\n }\n\n /**\n * The decoder returns a completed block as its K' ssbg_mode1 symbols,\n * concatenated: split by T, trim by each symbol's length field, and hand\n * back the packets the parser does not have yet.\n */\n #settleDecoded(start: number, block: BlockState, output: Uint8Array): Uint8Array[] {\n this.#settle(start)\n if (output.byteLength !== block.ssbLength * this.symbolSize) {\n this.#stats.blocksFailed++\n this.#stats.packetsLost += block.ssbLength - block.received\n this.#noteMixed(block, 'failed')\n this.#drop('block:decoded-length')\n return []\n }\n const recovered: Uint8Array[] = []\n let refused = 0\n for (let i = 0; i < block.ssbLength; i++) {\n const ssId = serial32Add(start, i)\n if (this.#have.has(ssId)) continue\n const symbol = output.subarray(i * this.symbolSize, (i + 1) * this.symbolSize)\n const packet = trimSsbg1SourceSymbol(symbol)\n if (!packet) {\n refused++\n continue\n }\n this.#have.set(ssId, 0)\n recovered.push(packet.slice())\n }\n if (refused > 0) {\n this.#stats.blocksFailed++\n this.#stats.packetsLost += refused\n this.#noteMixed(block, 'failed')\n this.#drop('block:bad-ssbg1-symbol')\n } else if (recovered.length === 0) {\n this.#stats.blocksClean++\n this.#noteMixed(block, 'clean')\n } else {\n this.#stats.blocksRecovered++\n this.#noteMixed(block, 'recovered')\n }\n this.#stats.packetsRecovered += recovered.length\n return recovered\n }\n\n /**\n * Retires every block, and every SS_ID the session remembers, that the\n * newest SS_ID has moved past the retention. An unsettled block retired\n * here is a failure: more of its sources were lost than its repair could\n * restore, or its repair never arrived.\n */\n #retireBehindHorizon(): void {\n const decoder = this.#decoder\n const newest = this.#newest\n if (!decoder || newest === undefined) return\n try {\n decoder.cleanup_by_ss_start(newest)\n } catch {\n // Block fate below does not depend on the decoder's own sweep.\n }\n for (const [start, block] of this.#blocks) {\n if (!this.#behindHorizon(start)) continue\n this.#settle(start)\n this.#stats.blocksFailed++\n this.#stats.packetsLost += block.ssbLength - block.received\n this.#noteMixed(block, 'failed')\n }\n // What is remembered only needs pruning once a block's worth of SS_IDs\n // has gone by, not on every packet.\n const prunedAt = this.#prunedAt\n // #newest only moves forward, so the distance is how far it has moved.\n if (prunedAt !== undefined && serial32Distance(prunedAt, newest) < this.sourceSymbols) return\n this.#prunedAt = newest\n for (const ssId of this.#have.keys()) {\n if (this.#behindHorizon(ssId)) this.#have.delete(ssId)\n }\n for (const start of this.#settled) {\n if (this.#behindHorizon(start)) this.#settled.delete(start)\n }\n }\n}\n\nfunction layerZero(total: number): HeldLayerAdmission {\n return new HeldLayerAdmission([{ layer: 0, repairTrackName: '', rsIdOffset: 0, repairSymbols: total }])\n}\n\nfunction buildDecoder(Decoder: MmtpFecSessionDecoderConstructor, session: MmtpFecSession): MmtpFecSessionDecoder {\n if (typeof Decoder !== 'function') {\n throw new Error('MmtpFecSession: a decoder constructor (wasm_bindgen.MmtFecDecoder) is required')\n }\n const decoder = new Decoder(session.retentionBlocks, session.blockProfile, session.algorithm)\n try {\n decoder.configure_params(session.sourceSymbols, session.symbolSize)\n } catch (error) {\n decoder.free()\n throw error\n }\n return decoder\n}\n\nfunction requirePositiveInteger(value: unknown, path: string): number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {\n throw new Error(`MmtpFecSession: ${path} must be a positive integer (got ${String(value)})`)\n }\n return value\n}\n\nfunction requireNumber(value: unknown, path: string): number {\n if (typeof value !== 'number') {\n throw new Error(`MmtpFecSession: ${path} is required (got ${String(value)}); it is never defaulted`)\n }\n return value\n}\n", "/**\n * The fec-01 6.3.9 receiver check (held-layer anchored RSB_length), in one\n * place. FECManager and MmtpFecSession both run their repair packets through\n * HeldLayerAdmission; neither restates the rule.\n */\n\nimport { serial32Distance, serial32Precedes } from '@blockcast/mmt-base'\n\n/**\n * Why a source track's FEC failed closed at runtime (fec-01 6.3.9): a repair\n * packet stated an RSB_length below a held layer's total B_i on that layer's\n * own repair track (`below_own_track_total`), or at or after the anchor, the\n * earliest block in SS_Start serial order whose repair carried >= B_i\n * (`below_anchored_total`). Repair for the track is refused from then on.\n */\nexport interface FecFailClosedReason {\n code: 'below_own_track_total' | 'below_anchored_total'\n layer: number\n total: number\n rsbLength: number\n ssStart: number\n anchor?: number\n}\n\n/** One repair layer as the catalog states it: B_i = rsIdOffset + repairSymbols. */\nexport interface HeldLayerInput {\n layer: number\n repairTrackName: string\n rsIdOffset: number\n repairSymbols: number\n}\n\ninterface HeldLayerState {\n layer: number\n repairTrackName: string\n total: number\n /** Earliest (serial order) block start whose repair carried >= total. */\n anchor?: number\n /** Latest (serial order) block start whose repair carried < total. */\n latestBelow?: number\n /** RSB_length that latestBelow's repair carried, for the fail-closed reason. */\n latestBelowRsb?: number\n /** Newest block start seen; anchor and latestBelow are retired behind it. */\n newest?: number\n /**\n * The anchor fell behind the horizon, so no block that can still arrive\n * precedes it: every later packet must carry >= total. Its serial is no\n * longer compared, so SS_Start wrapping past it cannot misorder it.\n */\n settled?: boolean\n}\n\n/** The largest horizon compared, so every compared serial stays in one half-window. */\nconst HORIZON_CAP = 0x40000000\n\n/**\n * How far (in SS_Start symbols) behind the newest block a 6.3.9 anchor or\n * below-total block can still matter: the iso retention in blocks times K,\n * capped at 2^30. Unknown inputs give the cap.\n */\nexport function heldLayerAnchorHorizon(retentionBlocks: number | undefined, k: number): number {\n return retentionBlocks === undefined || k <= 0 ? HORIZON_CAP : Math.min(HORIZON_CAP, retentionBlocks * k)\n}\n\n/**\n * fec-01 6.3.9 anchored state for one FEC instance's held layers. For each\n * held layer i with total B_i = O_i + P_i: the anchor is the earliest block,\n * in SS_Start serial order, whose repair carried RSB_length >= B_i. The\n * instance fails closed on (a) a packet on layer i's own repair track (i > 0)\n * below B_i, or (b) any packet of the instance at or after the anchor below\n * B_i. Blocks before the anchor (pre-append) stay valid at the previous total.\n * The result is not latched here: the caller owns what failing closed means.\n */\nexport class HeldLayerAdmission {\n readonly #layerNames: ReadonlySet<string>\n readonly #states: HeldLayerState[]\n\n constructor(layers: readonly HeldLayerInput[], heldLayers: readonly number[] = [0]) {\n const held = new Set(heldLayers)\n this.#layerNames = new Set(layers.map((l) => l.repairTrackName))\n this.#states = layers\n .filter((l) => held.has(l.layer))\n .map((l) => ({ layer: l.layer, repairTrackName: l.repairTrackName, total: l.rsIdOffset + l.repairSymbols }))\n }\n\n /**\n * Judges one repair packet, after the 4.5 discards and the size check and\n * before it places a block. `repairTrackName` is the subscribed repair\n * track, never inferred from RS_ID; without it only (b) runs. A packet on a\n * repair track the catalog does not list is `'discard'` and anchors nothing.\n * `sourceFrontier` is the newest source SS_ID, if any: a repair more than\n * `horizon` past it is judged without moving the anchored state.\n *\n * @returns undefined to admit, `'discard'`, or the fail-closed reason.\n */\n admit(\n repairTrackName: string | undefined,\n ssStart: number,\n rsbLength: number,\n horizon: number,\n sourceFrontier?: number,\n ): FecFailClosedReason | 'discard' | undefined {\n return this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, true)\n }\n\n #admit(\n repairTrackName: string | undefined,\n ssStart: number,\n rsbLength: number,\n horizon: number,\n sourceFrontier: number | undefined,\n retroactive: boolean,\n ): FecFailClosedReason | 'discard' | undefined {\n if (repairTrackName !== undefined && !this.#layerNames.has(repairTrackName)) return 'discard'\n // A repair may run ahead of the media, but one more than `horizon` past the\n // source frontier must not move the held-layer state: advancing `newest` on\n // it would retire every real anchor and skip this check until traffic caught\n // up. Such a repair is still judged below the total: it is on its own track,\n // or after any anchor by definition. With no anchor yet it says nothing.\n // It is still admitted: in iso the frontier only moves when a repair places\n // held sources, so dropping it would stall recovery after a long outage.\n // IsoBlockLocator.placeRepair may still refuse the same repair as `ahead`:\n // it measures from `newest`, which held sources move, not from this frontier.\n const farAhead =\n sourceFrontier !== undefined &&\n serial32Precedes(sourceFrontier, ssStart) &&\n serial32Distance(sourceFrontier, ssStart) > horizon\n for (const state of this.#states) {\n let code: FecFailClosedReason['code'] | undefined\n if (farAhead) {\n if (rsbLength < state.total) {\n if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'\n else if (state.anchor !== undefined || state.settled) code = 'below_anchored_total'\n }\n } else {\n // Serials more than `horizon` behind the newest block are retired, so\n // every serial still compared sits inside one half-window and SS_Start\n // wrapping on a long-running stream cannot reorder them.\n if (state.newest === undefined || serial32Precedes(state.newest, ssStart)) state.newest = ssStart\n const stale = (s: number) => serial32Distance(s, state.newest!) > horizon\n if (state.latestBelow !== undefined && stale(state.latestBelow)) {\n delete state.latestBelow\n delete state.latestBelowRsb\n }\n if (state.anchor !== undefined && stale(state.anchor)) state.settled = true\n if (rsbLength < state.total) {\n if (state.layer > 0 && repairTrackName === state.repairTrackName) code = 'below_own_track_total'\n else if (stale(ssStart)) continue // past retention: undecodable, anchors nothing\n else if (state.settled || (state.anchor !== undefined && !serial32Precedes(ssStart, state.anchor))) code = 'below_anchored_total'\n else if (state.latestBelow === undefined || serial32Precedes(state.latestBelow, ssStart)) {\n state.latestBelow = ssStart\n state.latestBelowRsb = rsbLength\n }\n } else if (!state.settled && !stale(ssStart)) {\n if (state.anchor === undefined || serial32Precedes(ssStart, state.anchor)) state.anchor = ssStart\n if (retroactive && state.latestBelow !== undefined && !serial32Precedes(state.latestBelow, state.anchor)) {\n code = 'below_anchored_total'\n ssStart = state.latestBelow\n rsbLength = state.latestBelowRsb!\n }\n }\n }\n if (code !== undefined) {\n return {\n code, layer: state.layer, total: state.total, rsbLength, ssStart,\n ...(state.anchor === undefined ? {} : { anchor: state.anchor }),\n }\n }\n }\n return undefined\n }\n\n /**\n * The core-00 7.5 carve-out for a repair object about to be discarded\n * (RS_ID or scheme bound): whether its own RSB_length, below a held layer's\n * total on that layer's track or on an anchored block, fails the instance\n * closed under 7.3.2. Leaves the anchored state as it was: a discarded\n * object anchors nothing.\n */\n failsClosed(\n repairTrackName: string | undefined,\n ssStart: number,\n rsbLength: number,\n horizon: number,\n sourceFrontier?: number,\n ): FecFailClosedReason | undefined {\n const saved = this.#states.map((state) => ({ ...state }))\n try {\n // No retroactive verdict: a discarded object anchors nothing, so it\n // cannot fail an earlier below-total block, and every held layer is\n // still judged on the object's own RSB_length.\n const verdict = this.#admit(repairTrackName, ssStart, rsbLength, horizon, sourceFrontier, false)\n return typeof verdict === 'object' ? verdict : undefined\n } finally {\n saved.forEach((state, i) => { this.#states[i] = state })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["verdict"]
|
|
7
7
|
}
|