@blockcast/mmt-container 2.1.0 → 2.2.0-main.079cd7664c0b

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.
Files changed (99) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1271 -247
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +65 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +213 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +37 -2
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +98 -28
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +222 -45
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +97 -28
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +25 -2
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +1718 -54
  56. package/dist/mmtp-router.js.map +4 -4
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.d.ts +15 -2
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +15652 -14939
  62. package/dist/multicast-manager.js.map +4 -4
  63. package/dist/multicast-transport.d.ts +11 -3
  64. package/dist/multicast-transport.d.ts.map +1 -1
  65. package/dist/multicast-transport.js +281 -35
  66. package/dist/multicast-transport.js.map +4 -4
  67. package/dist/multicast-unwrap.js +12 -19
  68. package/dist/multicast-unwrap.js.map +2 -2
  69. package/dist/no-modules.js +215 -42
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.js.map +1 -1
  72. package/dist/repair-layer-subscriptions.d.ts +82 -0
  73. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  74. package/dist/repair-layer-subscriptions.js +192 -0
  75. package/dist/repair-layer-subscriptions.js.map +7 -0
  76. package/dist/signaling.js.map +2 -2
  77. package/dist/timestamp-codec.js +12 -19
  78. package/dist/timestamp-codec.js.map +2 -2
  79. package/dist/track-session.d.ts +1 -1
  80. package/dist/track-session.js.map +1 -1
  81. package/dist/wasm-mfu-reassembler.js.map +1 -1
  82. package/dist/wasm-mmtp-container-parser.js +97 -28
  83. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  84. package/package.json +8 -5
  85. package/schemas/catalog.schema.json +4 -0
  86. package/src/catalog-fec-params.ts +36 -1
  87. package/src/cmcd-producer.ts +194 -179
  88. package/src/fec-manager.ts +256 -6
  89. package/src/fec-worker-setup.ts +2 -2
  90. package/src/index.ts +18 -2
  91. package/src/iso-block-locator.ts +19 -0
  92. package/src/iso-bmff-timing.ts +40 -1
  93. package/src/mmtp-container-parser.ts +96 -8
  94. package/src/mmtp-fec-session.ts +115 -15
  95. package/src/mmtp-router.ts +344 -27
  96. package/src/multicast-manager.ts +26 -5
  97. package/src/multicast-transport.ts +44 -20
  98. package/src/repair-layer-subscriptions.ts +261 -0
  99. package/src/track-session.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/signalling.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/fec-bindings.ts", "../src/signaling.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 * 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-01 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-01 Section 7.2). 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 (moq-fec-01 Section 7.3). */\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-01 Sections 8.3\n * and 12.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 (moq-fec-01 Section 7.3);\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-01 Section 12.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 */\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 * 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 * MMTP Signaling Parser for Multicast\n *\n * Provides packet type detection and AL-FEC signaling message parsing.\n * Used by the multicast pipeline to classify and route MMTP packets.\n *\n * Packet types (ISO/IEC 23008-1:2023 Section 9.2.2):\n * - 0x00: MPU (Media Processing Unit) - video/audio data\n * - 0x02: Signaling - control messages including AL-FEC config\n * - 0x03: Repair - FEC repair symbols\n */\nimport {\n\tALFEC_MESSAGE_ID,\n\tFEC_CODE_RAPTORQ,\n\tFEC_CODE_REED_SOLOMON,\n\tPACKET_TYPE_MPU,\n\tPACKET_TYPE_REPAIR,\n\tPACKET_TYPE_SIGNALING,\n\tparseAlFecMessage,\n} from \"@blockcast/mmt-base\";\n\nexport {\n\tALFEC_MESSAGE_ID,\n\tFEC_CODE_RAPTORQ,\n\tFEC_CODE_REED_SOLOMON,\n\tPACKET_TYPE_MPU,\n\tPACKET_TYPE_REPAIR,\n\tPACKET_TYPE_SIGNALING,\n\tparseAlFecMessage,\n};\n\nexport type PacketClass = \"source\" | \"signaling\" | \"repair\" | \"unknown\";\n\n/** Get the MMTP packet type from raw multicast packet bytes.\n *\n * ISO/IEC 23008-1:2023 Figure 8 (V=0): byte 1 is\n * reserved(2)|packet_type(6), so signaling is 0x02 and repair is 0x03.\n * Pre-migration FFmpeg high-bits bytes (0x08/0x0c) are NOT accepted:\n * writers and readers move together in the pinned smoke train.\n */\nexport function getPacketType(mmtpPacket: Uint8Array): number {\n\tif (mmtpPacket.length < 2) return -1;\n\treturn mmtpPacket[1] & 0x3f;\n}\n\n/** Check if packet is a signaling packet (type 0x02). */\nexport function isSignalingPacket(mmtpPacket: Uint8Array): boolean {\n\treturn getPacketType(mmtpPacket) === PACKET_TYPE_SIGNALING;\n}\n\n/** Determine if packet is a source (video/audio) packet. */\nexport function isSourcePacket(mmtpPacket: Uint8Array): boolean {\n\tif (mmtpPacket.length < 2) return false;\n\tconst packetType = mmtpPacket[1] & 0x3f;\n\tconst fecType = (mmtpPacket[0] >> 3) & 0x03;\n\treturn packetType === 0x00 && fecType <= 1;\n}\n\n/** Classify MMTP packet for multicast pipeline routing. */\nexport function classifyPacket(mmtpPacket: Uint8Array): PacketClass {\n\tif (mmtpPacket.length < 2) return \"unknown\";\n\tconst packetType = mmtpPacket[1] & 0x3f;\n\tconst fecType = (mmtpPacket[0] >> 3) & 0x03;\n\tif ((mmtpPacket[0] >> 6) !== 0) return \"unknown\";\n\tswitch (packetType) {\n\t\tcase 0x00:\n\t\t\tif (mmtpPacket.length < 20) return \"unknown\";\n\t\t\tif (fecType >= 2) return \"repair\";\n\t\t\treturn \"source\";\n\t\tcase 0x02:\n\t\t\treturn \"signaling\";\n\t\tcase 0x03:\n\t\t\tif (mmtpPacket.length < 20) return \"unknown\";\n\t\t\treturn \"repair\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n"],
5
- "mappings": ";AAqEO,IAAM,mBAAmB;AACzB,IAAM,kBAAkB;AAI/B,IAAM,wBAAwB;AAC9B,IAAM,4BAA4B,kBAAkB;AAI7C,IAAM,kBAAkB;AACxB,IAAM,wBAAwB;AAC9B,IAAM,qBAAqB;AAG3B,IAAM,mBAAmB;AAgBzB,IAAM,wBAAwB;AAC9B,IAAM,mBAAmB;AAK1B,SAAU,gBAAgB,QAAkB;AAChD,MAAI,OAAO,SAAS,kBAAkB;AACpC,UAAM,IAAI,MAAM,0BAA0B,OAAO,MAAM,QAAQ;EACjE;AAEA,QAAM,OAAO,IAAI,SAAS,OAAO,QAAQ,OAAO,YAAY,OAAO,UAAU;AAE7E,QAAM,UAAW,OAAO,CAAC,KAAM,IAAK;AACpC,MAAI,YAAY,GAAG;AACjB,UAAM,IAAI,MAAM,6BAA6B,OAAO,EAAE;EACxD;AAGA,QAAM,UAAW,OAAO,CAAC,KAAM,IAAK;AACpC,QAAM,qBAAqB,OAAO,CAAC,IAAK,QAAU;AAClD,QAAM,iBAAiB,OAAO,CAAC,IAAK,OAAU;AAG9C,QAAM,WAAW,OAAO,CAAC,IAAK,OAAU;AAIxC,QAAM,aAAa,OAAO,CAAC,IAAK;AAEhC,MAAI,SAAS;AAGb,QAAM,WAAW,KAAK,UAAU,QAAQ,KAAK;AAC7C,YAAU;AAGV,QAAM,YAAY,OAAO,KAAK,UAAU,QAAQ,KAAK,CAAC;AACtD,YAAU;AAGV,QAAM,iBAAiB,KAAK,UAAU,QAAQ,KAAK;AACnD,YAAU;AAEV,MAAI;AACJ,MAAI,mBAAmB;AACrB,QAAI,SAAS,IAAI,OAAO;AAAQ,YAAM,IAAI,MAAM,yCAAyC;AACzF,oBAAgB,KAAK,UAAU,QAAQ,KAAK;AAC5C,cAAU;EACZ;AAEA,MAAI;AACJ,MAAI,eAAe;AACjB,QAAI,SAAS,IAAI,OAAO;AAAQ,YAAM,IAAI,MAAM,2CAA2C;AAC3F,UAAM,OAAO,KAAK,UAAU,QAAQ,KAAK;AACzC,UAAM,SAAS,KAAK,UAAU,SAAS,GAAG,KAAK;AAC/C,cAAU;AACV,QAAI,SAAS,SAAS,OAAO;AAAQ,YAAM,IAAI,MAAM,iDAAiD;AACtG,gBAAY,EAAE,MAAM,MAAM,OAAO,SAAS,QAAQ,SAAS,MAAM,EAAC;AAClE,cAAU;EACZ;AAEA,SAAO;IACL;IACA;IACA;IACA;IACA;IACA,eAAe,OAAO,SAAS;IAC/B;IACA;IACA;IACA;IACA;IACA,cAAc;IACd;;AAEJ;;;ACpKO,IAAM,iBAAiB;AAGvB,IAAM,oBAAoB,KAAK;AAQ/B,IAAM,uBAAuB,MAAM,iBAAiB;;;ACa3D,IAAM,sBAAN,cAAkC,MAAK;;AAEvC,IAAM,aAAN,MAAgB;EACL;EACA;EACT;EAEA,YAAY,OAAmB,QAAQ,GAAG,MAAM,MAAM,QAAM;AAC1D,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,OAAO;EACd;EAEA,IAAI,SAAM;AACR,WAAO,KAAK;EACd;EAEA,IAAI,YAAS;AACX,WAAO,KAAK,OAAO,KAAK;EAC1B;EAEA,MAAM,QAAc;AAClB,QAAI,SAAS,KAAK,SAAS,KAAK;AAAW,YAAM,IAAI,oBAAoB,WAAW;AACpF,UAAM,KAAK,KAAK;AAChB,SAAK,WAAW;AAChB,WAAO;EACT;EAEA,KAAE;AACA,WAAO,KAAK,OAAO,KAAK,MAAM,CAAC,CAAC;EAClC;EAEA,MAAG;AACD,UAAM,KAAK,KAAK,MAAM,CAAC;AACvB,WAAQ,KAAK,OAAO,EAAE,KAAM,IAAK,KAAK,OAAO,KAAK,CAAC;EACrD;EAEA,MAAG;AACD,UAAM,KAAK,KAAK,MAAM,CAAC;AACvB,WAAQ,KAAK,OAAO,EAAE,KAAM,KAAO,KAAK,OAAO,KAAK,CAAC,KAAM,IAAK,KAAK,OAAO,KAAK,CAAC;EACpF;EAEA,MAAG;AACD,UAAM,KAAK,KAAK,MAAM,CAAC;AACvB,YAAS,KAAK,OAAO,EAAE,KAAM,KAAO,KAAK,OAAO,KAAK,CAAC,KAAM,KACzD,KAAK,OAAO,KAAK,CAAC,KAAM,IAAK,KAAK,OAAO,KAAK,CAAC,OAAQ;EAC5D;;EAGA,MAAM,QAAc;AAClB,UAAM,KAAK,KAAK,MAAM,MAAM;AAC5B,WAAO,KAAK,OAAO,MAAM,IAAI,KAAK,MAAM;EAC1C;;EAGA,YAAS;AACP,QAAI,KAAK,cAAc;AAAG,YAAM,IAAI,oBAAoB,gBAAgB;EAC1E;;AAGF,SAAS,KAAK,QAAc;AAC1B,QAAM,IAAI,oBAAoB,MAAM;AACtC;AAGA,SAAS,MAAS,OAAqB;AACrC,MAAI;AACF,WAAO,MAAK;EACd,SAAS,OAAO;AACd,QAAI,iBAAiB;AAAqB,aAAO;AACjD,UAAM;EACR;AACF;AA0BO,IAAM,oCAAoC;AAU3C,SAAU,uBAAuB,YAAsB;AAC3D,MAAI;AACJ,MAAI;AACF,aAAS,gBAAgB,UAAU;EACrC,QAAQ;AACN,WAAO;EACT;AACA,MAAI,OAAO,eAAe,yBAAyB,OAAO,YAAY;AAAG,WAAO;AAChF,QAAM,UAAU,WAAW,SAAS,OAAO,YAAY;AACvD,SAAO,MAAM,MAAM,uBAAuB,OAAO,CAAC;AACpD;AAEA,SAAS,uBAAuB,SAAmB;AACjD,MAAI,QAAQ,SAAS;AAAG,SAAK,0BAA0B;AACvD,QAAM,SAAS,IAAI,WAAW,OAAO;AACrC,QAAM,QAAQ,OAAO,GAAE;AACvB,QAAM,gBAAgB,SAAS;AAC/B,QAAM,kBAAmB,SAAS,IAAK,OAAO;AAC9C,QAAM,cAAc,QAAQ,OAAO;AACnC,QAAM,kBAAkB,OAAO,GAAE;AACjC,MAAI,kBAAkB;AAAmC,SAAK,+BAA+B;AAG7F,MAAI,oBAAoB;AAAG,SAAK,oCAAoC;AAEpE,MAAI,CAAC,YAAY;AAEf,QAAI,OAAO,cAAc;AAAG,WAAK,0BAA0B;AAC3D,WAAO,EAAE,MAAM,eAAe,UAAU,CAAC,QAAQ,SAAS,OAAO,MAAM,CAAC,EAAC;EAC3E;AAEA,QAAM,WAAyB,CAAA;AAC/B,SAAO,OAAO,YAAY,GAAG;AAC3B,UAAM,SAAS,iBAAiB,OAAO,IAAG,IAAK,OAAO,IAAG;AACzD,QAAI,WAAW;AAAG,WAAK,iBAAiB;AACxC,UAAM,QAAQ,OAAO;AACrB,WAAO,MAAM,MAAM;AACnB,aAAS,KAAK,QAAQ,SAAS,OAAO,QAAQ,MAAM,CAAC;EACvD;AACA,MAAI,SAAS,WAAW;AAAG,SAAK,oCAAoC;AACpE,SAAO,EAAE,MAAM,eAAe,SAAQ;AACxC;AAMO,IAAM,gBAAgB;AACtB,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;AAE7B,IAAM,0BAA0B;AAMjC,SAAU,gCAAgC,WAAiB;AAC/D,SAAO,cAAc,iBAClB,aAAa,wBAAwB,aAAa,uBAClD,aAAa,SAAU,aAAa;AACzC;AAMA,SAAS,kBAAkB,SAAmB;AAC5C,QAAM,SAAS,IAAI,WAAW,OAAO;AACrC,QAAM,YAAY,OAAO,IAAG;AAC5B,QAAM,UAAU,OAAO,GAAE;AACzB,QAAM,SAAS,gCAAgC,SAAS,IAAI,OAAO,IAAG,IAAK,OAAO,IAAG;AAErF,MAAI,WAAW,KAAK,WAAW,OAAO;AAAW,SAAK,yBAAyB;AAC/E,SAAO,EAAE,QAAQ,WAAW,QAAO;AACrC;AAOO,IAAM,4BAA4B;AAClC,IAAM,iCAAiC;AACvC,IAAM,iCAAiC;AACvC,IAAM,mCAAmC;AAMzC,IAAM,cAAc;AAyE3B,SAAS,iBAAiB,QAAkB;AAC1C,QAAM,OAAO,OAAO,GAAE;AACtB,SAAO,EAAE,aAAc,QAAQ,GAAa,MAAM,OAAO,MAAM,OAAO,GAAI,EAAC;AAC7E;AAEA,SAAS,mBACP,QACA,cACA,WACA,cAA2C;AAE3C,QAAM,WAAW,OAAO,IAAG;AAC3B,QAAM,WAAW,OAAO,IAAG;AAC3B,QAAM,uBAAuB,OAAO,IAAG;AACvC,QAAM,uBAAuB,OAAO,IAAG;AACvC,SAAO;IACL;IACA;IACA,GAAI,eAAe,EAAE,aAAY,IAAK,CAAA;IACtC;IACA;IACA;IACA;;AAEJ;AAEA,SAAS,eAAe,QAAoB,gBAAuB;AACjE,QAAM,eAAe,OAAO,IAAG;AAC/B,QAAM,YAAY,OAAO,GAAE;AAC3B,QAAM,eAAe,iBAAiB,iBAAiB,MAAM,IAAI;AACjE,SAAO,mBAAmB,QAAQ,cAAc,WAAW,YAAY;AACzE;AAEA,SAAS,YAAY,QAAoB,gBAAuB;AAC9D,QAAM,YAAY,OAAO,GAAE;AAC3B,QAAM,eAAe,OAAO,GAAE;AAC9B,QAAM,iBAAiB,OAAO,GAAE;AAChC,MAAI,mBAAmB;AAAG,SAAK,wBAAwB;AACvD,QAAM,YAAsB,CAAA;AAC5B,WAAS,IAAI,GAAG,IAAI,gBAAgB;AAAK,cAAU,KAAK,OAAO,IAAG,CAAE;AAEpE,QAAM,SAAS,OAAO,GAAE;AACxB,QAAM,qBAAqB,UAAU;AACrC,QAAM,WAAY,UAAU,IAAK;AACjC,QAAM,cAAgB,UAAU,IAAK;AACrC,QAAM,mBAAoB,SAAS;AACnC,MAAI,aAAa;AAAM,SAAK,oBAAoB;AAChD,QAAM,mBAAmB,OAAO,IAAG;AAEnC,QAAM,OAAkB;IACtB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,aAAa,CAAA;;AAEf,MAAI,aAAa,aAAa;AAC5B,SAAK,YAAY;MACf,0BAA0B,OAAO,IAAG;MACpC,iCAAiC,OAAO,IAAG;;EAE/C;AAEA,UAAQ,oBAAoB;IAC1B,KAAK;AACH;IACF,KAAK;AACH,WAAK,YAAY,KAAK,eAAe,QAAQ,cAAc,CAAC;AAC5D;IACF,KAAK,gCAAgC;AACnC,WAAK,4BAA4B,OAAO,GAAE;AAC1C,YAAM,sBAAsB,OAAO,GAAE;AACrC,eAAS,IAAI,GAAG,IAAI,qBAAqB,KAAK;AAC5C,aAAK,YAAY,KAAK,eAAe,QAAQ,cAAc,CAAC;MAC9D;AACA;IACF;IACA,KAAK,kCAAkC;AACrC,YAAM,iBAAiB,OAAO,GAAE;AAChC,YAAM,YAAY,OAAO,GAAE;AAC3B,YAAM,eAAe,iBAAiB,iBAAiB,MAAM,IAAI;AACjE,eAAS,IAAI,GAAG,IAAI,gBAAgB,KAAK;AACvC,aAAK,YAAY,KAAK,mBAAmB,QAAQ,OAAO,IAAG,GAAI,WAAW,YAAY,CAAC;MACzF;AACA;IACF;IACA;AAKE,WAAK,oCAAoC,kBAAkB,EAAE;EACjE;AACA,SAAO;AACT;AAOM,SAAU,sBAAsB,SAAmB;AACvD,SAAO,MAAM,MAAK;AAChB,UAAM,EAAE,QAAQ,WAAW,QAAO,IAAK,kBAAkB,OAAO;AAChE,QAAI,cAAc;AAAkB,aAAO;AAC3C,UAAM,QAAQ,OAAO,GAAE;AACvB,UAAM,UAAW,SAAS;AAC1B,UAAM,iBAAmB,SAAS,IAAK;AACvC,QAAI,YAAY,GAAG;AAGjB,aAAO,UAAS;AAChB,aAAO,EAAE,SAAS,SAAS,gBAAgB,OAAO,CAAA,EAAE;IACtD;AACA,UAAM,mBAAmB,OAAO,IAAG;AACnC,QAAI,qBAAqB,KAAK,qBAAqB,OAAO,WAAW;AACnE,WAAK,wCAAwC;IAC/C;AACA,UAAM,mBAAmB,OAAO,GAAE;AAClC,QAAI,qBAAqB;AAAG,WAAK,6BAA6B;AAC9D,UAAM,QAAqB,CAAA;AAC3B,aAAS,IAAI,GAAG,IAAI,kBAAkB;AAAK,YAAM,KAAK,YAAY,QAAQ,mBAAmB,CAAC,CAAC;AAC/F,WAAO,UAAS;AAChB,WAAO,EAAE,SAAS,SAAS,gBAAgB,MAAK;EAClD,CAAC;AACH;AAOM,SAAU,kBAAkB,YAAsB;AACtD,QAAM,SAAS,sBAAsB,UAAU;AAC/C,MAAI,CAAC;AAAQ,WAAO;AACpB,QAAM,QAAQ,OAAO,SAAS,OAC5B,CAAC,YAA0E,QAAQ,SAAS,QAAQ;AAEtG,MAAI,MAAM,WAAW;AAAG,WAAO;AAC/B,SAAO,EAAE,GAAG,MAAM,CAAC,EAAG,SAAS,aAAa,OAAO,YAAW;AAChE;AAiIA,SAAS,kBAAkB,QAAkB;AAC3C,QAAM,eAAe,OAAO,GAAE;AAC9B,UAAQ,cAAc;IACpB,KAAK;AACH,aAAO,EAAE,cAAc,UAAU,OAAO,IAAG,EAAE;IAC/C,KAAK;IACL,KAAK,GAAM;AACT,YAAM,gBAAgB,iBAAiB,IAAO,IAAI;AAClD,YAAM,aAAa,OAAO,MAAM,aAAa;AAC7C,YAAM,aAAa,OAAO,MAAM,aAAa;AAC7C,YAAM,UAAU,OAAO,IAAG;AAC1B,aAAO,EAAE,cAAc,YAAY,YAAY,SAAS,UAAU,OAAO,IAAG,EAAE;IAChF;IACA,KAAK,GAAM;AACT,YAAM,YAAY,OAAO,IAAG;AAC5B,YAAM,oBAAoB,OAAO,IAAG;AACpC,aAAO,EAAE,cAAc,WAAW,mBAAmB,UAAU,OAAO,IAAG,IAAK,KAAM;IACtF;IACA,KAAK,GAAM;AACT,YAAM,aAAa,OAAO,MAAM,EAAE;AAClC,YAAM,aAAa,OAAO,MAAM,EAAE;AAClC,YAAM,UAAU,OAAO,IAAG;AAC1B,aAAO,EAAE,cAAc,YAAY,YAAY,SAAS,UAAU,OAAO,IAAG,IAAK,KAAM;IACzF;IACA,KAAK;AACH,aAAO,EAAE,cAAc,KAAK,OAAO,MAAM,OAAO,GAAE,CAAE,EAAC;IACvD,KAAK;AACH,aAAO,EAAE,cAAc,MAAM,OAAO,MAAM,OAAO,IAAG,CAAE,EAAC;IACzD;AACE,WAAK,iBAAiB,YAAY,2BAA2B;EACjE;AACF;AAUA,SAAS,sBAAsB,QAAkB;AAC/C,QAAM,iBAAiB,OAAO,GAAE;AAChC,UAAQ,gBAAgB;IACtB,KAAK,GAAM;AACT,YAAM,gBAAgB,OAAO,IAAG;AAChC,aAAO,EAAE,gBAAgB,eAAe,SAAS,OAAO,MAAM,OAAO,IAAG,CAAE,EAAC;IAC7E;IACA,KAAK,GAAM;AACT,YAAM,OAAqB,CAAA;AAC3B,YAAM,WAAW,OAAO,IAAG;AAC3B,eAAS,IAAI,GAAG,IAAI,UAAU;AAAK,aAAK,KAAK,OAAO,MAAM,OAAO,IAAG,CAAE,CAAC;AACvE,aAAO,EAAE,gBAAgB,KAAI;IAC/B;IACA,KAAK;AACH,aAAO,EAAE,gBAAgB,OAAO,OAAO,MAAM,OAAO,IAAG,CAAE,EAAC;IAC5D,KAAK;AACH,aAAO,EAAE,gBAAgB,kBAAkB,OAAO,MAAM,OAAO,IAAG,CAAE,EAAC;IACvE;AACE,aAAO,EAAE,gBAAgB,mBAAmB,OAAO,MAAM,OAAO,IAAG,CAAE,EAAC;EAC1E;AACF;AAOO,IAAM,uBAAuB;AAC7B,IAAM,uBAAuB;AAC7B,IAAM,qBAAqB;AAC3B,IAAM,oBAAoB;AA4BjC,SAAS,YAAY,QAAkB;AACrC,QAAM,UAAU,OAAO,GAAE;AACzB,MAAI,UAAU,wBAAwB,UAAU;AAAsB,SAAK,iBAAiB;AAC5F,QAAM,UAAU,OAAO,GAAE;AACzB,QAAM,SAAS,OAAO,IAAG;AACzB,MAAI,WAAW;AAAG,SAAK,sBAAsB;AAC7C,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM,MAAM,CAAC;AAEjD,QAAM,cAAc,MAAM,GAAE,IAAK;AACjC,QAAM,SAAkB,EAAE,SAAS,SAAS,aAAa,QAAQ,CAAA,EAAE;AACnE,MAAI,YAAY,wBAAwB,YAAY,sBAAsB;AACxE,WAAO,YAAY,MAAM,MAAM,MAAM,GAAE,CAAE;AACzC,WAAO,cAAc,MAAM,MAAM,MAAM,IAAG,CAAE;EAC9C;AACA,QAAM,iBAAiB,MAAM,GAAE;AAC/B,WAAS,IAAI,GAAG,IAAI,gBAAgB,KAAK;AACvC,UAAM,aAAa,sBAAsB,KAAK;AAC9C,UAAM,YAAY,OAAO,aAAa,GAAG,MAAM,MAAM,CAAC,CAAC;AACvD,UAAM,QAAQ,MAAM,GAAE;AACtB,UAAM,QAAiB;MACrB;MACA;MACA,wBAAyB,SAAS,IAAK,OAAO;MAC9C,mBAAoB,SAAS,IAAK,OAAO;MACzC,WAAW,CAAA;MACX,aAAa,IAAI,WAAW,CAAC;;AAE/B,SAAK,QAAQ,OAAO,GAAG;AACrB,YAAM,uBAAuB,MAAM,GAAE;AACrC,WAAK,MAAM,GAAE,IAAK,OAAO;AAAG,cAAM,iBAAiB,MAAM,IAAG;IAC9D;AACA,UAAM,gBAAgB,MAAM,GAAE;AAC9B,aAAS,IAAI,GAAG,IAAI,eAAe;AAAK,YAAM,UAAU,KAAK,kBAAkB,KAAK,CAAC;AACrF,UAAM,cAAc,MAAM,MAAM,MAAM,IAAG,CAAE;AAC3C,WAAO,OAAO,KAAK,KAAK;EAC1B;AACA,QAAM,UAAS;AACf,SAAO;AACT;AAGM,SAAU,aAAa,OAAiB;AAC5C,SAAO,MAAM,MAAK;AAChB,UAAM,SAAS,IAAI,WAAW,KAAK;AACnC,UAAM,QAAQ,YAAY,MAAM;AAChC,WAAO,UAAS;AAChB,WAAO;EACT,CAAC;AACH;AA+CM,SAAU,mBAAmB,SAAmB;AACpD,SAAO,MAAM,MAAK;AAChB,UAAM,EAAE,QAAQ,WAAW,QAAO,IAAK,kBAAkB,OAAO;AAChE,QAAI,cAAc;AAAe,aAAO;AACxC,UAAM,iBAAiB,OAAO,GAAE;AAChC,UAAM,UAA4E,CAAA;AAClF,aAAS,IAAI,GAAG,IAAI,gBAAgB,KAAK;AACvC,cAAQ,KAAK,EAAE,SAAS,OAAO,GAAE,GAAI,cAAc,OAAO,GAAE,GAAI,aAAa,OAAO,IAAG,EAAE,CAAE;IAC7F;AACA,UAAM,SAA+B,CAAA;AACrC,UAAM,WAAsB,CAAA;AAC5B,eAAW,SAAS,SAAS;AAC3B,YAAM,QAAQ,OAAO;AACrB,YAAM,UAAU,OAAO,GAAE;AACzB,YAAM,eAAe,OAAO,GAAE;AAC9B,YAAM,SAAS,OAAO,IAAG;AACzB,UAAI,WAAW;AAAG,aAAK,mBAAmB;AAC1C,aAAO,MAAM,MAAM;AACnB,YAAM,QAAQ,WAAW,sBAAsB,WAAW;AAC1D,UAAI,YAAY,MAAM,WAAW,iBAAiB,MAAM,gBACnD,CAAC,SAAS,WAAW,MAAM,aAAc;AAC5C,aAAK,0CAA0C;MACjD;AACA,YAAM,QAAQ,QAAQ,SAAS,OAAO,OAAO,MAAM;AACnD,aAAO,KAAK,EAAE,SAAS,SAAS,cAAc,MAAK,CAAE;AACrD,UAAI,WAAW,wBAAwB,WAAW,sBAAsB;AACtE,cAAM,QAAQ,aAAa,KAAK;AAChC,YAAI,CAAC;AAAO,eAAK,kCAAkC;AACnD,iBAAS,KAAK,KAAK;MACrB;IACF;AACA,WAAO,UAAS;AAChB,WAAO,EAAE,SAAS,QAAQ,SAAQ;EACpC,CAAC;AACH;AAGM,SAAU,oBAAoB,SAAmB;AACrD,SAAO,MAAM,MAAK;AAChB,UAAM,EAAE,QAAQ,WAAW,QAAO,IAAK,kBAAkB,OAAO;AAChE,QAAI,YAAY,wBAAwB,YAAY;AAAyB,aAAO;AACpF,UAAM,QAAQ,YAAY,MAAM;AAChC,WAAO,UAAS;AAChB,WAAO,EAAE,WAAW,SAAS,MAAK;EACpC,CAAC;AACH;AAkBA,SAAS,wBAAwB,OAAiB;AAChD,QAAM,EAAE,WAAW,QAAO,IAAK,kBAAkB,KAAK;AACtD,MAAI,cAAc,kBAAkB;AAClC,UAAM,UAAU,sBAAsB,KAAK;AAC3C,QAAI,CAAC;AAAS,WAAK,0BAA0B;AAC7C,WAAO,EAAE,MAAM,UAAU,QAAO;EAClC;AACA,MAAI,cAAc,eAAe;AAC/B,UAAM,UAAU,mBAAmB,KAAK;AACxC,QAAI,CAAC;AAAS,WAAK,sBAAsB;AACzC,WAAO,EAAE,MAAM,MAAM,QAAO;EAC9B;AACA,MAAI,aAAa,wBAAwB,aAAa,yBAAyB;AAC7E,UAAM,UAAU,oBAAoB,KAAK;AACzC,QAAI,CAAC;AAAS,WAAK,uBAAuB;AAC1C,WAAO,EAAE,MAAM,OAAO,QAAO;EAC/B;AACA,SAAO,EAAE,MAAM,SAAS,WAAW,SAAS,MAAK;AACnD;AAQM,SAAU,sBAAsB,YAAsB;AAC1D,QAAM,UAAU,uBAAuB,UAAU;AACjD,MAAI,CAAC;AAAS,WAAO;AACrB,SAAO,MAAM,OAAO;IAClB,aAAa,QAAQ;IACrB,UAAU,QAAQ,SAAS,IAAI,uBAAuB;IACtD;AACJ;;;ACx0BO,IAAM,qBAAiD,OAAO,OAAO;EAC1E;CACD;;;ACTM,IAAM,uBAA2C,OAAO,OAAO;EACpE,SAAS,OAAO,OAAO,CAAC,WAAW,cAAc,CAAC;EAClD,eAAe,OAAO,OAAO,CAAC,cAAc,CAAC;EAC7C,UAAU;CACX;;;ACUM,SAAS,cAAc,YAAgC;AAC7D,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,SAAO,WAAW,CAAC,IAAI;AACxB;AAGO,SAAS,kBAAkB,YAAiC;AAClE,SAAO,cAAc,UAAU,MAAM;AACtC;AAGO,SAAS,eAAe,YAAiC;AAC/D,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,QAAM,aAAa,WAAW,CAAC,IAAI;AACnC,QAAM,UAAW,WAAW,CAAC,KAAK,IAAK;AACvC,SAAO,eAAe,KAAQ,WAAW;AAC1C;AAGO,SAAS,eAAe,YAAqC;AACnE,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,QAAM,aAAa,WAAW,CAAC,IAAI;AACnC,QAAM,UAAW,WAAW,CAAC,KAAK,IAAK;AACvC,MAAK,WAAW,CAAC,KAAK,MAAO,EAAG,QAAO;AACvC,UAAQ,YAAY;AAAA,IACnB,KAAK;AACJ,UAAI,WAAW,SAAS,GAAI,QAAO;AACnC,UAAI,WAAW,EAAG,QAAO;AACzB,aAAO;AAAA,IACR,KAAK;AACJ,aAAO;AAAA,IACR,KAAK;AACJ,UAAI,WAAW,SAAS,GAAI,QAAO;AACnC,aAAO;AAAA,IACR;AACC,aAAO;AAAA,EACT;AACD;",
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 * 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 * MMTP Signaling Parser for Multicast\n *\n * Provides packet type detection and AL-FEC signaling message parsing.\n * Used by the multicast pipeline to classify and route MMTP packets.\n *\n * Packet types (ISO/IEC 23008-1:2023 Section 9.2.2):\n * - 0x00: MPU (Media Processing Unit) - video/audio data\n * - 0x02: Signaling - control messages including AL-FEC config\n * - 0x03: Repair - FEC repair symbols\n */\nimport {\n\tALFEC_MESSAGE_ID,\n\tFEC_CODE_RAPTORQ,\n\tFEC_CODE_REED_SOLOMON,\n\tPACKET_TYPE_MPU,\n\tPACKET_TYPE_REPAIR,\n\tPACKET_TYPE_SIGNALING,\n\tparseAlFecMessage,\n} from \"@blockcast/mmt-base\";\n\nexport {\n\tALFEC_MESSAGE_ID,\n\tFEC_CODE_RAPTORQ,\n\tFEC_CODE_REED_SOLOMON,\n\tPACKET_TYPE_MPU,\n\tPACKET_TYPE_REPAIR,\n\tPACKET_TYPE_SIGNALING,\n\tparseAlFecMessage,\n};\n\nexport type PacketClass = \"source\" | \"signaling\" | \"repair\" | \"unknown\";\n\n/** Get the MMTP packet type from raw multicast packet bytes.\n *\n * ISO/IEC 23008-1:2023 Figure 8 (V=0): byte 1 is\n * reserved(2)|packet_type(6), so signaling is 0x02 and repair is 0x03.\n * Pre-migration FFmpeg high-bits bytes (0x08/0x0c) are NOT accepted:\n * writers and readers move together in the pinned smoke train.\n */\nexport function getPacketType(mmtpPacket: Uint8Array): number {\n\tif (mmtpPacket.length < 2) return -1;\n\treturn mmtpPacket[1] & 0x3f;\n}\n\n/** Check if packet is a signaling packet (type 0x02). */\nexport function isSignalingPacket(mmtpPacket: Uint8Array): boolean {\n\treturn getPacketType(mmtpPacket) === PACKET_TYPE_SIGNALING;\n}\n\n/** Determine if packet is a source (video/audio) packet. */\nexport function isSourcePacket(mmtpPacket: Uint8Array): boolean {\n\tif (mmtpPacket.length < 2) return false;\n\tconst packetType = mmtpPacket[1] & 0x3f;\n\tconst fecType = (mmtpPacket[0] >> 3) & 0x03;\n\treturn packetType === 0x00 && fecType <= 1;\n}\n\n/** Classify MMTP packet for multicast pipeline routing. */\nexport function classifyPacket(mmtpPacket: Uint8Array): PacketClass {\n\tif (mmtpPacket.length < 2) return \"unknown\";\n\tconst packetType = mmtpPacket[1] & 0x3f;\n\tconst fecType = (mmtpPacket[0] >> 3) & 0x03;\n\tif ((mmtpPacket[0] >> 6) !== 0) return \"unknown\";\n\tswitch (packetType) {\n\t\tcase 0x00:\n\t\t\tif (mmtpPacket.length < 20) return \"unknown\";\n\t\t\tif (fecType >= 2) return \"repair\";\n\t\t\treturn \"source\";\n\t\tcase 0x02:\n\t\t\treturn \"signaling\";\n\t\tcase 0x03:\n\t\t\tif (mmtpPacket.length < 20) return \"unknown\";\n\t\t\treturn \"repair\";\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -152,30 +152,26 @@ function isEmptyOrExactLegacyTrailer(payload, start, end) {
152
152
  const authLength = view.getUint16(2, false);
153
153
  return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end;
154
154
  }
155
- function resolveDeclaredTrailers(payload, declaredEnd, fecType, options, rapFlag = false) {
155
+ function resolveDeclaredTrailers(payload, declaredEnd, fecType, options) {
156
156
  if (declaredEnd > payload.length)
157
157
  return null;
158
158
  if (options?.skipFecId) {
159
- return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false, hasOverlayFecId: false } : null;
159
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length) ? { hasFecId: false } : null;
160
160
  }
161
- const overlay = options?.overlayDeclared === true && rapFlag;
162
161
  if (fecType === 1) {
163
- const fecStart = payload.length - sourceFecTrailerSize(overlay);
162
+ const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE;
164
163
  if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
165
164
  return null;
166
165
  }
167
- return { hasFecId: true, hasOverlayFecId: overlay };
166
+ return { hasFecId: true };
168
167
  }
169
- if (fecType !== 0 || overlay)
168
+ if (fecType !== 0)
170
169
  return null;
171
170
  if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
172
- return { hasFecId: false, hasOverlayFecId: false };
171
+ return { hasFecId: false };
173
172
  }
174
173
  return null;
175
174
  }
176
- function sourceFecTrailerSize(overlay) {
177
- return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE;
178
- }
179
175
  function extractNALUnit(mmtpPacket) {
180
176
  const extraction = extractMFUPayload(mmtpPacket);
181
177
  if (!extraction)
@@ -234,11 +230,11 @@ function planMfuExtraction(mmtpPacket, options, parsedHeader) {
234
230
  if ("rejection" in layoutResult)
235
231
  return layoutResult;
236
232
  const { layout } = layoutResult;
237
- const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options, header.rapFlag);
233
+ const trailerLayout = resolveDeclaredTrailers(payload, layout.dataEnd, header.fecType, options);
238
234
  if (!trailerLayout) {
239
235
  if (!options?.skipFecId && header.fecType === 1) {
240
236
  const observed = payload.length - layout.dataEnd;
241
- const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag);
237
+ const expected = SOURCE_FEC_PAYLOAD_ID_SIZE;
242
238
  if (observed !== expected) {
243
239
  return {
244
240
  rejection: {
@@ -270,13 +266,10 @@ function extractPlannedMfu(mmtpPacket, result) {
270
266
  const mfuDuHeader = mfuDuHeaderPresent && mpu.timedFlag === 1 ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE) : null;
271
267
  const itemId = mfuDuHeaderPresent && mpu.timedFlag === 0 ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false) : null;
272
268
  const data = payload.subarray(mfuOffset, dataEnd);
273
- const trailerStart = mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId);
274
- const ssIdAt = (offset) => ({
275
- ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false)
276
- });
277
- const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null;
278
- const overlayFecPayloadId = trailerLayout.hasOverlayFecId ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE) : null;
279
- return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId };
269
+ const fecPayloadId = trailerLayout.hasFecId ? {
270
+ ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE, 4).getUint32(0, false)
271
+ } : null;
272
+ return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId };
280
273
  }
281
274
  function extractTimestamp(mmtpPacket) {
282
275
  const { header } = unwrapMMTP(mmtpPacket);