@blockcast/mmt-container 2.1.0 → 2.2.0-main.050b52fb9b00

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 (105) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1949 -374
  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 +70 -12
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +403 -58
  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 +6 -5
  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 +48 -13
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +97 -25
  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 +79 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +219 -45
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +342 -63
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +218 -45
  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 +43 -9
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +2006 -95
  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 +28 -5
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +16116 -15005
  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 +336 -59
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.d.ts +36 -8
  72. package/dist/repair-fetch.d.ts.map +1 -1
  73. package/dist/repair-fetch.js +285 -26
  74. package/dist/repair-fetch.js.map +3 -3
  75. package/dist/repair-layer-subscriptions.d.ts +82 -0
  76. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  77. package/dist/repair-layer-subscriptions.js +192 -0
  78. package/dist/repair-layer-subscriptions.js.map +7 -0
  79. package/dist/signaling.js.map +2 -2
  80. package/dist/timestamp-codec.js +12 -19
  81. package/dist/timestamp-codec.js.map +2 -2
  82. package/dist/track-session.d.ts +1 -1
  83. package/dist/track-session.d.ts.map +1 -1
  84. package/dist/track-session.js +1 -0
  85. package/dist/track-session.js.map +2 -2
  86. package/dist/wasm-mfu-reassembler.js.map +1 -1
  87. package/dist/wasm-mmtp-container-parser.js +218 -45
  88. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  89. package/package.json +8 -5
  90. package/schemas/catalog.schema.json +4 -0
  91. package/src/catalog-fec-params.ts +36 -1
  92. package/src/cmcd-producer.ts +194 -179
  93. package/src/fec-manager.ts +433 -32
  94. package/src/fec-worker-setup.ts +2 -2
  95. package/src/index.ts +20 -2
  96. package/src/iso-block-locator.ts +133 -31
  97. package/src/iso-bmff-timing.ts +40 -1
  98. package/src/mmtp-container-parser.ts +306 -29
  99. package/src/mmtp-fec-session.ts +115 -15
  100. package/src/mmtp-router.ts +629 -54
  101. package/src/multicast-manager.ts +70 -11
  102. package/src/multicast-transport.ts +44 -20
  103. package/src/repair-fetch.ts +363 -49
  104. package/src/repair-layer-subscriptions.ts +261 -0
  105. package/src/track-session.ts +4 -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/fec-block-profile.ts", "../../mmt-base/src/fec-catalog.ts", "../../mmt-base/src/fec-bindings.ts", "../src/catalog-fec-params.ts", "../src/carp-catalog.ts", "../src/repair-layers.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 * 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", "// FEC catalog binding validator (draft-ramadan-moq-fec-catalog-00, as posted\n// 2026-11-02, moqcast-draft 5f49172).\n//\n// The catalog-00 shape only: a track's `fec` is an array of instance objects,\n// and a repair or symbol track binds to its instance by `source` + `instanceId`\n// (or by being named in `repairTrack` / `symbolTrack`). The draft-01 object\n// shape (`fec: {...}`, `depends`) is not accepted: an object `fec` is a type\n// violation, and a repair track that only `depends` on its source is an orphan.\n//\n// Validation fails closed per instance (fec-core-00 10.3): a violation stops\n// FEC for that instance and no other. Three outcomes, each reported with the\n// catalog path that caused it:\n// ok - FEC may start for the instance.\n// unavailable - the receiver forgoes FEC for it (fec-core-00 10.3 rule 1,\n// 4.5); not a validation failure.\n// violation - the configuration is invalid for this receiver.\n// An orphan repair or symbol track (fec-catalog-00 5 rule 6) is reported and\n// stops nothing. `enhancementRepair` anywhere rejects the catalog (rule 4).\n//\n// The Rust twin is `mmt_fec::fec_catalog`; both run vectors/fec-catalog-00/.\n\nexport type FecCatalogOutcome = \"ok\" | \"unavailable\" | \"violation\";\n\nexport type FecCatalogCode =\n\t// Unavailable (fec-core-00 10.3 rule 1; fec-catalog-00 3.1).\n\t| \"fec_scheme_absent\"\n\t| \"fec_scheme_unimplemented\"\n\t| \"fec_block_profile_absent\"\n\t| \"fec_block_profile_unimplemented\"\n\t| \"fec_block_profile_packaging\"\n\t// Violations.\n\t| \"fec_member\" // fec-catalog-00 5 rule 1; fec-core-00 10.3 rule 11\n\t| \"fec_naming\" // fec-catalog-00 5 rule 2\n\t| \"fec_resolution\" // fec-catalog-00 5 rule 3\n\t| \"fec_enhancement_repair\" // fec-catalog-00 5 rule 4, 3.4\n\t| \"fec_orphan\" // fec-catalog-00 5 rule 6 (report only)\n\t| \"fec_layers\" // fec-core-00 10.3 rule 3\n\t| \"fec_layer0\" // fec-core-00 10.3 rule 4\n\t| \"fec_bounds\" // fec-core-00 10.3 rule 5; block-codes-00 7; mmt-02 7.1\n\t| \"fec_instance_id\" // fec-core-00 10.3 rule 7\n\t| \"fec_symbol_tracks\" // fec-core-00 10.3 rule 7\n\t| \"fec_track_geometry\" // fec-core-00 10.3 rule 8\n\t| \"fec_priority\" // fec-core-00 10.3 rule 10\n\t| \"fec_overlay\" // mmt-02 7.4.1\n\t| \"fec_overlay_unsupported\"; // mmt-02 7.4.4\n\nexport interface FecCatalogIssue {\n\treadonly code: FecCatalogCode;\n\t/** JSON Pointer into the catalog. */\n\treadonly path: string;\n}\n\nexport interface FecCatalogInstance {\n\t/** JSON Pointer of the instance object, `/tracks/<i>/fec/<j>`. */\n\treadonly path: string;\n\treadonly track: string | null;\n\t/** Resolved Instance ID; null when `instanceId` is malformed. */\n\treadonly instanceId: number | null;\n\treadonly outcome: FecCatalogOutcome;\n\treadonly issues: readonly FecCatalogIssue[];\n\t/** Index into `tracks` of the source track declaring the instance. */\n\treadonly trackIndex: number;\n\t/** Track index of the repair track `repairTrack` names, or null. */\n\treadonly repairTrack: number | null;\n\t/** Track indices of the other repair tracks bound by `source` + `instanceId`. */\n\treadonly boundRepairTracks: readonly number[];\n\t/** Track index of the instance's symbol track (named or bound), or null. */\n\treadonly symbolTrack: number | null;\n}\n\nexport interface FecCatalogReport {\n\treadonly instances: readonly FecCatalogInstance[];\n\t/** Every report, instance-attributed or not, in discovery order. */\n\treadonly issues: readonly FecCatalogIssue[];\n}\n\nexport interface FecCatalogReceiver {\n\t/** Schemes the receiver implements. Default: reed-solomon, raptorq. */\n\treadonly schemes?: readonly string[];\n\t/** Block profiles the receiver implements. Default: moq-object-v1, iso-ssbg1-v1. */\n\treadonly blockProfiles?: readonly string[];\n\t/** Whether the receiver implements the mmt-02 7.4 keyframe overlay. Default true. */\n\treadonly overlays?: boolean;\n}\n\nconst DEFAULT_SCHEMES = [\"reed-solomon\", \"raptorq\"];\nconst DEFAULT_PROFILES = [\"moq-object-v1\", \"iso-ssbg1-v1\"];\nconst RS_MAX_BLOCK = 255; // block-codes-00 7\nconst RAPTORQ_MAX_K = 56403; // block-codes-00 7\nconst MAX_24 = 0xffffff; // fec-core-00 10.3 rule 5\nconst ISO_MIN_T = 14; // mmt-02 7.1\nconst GEOMETRY_KEYS = [\n\t\"scheme\",\n\t\"blockProfile\",\n\t\"mmtpMode\",\n\t\"timescale\",\n\t\"symbolSize\",\n\t\"interleaveDepthMs\",\n\t\"reorderToleranceMs\",\n\t\"sourceSymbols\",\n\t\"fec\",\n];\n\ntype Json = unknown;\ntype Known =\n\t| \"tracks\" | \"fec\" | \"name\" | \"namespace\" | \"packaging\" | \"source\" | \"instanceId\"\n\t| \"repairLayer\" | \"repairSymbols\" | \"priority\" | \"scheme\" | \"blockProfile\"\n\t| \"sourceSymbols\" | \"symbolSize\" | \"repairTrack\" | \"symbolTrack\"\n\t| \"interleaveDepthMs\" | \"reorderToleranceMs\" | \"label\" | \"scope\";\ntype Obj = Record<string, Json> & Partial<Record<Known, Json>>;\n\nconst isObj = (v: Json): v is Obj => typeof v === \"object\" && v !== null && !Array.isArray(v);\nconst has = (o: Obj, k: string) => Object.prototype.hasOwnProperty.call(o, k);\nconst isInt = (v: Json): v is number => typeof v === \"number\" && Number.isInteger(v);\nconst intIn = (v: Json, lo: number, hi = Number.MAX_SAFE_INTEGER): v is number =>\n\tisInt(v) && v >= lo && v <= hi;\n\n// moq-object-v1 admits any packaging whose objects carry media as whole\n// objects (fec-core-00 15.3); iso-ssbg1-v1 admits mmtp (mmt-02 7.1).\nconst profileAdmits = (profile: string, packaging: Json) => profile !== \"iso-ssbg1-v1\" || packaging === \"mmtp\";\n\ninterface Inst {\n\tpath: string;\n\tti: number;\n\ttrack: Obj;\n\tobj: Obj | null;\n\tid: number | null;\n\tunavailable: boolean;\n\toverlay: boolean;\n\tissues: FecCatalogIssue[];\n\tlayer0: number | null; // track index of the resolved repairTrack\n\tsymbolNamed: number | null;\n\tattached: number[]; // unnamed fec-repair/fec-source tracks bound by source\n}\n\nexport function validateFecCatalog(catalog: Json, receiver: FecCatalogReceiver = {}): FecCatalogReport {\n\tconst schemes = receiver.schemes ?? DEFAULT_SCHEMES;\n\tconst profiles = receiver.blockProfiles ?? DEFAULT_PROFILES;\n\tconst overlays = receiver.overlays ?? true;\n\tconst all: FecCatalogIssue[] = [];\n\tconst report = (code: FecCatalogCode, path: string, inst?: Inst) => {\n\t\tconst issue = { code, path };\n\t\tall.push(issue);\n\t\tinst?.issues.push(issue);\n\t};\n\n\tconst tracks: Json[] = isObj(catalog) && Array.isArray(catalog.tracks) ? catalog.tracks : [];\n\tif (!isObj(catalog) || !Array.isArray(catalog.tracks)) report(\"fec_member\", \"/tracks\");\n\n\t// Rule 4: enhancementRepair at track level or inside an instance object.\n\tlet rejectAll = false;\n\ttracks.forEach((t, ti) => {\n\t\tif (!isObj(t)) return;\n\t\tif (has(t, \"enhancementRepair\")) {\n\t\t\trejectAll = true;\n\t\t\treport(\"fec_enhancement_repair\", `/tracks/${ti}/enhancementRepair`);\n\t\t}\n\t\tif (Array.isArray(t.fec))\n\t\t\tt.fec.forEach((o, ii) => {\n\t\t\t\tif (isObj(o) && has(o, \"enhancementRepair\")) {\n\t\t\t\t\trejectAll = true;\n\t\t\t\t\treport(\"fec_enhancement_repair\", `/tracks/${ti}/fec/${ii}/enhancementRepair`);\n\t\t\t\t}\n\t\t\t});\n\t});\n\n\t// Track lookup by (namespace, name): a name resolves in the source track's namespace.\n\tconst key = (ns: Json, name: string) => JSON.stringify([ns ?? null, name]);\n\tconst byName = new Map<string, number>();\n\ttracks.forEach((t, ti) => {\n\t\tif (isObj(t) && typeof t.name === \"string\") {\n\t\t\tconst k = key(t.namespace, t.name);\n\t\t\tif (!byName.has(k)) byName.set(k, ti);\n\t\t}\n\t});\n\tconst trackAt = (ti: number) => tracks[ti] as Obj;\n\n\t// Collect instances.\n\tconst insts: Inst[] = [];\n\ttracks.forEach((t, ti) => {\n\t\t// A repair or symbol track's own `fec` is geometry (core rule 8), not instances.\n\t\tif (!isObj(t) || !has(t, \"fec\") || t.packaging === \"fec-repair\" || t.packaging === \"fec-source\") return;\n\t\tif (!Array.isArray(t.fec)) {\n\t\t\treport(\"fec_member\", `/tracks/${ti}/fec`);\n\t\t\treturn;\n\t\t}\n\t\tt.fec.forEach((o, ii) => {\n\t\t\tconst path = `/tracks/${ti}/fec/${ii}`;\n\t\t\tconst inst: Inst = {\n\t\t\t\tpath,\n\t\t\t\tti,\n\t\t\t\ttrack: t,\n\t\t\t\tobj: isObj(o) ? o : null,\n\t\t\t\tid: null,\n\t\t\t\tunavailable: false,\n\t\t\t\toverlay: false,\n\t\t\t\tissues: [],\n\t\t\t\tlayer0: null,\n\t\t\t\tsymbolNamed: null,\n\t\t\t\tattached: [],\n\t\t\t};\n\t\t\tinsts.push(inst);\n\t\t\tif (!inst.obj) return report(\"fec_member\", path, inst);\n\t\t\tconst o2 = inst.obj;\n\t\t\tif (!has(o2, \"instanceId\")) inst.id = 0;\n\t\t\telse if (intIn(o2.instanceId, 0, 255)) inst.id = o2.instanceId;\n\t\t\telse report(\"fec_member\", `${path}/instanceId`, inst);\n\t\t\tinst.overlay = has(o2, \"scope\");\n\t\t});\n\t});\n\tconst declaresFec = insts.length > 0;\n\n\t// Availability (fec-core-00 10.3 rule 1). Scheme first, then profile.\n\tfor (const inst of insts) {\n\t\tconst o = inst.obj;\n\t\tif (!o) continue;\n\t\tconst avail = (field: \"scheme\" | \"blockProfile\", impl: readonly string[]) => {\n\t\t\tconst p = `${inst.path}/${field}`;\n\t\t\tconst absent = field === \"scheme\" ? \"fec_scheme_absent\" : \"fec_block_profile_absent\";\n\t\t\tconst unimpl = field === \"scheme\" ? \"fec_scheme_unimplemented\" : \"fec_block_profile_unimplemented\";\n\t\t\tif (!has(o, field)) return report(absent, p, inst), false;\n\t\t\tif (typeof o[field] !== \"string\") return report(\"fec_member\", p, inst), true;\n\t\t\tif (!impl.includes(o[field] as string)) return report(unimpl, p, inst), false;\n\t\t\treturn true;\n\t\t};\n\t\tif (!avail(\"scheme\", schemes) || !avail(\"blockProfile\", profiles)) inst.unavailable = true;\n\t\telse if (typeof o.blockProfile === \"string\" && !profileAdmits(o.blockProfile, inst.track.packaging)) {\n\t\t\treport(\"fec_block_profile_packaging\", `${inst.path}/blockProfile`, inst);\n\t\t\tinst.unavailable = true;\n\t\t}\n\t}\n\tconst live = (i: Inst) => !i.unavailable;\n\n\t// Rule 1 on instance members, and core rule 11.\n\tfor (const inst of insts.filter(live)) {\n\t\tconst o = inst.obj;\n\t\tif (!o) continue;\n\t\tconst p = inst.path;\n\t\tfor (const m of [\"sourceSymbols\", \"repairSymbols\", \"symbolSize\"])\n\t\t\tif (!intIn(o[m], 1)) report(\"fec_member\", `${p}/${m}`, inst);\n\t\tif (typeof o.repairTrack !== \"string\") report(\"fec_member\", `${p}/repairTrack`, inst);\n\t\tif (has(o, \"symbolTrack\") && typeof o.symbolTrack !== \"string\") report(\"fec_member\", `${p}/symbolTrack`, inst);\n\t\tif (has(o, \"interleaveDepthMs\")) {\n\t\t\tconst d = o.interleaveDepthMs;\n\t\t\tif (!intIn(d, 0) || (d === 0 && o.blockProfile !== \"iso-ssbg1-v1\"))\n\t\t\t\treport(\"fec_member\", `${p}/interleaveDepthMs`, inst);\n\t\t}\n\t\tif (has(o, \"reorderToleranceMs\")) {\n\t\t\tconst r = o.reorderToleranceMs;\n\t\t\tif (typeof r !== \"number\" || !Number.isFinite(r) || r < 0) report(\"fec_member\", `${p}/reorderToleranceMs`, inst);\n\t\t}\n\t\tif (has(o, \"label\") && typeof o.label !== \"string\") report(\"fec_member\", `${p}/label`, inst);\n\t\tif (inst.overlay && o.scope !== \"keyframe\") report(\"fec_member\", `${p}/scope`, inst);\n\t}\n\n\t// Core rule 7: unique Instance IDs within a track.\n\tconst idGroups = new Map<string, Inst[]>();\n\tfor (const inst of insts)\n\t\tif (inst.id !== null) {\n\t\t\tconst k = `${inst.ti}:${inst.id}`;\n\t\t\tidGroups.set(k, [...(idGroups.get(k) ?? []), inst]);\n\t\t}\n\tfor (const group of idGroups.values())\n\t\tif (group.length > 1)\n\t\t\tfor (const inst of group.filter(live))\n\t\t\t\treport(\"fec_instance_id\", inst.obj && has(inst.obj, \"instanceId\") ? `${inst.path}/instanceId` : inst.path, inst);\n\n\t// Rule 2: repairTrack / symbolTrack name a track of the right packaging, once\n\t// per catalog. A name used twice binds no layer 0 or symbol track for either\n\t// namer; the track is still claimed by the first, so it is not re-attached.\n\tconst nameUses = new Map<string, { inst: Inst; field: \"repairTrack\" | \"symbolTrack\" }[]>();\n\tfor (const inst of insts)\n\t\tfor (const field of [\"repairTrack\", \"symbolTrack\"] as const)\n\t\t\tif (inst.obj && typeof inst.obj[field] === \"string\") {\n\t\t\t\tconst k = key(inst.track.namespace, inst.obj[field] as string);\n\t\t\t\tnameUses.set(k, [...(nameUses.get(k) ?? []), { inst, field }]);\n\t\t\t}\n\tconst namedBy = new Map<number, Inst>();\n\tfor (const [k, uses] of nameUses) {\n\t\tconst tj = byName.get(k);\n\t\tfor (const { inst, field } of uses) {\n\t\t\tconst path = `${inst.path}/${field}`;\n\t\t\tif (tj === undefined || trackAt(tj).packaging !== (field === \"repairTrack\" ? \"fec-repair\" : \"fec-source\")) {\n\t\t\t\tif (live(inst)) report(\"fec_naming\", path, inst);\n\t\t\t} else if (uses.length > 1) {\n\t\t\t\tif (live(inst)) report(\"fec_naming\", path, inst);\n\t\t\t\tif (!namedBy.has(tj)) namedBy.set(tj, inst);\n\t\t\t} else {\n\t\t\t\tnamedBy.set(tj, inst);\n\t\t\t\tif (field === \"repairTrack\") inst.layer0 = tj;\n\t\t\t\telse inst.symbolNamed = tj;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Repair and symbol tracks: resolve each to its instance.\n\tconst owner = new Map<number, Inst>();\n\tif (declaresFec)\n\t\ttracks.forEach((t, tj) => {\n\t\t\tif (!isObj(t) || (t.packaging !== \"fec-repair\" && t.packaging !== \"fec-source\")) return;\n\t\t\tconst tp = `/tracks/${tj}`;\n\t\t\tconst named = namedBy.get(tj);\n\t\t\tif (named) {\n\t\t\t\towner.set(tj, named);\n\t\t\t\tif (!live(named)) return;\n\t\t\t\t// Rule 3: a named track's source / instanceId agree with its namer.\n\t\t\t\tif (has(t, \"source\") && t.source !== named.track.name) report(\"fec_resolution\", `${tp}/source`, named);\n\t\t\t\tif (has(t, \"instanceId\") && t.instanceId !== named.id) report(\"fec_resolution\", `${tp}/instanceId`, named);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!has(t, \"source\")) return report(\"fec_orphan\", tp); // rule 6, stops nothing\n\t\t\tconst id = has(t, \"instanceId\") ? t.instanceId : 0;\n\t\t\tconst src = typeof t.source === \"string\" ? byName.get(key(t.namespace, t.source)) : undefined;\n\t\t\tconst target = src === undefined ? undefined : insts.find((i) => i.ti === src && i.id === id);\n\t\t\tif (!target) return report(\"fec_naming\", has(t, \"instanceId\") && src !== undefined ? `${tp}/instanceId` : `${tp}/source`);\n\t\t\towner.set(tj, target);\n\t\t\ttarget.attached.push(tj);\n\t\t});\n\n\t// Track fields (rule 1) and no geometry (core rule 8), charged to the owner.\n\tfor (const [tj, inst] of owner) {\n\t\tif (!live(inst)) continue;\n\t\tconst t = trackAt(tj);\n\t\tconst tp = `/tracks/${tj}`;\n\t\tconst symbol = t.packaging === \"fec-source\";\n\t\tif (has(t, \"source\") && typeof t.source !== \"string\") report(\"fec_member\", `${tp}/source`, inst);\n\t\tif (has(t, \"instanceId\") && !intIn(t.instanceId, 0, 255)) report(\"fec_member\", `${tp}/instanceId`, inst);\n\t\tfor (const m of [\"repairLayer\", \"repairSymbols\"]) {\n\t\t\tif (!has(t, m)) continue;\n\t\t\tif (symbol || !intIn(t[m], m === \"repairLayer\" ? 0 : 1)) report(\"fec_member\", `${tp}/${m}`, inst);\n\t\t}\n\t\tif (has(t, \"priority\") && !intIn(t.priority, symbol ? 0 : 192, 255)) report(\"fec_member\", `${tp}/priority`, inst);\n\t\tfor (const k of Object.keys(t))\n\t\t\tif (GEOMETRY_KEYS.includes(k) || k.startsWith(\"groupDuration\"))\n\t\t\t\treport(\"fec_track_geometry\", `${tp}/${k.replace(/~/g, \"~0\").replace(/\\//g, \"~1\")}`, inst); // RFC 6901\n\t}\n\n\t// Per instance: layers, symbol tracks, priorities, scheme bounds.\n\tfor (const inst of insts.filter(live)) {\n\t\tconst o = inst.obj;\n\t\tif (!o) continue;\n\t\tconst repairs = inst.attached.filter((tj) => trackAt(tj).packaging === \"fec-repair\");\n\t\tconst symbols = inst.attached.filter((tj) => trackAt(tj).packaging === \"fec-source\");\n\t\tif (symbols.length + (inst.symbolNamed !== null ? 1 : 0) > 1)\n\t\t\tfor (const tj of symbols) report(\"fec_symbol_tracks\", `/tracks/${tj}`, inst);\n\n\t\tconst l0 = inst.layer0 === null ? null : trackAt(inst.layer0);\n\t\tconst l0p = `/tracks/${inst.layer0}`;\n\t\tif (inst.overlay) {\n\t\t\t// mmt-02 7.4.1: an overlay has one repair track and no repairLayer.\n\t\t\tif (l0 && has(l0, \"repairLayer\")) report(\"fec_overlay\", `${l0p}/repairLayer`, inst);\n\t\t\tfor (const tj of repairs)\n\t\t\t\treport(\"fec_overlay\", has(trackAt(tj), \"repairLayer\") ? `/tracks/${tj}/repairLayer` : `/tracks/${tj}`, inst);\n\t\t} else {\n\t\t\tif (l0 && has(l0, \"repairLayer\") && l0.repairLayer !== 0) report(\"fec_layers\", `${l0p}/repairLayer`, inst);\n\t\t\tif (l0 && has(l0, \"repairSymbols\") && intIn(o.repairSymbols, 1) && l0.repairSymbols !== o.repairSymbols)\n\t\t\t\treport(\"fec_layer0\", `${l0p}/repairSymbols`, inst);\n\t\t\tconst layers: { tj: number; layer: number }[] = [];\n\t\t\tfor (const tj of repairs) {\n\t\t\t\tconst t = trackAt(tj);\n\t\t\t\tif (!has(t, \"repairLayer\")) report(\"fec_member\", `/tracks/${tj}/repairLayer`, inst);\n\t\t\t\telse if (t.repairLayer === 0) report(\"fec_layers\", `/tracks/${tj}/repairLayer`, inst);\n\t\t\t\telse if (intIn(t.repairLayer, 1)) layers.push({ tj, layer: t.repairLayer });\n\t\t\t\tif (!has(t, \"repairSymbols\")) report(\"fec_member\", `/tracks/${tj}/repairSymbols`, inst);\n\t\t\t}\n\t\t\t// Core rule 3: layers 1..n contiguous, no duplicates.\n\t\t\tconst counts = new Map<number, number>();\n\t\t\tfor (const { layer } of layers) counts.set(layer, (counts.get(layer) ?? 0) + 1);\n\t\t\tlet missing = 1;\n\t\t\twhile (counts.has(missing)) missing++;\n\t\t\tfor (const { tj, layer } of layers)\n\t\t\t\tif (counts.get(layer)! > 1 || layer > missing) report(\"fec_layers\", `/tracks/${tj}/repairLayer`, inst);\n\t\t\t// Core rule 10: priority non-decreasing with layer; layer 0 defaults to 240.\n\t\t\tconst chain = [\n\t\t\t\t{ layer: 0, tj: inst.layer0, pri: l0 && intIn(l0.priority, 192, 255) ? l0.priority : l0 && has(l0, \"priority\") ? null : 240 },\n\t\t\t\t...layers.map(({ tj, layer }) => {\n\t\t\t\t\tconst pr = trackAt(tj).priority;\n\t\t\t\t\treturn { layer, tj, pri: intIn(pr, 192, 255) ? pr : null };\n\t\t\t\t}),\n\t\t\t].sort((a, b) => a.layer - b.layer);\n\t\t\tlet prev: number | null = null;\n\t\t\tfor (const c of chain) {\n\t\t\t\tif (c.pri === null) continue;\n\t\t\t\tif (prev !== null && c.pri < prev) report(\"fec_priority\", `/tracks/${c.tj}/priority`, inst);\n\t\t\t\tprev = Math.max(prev ?? c.pri, c.pri);\n\t\t\t}\n\t\t}\n\n\t\t// Core rule 5 and the scheme / profile bounds.\n\t\tconst K = o.sourceSymbols;\n\t\tconst T = o.symbolSize;\n\t\tconst P0 = o.repairSymbols;\n\t\tif (intIn(K, 1) && intIn(P0, 1)) {\n\t\t\tlet P = P0;\n\t\t\tif (!inst.overlay)\n\t\t\t\tfor (const tj of repairs) {\n\t\t\t\t\tconst t = trackAt(tj);\n\t\t\t\t\tif (intIn(t.repairLayer, 1) && intIn(t.repairSymbols, 1)) P += t.repairSymbols;\n\t\t\t\t}\n\t\t\tif (P > MAX_24 || K + P - 1 > MAX_24) report(\"fec_bounds\", inst.path, inst);\n\t\t\telse if (o.scheme === \"reed-solomon\" && K + P > RS_MAX_BLOCK) report(\"fec_bounds\", inst.path, inst);\n\t\t}\n\t\tif (o.scheme === \"raptorq\" && intIn(K, 1) && K > RAPTORQ_MAX_K) report(\"fec_bounds\", `${inst.path}/sourceSymbols`, inst);\n\t\tif (intIn(T, 1)) {\n\t\t\tif (o.scheme === \"raptorq\" && T % 8 !== 0) report(\"fec_bounds\", `${inst.path}/symbolSize`, inst);\n\t\t\telse if (o.blockProfile === \"iso-ssbg1-v1\" && T < ISO_MIN_T) report(\"fec_bounds\", `${inst.path}/symbolSize`, inst);\n\t\t}\n\t}\n\n\t// mmt-02 7.4.1 / 7.4.4: overlays, per source track.\n\tconst overlayTracks = new Set(insts.filter((i) => live(i) && i.overlay).map((i) => i.ti));\n\tfor (const ti of overlayTracks) {\n\t\tconst ofTrack = insts.filter((i) => i.ti === ti && live(i));\n\t\tconst ovs = ofTrack.filter((i) => i.overlay);\n\t\tif (!overlays) {\n\t\t\tfor (const ov of ovs) report(\"fec_overlay_unsupported\", `${ov.path}/scope`, ov);\n\t\t\tfor (const i of ofTrack) i.issues.length === 0 && i.issues.push({ code: \"fec_overlay_unsupported\", path: `${ovs[0].path}/scope` });\n\t\t\tcontinue;\n\t\t}\n\t\tif (ovs.length > 1) for (const ov of ovs) report(\"fec_overlay\", `${ov.path}/scope`, ov);\n\t\tfor (const ov of ovs)\n\t\t\tif (ov.obj?.blockProfile !== \"iso-ssbg1-v1\") report(\"fec_overlay\", `${ov.path}/blockProfile`, ov);\n\t\tconst cause = ovs.flatMap((ov) => ov.issues).find((x) => x.code === \"fec_overlay\");\n\t\tif (cause) for (const ov of ovs) if (ov.issues.length === 0) ov.issues.push(cause);\n\t}\n\n\t// Rule 4 rejects every instance; each carries the cause so a consumer reading\n\t// `instances[].issues` sees why.\n\tconst rule4 = all.find((x) => x.code === \"fec_enhancement_repair\");\n\tif (rejectAll && rule4) for (const i of insts) if (i.issues.length === 0) i.issues.push(rule4);\n\n\treturn {\n\t\tinstances: insts.map((i) => ({\n\t\t\tpath: i.path,\n\t\t\ttrack: typeof i.track.name === \"string\" ? i.track.name : null,\n\t\t\tinstanceId: i.id,\n\t\t\toutcome: rejectAll || (!i.unavailable && i.issues.length > 0) ? \"violation\" : i.unavailable ? \"unavailable\" : \"ok\",\n\t\t\tissues: i.issues,\n\t\t\ttrackIndex: i.ti,\n\t\t\trepairTrack: i.layer0,\n\t\t\tboundRepairTracks: i.attached.filter((tj) => trackAt(tj).packaging === \"fec-repair\"),\n\t\t\tsymbolTrack: i.symbolNamed ?? i.attached.find((tj) => trackAt(tj).packaging === \"fec-source\") ?? null,\n\t\t})),\n\t\tissues: all,\n\t};\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", "// Catalog \u2192 per-track FEC geometry resolution.\n//\n// Moved from hang-mmt-fec's js/watch/src/track-fec-params.ts so every player\n// (moq-lib/hang, moqtail, shaka MSF) resolves catalog FEC semantics through\n// one shared implementation \u2014 players keep only wiring, not MMT/catalog\n// interpretation. Structural track shape ({ name, type }) keeps this module\n// player-agnostic.\n//\n// FEC comes only from `resolveFecBindings` (mmt-base), which reads\n// `validateFecCatalog`'s per-instance result over the\n// draft-ramadan-moq-fec-catalog-00 array (BLO-50171). Nothing here reads a\n// `fec` block or a repair track itself, and nothing throws on catalog input:\n// a source whose base instance is not `ok` has no FEC and a typed reason, and\n// no other track is affected.\n\nimport {\n\tFEC_BLOCK_PROFILES,\n\tisFecBlockProfile,\n\tparseFecBlockProfile,\n\tresolveFecBindings,\n\ttype FecBindingUnavailable,\n\ttype FecBlockProfile,\n\ttype FecSourceBinding,\n} from \"@blockcast/mmt-base\";\nimport { catalogFecInterleaveMs, type FecInterleaveCatalog } from \"@blockcast/mmt-fec\";\nimport { resolveGroupDurationMs } from \"./carp-catalog.js\";\nimport type {\n\tRepairLayerParams,\n\tRepairLayersResolution,\n\tRepairOverlayParams,\n} from \"./repair-layers.js\";\nimport type { RepairTarget } from \"./repair-fetch.js\";\n\nexport { catalogFecInterleaveMs, type FecInterleaveCatalog };\nexport { FEC_BLOCK_PROFILES, isFecBlockProfile, parseFecBlockProfile, type FecBlockProfile };\n// Re-exported here, not from index.ts: index's bundle keeps `./*.js` external,\n// so a direct mmt-base re-export there leaves a dangling `./mmtp.js` import.\nexport {\n\tvalidateFecCatalog,\n\ttype FecCatalogCode,\n\ttype FecCatalogInstance,\n\ttype FecCatalogIssue,\n\ttype FecCatalogOutcome,\n\ttype FecCatalogReceiver,\n\ttype FecCatalogReport,\n} from \"@blockcast/mmt-base\";\nexport {\n\tDEFAULT_REPAIR_PRIORITY,\n\tMAX_ENCODING_SYMBOL_ID,\n\tMAX_RAPTORQ_SOURCE_SYMBOLS,\n\tMAX_REPAIR_SYMBOLS,\n\tREPAIR_PRIORITY_MAX,\n\tREPAIR_PRIORITY_MIN,\n\ttype RepairLayerParams,\n\ttype RepairLayersResolution,\n\ttype RepairOverlayParams,\n} from \"./repair-layers.js\";\n\n/**\n * Minimal structural shape of a resolved endpoint track. Callers may carry\n * extra fields (e.g. hang's ResolvedEndpointTrack adds `pid`) \u2014 the resolver\n * only reads `name` and `type`.\n */\nexport type CatalogTrackRef = {\n\tname: string;\n\ttype: \"video\" | \"audio\" | (string & {});\n};\n\n\n/** The container fields the group-duration reader consults. */\ntype ContainerCatalog = { kind?: unknown; timescale?: unknown };\n\ntype RenditionCatalog = KeyframeCadenceCatalog & {\n\tcontainer?: ContainerCatalog;\n\tfec?: unknown;\n\tframerate?: unknown;\n\tsampleRate?: unknown;\n\tsamplerate?: unknown;\n};\n\ntype CatalogWithRenditions = {\n\tvideo?: { renditions?: Record<string, RenditionCatalog> };\n\taudio?: { renditions?: Record<string, RenditionCatalog> };\n\t/** hang-native repair renditions, bound by `source` + `instanceId`. */\n\trepair?: { renditions?: Record<string, unknown> };\n};\n\n/** An MSF/CMSF catalog: `tracks[]` entries, each self-describing. */\ntype CatalogWithTracks = {\n\ttracks?: unknown;\n};\n\n/** Either catalog dialect `resolveRepairLayers` reads. */\nexport type RepairLayerCatalog = CatalogWithRenditions | CatalogWithTracks;\n\nexport type GroupDurationCatalog = {\n\tgroupDurationTicks?: number;\n\tgroupDurationMs?: number;\n\ttimescale?: number;\n};\n\n/** Rendition fields the group-duration reader consumes. */\nexport type GroupDurationRendition = GroupDurationCatalog & {\n\tcontainer?: ContainerCatalog;\n};\n\n/**\n * Largest tick count per Hz of timescale whose millisecond duration\n * `resolveGroupDurationMs` still accepts: `(ticks / timescale) * 1000` must not\n * exceed `Number.MAX_SAFE_INTEGER`, so `ticks <= timescale * (MAX_SAFE / 1000)`\n * (Hz times seconds is ticks).\n */\nconst MAX_SAFE_TICKS_PER_HZ = Number.MAX_SAFE_INTEGER / 1000;\n\n/** A positive safe-integer Hz timescale, or undefined. */\nfunction timescaleHz(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value > 0 ? value : undefined;\n}\n\n/**\n * A rendition's publisher group duration in ms, or undefined when the catalog\n * carries none.\n *\n * `groupDurationTicks` over a timescale wins when both are present, through\n * `resolveGroupDurationMs` (draft-ramadan-moq-mmt-01 \u00A74.4.2). The timescale is\n * the MMTP container's when the rendition declares one, else the rendition's\n * own `timescale`. Only the ticks are forwarded: a catalog `groupDurationMs`\n * is often a rounded fraction (21.33 for 1024/48000), which the ticks path\n * never reads but `resolveGroupDurationMs` would reject as a non-integral ms.\n * Ticks with no usable timescale, and ticks that `resolveGroupDurationMs`\n * would reject, fall back to `groupDurationMs` rather than guessing a clock.\n * That rejection is on the product, not the operand: the count must be a safe\n * integer and `(ticks / timescale) * 1000` must stay within\n * `Number.MAX_SAFE_INTEGER`, which a safe-integer count can still exceed when\n * `timescale < 1000`, so the guard below bounds the count by\n * `timescale * (Number.MAX_SAFE_INTEGER / 1000)` and the reader never throws.\n * The production catalog signals both fields on every rendition and no\n * rendition-level timescale, so the `groupDurationMs` fallback is the path it\n * takes.\n */\nexport function catalogGroupDurationMs(rendition: GroupDurationRendition | undefined): number | undefined {\n\tif (!rendition) return undefined;\n\tconst ticks = numericField(rendition.groupDurationTicks);\n\tconst ms = numericField(rendition.groupDurationMs);\n\tconst timescale =\n\t\ttimescaleHz(rendition.container?.kind === \"mmtp\" ? rendition.container.timescale : undefined) ??\n\t\ttimescaleHz(rendition.timescale);\n\tif (\n\t\tticks !== undefined &&\n\t\ttimescale !== undefined &&\n\t\tNumber.isSafeInteger(ticks) &&\n\t\tticks <= timescale * MAX_SAFE_TICKS_PER_HZ\n\t) {\n\t\treturn resolveGroupDurationMs({ groupDurationTicks: ticks }, timescale);\n\t}\n\treturn ms;\n}\n\n/**\n * The group duration of the catalog rendition an endpoint track was resolved\n * from, by type and name (the same lookup `resolveCatalogTrackFec` performs\n * for FEC geometry). Undefined when the rendition is missing or carries no\n * group duration, so a caller that needs one can fail closed by name.\n */\nexport function resolveCatalogTrackGroupDurationMs(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttrack: CatalogTrackRef,\n): number | undefined {\n\tconst renditions = track.type === \"audio\" ? catalog?.audio?.renditions : catalog?.video?.renditions;\n\treturn catalogGroupDurationMs(renditions?.[track.name]);\n}\n\nexport type KeyframeCadenceCatalog = GroupDurationCatalog & {\n\tkeyframeDurationTicks?: number;\n\tkeyframeDurationMs?: number;\n\tkeyframeIntervalTicks?: number;\n\tkeyframeIntervalMs?: number;\n\tgopDurationTicks?: number;\n\tgopDurationMs?: number;\n};\n\nfunction positiveNumber(value: number | undefined): number | undefined {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\n/**\n * A rendition's explicit keyframe cadence in ms: `keyframeInterval*`, then\n * `keyframeDuration*`, then `gopDuration*`, each preferring integer ticks over\n * the timescale to the ms field. Undefined when none is stated; a group\n * duration is not a keyframe cadence and is never substituted.\n */\nexport function catalogKeyframeCadenceMs(rendition: KeyframeCadenceCatalog | undefined): number | undefined {\n\tif (!rendition) return undefined;\n\tconst timescale = positiveNumber(rendition.timescale);\n\tconst ticksToMs = (ticks: number | undefined) => {\n\t\tconst value = positiveNumber(ticks);\n\t\treturn value !== undefined && timescale !== undefined ? (value * 1000) / timescale : undefined;\n\t};\n\treturn (\n\t\tticksToMs(rendition.keyframeIntervalTicks) ??\n\t\tpositiveNumber(rendition.keyframeIntervalMs) ??\n\t\tticksToMs(rendition.keyframeDurationTicks) ??\n\t\tpositiveNumber(rendition.keyframeDurationMs) ??\n\t\tticksToMs(rendition.gopDurationTicks) ??\n\t\tpositiveNumber(rendition.gopDurationMs)\n\t);\n}\n\n/**\n * Media units per second an FEC track's block geometry is built on, read only\n * from what the catalog states: video frames per second from `framerate`;\n * audio AAC access units (1024 samples) per second from the endpoint sample\n * rate, then the catalog `sampleRate`, then `timescale` (MP4-shaped audio).\n * The first value present is the one validated, so a declared but unusable\n * rate is not replaced by the next. Undefined when no usable rate is stated:\n * there is no default, since this sizes interleave geometry. A video track\n * never reads its timescale as a rate.\n */\nexport function catalogFecMediaUnitsPerSecond(\n\ttype: \"video\" | \"audio\",\n\trendition: { framerate?: number; sampleRate?: number; timescale?: number } | undefined,\n\tendpointSampleRate?: number,\n): number | undefined {\n\tif (type === \"video\") return positiveNumber(rendition?.framerate);\n\tconst sampleRate = positiveNumber(endpointSampleRate ?? rendition?.sampleRate ?? rendition?.timescale);\n\treturn sampleRate === undefined ? undefined : sampleRate / 1024;\n}\n/**\n * Why a track that declares FEC resolved to no FEC: its base instance is not\n * `ok` for this receiver (`resolveFecBindings`). `code` and `path` are the\n * validator's (`validateFecCatalog`, draft-ramadan-moq-fec-catalog-00 5,\n * fec-core-00 10.3); `outcome` tells an instance this receiver forgoes\n * (`unavailable`, e.g. a block profile it does not decode) from an invalid\n * one (`violation`). A draft-01 object `fec` is `fec_member` at that `fec`.\n * Per fec-core-00 10.3 the track's source is consumed without FEC and none of\n * its repair tracks is joined; it is the only track affected.\n */\nexport type TrackFecUnavailable = FecBindingUnavailable;\n\nexport type TrackFecParams = {\n\tfecK: number;\n\t/** The base instance's `scheme`: present exactly when the track has FEC. */\n\tfecAlgorithm?: \"raptorq\" | \"reed-solomon\";\n\t/** The base instance's `blockProfile`, present exactly when the track has\n\t * FEC. This runtime decodes `iso-ssbg1-v1` only (ssbg_mode1, K' =\n\t * SSB_length, consecutive SS_IDs); it is never defaulted. */\n\tfecBlockProfile?: FecBlockProfile;\n\t/** Present exactly when the track declares FEC and its base instance is\n\t * not `ok` (see `TrackFecUnavailable`). The track then resolves to no FEC\n\t * (`fecK` 0, no `fecBlockProfile`, no repair track) and this is the\n\t * reason a caller surfaces. */\n\tfecUnavailable?: TrackFecUnavailable;\n\t/** P_0: the base instance's `repairSymbols` (layer 0). */\n\tfecRepairCount: number;\n\t/** Total repair symbols per block across the BASE layers: the wire\n\t * RSB_length on every base layer packet. Equals fecRepairCount on a\n\t * single-layer track. A keyframe overlay is a separate FEC instance with\n\t * its own RSB_length and is not included. */\n\tfecTotalRepairCount: number;\n\tfecSymbolSize: number;\n\tfecInterleaveMs: number;\n\t/** Catalog fec.reorderToleranceMs \u2014 signaled jitter/reorder allowance, ADDED\n\t * to the block span to form a deadline. Deliberately `number | undefined`\n\t * and NOT floored to 0: a declared 0 (\"this path needs no slack\") and an\n\t * absent value (\"the encoder never said\") must stay distinguishable, or a\n\t * consumer that wants to fail loud on absence silently reads a default\n\t * instead \u2014 which is exactly how the magic 3.0 survived. */\n\tfecReorderToleranceMs?: number;\n\trepairTrackName?: string;\n\t/** Every base repair layer in order, layer 0 first, with derived RS_ID\n\t * offsets. Present whenever repairTrackName is. */\n\trepairLayers?: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances declared for this track. Present (possibly\n\t * empty) whenever repairLayers is. */\n\trepairOverlays?: readonly RepairOverlayParams[];\n\tfps?: number;\n\tsampleRate?: number;\n};\n\nconst NO_FEC: TrackFecParams = {\n\tfecK: 0,\n\tfecRepairCount: 0,\n\tfecTotalRepairCount: 0,\n\tfecSymbolSize: 0,\n\tfecInterleaveMs: 0,\n};\n\n/**\n * Repair symbols per block a receiver has actually joined: the sum of\n * `repairSymbols` over the BASE layers whose repair track it joined\n * (draft-ramadan-moq-fec 6.3). This is a symbol count, not a count of layers.\n *\n * A keyframe overlay is a separate FEC instance with its own K, its own\n * RSB_length and its own RS_ID space, so it is never added here however it was\n * joined. Counting it would overstate the base instance's P and misreport\n * CMCD `x-bc-fec-p`.\n *\n * Falls back to P_0 when the track declares no layers, which is the\n * single-layer identity rather than a default: with one layer, P_0 is the\n * total.\n */\nexport function joinedRepairSymbolCount(\n\tparams: Pick<TrackFecParams, \"fecRepairCount\" | \"repairLayers\">,\n\tjoinedRepairTrackNames: Iterable<string>,\n): number {\n\tconst layers = params.repairLayers;\n\tif (layers === undefined || layers.length === 0) return params.fecRepairCount;\n\tconst joined = new Set(joinedRepairTrackNames);\n\tlet total = 0;\n\tfor (const layer of layers) {\n\t\tif (joined.has(layer.repairTrackName)) total += layer.repairSymbols;\n\t}\n\treturn total;\n}\n\n/** The binding of one source, scoped to a typed ref's role. */\nfunction bindingOf(catalog: unknown, source: string | CatalogTrackRef): FecSourceBinding | undefined {\n\tif (!catalog) return undefined;\n\tconst name = typeof source === \"string\" ? source : source.name;\n\tconst binding = resolveFecBindings(catalog).get(name);\n\tif (binding === undefined || typeof source === \"string\") return binding;\n\tconst role = binding.pointer.split(\"/\")[1];\n\t// A typed ref resolves only within its own role; a `tracks[]` entry has none.\n\treturn role === \"tracks\" || role === source.type ? binding : undefined;\n}\n\n/**\n * Why `source` has no FEC although it declares some, or undefined (it has FEC,\n * or declares none).\n */\nexport function catalogTrackFecUnavailable(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): TrackFecUnavailable | undefined {\n\tconst binding = bindingOf(catalog, source);\n\treturn binding?.status === \"unavailable\" ? binding.reason : undefined;\n}\n\n/**\n * Every repair track of one source track: base layers with derived RS_ID\n * offsets and its keyframe overlay, from either catalog dialect, as\n * `resolveFecBindings` binds them. Undefined when the source declares no FEC\n * or its base instance is not `ok` (`catalogTrackFecUnavailable` says why): a\n * receiver then joins none of its repair tracks. Never throws.\n */\nexport function resolveRepairLayers(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): RepairLayersResolution | undefined {\n\tconst binding = bindingOf(catalog, source);\n\tif (binding?.status !== \"ok\") return undefined;\n\tconst overlays: RepairOverlayParams[] = binding.overlay\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\trepairTrackName: binding.overlay.repairTrackName,\n\t\t\t\t\tscope: \"keyframe\",\n\t\t\t\t\tsourceSymbols: binding.overlay.sourceSymbols,\n\t\t\t\t\trepairSymbols: binding.overlay.repairSymbols,\n\t\t\t\t\tpriority: binding.overlay.priority,\n\t\t\t\t},\n\t\t\t]\n\t\t: [];\n\treturn {\n\t\tsourceTrackName: binding.track,\n\t\tbaseLayers: binding.layers as readonly RepairLayerParams[],\n\t\toverlays,\n\t\ttotalBaseRepairSymbols: binding.totalBaseRepairSymbols,\n\t};\n}\n\n/** A MoQ Transport priority (0..255, lower first), or null. */\nfunction moqtPriority(value: unknown): number | null {\n\treturn Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= 255 ? (value as number) : null;\n}\n\n/**\n * The on-demand repair target of one source track: its base layer 0 repair\n * track and the two catalog priorities draft-ramadan-moq-fec \u00A711.4.5 derives\n * the FETCH's SUBSCRIBER_PRIORITY from (see `repairFetchSubscriberPriority`).\n *\n * - `repairPriority`: layer 0's catalog `priority`, or 240 when it declares none.\n * - `sourcePriority`: the source's MSF `tracks[]` entry `priority` (MoQ\n * Transport scale), or null for a hang rendition, which carries none.\n *\n * Null when the source has no `ok` base instance, or no positive\n * `interleaveDepthMs`: unicast repair requires a known elapsed FEC deadline\n * (\u00A711.4).\n */\nexport function catalogRepairTarget(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): RepairTarget | null {\n\tconst binding = bindingOf(catalog, source);\n\tif (binding?.status !== \"ok\" || !(binding.base.interleaveDepthMs ?? 0)) return null;\n\tconst layerZero = binding.layers[0]!;\n\tconst m = /^\\/tracks\\/(\\d+)$/.exec(binding.pointer);\n\tconst tracks = (catalog as CatalogWithTracks).tracks;\n\tconst entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : undefined;\n\tconst sourcePriority = entry ? moqtPriority((entry as Record<string, unknown>)[\"priority\"]) : null;\n\treturn { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };\n}\n\nfunction numericField(value: unknown): number | undefined {\n\tconst n = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n\treturn Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\n/** Like `numericField` but admits an explicit 0, and still rejects absent,\n * negative and non-finite input. Absence returns undefined so callers can\n * tell \"declared none\" from \"never declared\". */\nfunction nonNegativeField(value: unknown): number | undefined {\n\tconst n = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n\treturn Number.isFinite(n) && n >= 0 ? n : undefined;\n}\n\n/**\n * Per-track FEC geometry from the source's binding: `NO_FEC` when it declares\n * none, `NO_FEC` plus `fecUnavailable` when its base instance is not `ok`.\n */\nfunction fecParams(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttrack: CatalogTrackRef | undefined,\n\tfps?: number,\n\tsampleRate?: number,\n): TrackFecParams {\n\tconst mediaRate = sampleRate === undefined ? { fps } : { fps, sampleRate };\n\tconst binding = track ? bindingOf(catalog, track) : undefined;\n\tif (binding === undefined) return { ...NO_FEC, ...mediaRate };\n\tif (binding.status === \"unavailable\") return { ...NO_FEC, ...mediaRate, fecUnavailable: binding.reason };\n\tconst { base } = binding;\n\tconst repair = resolveRepairLayers(catalog, track!)!;\n\treturn {\n\t\tfecK: base.sourceSymbols,\n\t\tfecAlgorithm: base.scheme,\n\t\tfecBlockProfile: parseFecBlockProfile(base.blockProfile, `${binding.pointer}/fec/blockProfile`),\n\t\tfecRepairCount: base.repairSymbols,\n\t\t// The wire RSB_length: every base layer's packets carry the base total.\n\t\tfecTotalRepairCount: binding.totalBaseRepairSymbols,\n\t\tfecSymbolSize: base.symbolSize,\n\t\t// Zero/absence leaves the elapsed deadline unknown; geometry stays in-band.\n\t\tfecInterleaveMs: catalogFecInterleaveMs(base) ?? 0,\n\t\t// Additive to the span, never a multiplier; a declared 0 stays 0.\n\t\tfecReorderToleranceMs: nonNegativeField(base.reorderToleranceMs),\n\t\trepairTrackName: base.repairTrack,\n\t\trepairLayers: repair.baseLayers,\n\t\trepairOverlays: repair.overlays,\n\t\t...mediaRate,\n\t};\n}\n\n/**\n * Resolves per-track FEC geometry from the catalog for the active endpoint tracks.\n *\n * `video`/`audio` (and the top-level names and `catalogFps`) describe the\n * FIRST video and audio track of `tracks`. `forTrack` resolves any track from\n * its OWN rendition, scoped by its type: a second video track (an ABR rung or\n * simulcast variant, possibly sharing a packetId) gets its own framerate,\n * interleave and repair layers, never the first rendition's. A track with no\n * rendition of its type resolves to no FEC (`fecK` 0) and no media rate.\n *\n * FEC is per track and per instance (fec-core-00 10.3): a track whose base\n * instance is not `ok` resolves to no FEC with `fecUnavailable` saying why,\n * and every other track resolves as usual. Never throws on catalog input.\n */\nexport function resolveCatalogTrackFec(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttracks: readonly CatalogTrackRef[],\n): {\n\tvideoTrackName?: string;\n\taudioTrackName?: string;\n\tmissingVideoRendition?: string;\n\tmissingAudioRendition?: string;\n\tvideoForKeyframeRepair?: KeyframeCadenceCatalog;\n\tcatalogFps?: number;\n\tvideo: TrackFecParams;\n\taudio: TrackFecParams;\n\tforTrack(track: CatalogTrackRef): TrackFecParams;\n} {\n\tconst videoTrackName = tracks.find((t) => t.type === \"video\")?.name;\n\tconst audioTrackName = tracks.find((t) => t.type === \"audio\")?.name;\n\tconst videoRenditions = catalog?.video?.renditions;\n\tconst audioRenditions = catalog?.audio?.renditions;\n\tconst video = videoTrackName ? videoRenditions?.[videoTrackName] : undefined;\n\tconst audio = audioTrackName ? audioRenditions?.[audioTrackName] : undefined;\n\tconst catalogFps = numericField(video?.framerate);\n\tconst sampleRate = numericField(audio?.sampleRate ?? audio?.samplerate);\n\tconst audioFps = sampleRate ? sampleRate / 1024 : undefined;\n\tconst videoParams = fecParams(catalog, videoTrackName ? { name: videoTrackName, type: \"video\" } : undefined, catalogFps);\n\tconst audioParams = fecParams(\n\t\tcatalog,\n\t\taudioTrackName ? { name: audioTrackName, type: \"audio\" } : undefined,\n\t\taudioFps,\n\t\tsampleRate,\n\t);\n\n\tconst forTrack = (track: CatalogTrackRef): TrackFecParams => {\n\t\tif (track.type === \"audio\") {\n\t\t\tif (track.name === audioTrackName) return audioParams;\n\t\t\tconst rendition = audioRenditions?.[track.name];\n\t\t\tif (!rendition) return { ...NO_FEC };\n\t\t\tconst rate = numericField(rendition.sampleRate ?? rendition.samplerate);\n\t\t\treturn fecParams(catalog, { name: track.name, type: \"audio\" }, rate ? rate / 1024 : undefined, rate);\n\t\t}\n\t\tif (track.name === videoTrackName) return videoParams;\n\t\tconst rendition = videoRenditions?.[track.name];\n\t\tif (!rendition) return { ...NO_FEC };\n\t\treturn fecParams(catalog, { name: track.name, type: \"video\" }, numericField(rendition.framerate));\n\t};\n\n\treturn {\n\t\tvideoTrackName,\n\t\taudioTrackName,\n\t\tmissingVideoRendition: videoTrackName && videoRenditions && !video ? videoTrackName : undefined,\n\t\tmissingAudioRendition: audioTrackName && audioRenditions && !audio ? audioTrackName : undefined,\n\t\tvideoForKeyframeRepair: video,\n\t\tcatalogFps,\n\t\tvideo: videoParams,\n\t\taudio: audioParams,\n\t\tforTrack,\n\t};\n}\n", "/**\n * CARP Catalog Types \u2014 FEC and Multicast catalog extensions.\n *\n * Per draft-law-ramadan-moq-carp-reliability \u00A73 (FEC) and \u00A74 (Multicast).\n * Plain TypeScript interfaces + type guards \u2014 no Zod dependency.\n *\n * These are the canonical type definitions. Upstream Zod schemas\n * should derive from these via z.infer<> to avoid duplication.\n */\n\nimport {\n\tMMTP_PACKET_ID_MAX,\n\tMMTP_SIGNALING_PACKET_ID,\n\ttype FecBlockProfile,\n} from \"@blockcast/mmt-base\";\nimport { multicastEndpointSource } from \"@blockcast/mmt-base\";\nimport type { EndpointAuthConfig, MulticastAuthConfig } from \"./multicast-auth-catalog.js\";\n\n// \u2500\u2500 Track Role (Phase 14, D-02..D-05) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Track role derived from the last slash-segment of the track name.\n *\n * Convention: `{content}/base`, `{content}/delta`, `{content}/repair`.\n * Single-track (no slash) legacy streams use `\"single\"`.\n *\n * D-01: No `trackRole` / `role` / `kind` field in the catalog.\n * D-02: Role is expressed via track name suffix only.\n * D-03: Unknown suffixes throw at parse time (error on bad init).\n * D-04: Legacy single-track streams (no slash suffix) return `\"single\"`.\n * D-05: Single role per track \u2014 no multi-role arrays.\n */\nexport type TrackRole = \"base\" | \"delta\" | \"repair\" | \"single\";\n\nconst VALID_ROLE_SUFFIXES = new Set<string>([\"base\", \"delta\", \"repair\"]);\n\n/**\n * Derive the role of a track from its name.\n *\n * - Names with no `/` \u2192 `\"single\"` (legacy single-track streams, D-04).\n * - Names ending with `/base`, `/delta`, or `/repair` \u2192 that role (D-02).\n * - Empty name \u2192 throws (error on bad init).\n * - Trailing slash (empty last segment) \u2192 throws.\n * - Non-ASCII or control characters anywhere in the name \u2192 throws (T-14-01 fuzz hardening).\n * - Unknown last segment \u2192 throws.\n *\n * @throws {Error} On empty name, non-ASCII/control chars, empty suffix, or unknown suffix.\n */\nexport function parseTrackRole(trackName: string): TrackRole {\n\tif (trackName === \"\") {\n\t\tthrow new Error(\"[carp-catalog] empty track name\");\n\t}\n\n\t// T-14-01 fuzz hardening: reject non-ASCII and control characters.\n\t// This prevents catalog injection attacks where a publisher sends a crafted\n\t// track name that bypasses role checks by embedding unusual code points.\n\tfor (let i = 0; i < trackName.length; i++) {\n\t\tconst cp = trackName.charCodeAt(i);\n\t\t// Reject control characters (0x00-0x1F, 0x7F) and non-ASCII (>0x7E)\n\t\tif (cp <= 0x1f || cp === 0x7f || cp > 0x7e) {\n\t\t\tthrow new Error(\n\t\t\t\t`[carp-catalog] track name contains non-ASCII or control character (code point 0x${cp.toString(16)}) in \"${trackName}\"`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst lastSlash = trackName.lastIndexOf(\"/\");\n\tif (lastSlash === -1) {\n\t\t// No slash \u2014 legacy single-track stream (D-04)\n\t\treturn \"single\";\n\t}\n\n\tconst suffix = trackName.slice(lastSlash + 1);\n\tif (suffix === \"\") {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] empty role suffix in track name \"${trackName}\"; track names must not end with \"/\"`,\n\t\t);\n\t}\n\n\tif (!VALID_ROLE_SUFFIXES.has(suffix)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] unknown role suffix \"${suffix}\" in track name \"${trackName}\"; expected one of: base, delta, repair`,\n\t\t);\n\t}\n\n\treturn suffix as TrackRole;\n}\n\n/**\n * Non-throwing switching-set detector (BLO-8158).\n *\n * Returns `true` iff at least one track name carries a media role suffix\n * (`base` or `delta`) \u2014 i.e. the set looks like a base/delta/repair switching\n * set. A `/repair` track alone is ordinary FEC topology and is not sufficient\n * evidence of a switching set. Unlike `parseTrackRole`, this NEVER throws:\n * names without a `/`, and names with an unrecognised suffix (e.g. an ABR\n * resolution ladder like `video/1080p`, `video/720p`, `video/8k`), simply do\n * not count as a role.\n *\n * Use this for DETECTION (\"is this a switching set?\"). Once a switching set is\n * confirmed, `parseTrackRole` / `validateSwitchingSet` still strictly gate the\n * actual wiring. ABR ladders yield `false` here and fall through to the\n * ABR/single path instead of throwing.\n */\nexport function hasSwitchingSetRole(names: string[]): boolean {\n\treturn names.some((name) => {\n\t\tconst lastSlash = name.lastIndexOf(\"/\");\n\t\tif (lastSlash === -1) return false;\n\t\tconst suffix = name.slice(lastSlash + 1);\n\t\treturn suffix === \"base\" || suffix === \"delta\";\n\t});\n}\n\n/**\n * Validate that a set of track names forms a complete switching set.\n *\n * - Empty or single-track sets (all parsing to `\"single\"`) \u2192 `{ ok: true }`.\n * - Multi-track sets (any name has a role suffix) MUST include `base` and\n * `repair`. `delta` is optional (BLO-49199): base is the self-decodable leg,\n * so a base-only set plays in base-only mode; a delta without base cannot.\n * Missing roles are listed.\n *\n * Propagates throws from `parseTrackRole` (non-ASCII, unknown suffix, etc.).\n *\n * D-04: Legacy single-track streams pass without enforcement.\n * D-04: Once any suffix appears, the set is multi-track and needs base + repair.\n */\nexport function validateSwitchingSet(\n\ttrackNames: string[],\n): { ok: true } | { ok: false; missing: TrackRole[] } {\n\tif (trackNames.length === 0) return { ok: true };\n\n\tconst roles = trackNames.map(parseTrackRole);\n\n\t// If every role is \"single\", this is a legacy single-track stream \u2014 pass.\n\tif (roles.every((r) => r === \"single\")) {\n\t\treturn { ok: true };\n\t}\n\n\t// Multi-track: at least one name has a role suffix.\n\t// The set MUST contain base and repair; delta is optional (BLO-49199).\n\t// Explicit Set<TrackRole> so the TrackRole-typed `required.filter`\n\t// callback can call .has(r) without a narrow-mismatch complaint.\n\tconst presentRoles: Set<TrackRole> = new Set(roles.filter((r) => r !== \"single\"));\n\tconst required: TrackRole[] = [\"base\", \"repair\"];\n\tconst missing = required.filter((r) => !presentRoles.has(r));\n\n\tif (missing.length > 0) {\n\t\treturn { ok: false, missing };\n\t}\n\treturn { ok: true };\n}\n\n// \u2500\u2500 FEC Catalog Extension (draft-ramadan-moq-fec-catalog-00 3) \u2500\n\n/**\n * One FEC instance object (draft-ramadan-moq-fec-catalog-00 3.1). A track's\n * `fec` is an array of these ({@link FecCatalogConfig}); an empty array, or no\n * `fec`, declares no FEC. Member names are case sensitive. This is the shape\n * only: whether an instance is usable is `validateFecCatalog`'s answer (fail\n * closed per instance), and runtime readers take it from `resolveFecBindings`,\n * never from a local predicate.\n */\nexport interface FecInstanceConfig {\n\t/** 0..255, unique per track; absent means 0. */\n\tinstanceId?: number;\n\t/** `raptorq`, `reed-solomon`, or any other registered token, which a\n\t * receiver that does not implement it consumes without FEC. */\n\tscheme: string;\n\t/** REQUIRED, never defaulted. This library implements `iso-ssbg1-v1`\n\t * (BLO-35597); any other token is no FEC on that instance. */\n\tblockProfile: FecBlockProfile | (string & {});\n\t/** K. */\n\tsourceSymbols: number;\n\t/** P of layer 0, >= 1. */\n\trepairSymbols: number;\n\t/** T, bytes per symbol. */\n\tsymbolSize: number;\n\tinterleaveDepthMs?: number;\n\t/** Additive jitter/reorder allowance in milliseconds (never a multiplier). */\n\treorderToleranceMs?: number;\n\t/** Layer-0 repair track. Further layers and an overlay are `fec-repair`\n\t * tracks naming this track in `source` and this instance's `instanceId`. */\n\trepairTrack: string;\n\tsymbolTrack?: string;\n\t/** Display only; never drives behaviour. */\n\tlabel?: string;\n\t/** Phase-14 base-only fallback window in FEC blocks (D-11). Blockcast\n\t * extension: an unrecognized instance key the validator ignores\n\t * (catalog-00 3.4); read by `resolveDeltaFallback`. */\n\tdeltaFailWindowBlocks?: number;\n\t/** Phase-14 delta re-subscribe probe interval in ms (D-13). Blockcast\n\t * extension, as above. */\n\tdeltaProbeIntervalMs?: number;\n}\n\n/** A track's `fec` member (draft-ramadan-moq-fec-catalog-00 3). */\nexport type FecCatalogConfig = readonly FecInstanceConfig[];\n\n// \u2500\u2500 Multicast Catalog Extension (CARP \u00A74.1-4.4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * An endpoint's join mode, per draft-ramadan-moq-multicast-01 4.1 at\n * Blockcast/moqcast-draft@fd962fd73d21ed281b60219c11ea61cc9e4e91c4.\n * `sourceAddress` selects it (present: SSM, absent: ASM) and `protocol` only\n * restates it. A bearer (AMT, ATSC 3.0) is a `networkSource` entry, never a\n * protocol; `multicastEndpointSource` in `@blockcast/mmt-base` derives the\n * mode and `S` for every reader.\n */\nexport type MulticastProtocol = \"ssm\" | \"asm\";\n\n/**\n * Per-track endpoint membership, per draft-ramadan-moq-multicast-01 \u00A74.1.\n *\n * `packetId` is the MMTP packet_id used for packet-level demux on the\n * multicast wire, in 1..65535: packet_id 0 (`MMTP_SIGNALING_PACKET_ID`) is\n * the MMTP signaling flow and is never a track's. New emitters MUST use this\n * object form; the legacy\n * `string` form is accepted by parsers for one release for backward\n * compatibility (BLO-3323 migration).\n */\nexport interface MulticastTrackObject {\n\tname: string;\n\tpacketId: number;\n}\n\n/** Track entry in a multicast endpoint \u2014 accepts both legacy and v2 shape. */\nexport type MulticastTrack = string | MulticastTrackObject;\n\nexport interface MulticastEndpoint {\n\t/** Restates the mode `sourceAddress` selects; an endpoint where it disagrees is ignored. */\n\tprotocol?: MulticastProtocol;\n\t/** SSM source address; its absence selects ASM. */\n\tsourceAddress?: string;\n\t/** Multicast group address (REQUIRED). */\n\tgroupAddress: string;\n\t/** UDP port number (REQUIRED). */\n\tport: number;\n\t/**\n\t * Tracks available on this endpoint (REQUIRED).\n\t *\n\t * Accepts both legacy `string` (track name only) and v2 object form\n\t * (`{ name, packetId }`) for backward compatibility. New emitters\n\t * MUST use the object form (BLO-3323 / draft \u00A74.1). Consumers can\n\t * type-narrow with `typeof t === \"string\"` to handle both.\n\t */\n\ttracks: MulticastTrack[];\n\t/**\n\t * Aggregate bandwidth of this endpoint in bits per second.\n\t * RECOMMENDED per draft \u00A74.1 \u2014 enables capacity-aware joins; REQUIRED on a\n\t * protected endpoint, where it bounds the receiver's hold queues (7.2.6).\n\t */\n\tbandwidth?: number;\n\t/**\n\t * Marks the endpoint's flow protected (7.2.6). REQUIRED when the\n\t * `multicast` object carries `auth`, forbidden when it does not.\n\t */\n\tauth?: EndpointAuthConfig;\n\t/** Bearers for this endpoint alone (4.2); readers prefer it to the catalog-level array. */\n\tnetworkSource?: NetworkSource[];\n}\n\nexport type AmtDiscovery = \"dns\" | \"driad\" | \"manual\";\n\n/**\n * One `multicast.networkSource[]` entry, per draft-ramadan-moq-multicast \u00A74.1.\n *\n * Mirrors moq-rs `NetworkSource` (the normative validator) and the\n * `networkSource` `$def` in `catalog.schema.json` 1:1 \u2014 a change here needs the\n * same change in both. `discovery` is OPTIONAL (draft -01 4.2.1): `\"dns\"`\n * names a relay service hostname whose A/AAAA answers are candidates,\n * `\"manual\"` a literal relay, and omitted means `\"dns\"` when `relay` is a\n * hostname (DRIAD when `relay` is absent too). `\"dns\"` and `\"manual\"` require\n * `relay`; that relay is the one the player joins. Under `discovery: \"driad\"` the relay is resolved at\n * runtime and `relay` MUST be omitted (draft -01 4.2.1); the schema, the\n * runtime validator and `multicastFromConfig` all reject one. The tolerant\n * readers (`findAmtRelay`, `catalogAmtNetworkSource`) still skip a DRIAD\n * entry's relay, so a DRIAD entry never pins a relay.\n * `port` is OPTIONAL on the wire: the join boundary itself has no default, but\n * every reader resolves one before that boundary, so a manual relay without a\n * port joins on RFC 7450's 2268 rather than failing (BLO-38176).\n *\n * Not to be confused with `CatalogNetworkSource` in `catalog-network-source.ts`,\n * which is also exported from the package root and also describes\n * `$defs/networkSource`. The split is deliberate and the two must NOT be\n * unified: this type is the strict EMITTER/wire shape, so tightening it is what\n * keeps published catalogs normative. That one is the tolerant READER \u2014 every\n * field optional, and it also accepts the bare-object form that hang's zod\n * RootSchema and older hand-authored catalogs emit. Tightening the reader would\n * reject catalogs already in the wild, so when this type changes, widen the\n * reader only if the new shape must be consumed; never narrow it to match.\n *\n * This is the one place a catalog declares an AMT relay: in the `multicast`\n * object for every endpoint, or in one endpoint's own `networkSource`\n * (draft -01 4.2). The endpoint model carries no relay field.\n */\nexport type NetworkSource =\n\t| { type: \"amt\"; discovery?: AmtDiscovery; relay?: string; port?: number }\n\t| {\n\t\t\ttype: \"atsc3\";\n\t\t\t/** RF centre frequency in kHz (draft -01 4.2.2). The only REQUIRED tuning field. */\n\t\t\tfrequency: number;\n\t\t\tplpId?: number;\n\t\t\tserviceId?: number;\n\t\t\t/**\n\t\t\t * SLS of the MMTP service, MP table included (A/331:2026-04 7.2). It\n\t\t\t * describes the MMTP session and nothing of this profile: the flow\n\t\t\t * parameters and keys come from provisioning. libmmt does not fetch it.\n\t\t\t */\n\t\t\tslsUri?: string;\n\t };\n\nexport interface MulticastCatalogConfig {\n\t/** Enable/disable multicast (OPTIONAL, default true). */\n\tenabled?: boolean;\n\t/** Multicast endpoints (REQUIRED). */\n\tendpoints: MulticastEndpoint[];\n\t/**\n\t * Relay/tuning discovery applying to every endpoint (4.2); omitted for\n\t * native joins, whose endpoint tuple is the whole join. An endpoint's own\n\t * `networkSource` takes precedence for that endpoint.\n\t */\n\tnetworkSource?: NetworkSource[];\n\t/** bc-provenance version 2 profile and keys (7.2.6). */\n\tauth?: MulticastAuthConfig;\n\t/**\n\t * AMT fallback configuration.\n\t *\n\t * @deprecated Superseded by `networkSource` \u2014 `{discovery, relayDns}` maps\n\t * onto `{type: \"amt\", discovery, relay}`. `multicastFromConfig` lifts it and\n\t * no longer emits it; the schema rejects it.\n\t */\n\tamtFallback?: {\n\t\tdiscovery: AmtDiscovery;\n\t\trelayDns?: string;\n\t};\n\t/**\n\t * Monotonic catalog version. Publishers MUST bump on any\n\t * `endpoints[]` mutation (BLO-3323 catalog freshness model).\n\t */\n\tversion?: number;\n\t/**\n\t * Unix-ms timestamp after which subscribers SHOULD refetch the\n\t * catalog. Refetch-on-multicast-receive-timeout short-circuits this.\n\t */\n\tvalidUntil?: number;\n}\n\nexport function isMulticastTrackObject(obj: unknown): obj is MulticastTrackObject {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn typeof o[\"name\"] === \"string\" && typeof o[\"packetId\"] === \"number\";\n}\n\n/**\n * Wire-shape predicate: returns `true` when `obj` matches the\n * MulticastEndpoint JSON shape strictly enough to be safely cast.\n *\n * Intentionally less strict than `multicastFromConfig`'s\n * `validateEndpoint` (in `multicast-input.ts`):\n *\n * - `tracks: []` is accepted here (Array.isArray check only) but\n * rejected by `validateEndpoint` as `empty_tracks`.\n * - SSM-specific group-range and source-required checks are not\n * enforced here.\n * - Bandwidth/AmtFallback/version are not type-checked.\n *\n * That asymmetry is deliberate: this predicate guards JSON parsed off\n * the wire (already-published catalogs that downstream may have\n * trimmed), while `validateEndpoint` guards publisher input *before*\n * a catalog is composed. Subscribers are tolerant; publishers are\n * strict \u2014 the catalog format never re-validates upstream invariants.\n */\nexport function isMulticastEndpoint(obj: unknown): obj is MulticastEndpoint {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn (\n\t\tmulticastEndpointSource(o) !== undefined &&\n\t\ttypeof o[\"groupAddress\"] === \"string\" && (o[\"groupAddress\"] as string).length > 0 &&\n\t\ttypeof o[\"port\"] === \"number\" && (o[\"port\"] as number) > 0 && (o[\"port\"] as number) <= 65535 &&\n\t\tArray.isArray(o[\"tracks\"]) &&\n\t\t(o[\"tracks\"] as unknown[]).every((t: unknown) => typeof t === \"string\" || isMulticastTrackObject(t))\n\t);\n}\n\nexport function isMulticastCatalogConfig(obj: unknown): obj is MulticastCatalogConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn (\n\t\tArray.isArray(o[\"endpoints\"]) &&\n\t\t(o[\"endpoints\"] as unknown[]).every((ep: unknown) => isMulticastEndpoint(ep)) &&\n\t\t// Shape only, not contents \u2014 entry validation is `multicastFromConfig`'s.\n\t\t// `networkSource` now carries the relay, so a non-array here must not\n\t\t// narrow clean: every reader of it indexes it.\n\t\t//\n\t\t// `=== undefined`, not `!(\"networkSource\" in o)`: `multicastFromConfig`\n\t\t// tests definedness, and the two must agree on what \"set\" means. A key\n\t\t// present with value `undefined` is a legal `MulticastCatalogConfig`\n\t\t// (`strict` without `exactOptionalPropertyTypes`) and is what a spread\n\t\t// of an optional field produces \u2014 `in` would narrow that valid config\n\t\t// to `false` and a consumer gating on this guard would discard a\n\t\t// working multicast block.\n\t\t(o[\"networkSource\"] === undefined || Array.isArray(o[\"networkSource\"]))\n\t);\n}\n\n// \u2500\u2500 Track Timing (mmt-01 \u00A74.4.2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Per-track group-duration signaling.\n *\n * The catalog publishes either `groupDurationMs` (integer ms) or the exact\n * `groupDurationTicks` value used by `groupIdForTicks` in align.ts. The tick\n * form is required for cases where milliseconds cannot represent the cadence\n * exactly:\n *\n * - Per-frame group durations at non-integer-ms cadences\n * (1/30 s = 33.333\u2026 ms; 1/60 s = 16.666\u2026 ms).\n * - Audio sample-rate timescales not divisible by 1000 Hz\n * (e.g. 44 100 Hz \u00D7 333 ms = 14 685.3 ticks).\n *\n * If both fields are present, `groupDurationTicks` wins (matches the\n * spec, mmt-01 \u00A74.4.2).\n *\n * @see draft-ramadan-moq-mmt-01 \u00A74.4.2\n * @see ./align.ts groupIdForTicks\n */\nexport interface TrackTimingConfig {\n\t/** Group duration in milliseconds (positive integer). Required unless an\n\t * exact groupDurationTicks value is supplied. */\n\tgroupDurationMs?: number;\n\t/** Integer-tick override (OPTIONAL, positive integer). Wins over the\n\t * ms field when both are present. Required when\n\t * `(groupDurationMs * timescale) % 1000 != 0`. */\n\tgroupDurationTicks?: number;\n}\n\nexport function isTrackTimingConfig(obj: unknown): obj is TrackTimingConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\tconst ms = o[\"groupDurationMs\"];\n\tconst ticks = o[\"groupDurationTicks\"];\n\tif (ms === undefined && ticks === undefined) return false;\n\tif (ms !== undefined && (typeof ms !== \"number\" || !Number.isInteger(ms) || ms <= 0)) return false;\n\tif (\"groupDurationTicks\" in o) {\n\t\tif (ticks === undefined) return ms !== undefined;\n\t\tif (typeof ticks !== \"number\" || !Number.isInteger(ticks) || ticks <= 0) return false;\n\t}\n\treturn true;\n}\n\n/** Resolve catalog group timing to an operational wall-clock duration.\n *\n * `groupDurationTicks` wins when both wire fields are present, matching\n * mmt-01 \u00A74.4.2. The result may be fractional because exact frame cadences\n * such as 33_333 ticks at 1 MHz cannot be represented by the wire catalog's\n * integer `groupDurationMs` field.\n */\nexport function resolveGroupDurationMs(\n\ttiming: TrackTimingConfig,\n\ttimescale: number,\n): number {\n\tif (!Number.isSafeInteger(timescale) || timescale <= 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: timescale must be a positive safe integer Hz, got ${timescale}`,\n\t\t);\n\t}\n\tif (!isTrackTimingConfig(timing)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: invalid TrackTimingConfig: ${JSON.stringify(timing)}`,\n\t\t);\n\t}\n\tif (timing.groupDurationTicks === undefined) {\n\t\treturn timing.groupDurationMs!;\n\t}\n\n\tconst durationMs = (timing.groupDurationTicks / timescale) * 1000;\n\tif (!Number.isFinite(durationMs) || durationMs <= 0 || durationMs > Number.MAX_SAFE_INTEGER) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: groupDurationTicks=${timing.groupDurationTicks} ` +\n\t\t\t\t`at timescale=${timescale} cannot be represented as a positive safe millisecond duration`,\n\t\t);\n\t}\n\treturn durationMs;\n}\n\n/**\n * Optional unicast archive tier (BLO-41290).\n *\n * `origin` is scheme-and-authority only. Object paths are never advertised:\n * a consumer derives them via the v1 ContentId profile \u2014\n * `/v1/{ns}/{track}/{group}[/{object}]` under this origin\n * (`mmt-archive/src/content_id.rs`). Advertising the pair a second time would\n * be a second source of truth that can disagree with `tracks[]`.\n *\n * `{track}` is the track's `name`. `{ns}` is the track's own `namespace` when\n * it declares one, and otherwise the MoQ subscription's Track Namespace tuple,\n * of which `ContentId::from_moq` admits exactly one element. The catalog does\n * not carry that tuple, so for a track omitting `namespace` the derivation\n * needs the subscription context \u2014 the catalog alone is not sufficient. That\n * inheritance is the load-bearing half of the contract: `namespace` is\n * optional in the schema, so a catalog can advertise an archive whose object\n * paths only resolve in a subscribing consumer.\n *\n * One origin at the top level, not one per track: the `(namespace, track)` key\n * lives in the derived path rather than in the origin, so a single origin\n * already disambiguates every track however many namespaces the catalog spans.\n * A per-track origin would only buy splitting one catalog across several\n * archives, which nothing asks for.\n *\n * Absence means \"no archive advertised\". It is never a default URL.\n */\nexport interface ArchiveConfig {\n\torigin: string;\n}\n\n/**\n * The advertised archive origin, or `undefined` when none is advertised.\n *\n * Takes a catalog already through `validateCmsfMmtpCatalog`, so the returned\n * string is canonical and safe to use as a base \u2014 no string surgery on a\n * config blob, and no second place that decides what an origin may look like.\n */\nexport function archiveOrigin(catalog: CmsfCatalog): string | undefined {\n\treturn catalog.archive?.origin;\n}\n\n/**\n * Pin the archive origin to exactly one spelling per archive.\n *\n * The ContentId codec rejects percent-encoding so every identity has exactly\n * one URL path; an advertised base must not reintroduce aliasing above it.\n * `value === new URL(value).origin` is most of the rule \u2014 `URL.origin` drops\n * path, query, fragment and userinfo, lowercases scheme and host, strips the\n * default port and any trailing slash, and normalizes percent-escapes. So a\n * separate percent-encoding check would be redundant, not missing: `%6d` in a\n * host decodes and fails the comparison. `https:` is checked separately\n * because `http://host` is already its own origin and passes the comparison.\n *\n * The comparison alone admits one alias, so the trailing dot is rejected\n * explicitly: `https://host.` is its own `URL.origin` verbatim, while `host.`\n * and `host` are the same DNS name. That aliases the advertised *base*, not\n * the codec input \u2014 the origin contributes no path segments, so a trailing\n * dot never reaches `ContentId::parse_path` \u2014 but it still gives one archive\n * two cache keys, and some TLS stacks treat `host.` as a distinct SNI/CORS\n * origin.\n *\n * `additionalProperties: false` on `archive` is enforced here as well as in\n * the schema. The runtime validator is the gate `archiveOrigin` documents\n * itself against, so a second spelling of the origin smuggled in under\n * another key must be rejected by both, not just by AJV.\n */\nfunction validateArchive(catalog: Record<string, unknown>): void {\n\tconst archive = catalog[\"archive\"];\n\tif (archive === undefined) return;\n\tif (typeof archive !== \"object\" || archive === null || Array.isArray(archive)) {\n\t\tcatalogError(\"archive_object\", \"$.archive\", \"archive must be an object\");\n\t}\n\tconst unknown = Object.keys(archive as Record<string, unknown>).filter(key => key !== \"origin\");\n\tif (unknown.length > 0) {\n\t\tcatalogError(\"archive_object\", \"$.archive\", `archive has unknown field(s) ${unknown.map(key => JSON.stringify(key)).join(\", \")}; only \"origin\" is allowed`);\n\t}\n\tconst origin = (archive as Record<string, unknown>)[\"origin\"];\n\t// Absent and present-but-wrong-type are different mistakes and the schema\n\t// already distinguishes them (`required` vs `type`). Collapsing both into\n\t// \"must be a string\" tells someone hand-writing a catalog to fix a field\n\t// they never wrote. Same wording convention as `codec is required`.\n\t//\n\t// This is the one archive rule where the two gates report different fields,\n\t// deliberately: AJV raises `required` at the parent, `/archive` (its\n\t// placement, not a choice \u2014 same shape as `fec-block-profile-missing`),\n\t// while this raises at `$.archive.origin`. Pointing at the field someone has\n\t// to add is the more useful of the two, so the \"same field\" convention\n\t// documented above bends here rather than the message getting vaguer.\n\tif (origin === undefined) {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", \"origin is required when an archive block is present\");\n\t}\n\tif (typeof origin !== \"string\") {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", \"origin must be a string\");\n\t}\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(origin);\n\t} catch {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin must be an absolute URL, got ${JSON.stringify(origin)}`);\n\t}\n\tif (parsed.protocol !== \"https:\") {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin must use https, got ${JSON.stringify(parsed.protocol)}`);\n\t}\n\tif (origin !== parsed.origin) {\n\t\tcatalogError(\n\t\t\t\"archive_origin\",\n\t\t\t\"$.archive.origin\",\n\t\t\t`origin must be scheme and authority in canonical form with no path, query, fragment, userinfo or trailing slash; expected ${JSON.stringify(parsed.origin)}, got ${JSON.stringify(origin)}`,\n\t\t);\n\t}\n\tif (parsed.hostname.endsWith(\".\")) {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin host must not end in a trailing dot; ${JSON.stringify(parsed.hostname)} and ${JSON.stringify(parsed.hostname.slice(0, -1))} are the same host`);\n\t}\n}\n\nexport type CmsfCatalog = {\n\tversion: number;\n\ttracks: Record<string, unknown>[];\n\tmulticast?: Record<string, unknown>;\n\tarchive?: ArchiveConfig;\n\t[key: string]: unknown;\n};\n\nexport type CmsfCatalogErrorCode =\n\t| \"catalog_object\"\n\t| \"catalog_version\"\n\t| \"catalog_tracks\"\n\t| \"catalog_common_track_fields\"\n\t| \"track_object\"\n\t| \"track_name\"\n\t| \"track_packaging\"\n\t| \"track_selection_params\"\n\t| \"track_is_live\"\n\t| \"track_codec\"\n\t| \"track_mmtp_mode\"\n\t| \"track_presentation_anchor\"\n\t| \"track_init_ref\"\n\t| \"catalog_init_data_list\"\n\t| \"track_timescale\"\n\t| \"track_group_duration_ms\"\n\t| \"track_group_duration_ticks\"\n\t| \"track_group_duration_exact\"\n\t| \"track_keyframe_interval\"\n\t| \"track_fec\"\n\t| \"track_fec_bounds\"\n\t| \"track_fec_block_profile\"\n\t| \"track_fec_block_profile_unimplemented\"\n\t| \"track_repair_track\"\n\t| \"track_repair_depends\"\n\t| \"track_repair_layer\"\n\t| \"track_repair_priority\"\n\t| \"track_repair_overlay\"\n\t| \"track_repair_geometry\"\n\t| \"track_switching_set\"\n\t| \"multicast_auth\"\n\t| \"multicast_endpoint\"\n\t| \"multicast_endpoint_auth\"\n\t| \"multicast_track\"\n\t| \"multicast_packet_id\"\n\t| \"multicast_relay_address\"\n\t| \"multicast_network_source\"\n\t| \"archive_object\"\n\t| \"archive_origin\";\n\nexport class CmsfCatalogValidationError extends Error {\n\tconstructor(\n\t\tpublic readonly code: CmsfCatalogErrorCode,\n\t\tpublic readonly path: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(`[cmsf:${code}] ${path}: ${message}`);\n\t\tthis.name = \"CmsfCatalogValidationError\";\n\t}\n}\n\nfunction catalogError(code: CmsfCatalogErrorCode, path: string, message: string): never {\n\tthrow new CmsfCatalogValidationError(code, path, message);\n}\n\n// draft-ramadan-moq-multicast-01 4.2.1: DRIAD supplies the relay\n// candidates, so a `discovery: \"driad\"` entry MUST omit `relay`.\nfunction validateNetworkSourceRelays(sources: unknown, path: string): void {\n\tif (!Array.isArray(sources)) return;\n\tsources.forEach((source, i) => {\n\t\tconst s = source as Record<string, unknown> | null;\n\t\tif (s?.[\"type\"] === \"amt\" && s[\"discovery\"] === \"driad\" && s[\"relay\"] !== undefined) {\n\t\t\tcatalogError(\"multicast_network_source\", `${path}[${i}].relay`, 'relay must be omitted with discovery \"driad\"');\n\t\t}\n\t});\n}\n\n/**\n * Validate the common CMSF shape and the MMTP required-field profile. It never\n * throws on a draft -02 4.1 / 7.2.6 multicast rule: pass `catalogNamespace`\n * (the namespace of the catalog track) to `admittedMulticastEndpoints` /\n * `multicastCatalogIssues` instead. The option is accepted and unused.\n */\nexport function validateCmsfMmtpCatalog(\n\tvalue: unknown,\n\t_options: { catalogNamespace?: string } = {},\n): CmsfCatalog {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tcatalogError(\"catalog_object\", \"$\", \"catalog must be an object\");\n\t}\n\tconst catalog = value as Record<string, unknown>;\n\t// IETF-draft catalogs (cast `ladder_catalog.rs`, moq-server\n\t// `catalog-synthesizer.ts`, consumed by the hang player) carry a string\n\t// version tag such as \"draft-ietf-moq-catalogformat-01\" instead of the CMSF\n\t// `version: 1` integer, and the draft-ramadan-moq-*-01 examples carry\n\t// \"draft-01\". Nothing keys behavior off the catalog version, so\n\t// normalize the tag to a number rather than rejecting the whole catalog \u2014\n\t// mirrors the relay (dual-stack-relay `bridge/catalog.rs`\n\t// `deserialize_optional_u32_lenient`, #825) and moqtail-ts\n\t// `normalizeCatalogVersion`.\n\tconst rawVersion = catalog[\"version\"];\n\tconst normalizedVersion =\n\t\ttypeof rawVersion === \"string\"\n\t\t\t? Number.isFinite(Number(rawVersion))\n\t\t\t\t? Number(rawVersion)\n\t\t\t\t: rawVersion.startsWith(\"draft-ietf-moq-catalogformat\") || rawVersion === \"draft-01\"\n\t\t\t\t\t? 1\n\t\t\t\t\t: NaN\n\t\t\t: rawVersion;\n\tif (!Number.isInteger(normalizedVersion) || (normalizedVersion as number) <= 0) {\n\t\tcatalogError(\"catalog_version\", \"$.version\", \"version must be a positive integer or a recognized format tag\");\n\t}\n\tcatalog[\"version\"] = normalizedVersion;\n\tif (!Array.isArray(catalog[\"tracks\"])) {\n\t\tcatalogError(\"catalog_tracks\", \"$.tracks\", \"tracks must be an array\");\n\t}\n\n\tif (\"commonTrackFields\" in catalog) {\n\t\tcatalogError(\n\t\t\t\"catalog_common_track_fields\",\n\t\t\t\"$.commonTrackFields\",\n\t\t\t\"commonTrackFields is retired; every track carries its own fields\",\n\t\t);\n\t}\n\t// mmt-01 4.5 / MSF 5.1.7: entries carry an id and a type; an \"inline\"\n\t// entry carries its base64 init segment in data.\n\tconst initDataList = catalog[\"initDataList\"];\n\tif (initDataList !== undefined) {\n\t\tif (!Array.isArray(initDataList)) {\n\t\t\tcatalogError(\"catalog_init_data_list\", \"$.initDataList\", \"initDataList must be an array\");\n\t\t}\n\t\tinitDataList.forEach((entry: unknown, i: number) => {\n\t\t\tconst e = entry as Record<string, unknown> | null;\n\t\t\tconst entryPath = `$.initDataList[${i}]`;\n\t\t\tif (typeof e !== \"object\" || e === null || Array.isArray(e) ||\n\t\t\t\ttypeof e[\"id\"] !== \"string\" || e[\"id\"].length === 0 ||\n\t\t\t\ttypeof e[\"type\"] !== \"string\" || e[\"type\"].length === 0) {\n\t\t\t\tcatalogError(\"catalog_init_data_list\", entryPath, \"entry needs a non-empty string id and type\");\n\t\t\t}\n\t\t\tif (e[\"type\"] === \"inline\" && (typeof e[\"data\"] !== \"string\" || e[\"data\"].length === 0)) {\n\t\t\t\tcatalogError(\"catalog_init_data_list\", `${entryPath}.data`, 'an \"inline\" entry needs non-empty data');\n\t\t\t}\n\t\t});\n\t}\n\tconst tracks = catalog[\"tracks\"] as unknown[];\n\tconst trackRecords: Record<string, unknown>[] = [];\n\tconst positiveInteger = (input: unknown): input is number => Number.isSafeInteger(input) && (input as number) > 0;\n\tconst positiveNumber = (input: unknown): input is number =>\n\t\ttypeof input === \"number\" && Number.isFinite(input) && input > 0;\n\n\tfor (let index = 0; index < tracks.length; index++) {\n\t\tconst raw = tracks[index];\n\t\tconst path = `$.tracks[${index}]`;\n\t\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\t\tcatalogError(\"track_object\", path, \"track must be an object\");\n\t\t}\n\t\tconst track = raw as Record<string, unknown>;\n\t\tconst name = track[\"name\"];\n\t\tif (typeof name !== \"string\" || name.length === 0) {\n\t\t\tcatalogError(\"track_name\", `${path}.name`, \"name must be a non-empty string\");\n\t\t}\n\t\ttrackRecords.push(track);\n\n\t\tconst packaging = track[\"packaging\"];\n\t\tif (typeof packaging !== \"string\" || packaging.length === 0) {\n\t\t\tcatalogError(\"track_packaging\", `${path}.packaging`, \"packaging must be a non-empty string\");\n\t\t}\n\t\tconst hasIsLive = Object.prototype.hasOwnProperty.call(track, \"isLive\");\n\t\tconst isLive = track[\"isLive\"];\n\t\tif (hasIsLive && typeof isLive !== \"boolean\") {\n\t\t\tcatalogError(\"track_is_live\", `${path}.isLive`, \"isLive must be a boolean\");\n\t\t}\n\t\ttrack[\"isLive\"] = isLive ?? true;\n\t\tif (\"selectionParams\" in track) {\n\t\t\tcatalogError(\n\t\t\t\t\"track_selection_params\",\n\t\t\t\t`${path}.selectionParams`,\n\t\t\t\t\"selectionParams is retired; codec and the other selection fields are flat on the track\",\n\t\t\t);\n\t\t}\n\t\tconst codec = track[\"codec\"];\n\t\tif (packaging !== \"timeline\" && packaging !== \"mediatimeline\" && packaging !== \"eventtimeline\" && packaging !== \"fec-repair\" && packaging !== \"fec-source\" &&\n\t\t\t(typeof codec !== \"string\" || codec.length === 0)) {\n\t\t\tcatalogError(\"track_codec\", `${path}.codec`, `codec is required for packaging=${packaging}`);\n\t\t}\n\n\t\tconst timescale = track[\"timescale\"];\n\t\tconst groupDurationMs = track[\"groupDurationMs\"];\n\t\tconst groupDurationTicks = track[\"groupDurationTicks\"];\n\t\tconst keyframeIntervalMs = track[\"keyframeIntervalMs\"];\n\t\tconst keyframeIntervalTicks = track[\"keyframeIntervalTicks\"];\n\t\tif (groupDurationMs !== undefined && !positiveInteger(groupDurationMs)) {\n\t\t\tcatalogError(\"track_group_duration_ms\", `${path}.groupDurationMs`, \"must be a positive integer\");\n\t\t}\n\t\tif (groupDurationTicks !== undefined && !positiveInteger(groupDurationTicks)) {\n\t\t\tcatalogError(\"track_group_duration_ticks\", `${path}.groupDurationTicks`, \"must be a positive integer\");\n\t\t}\n\t\tif ((keyframeIntervalMs !== undefined && !positiveNumber(keyframeIntervalMs)) ||\n\t\t\t(keyframeIntervalTicks !== undefined && !positiveInteger(keyframeIntervalTicks))) {\n\t\t\tcatalogError(\"track_keyframe_interval\", path, \"keyframe interval fields must be positive numbers\");\n\t\t}\n\t\t// mmt-01 4.4.1: a signed integer in the track's timescale.\n\t\tconst anchor = track[\"presentationAnchorTicks\"];\n\t\tif (anchor !== undefined && !Number.isSafeInteger(anchor)) {\n\t\t\tcatalogError(\"track_presentation_anchor\", `${path}.presentationAnchorTicks`, \"must be a safe integer\");\n\t\t}\n\t\t// mmt-01 4.5: the id of a root initDataList entry.\n\t\tconst initRef = track[\"initRef\"];\n\t\tif (initRef !== undefined && (typeof initRef !== \"string\" || initRef.length === 0)) {\n\t\t\tcatalogError(\"track_init_ref\", `${path}.initRef`, \"must be a non-empty string\");\n\t\t}\n\t\t// FEC binding (draft-ramadan-moq-fec-catalog-00) is `validateFecCatalog`'s:\n\t\t// it fails closed per instance and never stops another track, so nothing\n\t\t// here throws on `fec` or on a repair or symbol track. A -01 single-object\n\t\t// `fec` therefore costs only that track's FEC (BLO-50171).\n\t\tif (packaging === \"mmtp\") {\n\t\t\t// mmt-01 12.1: MUST be \"mfu\"; a subscriber rejects anything else.\n\t\t\tif (track[\"mmtpMode\"] !== \"mfu\") {\n\t\t\t\tcatalogError(\"track_mmtp_mode\", `${path}.mmtpMode`, \"must be mfu\");\n\t\t\t}\n\t\t\tif (!positiveInteger(timescale)) {\n\t\t\t\tcatalogError(\"track_timescale\", `${path}.timescale`, \"must be a positive integer\");\n\t\t\t}\n\t\t\tif (groupDurationMs === undefined && groupDurationTicks === undefined) {\n\t\t\t\tcatalogError(\n\t\t\t\t\t\"track_group_duration_ms\",\n\t\t\t\t\tpath,\n\t\t\t\t\t\"groupDurationMs or groupDurationTicks must be a positive integer\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (groupDurationTicks === undefined &&\n\t\t\t\t(BigInt(groupDurationMs as number) * BigInt(timescale)) % 1000n !== 0n) {\n\t\t\t\tcatalogError(\"track_group_duration_exact\", `${path}.groupDurationMs`, \"does not resolve to an exact integer tick count\");\n\t\t\t}\n\t\t}\n\t}\n\n\t// mmt-01 \u00A74.4: alternates share group boundaries so a subscriber can switch\n\t// at any of them. An intra-only member (one frame per group, e.g. the demo's\n\t// forceIOnly `video/base`) has a boundary at every frame, so it switches\n\t// cleanly with any GOP peer whose group duration is a whole number of its\n\t// groups. Such a member is checked for divisibility against the first GOP\n\t// member; every GOP member must still match that anchor exactly (BLO-49202).\n\ttype SwitchingMember = { index: number; name: string; ticks: bigint; timescale: bigint; intraOnly: boolean };\n\tconst switchingSets = new Map<number, SwitchingMember[]>();\n\tfor (let index = 0; index < trackRecords.length; index++) {\n\t\tconst track = trackRecords[index]!;\n\t\tif (track[\"packaging\"] !== \"mmtp\") continue;\n\t\tconst altGroup = track[\"altGroup\"];\n\t\tif (!Number.isSafeInteger(altGroup)) continue;\n\t\tconst timescale = BigInt(track[\"timescale\"] as number);\n\t\tconst durationTicks = track[\"groupDurationTicks\"];\n\t\tconst ticks = durationTicks !== undefined\n\t\t\t? BigInt(durationTicks as number)\n\t\t\t: BigInt(track[\"groupDurationMs\"] as number) * timescale / 1000n;\n\t\tconst framerate = track[\"framerate\"];\n\t\t// Group duration == one frame period. `framerate` is a float (29.97), so\n\t\t// compare within 0.1%; a GOP track is off by a factor of >= 2.\n\t\tconst intraOnly = positiveNumber(framerate) &&\n\t\t\tMath.abs(Number(ticks) * framerate - Number(timescale)) <= Number(timescale) * 1e-3;\n\t\tconst members = switchingSets.get(altGroup as number) ?? [];\n\t\tmembers.push({ index, name: track[\"name\"] as string, ticks, timescale, intraOnly });\n\t\tswitchingSets.set(altGroup as number, members);\n\t}\n\tfor (const [altGroup, members] of switchingSets) {\n\t\tconst anchor = members.find((member) => !member.intraOnly) ?? members[0]!;\n\t\tfor (const member of members) {\n\t\t\tif (member === anchor || member.ticks * anchor.timescale === anchor.ticks * member.timescale) continue;\n\t\t\tconst nests = member.intraOnly && !anchor.intraOnly &&\n\t\t\t\t(anchor.ticks * member.timescale) % (member.ticks * anchor.timescale) === 0n;\n\t\t\tif (!nests) {\n\t\t\t\tcatalogError(\"track_switching_set\", `$.tracks[${member.index}]`, `group duration does not match ${JSON.stringify(anchor.name)} in altGroup ${altGroup}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst multicast = catalog[\"multicast\"];\n\tif (typeof multicast === \"object\" && multicast !== null && !Array.isArray(multicast)) {\n\t\tvalidateNetworkSourceRelays((multicast as Record<string, unknown>)[\"networkSource\"], \"$.multicast.networkSource\");\n\t\tconst endpoints = (multicast as Record<string, unknown>)[\"endpoints\"];\n\t\tif (Array.isArray(endpoints)) {\n\t\t\tfor (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {\n\t\t\t\tconst endpoint = endpoints[endpointIndex];\n\t\t\t\tif (typeof endpoint !== \"object\" || endpoint === null || Array.isArray(endpoint)) continue;\n\t\t\t\tvalidateNetworkSourceRelays(\n\t\t\t\t\t(endpoint as Record<string, unknown>)[\"networkSource\"],\n\t\t\t\t\t`$.multicast.endpoints[${endpointIndex}].networkSource`,\n\t\t\t\t);\n\t\t\t\t// The endpoint relay moved to `multicast.networkSource[{type:\"amt\",relay}]`.\n\t\t\t\t// Refuse rather than ignore: a dropped relay strands an AMT endpoint\n\t\t\t\t// with no tunnel and reads as a gateway failure, not a bad catalog.\n\t\t\t\tif (\"relayAddress\" in endpoint) {\n\t\t\t\t\tcatalogError(\n\t\t\t\t\t\t\"multicast_relay_address\",\n\t\t\t\t\t\t`$.multicast.endpoints[${endpointIndex}].relayAddress`,\n\t\t\t\t\t\t'relayAddress is retired; publish the relay as multicast.networkSource[{type:\"amt\",relay}]',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst refs = (endpoint as Record<string, unknown>)[\"tracks\"];\n\t\t\t\tif (!Array.isArray(refs)) continue;\n\t\t\t\tconst packetIds = new Map<number, string>();\n\t\t\t\tfor (let refIndex = 0; refIndex < refs.length; refIndex++) {\n\t\t\t\t\tconst ref = refs[refIndex];\n\t\t\t\t\tif (typeof ref !== \"object\" || ref === null || Array.isArray(ref)) continue;\n\t\t\t\t\tconst trackRef = ref as Record<string, unknown>;\n\t\t\t\t\tconst refPath = `$.multicast.endpoints[${endpointIndex}].tracks[${refIndex}]`;\n\t\t\t\t\tconst name = trackRef[\"name\"];\n\t\t\t\t\tif (typeof name !== \"string\" || !trackRecords.some((track) => track[\"name\"] === name)) {\n\t\t\t\t\t\tcatalogError(\"multicast_track\", `${refPath}.name`, `unknown catalog track ${JSON.stringify(name)}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst packetId = trackRef[\"packetId\"];\n\t\t\t\t\tif (packetId === MMTP_SIGNALING_PACKET_ID) {\n\t\t\t\t\t\tcatalogError(\n\t\t\t\t\t\t\t\"multicast_packet_id\",\n\t\t\t\t\t\t\t`${refPath}.packetId`,\n\t\t\t\t\t\t\t`packetId ${packetId} is the MMTP signaling flow and must not be advertised for a track; packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (!Number.isSafeInteger(packetId) || (packetId as number) < 1 || (packetId as number) > MMTP_PACKET_ID_MAX) {\n\t\t\t\t\t\tcatalogError(\"multicast_packet_id\", `${refPath}.packetId`, `packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst previous = packetIds.get(packetId as number);\n\t\t\t\t\tif (previous !== undefined) {\n\t\t\t\t\t\tcatalogError(\"multicast_packet_id\", `${refPath}.packetId`, `packetId is already assigned to ${JSON.stringify(previous)}`);\n\t\t\t\t\t}\n\t\t\t\t\tpacketIds.set(packetId as number, name as string);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// draft -02 4.1 / 7.2.6 (protocol and S, address literals, auth\n\t// all-or-nothing, the key set, one endpoint per (S, G, port),\n\t// foreign-namespace tracks) are never a catalog fault here: a receiver\n\t// ignores the endpoint an issue names, and a catalog-level issue makes the\n\t// multicast object yield no data (`admittedMulticastEndpoints`), while MoQ\n\t// unicast stays usable. `multicastCatalogIssues` reports them.\n\n\tvalidateArchive(catalog);\n\n\treturn catalog as CmsfCatalog;\n}\n\n/**\n * Resolve a track's group duration to integer ticks using the supplied\n * timescale (Hz). Implements mmt-01 \u00A74.4.2:\n *\n * - If `groupDurationTicks` is present, return it (override wins).\n * - Else compute `groupDurationMs * timescale / 1000`. The result MUST\n * be an exact integer; throws if `(ms * timescale) % 1000 != 0`.\n *\n * Throws on invalid timing config or non-positive timescale (CLAUDE.md:\n * error on bad init).\n */\nexport function resolveGroupDurationTicks(\n\ttiming: TrackTimingConfig,\n\ttimescale: number,\n): number {\n\tif (!Number.isFinite(timescale) || !Number.isInteger(timescale) || timescale <= 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: timescale must be a positive integer Hz, got ${timescale}`,\n\t\t);\n\t}\n\tif (!isTrackTimingConfig(timing)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: invalid TrackTimingConfig: ${JSON.stringify(timing)}`,\n\t\t);\n\t}\n\tif (timing.groupDurationTicks !== undefined) {\n\t\treturn timing.groupDurationTicks;\n\t}\n\tif (timing.groupDurationMs === undefined) {\n\t\tthrow new Error(\n\t\t\t\"[carp-catalog] resolveGroupDurationTicks: groupDurationMs is required when groupDurationTicks is absent\",\n\t\t);\n\t}\n\tconst product = timing.groupDurationMs * timescale;\n\tif (product % 1000 !== 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: groupDurationMs=${timing.groupDurationMs} at timescale=${timescale} ` +\n\t\t\t\t`produces non-integer ticks (${product / 1000}); publisher must use groupDurationTicks override (mmt-01 \u00A74.4.2)`,\n\t\t);\n\t}\n\treturn product / 1000;\n}\n\n/**\n * Validate that every track in a switching set agrees on the same\n * wall-clock group duration (mmt-01 \u00A74.4.2). Per-track timescales may\n * differ, so raw tick counts cannot be compared directly.\n *\n * Returns `{ ok: true }` when all tracks agree (or the set is empty);\n * `{ ok: false, mismatches }` listing each track whose effective ticks\n * differ from the first track's value. Subscribers per spec SHOULD\n * reject the catalog on mismatch.\n *\n * Throws if any track's timing config is invalid (per\n * `resolveGroupDurationTicks`).\n */\nexport function validateSwitchingSetTiming(\n\ttracks: Array<{ name: string; timing: TrackTimingConfig; timescale: number }>,\n):\n\t| { ok: true }\n\t| { ok: false; expectedTicks: number; mismatches: Array<{ name: string; ticks: number }> } {\n\tif (tracks.length === 0) return { ok: true };\n\tconst ticksByTrack = tracks.map((t) => ({\n\t\tname: t.name,\n\t\tticks: resolveGroupDurationTicks(t.timing, t.timescale),\n\t\ttimescale: t.timescale,\n\t}));\n\tconst expectedTicks = ticksByTrack[0].ticks;\n\tconst expectedTimescale = ticksByTrack[0].timescale;\n\tconst mismatches = ticksByTrack\n\t\t.slice(1)\n\t\t.filter(\n\t\t\t(t) =>\n\t\t\t\tBigInt(t.ticks) * BigInt(expectedTimescale) !==\n\t\t\t\tBigInt(expectedTicks) * BigInt(t.timescale),\n\t\t)\n\t\t.map(({ name, ticks }) => ({ name, ticks }));\n\tif (mismatches.length === 0) return { ok: true };\n\treturn { ok: false, expectedTicks, mismatches };\n}\n", "// Resolved repair layer and overlay shapes (draft-ramadan-moq-fec-catalog-00,\n// fec-core-00): the output of `resolveRepairLayers`. Every base layer comes\n// from one instance's partition and differs only in P_i; layer i owns RS_IDs\n// [O_i, O_i + P_i) with O_i = sum(P_j, j < i), and ESI = K + RS_ID. A keyframe\n// overlay is a second instance with its own K, P and RS_ID space.\n//\n// Binding (which repair tracks belong to which instance) is\n// `resolveFecBindings` in mmt-base; nothing here reads a catalog.\n\n/** Lowest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MIN = 192;\n/** Highest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MAX = 255;\n/** Priority of a base layer 0 track that declares none. */\nexport const DEFAULT_REPAIR_PRIORITY = 240;\n/** RFC 6330 \u00A74.4.1.2: the largest number of source symbols in one RaptorQ source block. */\nexport const MAX_RAPTORQ_SOURCE_SYMBOLS = 56403;\n/** The 24-bit RS_ID field (\u00A77.1) bounds the repair total of one FEC instance. */\nexport const MAX_REPAIR_SYMBOLS = 0xffffff;\n/** The 24-bit ESI bounds K + totalP - 1 for one FEC instance. */\nexport const MAX_ENCODING_SYMBOL_ID = 0xffffff;\n/** Reed-Solomon block bound: K + P <= 255 (draft-ramadan-moq-fec-01 4.5, 6.3.4 rule 12). */\nexport const MAX_REED_SOLOMON_BLOCK_SYMBOLS = 255;\n\n/** A resolved base repair layer with its derived RS_ID range. */\nexport type RepairLayerParams = {\n\t/** Layer index; 0 is the track named by the source's `fec.repairTrack`. */\n\tlayer: number;\n\trepairTrackName: string;\n\t/** P_i: repair symbols per block carried by this layer. */\n\trepairSymbols: number;\n\t/** O_i = sum(P_j for j < i): the first RS_ID this layer carries. ESI = K + RS_ID. */\n\trsIdOffset: number;\n\t/** MoQT-scale priority (lower first); 240 when layer 0 declares none. */\n\tpriority: number;\n};\n\n/** A resolved keyframe overlay: a separate FEC instance over RAP fragments only. */\nexport type RepairOverlayParams = {\n\trepairTrackName: string;\n\tscope: \"keyframe\";\n\t/** K_overlay: source symbols of the overlay instance. */\n\tsourceSymbols: number;\n\t/** P_overlay: repair symbols per overlay block; also the overlay's own RSB_length. */\n\trepairSymbols: number;\n\t/** MoQT-scale priority, independent of the base layer chain. */\n\tpriority: number;\n};\n\nexport type RepairLayersResolution = {\n\tsourceTrackName: string;\n\t/** Base layers in layer order, layer 0 first. Never empty. */\n\tbaseLayers: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances, in catalog order. Empty when none is declared. */\n\t/** At most one entry: the wire carries one overlay trailer per RAP packet. */\n\toverlays: readonly RepairOverlayParams[];\n\t/** Sum of P over base layers: the wire RSB_length on every base layer packet. */\n\ttotalBaseRepairSymbols: number;\n};\n\n"],
5
- "mappings": "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",
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 * 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", "// FEC catalog binding validator (draft-ramadan-moq-fec-catalog-00, as posted\n// 2026-11-02, moqcast-draft 5f49172).\n//\n// The catalog-00 shape only: a track's `fec` is an array of instance objects,\n// and a repair or symbol track binds to its instance by `source` + `instanceId`\n// (or by being named in `repairTrack` / `symbolTrack`). The draft-01 object\n// shape (`fec: {...}`, `depends`) is not accepted: an object `fec` is a type\n// violation, and a repair track that only `depends` on its source is an orphan.\n//\n// Validation fails closed per instance (fec-core-00 10.3): a violation stops\n// FEC for that instance and no other. Three outcomes, each reported with the\n// catalog path that caused it:\n// ok - FEC may start for the instance.\n// unavailable - the receiver forgoes FEC for it (fec-core-00 10.3 rule 1,\n// 4.5); not a validation failure.\n// violation - the configuration is invalid for this receiver.\n// An orphan repair or symbol track (fec-catalog-00 5 rule 6) is reported and\n// stops nothing. `enhancementRepair` anywhere rejects the catalog (rule 4).\n//\n// The Rust twin is `mmt_fec::fec_catalog`; both run vectors/fec-catalog-00/.\n\nexport type FecCatalogOutcome = \"ok\" | \"unavailable\" | \"violation\";\n\nexport type FecCatalogCode =\n\t// Unavailable (fec-core-00 10.3 rule 1; fec-catalog-00 3.1).\n\t| \"fec_scheme_absent\"\n\t| \"fec_scheme_unimplemented\"\n\t| \"fec_block_profile_absent\"\n\t| \"fec_block_profile_unimplemented\"\n\t| \"fec_block_profile_packaging\"\n\t// Violations.\n\t| \"fec_member\" // fec-catalog-00 5 rule 1; fec-core-00 10.3 rule 11\n\t| \"fec_naming\" // fec-catalog-00 5 rule 2\n\t| \"fec_resolution\" // fec-catalog-00 5 rule 3\n\t| \"fec_enhancement_repair\" // fec-catalog-00 5 rule 4, 3.4\n\t| \"fec_orphan\" // fec-catalog-00 5 rule 6 (report only)\n\t| \"fec_layers\" // fec-core-00 10.3 rule 3\n\t| \"fec_layer0\" // fec-core-00 10.3 rule 4\n\t| \"fec_bounds\" // fec-core-00 10.3 rule 5; block-codes-00 7; mmt-02 7.1\n\t| \"fec_instance_id\" // fec-core-00 10.3 rule 7\n\t| \"fec_symbol_tracks\" // fec-core-00 10.3 rule 7\n\t| \"fec_track_geometry\" // fec-core-00 10.3 rule 8\n\t| \"fec_priority\" // fec-core-00 10.3 rule 10\n\t| \"fec_overlay\" // mmt-02 7.4.1\n\t| \"fec_overlay_unsupported\"; // mmt-02 7.4.4\n\nexport interface FecCatalogIssue {\n\treadonly code: FecCatalogCode;\n\t/** JSON Pointer into the catalog. */\n\treadonly path: string;\n}\n\nexport interface FecCatalogInstance {\n\t/** JSON Pointer of the instance object, `/tracks/<i>/fec/<j>`. */\n\treadonly path: string;\n\treadonly track: string | null;\n\t/** Resolved Instance ID; null when `instanceId` is malformed. */\n\treadonly instanceId: number | null;\n\treadonly outcome: FecCatalogOutcome;\n\treadonly issues: readonly FecCatalogIssue[];\n\t/** Index into `tracks` of the source track declaring the instance. */\n\treadonly trackIndex: number;\n\t/** Track index of the repair track `repairTrack` names, or null. */\n\treadonly repairTrack: number | null;\n\t/** Track indices of the other repair tracks bound by `source` + `instanceId`. */\n\treadonly boundRepairTracks: readonly number[];\n\t/** Track index of the instance's symbol track (named or bound), or null. */\n\treadonly symbolTrack: number | null;\n}\n\nexport interface FecCatalogReport {\n\treadonly instances: readonly FecCatalogInstance[];\n\t/** Every report, instance-attributed or not, in discovery order. */\n\treadonly issues: readonly FecCatalogIssue[];\n}\n\nexport interface FecCatalogReceiver {\n\t/** Schemes the receiver implements. Default: reed-solomon, raptorq. */\n\treadonly schemes?: readonly string[];\n\t/** Block profiles the receiver implements. Default: moq-object-v1, iso-ssbg1-v1. */\n\treadonly blockProfiles?: readonly string[];\n\t/** Whether the receiver implements the mmt-02 7.4 keyframe overlay. Default true. */\n\treadonly overlays?: boolean;\n}\n\nconst DEFAULT_SCHEMES = [\"reed-solomon\", \"raptorq\"];\nconst DEFAULT_PROFILES = [\"moq-object-v1\", \"iso-ssbg1-v1\"];\nconst RS_MAX_BLOCK = 255; // block-codes-00 7\nconst RAPTORQ_MAX_K = 56403; // block-codes-00 7\nconst MAX_24 = 0xffffff; // fec-core-00 10.3 rule 5\nconst ISO_MIN_T = 14; // mmt-02 7.1\nconst GEOMETRY_KEYS = [\n\t\"scheme\",\n\t\"blockProfile\",\n\t\"mmtpMode\",\n\t\"timescale\",\n\t\"symbolSize\",\n\t\"interleaveDepthMs\",\n\t\"reorderToleranceMs\",\n\t\"sourceSymbols\",\n\t\"fec\",\n];\n\ntype Json = unknown;\ntype Known =\n\t| \"tracks\" | \"fec\" | \"name\" | \"namespace\" | \"packaging\" | \"source\" | \"instanceId\"\n\t| \"repairLayer\" | \"repairSymbols\" | \"priority\" | \"scheme\" | \"blockProfile\"\n\t| \"sourceSymbols\" | \"symbolSize\" | \"repairTrack\" | \"symbolTrack\"\n\t| \"interleaveDepthMs\" | \"reorderToleranceMs\" | \"label\" | \"scope\";\ntype Obj = Record<string, Json> & Partial<Record<Known, Json>>;\n\nconst isObj = (v: Json): v is Obj => typeof v === \"object\" && v !== null && !Array.isArray(v);\nconst has = (o: Obj, k: string) => Object.prototype.hasOwnProperty.call(o, k);\nconst isInt = (v: Json): v is number => typeof v === \"number\" && Number.isInteger(v);\nconst intIn = (v: Json, lo: number, hi = Number.MAX_SAFE_INTEGER): v is number =>\n\tisInt(v) && v >= lo && v <= hi;\n\n// moq-object-v1 admits any packaging whose objects carry media as whole\n// objects (fec-core-00 15.3); iso-ssbg1-v1 admits mmtp (mmt-02 7.1).\nconst profileAdmits = (profile: string, packaging: Json) => profile !== \"iso-ssbg1-v1\" || packaging === \"mmtp\";\n\ninterface Inst {\n\tpath: string;\n\tti: number;\n\ttrack: Obj;\n\tobj: Obj | null;\n\tid: number | null;\n\tunavailable: boolean;\n\toverlay: boolean;\n\tissues: FecCatalogIssue[];\n\tlayer0: number | null; // track index of the resolved repairTrack\n\tsymbolNamed: number | null;\n\tattached: number[]; // unnamed fec-repair/fec-source tracks bound by source\n}\n\nexport function validateFecCatalog(catalog: Json, receiver: FecCatalogReceiver = {}): FecCatalogReport {\n\tconst schemes = receiver.schemes ?? DEFAULT_SCHEMES;\n\tconst profiles = receiver.blockProfiles ?? DEFAULT_PROFILES;\n\tconst overlays = receiver.overlays ?? true;\n\tconst all: FecCatalogIssue[] = [];\n\tconst report = (code: FecCatalogCode, path: string, inst?: Inst) => {\n\t\tconst issue = { code, path };\n\t\tall.push(issue);\n\t\tinst?.issues.push(issue);\n\t};\n\n\tconst tracks: Json[] = isObj(catalog) && Array.isArray(catalog.tracks) ? catalog.tracks : [];\n\tif (!isObj(catalog) || !Array.isArray(catalog.tracks)) report(\"fec_member\", \"/tracks\");\n\n\t// Rule 4: enhancementRepair at track level or inside an instance object.\n\tlet rejectAll = false;\n\ttracks.forEach((t, ti) => {\n\t\tif (!isObj(t)) return;\n\t\tif (has(t, \"enhancementRepair\")) {\n\t\t\trejectAll = true;\n\t\t\treport(\"fec_enhancement_repair\", `/tracks/${ti}/enhancementRepair`);\n\t\t}\n\t\tif (Array.isArray(t.fec))\n\t\t\tt.fec.forEach((o, ii) => {\n\t\t\t\tif (isObj(o) && has(o, \"enhancementRepair\")) {\n\t\t\t\t\trejectAll = true;\n\t\t\t\t\treport(\"fec_enhancement_repair\", `/tracks/${ti}/fec/${ii}/enhancementRepair`);\n\t\t\t\t}\n\t\t\t});\n\t});\n\n\t// Track lookup by (namespace, name): a name resolves in the source track's namespace.\n\tconst key = (ns: Json, name: string) => JSON.stringify([ns ?? null, name]);\n\tconst byName = new Map<string, number>();\n\ttracks.forEach((t, ti) => {\n\t\tif (isObj(t) && typeof t.name === \"string\") {\n\t\t\tconst k = key(t.namespace, t.name);\n\t\t\tif (!byName.has(k)) byName.set(k, ti);\n\t\t}\n\t});\n\tconst trackAt = (ti: number) => tracks[ti] as Obj;\n\n\t// Collect instances.\n\tconst insts: Inst[] = [];\n\ttracks.forEach((t, ti) => {\n\t\t// A repair or symbol track's own `fec` is geometry (core rule 8), not instances.\n\t\tif (!isObj(t) || !has(t, \"fec\") || t.packaging === \"fec-repair\" || t.packaging === \"fec-source\") return;\n\t\tif (!Array.isArray(t.fec)) {\n\t\t\treport(\"fec_member\", `/tracks/${ti}/fec`);\n\t\t\treturn;\n\t\t}\n\t\tt.fec.forEach((o, ii) => {\n\t\t\tconst path = `/tracks/${ti}/fec/${ii}`;\n\t\t\tconst inst: Inst = {\n\t\t\t\tpath,\n\t\t\t\tti,\n\t\t\t\ttrack: t,\n\t\t\t\tobj: isObj(o) ? o : null,\n\t\t\t\tid: null,\n\t\t\t\tunavailable: false,\n\t\t\t\toverlay: false,\n\t\t\t\tissues: [],\n\t\t\t\tlayer0: null,\n\t\t\t\tsymbolNamed: null,\n\t\t\t\tattached: [],\n\t\t\t};\n\t\t\tinsts.push(inst);\n\t\t\tif (!inst.obj) return report(\"fec_member\", path, inst);\n\t\t\tconst o2 = inst.obj;\n\t\t\tif (!has(o2, \"instanceId\")) inst.id = 0;\n\t\t\telse if (intIn(o2.instanceId, 0, 255)) inst.id = o2.instanceId;\n\t\t\telse report(\"fec_member\", `${path}/instanceId`, inst);\n\t\t\tinst.overlay = has(o2, \"scope\");\n\t\t});\n\t});\n\tconst declaresFec = insts.length > 0;\n\n\t// Availability (fec-core-00 10.3 rule 1). Scheme first, then profile.\n\tfor (const inst of insts) {\n\t\tconst o = inst.obj;\n\t\tif (!o) continue;\n\t\tconst avail = (field: \"scheme\" | \"blockProfile\", impl: readonly string[]) => {\n\t\t\tconst p = `${inst.path}/${field}`;\n\t\t\tconst absent = field === \"scheme\" ? \"fec_scheme_absent\" : \"fec_block_profile_absent\";\n\t\t\tconst unimpl = field === \"scheme\" ? \"fec_scheme_unimplemented\" : \"fec_block_profile_unimplemented\";\n\t\t\tif (!has(o, field)) return report(absent, p, inst), false;\n\t\t\tif (typeof o[field] !== \"string\") return report(\"fec_member\", p, inst), true;\n\t\t\tif (!impl.includes(o[field] as string)) return report(unimpl, p, inst), false;\n\t\t\treturn true;\n\t\t};\n\t\tif (!avail(\"scheme\", schemes) || !avail(\"blockProfile\", profiles)) inst.unavailable = true;\n\t\telse if (typeof o.blockProfile === \"string\" && !profileAdmits(o.blockProfile, inst.track.packaging)) {\n\t\t\treport(\"fec_block_profile_packaging\", `${inst.path}/blockProfile`, inst);\n\t\t\tinst.unavailable = true;\n\t\t}\n\t}\n\tconst live = (i: Inst) => !i.unavailable;\n\n\t// Rule 1 on instance members, and core rule 11.\n\tfor (const inst of insts.filter(live)) {\n\t\tconst o = inst.obj;\n\t\tif (!o) continue;\n\t\tconst p = inst.path;\n\t\tfor (const m of [\"sourceSymbols\", \"repairSymbols\", \"symbolSize\"])\n\t\t\tif (!intIn(o[m], 1)) report(\"fec_member\", `${p}/${m}`, inst);\n\t\tif (typeof o.repairTrack !== \"string\") report(\"fec_member\", `${p}/repairTrack`, inst);\n\t\tif (has(o, \"symbolTrack\") && typeof o.symbolTrack !== \"string\") report(\"fec_member\", `${p}/symbolTrack`, inst);\n\t\tif (has(o, \"interleaveDepthMs\")) {\n\t\t\tconst d = o.interleaveDepthMs;\n\t\t\tif (!intIn(d, 0) || (d === 0 && o.blockProfile !== \"iso-ssbg1-v1\"))\n\t\t\t\treport(\"fec_member\", `${p}/interleaveDepthMs`, inst);\n\t\t}\n\t\tif (has(o, \"reorderToleranceMs\")) {\n\t\t\tconst r = o.reorderToleranceMs;\n\t\t\tif (typeof r !== \"number\" || !Number.isFinite(r) || r < 0) report(\"fec_member\", `${p}/reorderToleranceMs`, inst);\n\t\t}\n\t\tif (has(o, \"label\") && typeof o.label !== \"string\") report(\"fec_member\", `${p}/label`, inst);\n\t\tif (inst.overlay && o.scope !== \"keyframe\") report(\"fec_member\", `${p}/scope`, inst);\n\t}\n\n\t// Core rule 7: unique Instance IDs within a track.\n\tconst idGroups = new Map<string, Inst[]>();\n\tfor (const inst of insts)\n\t\tif (inst.id !== null) {\n\t\t\tconst k = `${inst.ti}:${inst.id}`;\n\t\t\tidGroups.set(k, [...(idGroups.get(k) ?? []), inst]);\n\t\t}\n\tfor (const group of idGroups.values())\n\t\tif (group.length > 1)\n\t\t\tfor (const inst of group.filter(live))\n\t\t\t\treport(\"fec_instance_id\", inst.obj && has(inst.obj, \"instanceId\") ? `${inst.path}/instanceId` : inst.path, inst);\n\n\t// Rule 2: repairTrack / symbolTrack name a track of the right packaging, once\n\t// per catalog. A name used twice binds no layer 0 or symbol track for either\n\t// namer; the track is still claimed by the first, so it is not re-attached.\n\tconst nameUses = new Map<string, { inst: Inst; field: \"repairTrack\" | \"symbolTrack\" }[]>();\n\tfor (const inst of insts)\n\t\tfor (const field of [\"repairTrack\", \"symbolTrack\"] as const)\n\t\t\tif (inst.obj && typeof inst.obj[field] === \"string\") {\n\t\t\t\tconst k = key(inst.track.namespace, inst.obj[field] as string);\n\t\t\t\tnameUses.set(k, [...(nameUses.get(k) ?? []), { inst, field }]);\n\t\t\t}\n\tconst namedBy = new Map<number, Inst>();\n\tfor (const [k, uses] of nameUses) {\n\t\tconst tj = byName.get(k);\n\t\tfor (const { inst, field } of uses) {\n\t\t\tconst path = `${inst.path}/${field}`;\n\t\t\tif (tj === undefined || trackAt(tj).packaging !== (field === \"repairTrack\" ? \"fec-repair\" : \"fec-source\")) {\n\t\t\t\tif (live(inst)) report(\"fec_naming\", path, inst);\n\t\t\t} else if (uses.length > 1) {\n\t\t\t\tif (live(inst)) report(\"fec_naming\", path, inst);\n\t\t\t\tif (!namedBy.has(tj)) namedBy.set(tj, inst);\n\t\t\t} else {\n\t\t\t\tnamedBy.set(tj, inst);\n\t\t\t\tif (field === \"repairTrack\") inst.layer0 = tj;\n\t\t\t\telse inst.symbolNamed = tj;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Repair and symbol tracks: resolve each to its instance.\n\tconst owner = new Map<number, Inst>();\n\tif (declaresFec)\n\t\ttracks.forEach((t, tj) => {\n\t\t\tif (!isObj(t) || (t.packaging !== \"fec-repair\" && t.packaging !== \"fec-source\")) return;\n\t\t\tconst tp = `/tracks/${tj}`;\n\t\t\tconst named = namedBy.get(tj);\n\t\t\tif (named) {\n\t\t\t\towner.set(tj, named);\n\t\t\t\tif (!live(named)) return;\n\t\t\t\t// Rule 3: a named track's source / instanceId agree with its namer.\n\t\t\t\tif (has(t, \"source\") && t.source !== named.track.name) report(\"fec_resolution\", `${tp}/source`, named);\n\t\t\t\tif (has(t, \"instanceId\") && t.instanceId !== named.id) report(\"fec_resolution\", `${tp}/instanceId`, named);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!has(t, \"source\")) return report(\"fec_orphan\", tp); // rule 6, stops nothing\n\t\t\tconst id = has(t, \"instanceId\") ? t.instanceId : 0;\n\t\t\tconst src = typeof t.source === \"string\" ? byName.get(key(t.namespace, t.source)) : undefined;\n\t\t\tconst target = src === undefined ? undefined : insts.find((i) => i.ti === src && i.id === id);\n\t\t\tif (!target) return report(\"fec_naming\", has(t, \"instanceId\") && src !== undefined ? `${tp}/instanceId` : `${tp}/source`);\n\t\t\towner.set(tj, target);\n\t\t\ttarget.attached.push(tj);\n\t\t});\n\n\t// Track fields (rule 1) and no geometry (core rule 8), charged to the owner.\n\tfor (const [tj, inst] of owner) {\n\t\tif (!live(inst)) continue;\n\t\tconst t = trackAt(tj);\n\t\tconst tp = `/tracks/${tj}`;\n\t\tconst symbol = t.packaging === \"fec-source\";\n\t\tif (has(t, \"source\") && typeof t.source !== \"string\") report(\"fec_member\", `${tp}/source`, inst);\n\t\tif (has(t, \"instanceId\") && !intIn(t.instanceId, 0, 255)) report(\"fec_member\", `${tp}/instanceId`, inst);\n\t\tfor (const m of [\"repairLayer\", \"repairSymbols\"]) {\n\t\t\tif (!has(t, m)) continue;\n\t\t\tif (symbol || !intIn(t[m], m === \"repairLayer\" ? 0 : 1)) report(\"fec_member\", `${tp}/${m}`, inst);\n\t\t}\n\t\tif (has(t, \"priority\") && !intIn(t.priority, symbol ? 0 : 192, 255)) report(\"fec_member\", `${tp}/priority`, inst);\n\t\tfor (const k of Object.keys(t))\n\t\t\tif (GEOMETRY_KEYS.includes(k) || k.startsWith(\"groupDuration\"))\n\t\t\t\treport(\"fec_track_geometry\", `${tp}/${k.replace(/~/g, \"~0\").replace(/\\//g, \"~1\")}`, inst); // RFC 6901\n\t}\n\n\t// Per instance: layers, symbol tracks, priorities, scheme bounds.\n\tfor (const inst of insts.filter(live)) {\n\t\tconst o = inst.obj;\n\t\tif (!o) continue;\n\t\tconst repairs = inst.attached.filter((tj) => trackAt(tj).packaging === \"fec-repair\");\n\t\tconst symbols = inst.attached.filter((tj) => trackAt(tj).packaging === \"fec-source\");\n\t\tif (symbols.length + (inst.symbolNamed !== null ? 1 : 0) > 1)\n\t\t\tfor (const tj of symbols) report(\"fec_symbol_tracks\", `/tracks/${tj}`, inst);\n\n\t\tconst l0 = inst.layer0 === null ? null : trackAt(inst.layer0);\n\t\tconst l0p = `/tracks/${inst.layer0}`;\n\t\tif (inst.overlay) {\n\t\t\t// mmt-02 7.4.1: an overlay has one repair track and no repairLayer.\n\t\t\tif (l0 && has(l0, \"repairLayer\")) report(\"fec_overlay\", `${l0p}/repairLayer`, inst);\n\t\t\tfor (const tj of repairs)\n\t\t\t\treport(\"fec_overlay\", has(trackAt(tj), \"repairLayer\") ? `/tracks/${tj}/repairLayer` : `/tracks/${tj}`, inst);\n\t\t} else {\n\t\t\tif (l0 && has(l0, \"repairLayer\") && l0.repairLayer !== 0) report(\"fec_layers\", `${l0p}/repairLayer`, inst);\n\t\t\tif (l0 && has(l0, \"repairSymbols\") && intIn(o.repairSymbols, 1) && l0.repairSymbols !== o.repairSymbols)\n\t\t\t\treport(\"fec_layer0\", `${l0p}/repairSymbols`, inst);\n\t\t\tconst layers: { tj: number; layer: number }[] = [];\n\t\t\tfor (const tj of repairs) {\n\t\t\t\tconst t = trackAt(tj);\n\t\t\t\tif (!has(t, \"repairLayer\")) report(\"fec_member\", `/tracks/${tj}/repairLayer`, inst);\n\t\t\t\telse if (t.repairLayer === 0) report(\"fec_layers\", `/tracks/${tj}/repairLayer`, inst);\n\t\t\t\telse if (intIn(t.repairLayer, 1)) layers.push({ tj, layer: t.repairLayer });\n\t\t\t\tif (!has(t, \"repairSymbols\")) report(\"fec_member\", `/tracks/${tj}/repairSymbols`, inst);\n\t\t\t}\n\t\t\t// Core rule 3: layers 1..n contiguous, no duplicates.\n\t\t\tconst counts = new Map<number, number>();\n\t\t\tfor (const { layer } of layers) counts.set(layer, (counts.get(layer) ?? 0) + 1);\n\t\t\tlet missing = 1;\n\t\t\twhile (counts.has(missing)) missing++;\n\t\t\tfor (const { tj, layer } of layers)\n\t\t\t\tif (counts.get(layer)! > 1 || layer > missing) report(\"fec_layers\", `/tracks/${tj}/repairLayer`, inst);\n\t\t\t// Core rule 10: priority non-decreasing with layer; layer 0 defaults to 240.\n\t\t\tconst chain = [\n\t\t\t\t{ layer: 0, tj: inst.layer0, pri: l0 && intIn(l0.priority, 192, 255) ? l0.priority : l0 && has(l0, \"priority\") ? null : 240 },\n\t\t\t\t...layers.map(({ tj, layer }) => {\n\t\t\t\t\tconst pr = trackAt(tj).priority;\n\t\t\t\t\treturn { layer, tj, pri: intIn(pr, 192, 255) ? pr : null };\n\t\t\t\t}),\n\t\t\t].sort((a, b) => a.layer - b.layer);\n\t\t\tlet prev: number | null = null;\n\t\t\tfor (const c of chain) {\n\t\t\t\tif (c.pri === null) continue;\n\t\t\t\tif (prev !== null && c.pri < prev) report(\"fec_priority\", `/tracks/${c.tj}/priority`, inst);\n\t\t\t\tprev = Math.max(prev ?? c.pri, c.pri);\n\t\t\t}\n\t\t}\n\n\t\t// Core rule 5 and the scheme / profile bounds.\n\t\tconst K = o.sourceSymbols;\n\t\tconst T = o.symbolSize;\n\t\tconst P0 = o.repairSymbols;\n\t\tif (intIn(K, 1) && intIn(P0, 1)) {\n\t\t\tlet P = P0;\n\t\t\tif (!inst.overlay)\n\t\t\t\tfor (const tj of repairs) {\n\t\t\t\t\tconst t = trackAt(tj);\n\t\t\t\t\tif (intIn(t.repairLayer, 1) && intIn(t.repairSymbols, 1)) P += t.repairSymbols;\n\t\t\t\t}\n\t\t\tif (P > MAX_24 || K + P - 1 > MAX_24) report(\"fec_bounds\", inst.path, inst);\n\t\t\telse if (o.scheme === \"reed-solomon\" && K + P > RS_MAX_BLOCK) report(\"fec_bounds\", inst.path, inst);\n\t\t}\n\t\tif (o.scheme === \"raptorq\" && intIn(K, 1) && K > RAPTORQ_MAX_K) report(\"fec_bounds\", `${inst.path}/sourceSymbols`, inst);\n\t\tif (intIn(T, 1)) {\n\t\t\tif (o.scheme === \"raptorq\" && T % 8 !== 0) report(\"fec_bounds\", `${inst.path}/symbolSize`, inst);\n\t\t\telse if (o.blockProfile === \"iso-ssbg1-v1\" && T < ISO_MIN_T) report(\"fec_bounds\", `${inst.path}/symbolSize`, inst);\n\t\t}\n\t}\n\n\t// mmt-02 7.4.1 / 7.4.4: overlays, per source track.\n\tconst overlayTracks = new Set(insts.filter((i) => live(i) && i.overlay).map((i) => i.ti));\n\tfor (const ti of overlayTracks) {\n\t\tconst ofTrack = insts.filter((i) => i.ti === ti && live(i));\n\t\tconst ovs = ofTrack.filter((i) => i.overlay);\n\t\tif (!overlays) {\n\t\t\tfor (const ov of ovs) report(\"fec_overlay_unsupported\", `${ov.path}/scope`, ov);\n\t\t\tfor (const i of ofTrack) i.issues.length === 0 && i.issues.push({ code: \"fec_overlay_unsupported\", path: `${ovs[0].path}/scope` });\n\t\t\tcontinue;\n\t\t}\n\t\tif (ovs.length > 1) for (const ov of ovs) report(\"fec_overlay\", `${ov.path}/scope`, ov);\n\t\tfor (const ov of ovs)\n\t\t\tif (ov.obj?.blockProfile !== \"iso-ssbg1-v1\") report(\"fec_overlay\", `${ov.path}/blockProfile`, ov);\n\t\tconst cause = ovs.flatMap((ov) => ov.issues).find((x) => x.code === \"fec_overlay\");\n\t\tif (cause) for (const ov of ovs) if (ov.issues.length === 0) ov.issues.push(cause);\n\t}\n\n\t// Rule 4 rejects every instance; each carries the cause so a consumer reading\n\t// `instances[].issues` sees why.\n\tconst rule4 = all.find((x) => x.code === \"fec_enhancement_repair\");\n\tif (rejectAll && rule4) for (const i of insts) if (i.issues.length === 0) i.issues.push(rule4);\n\n\treturn {\n\t\tinstances: insts.map((i) => ({\n\t\t\tpath: i.path,\n\t\t\ttrack: typeof i.track.name === \"string\" ? i.track.name : null,\n\t\t\tinstanceId: i.id,\n\t\t\toutcome: rejectAll || (!i.unavailable && i.issues.length > 0) ? \"violation\" : i.unavailable ? \"unavailable\" : \"ok\",\n\t\t\tissues: i.issues,\n\t\t\ttrackIndex: i.ti,\n\t\t\trepairTrack: i.layer0,\n\t\t\tboundRepairTracks: i.attached.filter((tj) => trackAt(tj).packaging === \"fec-repair\"),\n\t\t\tsymbolTrack: i.symbolNamed ?? i.attached.find((tj) => trackAt(tj).packaging === \"fec-source\") ?? null,\n\t\t})),\n\t\tissues: all,\n\t};\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", "// Catalog \u2192 per-track FEC geometry resolution.\n//\n// Moved from hang-mmt-fec's js/watch/src/track-fec-params.ts so every player\n// (moq-lib/hang, moqtail, shaka MSF) resolves catalog FEC semantics through\n// one shared implementation \u2014 players keep only wiring, not MMT/catalog\n// interpretation. Structural track shape ({ name, type }) keeps this module\n// player-agnostic.\n//\n// FEC comes only from `resolveFecBindings` (mmt-base), which reads\n// `validateFecCatalog`'s per-instance result over the\n// draft-ramadan-moq-fec-catalog-00 array (BLO-50171). Nothing here reads a\n// `fec` block or a repair track itself, and nothing throws on catalog input:\n// a source whose base instance is not `ok` has no FEC and a typed reason, and\n// no other track is affected.\n\nimport {\n\tFEC_BLOCK_PROFILES,\n\tisFecBlockProfile,\n\tparseFecBlockProfile,\n\tresolveFecBindings,\n\ttype FecBindingUnavailable,\n\ttype FecBlockProfile,\n\ttype FecSourceBinding,\n} from \"@blockcast/mmt-base\";\nimport { catalogFecInterleaveMs, type FecInterleaveCatalog } from \"@blockcast/mmt-fec\";\nimport { resolveGroupDurationMs } from \"./carp-catalog.js\";\nimport type {\n\tRepairLayerParams,\n\tRepairLayersResolution,\n\tRepairOverlayParams,\n} from \"./repair-layers.js\";\nimport type { RepairTarget } from \"./repair-fetch.js\";\nimport type { MoqRepairDelivery } from \"./repair-layer-subscriptions.js\";\n\nexport { catalogFecInterleaveMs, type FecInterleaveCatalog };\nexport { FEC_BLOCK_PROFILES, isFecBlockProfile, parseFecBlockProfile, type FecBlockProfile };\n// Re-exported here, not from index.ts: index's bundle keeps `./*.js` external,\n// so a direct mmt-base re-export there leaves a dangling `./mmtp.js` import.\nexport {\n\tvalidateFecCatalog,\n\ttype FecCatalogCode,\n\ttype FecCatalogInstance,\n\ttype FecCatalogIssue,\n\ttype FecCatalogOutcome,\n\ttype FecCatalogReceiver,\n\ttype FecCatalogReport,\n} from \"@blockcast/mmt-base\";\nexport {\n\tDEFAULT_REPAIR_PRIORITY,\n\tMAX_ENCODING_SYMBOL_ID,\n\tMAX_RAPTORQ_SOURCE_SYMBOLS,\n\tMAX_REPAIR_SYMBOLS,\n\tREPAIR_PRIORITY_MAX,\n\tREPAIR_PRIORITY_MIN,\n\ttype RepairLayerParams,\n\ttype RepairLayersResolution,\n\ttype RepairOverlayParams,\n} from \"./repair-layers.js\";\n\n/**\n * Minimal structural shape of a resolved endpoint track. Callers may carry\n * extra fields (e.g. hang's ResolvedEndpointTrack adds `pid`) \u2014 the resolver\n * only reads `name` and `type`.\n */\nexport type CatalogTrackRef = {\n\tname: string;\n\ttype: \"video\" | \"audio\" | (string & {});\n};\n\n\n/** The container fields the group-duration reader consults. */\ntype ContainerCatalog = { kind?: unknown; timescale?: unknown };\n\ntype RenditionCatalog = KeyframeCadenceCatalog & {\n\tcontainer?: ContainerCatalog;\n\tfec?: unknown;\n\tframerate?: unknown;\n\tsampleRate?: unknown;\n\tsamplerate?: unknown;\n};\n\ntype CatalogWithRenditions = {\n\tvideo?: { renditions?: Record<string, RenditionCatalog> };\n\taudio?: { renditions?: Record<string, RenditionCatalog> };\n\t/** hang-native repair renditions, bound by `source` + `instanceId`. */\n\trepair?: { renditions?: Record<string, unknown> };\n};\n\n/** An MSF/CMSF catalog: `tracks[]` entries, each self-describing. */\ntype CatalogWithTracks = {\n\ttracks?: unknown;\n};\n\n/** Either catalog dialect `resolveRepairLayers` reads. */\nexport type RepairLayerCatalog = CatalogWithRenditions | CatalogWithTracks;\n\nexport type GroupDurationCatalog = {\n\tgroupDurationTicks?: number;\n\tgroupDurationMs?: number;\n\ttimescale?: number;\n};\n\n/** Rendition fields the group-duration reader consumes. */\nexport type GroupDurationRendition = GroupDurationCatalog & {\n\tcontainer?: ContainerCatalog;\n};\n\n/**\n * Largest tick count per Hz of timescale whose millisecond duration\n * `resolveGroupDurationMs` still accepts: `(ticks / timescale) * 1000` must not\n * exceed `Number.MAX_SAFE_INTEGER`, so `ticks <= timescale * (MAX_SAFE / 1000)`\n * (Hz times seconds is ticks).\n */\nconst MAX_SAFE_TICKS_PER_HZ = Number.MAX_SAFE_INTEGER / 1000;\n\n/** A positive safe-integer Hz timescale, or undefined. */\nfunction timescaleHz(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value > 0 ? value : undefined;\n}\n\n/**\n * A rendition's publisher group duration in ms, or undefined when the catalog\n * carries none.\n *\n * `groupDurationTicks` over a timescale wins when both are present, through\n * `resolveGroupDurationMs` (draft-ramadan-moq-mmt-01 \u00A74.4.2). The timescale is\n * the MMTP container's when the rendition declares one, else the rendition's\n * own `timescale`. Only the ticks are forwarded: a catalog `groupDurationMs`\n * is often a rounded fraction (21.33 for 1024/48000), which the ticks path\n * never reads but `resolveGroupDurationMs` would reject as a non-integral ms.\n * Ticks with no usable timescale, and ticks that `resolveGroupDurationMs`\n * would reject, fall back to `groupDurationMs` rather than guessing a clock.\n * That rejection is on the product, not the operand: the count must be a safe\n * integer and `(ticks / timescale) * 1000` must stay within\n * `Number.MAX_SAFE_INTEGER`, which a safe-integer count can still exceed when\n * `timescale < 1000`, so the guard below bounds the count by\n * `timescale * (Number.MAX_SAFE_INTEGER / 1000)` and the reader never throws.\n * The production catalog signals both fields on every rendition and no\n * rendition-level timescale, so the `groupDurationMs` fallback is the path it\n * takes.\n */\nexport function catalogGroupDurationMs(rendition: GroupDurationRendition | undefined): number | undefined {\n\tif (!rendition) return undefined;\n\tconst ticks = numericField(rendition.groupDurationTicks);\n\tconst ms = numericField(rendition.groupDurationMs);\n\tconst timescale =\n\t\ttimescaleHz(rendition.container?.kind === \"mmtp\" ? rendition.container.timescale : undefined) ??\n\t\ttimescaleHz(rendition.timescale);\n\tif (\n\t\tticks !== undefined &&\n\t\ttimescale !== undefined &&\n\t\tNumber.isSafeInteger(ticks) &&\n\t\tticks <= timescale * MAX_SAFE_TICKS_PER_HZ\n\t) {\n\t\treturn resolveGroupDurationMs({ groupDurationTicks: ticks }, timescale);\n\t}\n\treturn ms;\n}\n\n/**\n * The group duration of the catalog rendition an endpoint track was resolved\n * from, by type and name (the same lookup `resolveCatalogTrackFec` performs\n * for FEC geometry). Undefined when the rendition is missing or carries no\n * group duration, so a caller that needs one can fail closed by name.\n */\nexport function resolveCatalogTrackGroupDurationMs(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttrack: CatalogTrackRef,\n): number | undefined {\n\tconst renditions = track.type === \"audio\" ? catalog?.audio?.renditions : catalog?.video?.renditions;\n\treturn catalogGroupDurationMs(renditions?.[track.name]);\n}\n\nexport type KeyframeCadenceCatalog = GroupDurationCatalog & {\n\tkeyframeDurationTicks?: number;\n\tkeyframeDurationMs?: number;\n\tkeyframeIntervalTicks?: number;\n\tkeyframeIntervalMs?: number;\n\tgopDurationTicks?: number;\n\tgopDurationMs?: number;\n};\n\nfunction positiveNumber(value: number | undefined): number | undefined {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\n/**\n * A rendition's explicit keyframe cadence in ms: `keyframeInterval*`, then\n * `keyframeDuration*`, then `gopDuration*`, each preferring integer ticks over\n * the timescale to the ms field. Undefined when none is stated; a group\n * duration is not a keyframe cadence and is never substituted.\n */\nexport function catalogKeyframeCadenceMs(rendition: KeyframeCadenceCatalog | undefined): number | undefined {\n\tif (!rendition) return undefined;\n\tconst timescale = positiveNumber(rendition.timescale);\n\tconst ticksToMs = (ticks: number | undefined) => {\n\t\tconst value = positiveNumber(ticks);\n\t\treturn value !== undefined && timescale !== undefined ? (value * 1000) / timescale : undefined;\n\t};\n\treturn (\n\t\tticksToMs(rendition.keyframeIntervalTicks) ??\n\t\tpositiveNumber(rendition.keyframeIntervalMs) ??\n\t\tticksToMs(rendition.keyframeDurationTicks) ??\n\t\tpositiveNumber(rendition.keyframeDurationMs) ??\n\t\tticksToMs(rendition.gopDurationTicks) ??\n\t\tpositiveNumber(rendition.gopDurationMs)\n\t);\n}\n\n/**\n * Media units per second an FEC track's block geometry is built on, read only\n * from what the catalog states: video frames per second from `framerate`;\n * audio AAC access units (1024 samples) per second from the endpoint sample\n * rate, then the catalog `sampleRate`, then `timescale` (MP4-shaped audio).\n * The first value present is the one validated, so a declared but unusable\n * rate is not replaced by the next. Undefined when no usable rate is stated:\n * there is no default, since this sizes interleave geometry. A video track\n * never reads its timescale as a rate.\n */\nexport function catalogFecMediaUnitsPerSecond(\n\ttype: \"video\" | \"audio\",\n\trendition: { framerate?: number; sampleRate?: number; timescale?: number } | undefined,\n\tendpointSampleRate?: number,\n): number | undefined {\n\tif (type === \"video\") return positiveNumber(rendition?.framerate);\n\tconst sampleRate = positiveNumber(endpointSampleRate ?? rendition?.sampleRate ?? rendition?.timescale);\n\treturn sampleRate === undefined ? undefined : sampleRate / 1024;\n}\n/**\n * Why a track that declares FEC resolved to no FEC: its base instance is not\n * `ok` for this receiver (`resolveFecBindings`). `code` and `path` are the\n * validator's (`validateFecCatalog`, draft-ramadan-moq-fec-catalog-00 5,\n * fec-core-00 10.3); `outcome` tells an instance this receiver forgoes\n * (`unavailable`, e.g. a block profile it does not decode) from an invalid\n * one (`violation`). A draft-01 object `fec` is `fec_member` at that `fec`.\n * Per fec-core-00 10.3 the track's source is consumed without FEC and none of\n * its repair tracks is joined; it is the only track affected.\n */\nexport type TrackFecUnavailable = FecBindingUnavailable;\n\nexport type TrackFecParams = {\n\tfecK: number;\n\t/** The base instance's `scheme`: present exactly when the track has FEC. */\n\tfecAlgorithm?: \"raptorq\" | \"reed-solomon\";\n\t/** The base instance's `blockProfile`, present exactly when the track has\n\t * FEC. This runtime decodes `iso-ssbg1-v1` only (ssbg_mode1, K' =\n\t * SSB_length, consecutive SS_IDs); it is never defaulted. */\n\tfecBlockProfile?: FecBlockProfile;\n\t/** Present exactly when the track declares FEC and its base instance is\n\t * not `ok` (see `TrackFecUnavailable`). The track then resolves to no FEC\n\t * (`fecK` 0, no `fecBlockProfile`, no repair track) and this is the\n\t * reason a caller surfaces. */\n\tfecUnavailable?: TrackFecUnavailable;\n\t/** P_0: the base instance's `repairSymbols` (layer 0). */\n\tfecRepairCount: number;\n\t/** Total repair symbols per block across the BASE layers: the wire\n\t * RSB_length on every base layer packet. Equals fecRepairCount on a\n\t * single-layer track. A keyframe overlay is a separate FEC instance with\n\t * its own RSB_length and is not included. */\n\tfecTotalRepairCount: number;\n\tfecSymbolSize: number;\n\tfecInterleaveMs: number;\n\t/** Catalog fec.reorderToleranceMs \u2014 signaled jitter/reorder allowance, ADDED\n\t * to the block span to form a deadline. Deliberately `number | undefined`\n\t * and NOT floored to 0: a declared 0 (\"this path needs no slack\") and an\n\t * absent value (\"the encoder never said\") must stay distinguishable, or a\n\t * consumer that wants to fail loud on absence silently reads a default\n\t * instead \u2014 which is exactly how the magic 3.0 survived. */\n\tfecReorderToleranceMs?: number;\n\trepairTrackName?: string;\n\t/** Every base repair layer in order, layer 0 first, with derived RS_ID\n\t * offsets. Present whenever repairTrackName is. */\n\trepairLayers?: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances declared for this track. Present (possibly\n\t * empty) whenever repairLayers is. */\n\trepairOverlays?: readonly RepairOverlayParams[];\n\t/** The publisher's MoQ delivery mode (`catalogTrackDeliveryMode`).\n\t * Present whenever repairLayers is; `RepairLayerSubscriptions` reads it. */\n\tdeliveryMode?: MoqRepairDelivery;\n\tfps?: number;\n\tsampleRate?: number;\n};\n\nconst NO_FEC: TrackFecParams = {\n\tfecK: 0,\n\tfecRepairCount: 0,\n\tfecTotalRepairCount: 0,\n\tfecSymbolSize: 0,\n\tfecInterleaveMs: 0,\n};\n\n/**\n * Repair symbols per block a receiver has actually joined: the sum of\n * `repairSymbols` over the BASE layers whose repair track it joined\n * (draft-ramadan-moq-fec 6.3). This is a symbol count, not a count of layers.\n *\n * A keyframe overlay is a separate FEC instance with its own K, its own\n * RSB_length and its own RS_ID space, so it is never added here however it was\n * joined. Counting it would overstate the base instance's P and misreport\n * CMCD `com.blockcast-fp`.\n *\n * Falls back to P_0 when the track declares no layers, which is the\n * single-layer identity rather than a default: with one layer, P_0 is the\n * total.\n */\nexport function joinedRepairSymbolCount(\n\tparams: Pick<TrackFecParams, \"fecRepairCount\" | \"repairLayers\">,\n\tjoinedRepairTrackNames: Iterable<string>,\n): number {\n\tconst layers = params.repairLayers;\n\tif (layers === undefined || layers.length === 0) return params.fecRepairCount;\n\tconst joined = new Set(joinedRepairTrackNames);\n\tlet total = 0;\n\tfor (const layer of layers) {\n\t\tif (joined.has(layer.repairTrackName)) total += layer.repairSymbols;\n\t}\n\treturn total;\n}\n\n/** The binding of one source, scoped to a typed ref's role. */\nfunction bindingOf(catalog: unknown, source: string | CatalogTrackRef): FecSourceBinding | undefined {\n\tif (!catalog) return undefined;\n\tconst name = typeof source === \"string\" ? source : source.name;\n\tconst binding = resolveFecBindings(catalog).get(name);\n\tif (binding === undefined || typeof source === \"string\") return binding;\n\tconst role = binding.pointer.split(\"/\")[1];\n\t// A typed ref resolves only within its own role; a `tracks[]` entry has none.\n\treturn role === \"tracks\" || role === source.type ? binding : undefined;\n}\n\n/**\n * Why `source` has no FEC although it declares some, or undefined (it has FEC,\n * or declares none).\n */\nexport function catalogTrackFecUnavailable(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): TrackFecUnavailable | undefined {\n\tconst binding = bindingOf(catalog, source);\n\treturn binding?.status === \"unavailable\" ? binding.reason : undefined;\n}\n\n/**\n * Every repair track of one source track: base layers with derived RS_ID\n * offsets and its keyframe overlay, from either catalog dialect, as\n * `resolveFecBindings` binds them. Undefined when the source declares no FEC\n * or its base instance is not `ok` (`catalogTrackFecUnavailable` says why): a\n * receiver then joins none of its repair tracks. Never throws.\n */\nexport function resolveRepairLayers(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): RepairLayersResolution | undefined {\n\tconst binding = bindingOf(catalog, source);\n\tif (binding?.status !== \"ok\") return undefined;\n\tconst overlays: RepairOverlayParams[] = binding.overlay\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\trepairTrackName: binding.overlay.repairTrackName,\n\t\t\t\t\tscope: \"keyframe\",\n\t\t\t\t\tsourceSymbols: binding.overlay.sourceSymbols,\n\t\t\t\t\trepairSymbols: binding.overlay.repairSymbols,\n\t\t\t\t\tpriority: binding.overlay.priority,\n\t\t\t\t},\n\t\t\t]\n\t\t: [];\n\treturn {\n\t\tsourceTrackName: binding.track,\n\t\tbaseLayers: binding.layers as readonly RepairLayerParams[],\n\t\toverlays,\n\t\ttotalBaseRepairSymbols: binding.totalBaseRepairSymbols,\n\t};\n}\n\n/** The catalog value a JSON Pointer (RFC 6901) names, or undefined. */\nfunction valueAt(catalog: unknown, pointer: string): unknown {\n\tlet node = catalog;\n\tfor (const segment of pointer.split(\"/\").slice(1)) {\n\t\tif (typeof node !== \"object\" || node === null) return undefined;\n\t\tnode = (node as Record<string, unknown>)[segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\")];\n\t}\n\treturn node;\n}\n\n/**\n * The publisher's MoQ delivery mode for one source track: `deliveryMode` on\n * its base track entry (the hang rendition or MSF `tracks[]` entry its FEC\n * binds to). `stream` means QUIC retransmits, so the receiver subscribes no\n * repair track. Absent or any other value is `datagram`, FFmpeg's\n * `moq_delivery_mode` default: a receiver never silently drops repair on a\n * lossy path. When a track is declared both as a hang rendition and as an\n * MSF `tracks[]` entry, the rendition is authoritative: a `deliveryMode` on\n * the shadowed `tracks[]` entry is not read (fails safe to `datagram`).\n * Never throws.\n */\nexport function catalogTrackDeliveryMode(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): MoqRepairDelivery {\n\tconst binding = bindingOf(catalog, source);\n\tconst entry = binding ? valueAt(catalog, binding.pointer) : undefined;\n\treturn (entry as { deliveryMode?: unknown } | undefined)?.deliveryMode === \"stream\" ? \"stream\" : \"datagram\";\n}\n\n/** A MoQ Transport priority (0..255, lower first), or null. */\nfunction moqtPriority(value: unknown): number | null {\n\treturn Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= 255 ? (value as number) : null;\n}\n\n/**\n * The on-demand repair target of one source track: its base layer 0 repair\n * track and the two catalog priorities draft-ramadan-moq-fec \u00A711.4.5 derives\n * the FETCH's SUBSCRIBER_PRIORITY from (see `repairFetchSubscriberPriority`).\n *\n * - `repairPriority`: layer 0's catalog `priority`, or 240 when it declares none.\n * - `sourcePriority`: the source's MSF `tracks[]` entry `priority` (MoQ\n * Transport scale), or null for a hang rendition, which carries none.\n *\n * Null when the source has no `ok` base instance, or no positive\n * `interleaveDepthMs`: unicast repair requires a known elapsed FEC deadline\n * (\u00A711.4).\n */\nexport function catalogRepairTarget(\n\tcatalog: RepairLayerCatalog | undefined,\n\tsource: string | CatalogTrackRef,\n): RepairTarget | null {\n\tconst binding = bindingOf(catalog, source);\n\tif (binding?.status !== \"ok\" || !(binding.base.interleaveDepthMs ?? 0)) return null;\n\tconst layerZero = binding.layers[0]!;\n\tconst m = /^\\/tracks\\/(\\d+)$/.exec(binding.pointer);\n\tconst tracks = (catalog as CatalogWithTracks).tracks;\n\tconst entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : undefined;\n\tconst sourcePriority = entry ? moqtPriority((entry as Record<string, unknown>)[\"priority\"]) : null;\n\treturn { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };\n}\n\nfunction numericField(value: unknown): number | undefined {\n\tconst n = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n\treturn Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\n/** Like `numericField` but admits an explicit 0, and still rejects absent,\n * negative and non-finite input. Absence returns undefined so callers can\n * tell \"declared none\" from \"never declared\". */\nfunction nonNegativeField(value: unknown): number | undefined {\n\tconst n = typeof value === \"number\" ? value : typeof value === \"string\" ? Number(value) : Number.NaN;\n\treturn Number.isFinite(n) && n >= 0 ? n : undefined;\n}\n\n/**\n * Per-track FEC geometry from the source's binding: `NO_FEC` when it declares\n * none, `NO_FEC` plus `fecUnavailable` when its base instance is not `ok`.\n */\nfunction fecParams(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttrack: CatalogTrackRef | undefined,\n\tfps?: number,\n\tsampleRate?: number,\n): TrackFecParams {\n\tconst mediaRate = sampleRate === undefined ? { fps } : { fps, sampleRate };\n\tconst binding = track ? bindingOf(catalog, track) : undefined;\n\tif (binding === undefined) return { ...NO_FEC, ...mediaRate };\n\tif (binding.status === \"unavailable\") return { ...NO_FEC, ...mediaRate, fecUnavailable: binding.reason };\n\tconst { base } = binding;\n\tconst repair = resolveRepairLayers(catalog, track!)!;\n\treturn {\n\t\tfecK: base.sourceSymbols,\n\t\tfecAlgorithm: base.scheme,\n\t\tfecBlockProfile: parseFecBlockProfile(base.blockProfile, `${binding.pointer}/fec/blockProfile`),\n\t\tfecRepairCount: base.repairSymbols,\n\t\t// The wire RSB_length: every base layer's packets carry the base total.\n\t\tfecTotalRepairCount: binding.totalBaseRepairSymbols,\n\t\tfecSymbolSize: base.symbolSize,\n\t\t// Zero/absence leaves the elapsed deadline unknown; geometry stays in-band.\n\t\tfecInterleaveMs: catalogFecInterleaveMs(base) ?? 0,\n\t\t// Additive to the span, never a multiplier; a declared 0 stays 0.\n\t\tfecReorderToleranceMs: nonNegativeField(base.reorderToleranceMs),\n\t\trepairTrackName: base.repairTrack,\n\t\trepairLayers: repair.baseLayers,\n\t\trepairOverlays: repair.overlays,\n\t\tdeliveryMode: catalogTrackDeliveryMode(catalog, track!),\n\t\t...mediaRate,\n\t};\n}\n\n/**\n * Resolves per-track FEC geometry from the catalog for the active endpoint tracks.\n *\n * `video`/`audio` (and the top-level names and `catalogFps`) describe the\n * FIRST video and audio track of `tracks`. `forTrack` resolves any track from\n * its OWN rendition, scoped by its type: a second video track (an ABR rung or\n * simulcast variant, possibly sharing a packetId) gets its own framerate,\n * interleave and repair layers, never the first rendition's. A track with no\n * rendition of its type resolves to no FEC (`fecK` 0) and no media rate.\n *\n * FEC is per track and per instance (fec-core-00 10.3): a track whose base\n * instance is not `ok` resolves to no FEC with `fecUnavailable` saying why,\n * and every other track resolves as usual. Never throws on catalog input.\n */\nexport function resolveCatalogTrackFec(\n\tcatalog: CatalogWithRenditions | undefined,\n\ttracks: readonly CatalogTrackRef[],\n): {\n\tvideoTrackName?: string;\n\taudioTrackName?: string;\n\tmissingVideoRendition?: string;\n\tmissingAudioRendition?: string;\n\tvideoForKeyframeRepair?: KeyframeCadenceCatalog;\n\tcatalogFps?: number;\n\tvideo: TrackFecParams;\n\taudio: TrackFecParams;\n\tforTrack(track: CatalogTrackRef): TrackFecParams;\n} {\n\tconst videoTrackName = tracks.find((t) => t.type === \"video\")?.name;\n\tconst audioTrackName = tracks.find((t) => t.type === \"audio\")?.name;\n\tconst videoRenditions = catalog?.video?.renditions;\n\tconst audioRenditions = catalog?.audio?.renditions;\n\tconst video = videoTrackName ? videoRenditions?.[videoTrackName] : undefined;\n\tconst audio = audioTrackName ? audioRenditions?.[audioTrackName] : undefined;\n\tconst catalogFps = numericField(video?.framerate);\n\tconst sampleRate = numericField(audio?.sampleRate ?? audio?.samplerate);\n\tconst audioFps = sampleRate ? sampleRate / 1024 : undefined;\n\tconst videoParams = fecParams(catalog, videoTrackName ? { name: videoTrackName, type: \"video\" } : undefined, catalogFps);\n\tconst audioParams = fecParams(\n\t\tcatalog,\n\t\taudioTrackName ? { name: audioTrackName, type: \"audio\" } : undefined,\n\t\taudioFps,\n\t\tsampleRate,\n\t);\n\n\tconst forTrack = (track: CatalogTrackRef): TrackFecParams => {\n\t\tif (track.type === \"audio\") {\n\t\t\tif (track.name === audioTrackName) return audioParams;\n\t\t\tconst rendition = audioRenditions?.[track.name];\n\t\t\tif (!rendition) return { ...NO_FEC };\n\t\t\tconst rate = numericField(rendition.sampleRate ?? rendition.samplerate);\n\t\t\treturn fecParams(catalog, { name: track.name, type: \"audio\" }, rate ? rate / 1024 : undefined, rate);\n\t\t}\n\t\tif (track.name === videoTrackName) return videoParams;\n\t\tconst rendition = videoRenditions?.[track.name];\n\t\tif (!rendition) return { ...NO_FEC };\n\t\treturn fecParams(catalog, { name: track.name, type: \"video\" }, numericField(rendition.framerate));\n\t};\n\n\treturn {\n\t\tvideoTrackName,\n\t\taudioTrackName,\n\t\tmissingVideoRendition: videoTrackName && videoRenditions && !video ? videoTrackName : undefined,\n\t\tmissingAudioRendition: audioTrackName && audioRenditions && !audio ? audioTrackName : undefined,\n\t\tvideoForKeyframeRepair: video,\n\t\tcatalogFps,\n\t\tvideo: videoParams,\n\t\taudio: audioParams,\n\t\tforTrack,\n\t};\n}\n", "/**\n * CARP Catalog Types \u2014 FEC and Multicast catalog extensions.\n *\n * Per draft-law-ramadan-moq-carp-reliability \u00A73 (FEC) and \u00A74 (Multicast).\n * Plain TypeScript interfaces + type guards \u2014 no Zod dependency.\n *\n * These are the canonical type definitions. Upstream Zod schemas\n * should derive from these via z.infer<> to avoid duplication.\n */\n\nimport {\n\tMMTP_PACKET_ID_MAX,\n\tMMTP_SIGNALING_PACKET_ID,\n\ttype FecBlockProfile,\n} from \"@blockcast/mmt-base\";\nimport { multicastEndpointSource } from \"@blockcast/mmt-base\";\nimport type { EndpointAuthConfig, MulticastAuthConfig } from \"./multicast-auth-catalog.js\";\n\n// \u2500\u2500 Track Role (Phase 14, D-02..D-05) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Track role derived from the last slash-segment of the track name.\n *\n * Convention: `{content}/base`, `{content}/delta`, `{content}/repair`.\n * Single-track (no slash) legacy streams use `\"single\"`.\n *\n * D-01: No `trackRole` / `role` / `kind` field in the catalog.\n * D-02: Role is expressed via track name suffix only.\n * D-03: Unknown suffixes throw at parse time (error on bad init).\n * D-04: Legacy single-track streams (no slash suffix) return `\"single\"`.\n * D-05: Single role per track \u2014 no multi-role arrays.\n */\nexport type TrackRole = \"base\" | \"delta\" | \"repair\" | \"single\";\n\nconst VALID_ROLE_SUFFIXES = new Set<string>([\"base\", \"delta\", \"repair\"]);\n\n/**\n * Derive the role of a track from its name.\n *\n * - Names with no `/` \u2192 `\"single\"` (legacy single-track streams, D-04).\n * - Names ending with `/base`, `/delta`, or `/repair` \u2192 that role (D-02).\n * - Empty name \u2192 throws (error on bad init).\n * - Trailing slash (empty last segment) \u2192 throws.\n * - Non-ASCII or control characters anywhere in the name \u2192 throws (T-14-01 fuzz hardening).\n * - Unknown last segment \u2192 throws.\n *\n * @throws {Error} On empty name, non-ASCII/control chars, empty suffix, or unknown suffix.\n */\nexport function parseTrackRole(trackName: string): TrackRole {\n\tif (trackName === \"\") {\n\t\tthrow new Error(\"[carp-catalog] empty track name\");\n\t}\n\n\t// T-14-01 fuzz hardening: reject non-ASCII and control characters.\n\t// This prevents catalog injection attacks where a publisher sends a crafted\n\t// track name that bypasses role checks by embedding unusual code points.\n\tfor (let i = 0; i < trackName.length; i++) {\n\t\tconst cp = trackName.charCodeAt(i);\n\t\t// Reject control characters (0x00-0x1F, 0x7F) and non-ASCII (>0x7E)\n\t\tif (cp <= 0x1f || cp === 0x7f || cp > 0x7e) {\n\t\t\tthrow new Error(\n\t\t\t\t`[carp-catalog] track name contains non-ASCII or control character (code point 0x${cp.toString(16)}) in \"${trackName}\"`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst lastSlash = trackName.lastIndexOf(\"/\");\n\tif (lastSlash === -1) {\n\t\t// No slash \u2014 legacy single-track stream (D-04)\n\t\treturn \"single\";\n\t}\n\n\tconst suffix = trackName.slice(lastSlash + 1);\n\tif (suffix === \"\") {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] empty role suffix in track name \"${trackName}\"; track names must not end with \"/\"`,\n\t\t);\n\t}\n\n\tif (!VALID_ROLE_SUFFIXES.has(suffix)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] unknown role suffix \"${suffix}\" in track name \"${trackName}\"; expected one of: base, delta, repair`,\n\t\t);\n\t}\n\n\treturn suffix as TrackRole;\n}\n\n/**\n * Non-throwing switching-set detector (BLO-8158).\n *\n * Returns `true` iff at least one track name carries a media role suffix\n * (`base` or `delta`) \u2014 i.e. the set looks like a base/delta/repair switching\n * set. A `/repair` track alone is ordinary FEC topology and is not sufficient\n * evidence of a switching set. Unlike `parseTrackRole`, this NEVER throws:\n * names without a `/`, and names with an unrecognised suffix (e.g. an ABR\n * resolution ladder like `video/1080p`, `video/720p`, `video/8k`), simply do\n * not count as a role.\n *\n * Use this for DETECTION (\"is this a switching set?\"). Once a switching set is\n * confirmed, `parseTrackRole` / `validateSwitchingSet` still strictly gate the\n * actual wiring. ABR ladders yield `false` here and fall through to the\n * ABR/single path instead of throwing.\n */\nexport function hasSwitchingSetRole(names: string[]): boolean {\n\treturn names.some((name) => {\n\t\tconst lastSlash = name.lastIndexOf(\"/\");\n\t\tif (lastSlash === -1) return false;\n\t\tconst suffix = name.slice(lastSlash + 1);\n\t\treturn suffix === \"base\" || suffix === \"delta\";\n\t});\n}\n\n/**\n * Validate that a set of track names forms a complete switching set.\n *\n * - Empty or single-track sets (all parsing to `\"single\"`) \u2192 `{ ok: true }`.\n * - Multi-track sets (any name has a role suffix) MUST include `base` and\n * `repair`. `delta` is optional (BLO-49199): base is the self-decodable leg,\n * so a base-only set plays in base-only mode; a delta without base cannot.\n * Missing roles are listed.\n *\n * Propagates throws from `parseTrackRole` (non-ASCII, unknown suffix, etc.).\n *\n * D-04: Legacy single-track streams pass without enforcement.\n * D-04: Once any suffix appears, the set is multi-track and needs base + repair.\n */\nexport function validateSwitchingSet(\n\ttrackNames: string[],\n): { ok: true } | { ok: false; missing: TrackRole[] } {\n\tif (trackNames.length === 0) return { ok: true };\n\n\tconst roles = trackNames.map(parseTrackRole);\n\n\t// If every role is \"single\", this is a legacy single-track stream \u2014 pass.\n\tif (roles.every((r) => r === \"single\")) {\n\t\treturn { ok: true };\n\t}\n\n\t// Multi-track: at least one name has a role suffix.\n\t// The set MUST contain base and repair; delta is optional (BLO-49199).\n\t// Explicit Set<TrackRole> so the TrackRole-typed `required.filter`\n\t// callback can call .has(r) without a narrow-mismatch complaint.\n\tconst presentRoles: Set<TrackRole> = new Set(roles.filter((r) => r !== \"single\"));\n\tconst required: TrackRole[] = [\"base\", \"repair\"];\n\tconst missing = required.filter((r) => !presentRoles.has(r));\n\n\tif (missing.length > 0) {\n\t\treturn { ok: false, missing };\n\t}\n\treturn { ok: true };\n}\n\n// \u2500\u2500 FEC Catalog Extension (draft-ramadan-moq-fec-catalog-00 3) \u2500\n\n/**\n * One FEC instance object (draft-ramadan-moq-fec-catalog-00 3.1). A track's\n * `fec` is an array of these ({@link FecCatalogConfig}); an empty array, or no\n * `fec`, declares no FEC. Member names are case sensitive. This is the shape\n * only: whether an instance is usable is `validateFecCatalog`'s answer (fail\n * closed per instance), and runtime readers take it from `resolveFecBindings`,\n * never from a local predicate.\n */\nexport interface FecInstanceConfig {\n\t/** 0..255, unique per track; absent means 0. */\n\tinstanceId?: number;\n\t/** `raptorq`, `reed-solomon`, or any other registered token, which a\n\t * receiver that does not implement it consumes without FEC. */\n\tscheme: string;\n\t/** REQUIRED, never defaulted. This library implements `iso-ssbg1-v1`\n\t * (BLO-35597); any other token is no FEC on that instance. */\n\tblockProfile: FecBlockProfile | (string & {});\n\t/** K. */\n\tsourceSymbols: number;\n\t/** P of layer 0, >= 1. */\n\trepairSymbols: number;\n\t/** T, bytes per symbol. */\n\tsymbolSize: number;\n\tinterleaveDepthMs?: number;\n\t/** Additive jitter/reorder allowance in milliseconds (never a multiplier). */\n\treorderToleranceMs?: number;\n\t/** Layer-0 repair track. Further layers and an overlay are `fec-repair`\n\t * tracks naming this track in `source` and this instance's `instanceId`. */\n\trepairTrack: string;\n\tsymbolTrack?: string;\n\t/** Display only; never drives behaviour. */\n\tlabel?: string;\n\t/** Phase-14 base-only fallback window in FEC blocks (D-11). Blockcast\n\t * extension: an unrecognized instance key the validator ignores\n\t * (catalog-00 3.4); read by `resolveDeltaFallback`. */\n\tdeltaFailWindowBlocks?: number;\n\t/** Phase-14 delta re-subscribe probe interval in ms (D-13). Blockcast\n\t * extension, as above. */\n\tdeltaProbeIntervalMs?: number;\n}\n\n/** A track's `fec` member (draft-ramadan-moq-fec-catalog-00 3). */\nexport type FecCatalogConfig = readonly FecInstanceConfig[];\n\n// \u2500\u2500 Multicast Catalog Extension (CARP \u00A74.1-4.4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * An endpoint's join mode, per draft-ramadan-moq-multicast-01 4.1 at\n * Blockcast/moqcast-draft@fd962fd73d21ed281b60219c11ea61cc9e4e91c4.\n * `sourceAddress` selects it (present: SSM, absent: ASM) and `protocol` only\n * restates it. A bearer (AMT, ATSC 3.0) is a `networkSource` entry, never a\n * protocol; `multicastEndpointSource` in `@blockcast/mmt-base` derives the\n * mode and `S` for every reader.\n */\nexport type MulticastProtocol = \"ssm\" | \"asm\";\n\n/**\n * Per-track endpoint membership, per draft-ramadan-moq-multicast-01 \u00A74.1.\n *\n * `packetId` is the MMTP packet_id used for packet-level demux on the\n * multicast wire, in 1..65535: packet_id 0 (`MMTP_SIGNALING_PACKET_ID`) is\n * the MMTP signaling flow and is never a track's. New emitters MUST use this\n * object form; the legacy\n * `string` form is accepted by parsers for one release for backward\n * compatibility (BLO-3323 migration).\n */\nexport interface MulticastTrackObject {\n\tname: string;\n\tpacketId: number;\n}\n\n/** Track entry in a multicast endpoint \u2014 accepts both legacy and v2 shape. */\nexport type MulticastTrack = string | MulticastTrackObject;\n\nexport interface MulticastEndpoint {\n\t/** Restates the mode `sourceAddress` selects; an endpoint where it disagrees is ignored. */\n\tprotocol?: MulticastProtocol;\n\t/** SSM source address; its absence selects ASM. */\n\tsourceAddress?: string;\n\t/** Multicast group address (REQUIRED). */\n\tgroupAddress: string;\n\t/** UDP port number (REQUIRED). */\n\tport: number;\n\t/**\n\t * Tracks available on this endpoint (REQUIRED).\n\t *\n\t * Accepts both legacy `string` (track name only) and v2 object form\n\t * (`{ name, packetId }`) for backward compatibility. New emitters\n\t * MUST use the object form (BLO-3323 / draft \u00A74.1). Consumers can\n\t * type-narrow with `typeof t === \"string\"` to handle both.\n\t */\n\ttracks: MulticastTrack[];\n\t/**\n\t * Aggregate bandwidth of this endpoint in bits per second.\n\t * RECOMMENDED per draft \u00A74.1 \u2014 enables capacity-aware joins; REQUIRED on a\n\t * protected endpoint, where it bounds the receiver's hold queues (7.2.6).\n\t */\n\tbandwidth?: number;\n\t/**\n\t * Marks the endpoint's flow protected (7.2.6). REQUIRED when the\n\t * `multicast` object carries `auth`, forbidden when it does not.\n\t */\n\tauth?: EndpointAuthConfig;\n\t/** Bearers for this endpoint alone (4.2); readers prefer it to the catalog-level array. */\n\tnetworkSource?: NetworkSource[];\n}\n\nexport type AmtDiscovery = \"dns\" | \"driad\" | \"manual\";\n\n/**\n * One `multicast.networkSource[]` entry, per draft-ramadan-moq-multicast \u00A74.1.\n *\n * Mirrors moq-rs `NetworkSource` (the normative validator) and the\n * `networkSource` `$def` in `catalog.schema.json` 1:1 \u2014 a change here needs the\n * same change in both. `discovery` is OPTIONAL (draft -01 4.2.1): `\"dns\"`\n * names a relay service hostname whose A/AAAA answers are candidates,\n * `\"manual\"` a literal relay, and omitted means `\"dns\"` when `relay` is a\n * hostname (DRIAD when `relay` is absent too). `\"dns\"` and `\"manual\"` require\n * `relay`; that relay is the one the player joins. Under `discovery: \"driad\"` the relay is resolved at\n * runtime and `relay` MUST be omitted (draft -01 4.2.1); the schema, the\n * runtime validator and `multicastFromConfig` all reject one. The tolerant\n * readers (`findAmtRelay`, `catalogAmtNetworkSource`) still skip a DRIAD\n * entry's relay, so a DRIAD entry never pins a relay.\n * `port` is OPTIONAL on the wire: the join boundary itself has no default, but\n * every reader resolves one before that boundary, so a manual relay without a\n * port joins on RFC 7450's 2268 rather than failing (BLO-38176).\n *\n * Not to be confused with `CatalogNetworkSource` in `catalog-network-source.ts`,\n * which is also exported from the package root and also describes\n * `$defs/networkSource`. The split is deliberate and the two must NOT be\n * unified: this type is the strict EMITTER/wire shape, so tightening it is what\n * keeps published catalogs normative. That one is the tolerant READER \u2014 every\n * field optional, and it also accepts the bare-object form that hang's zod\n * RootSchema and older hand-authored catalogs emit. Tightening the reader would\n * reject catalogs already in the wild, so when this type changes, widen the\n * reader only if the new shape must be consumed; never narrow it to match.\n *\n * This is the one place a catalog declares an AMT relay: in the `multicast`\n * object for every endpoint, or in one endpoint's own `networkSource`\n * (draft -01 4.2). The endpoint model carries no relay field.\n */\nexport type NetworkSource =\n\t| { type: \"amt\"; discovery?: AmtDiscovery; relay?: string; port?: number }\n\t| {\n\t\t\ttype: \"atsc3\";\n\t\t\t/** RF centre frequency in kHz (draft -01 4.2.2). The only REQUIRED tuning field. */\n\t\t\tfrequency: number;\n\t\t\tplpId?: number;\n\t\t\tserviceId?: number;\n\t\t\t/**\n\t\t\t * SLS of the MMTP service, MP table included (A/331:2026-04 7.2). It\n\t\t\t * describes the MMTP session and nothing of this profile: the flow\n\t\t\t * parameters and keys come from provisioning. libmmt does not fetch it.\n\t\t\t */\n\t\t\tslsUri?: string;\n\t };\n\nexport interface MulticastCatalogConfig {\n\t/** Enable/disable multicast (OPTIONAL, default true). */\n\tenabled?: boolean;\n\t/** Multicast endpoints (REQUIRED). */\n\tendpoints: MulticastEndpoint[];\n\t/**\n\t * Relay/tuning discovery applying to every endpoint (4.2); omitted for\n\t * native joins, whose endpoint tuple is the whole join. An endpoint's own\n\t * `networkSource` takes precedence for that endpoint.\n\t */\n\tnetworkSource?: NetworkSource[];\n\t/** bc-provenance version 2 profile and keys (7.2.6). */\n\tauth?: MulticastAuthConfig;\n\t/**\n\t * AMT fallback configuration.\n\t *\n\t * @deprecated Superseded by `networkSource` \u2014 `{discovery, relayDns}` maps\n\t * onto `{type: \"amt\", discovery, relay}`. `multicastFromConfig` lifts it and\n\t * no longer emits it; the schema rejects it.\n\t */\n\tamtFallback?: {\n\t\tdiscovery: AmtDiscovery;\n\t\trelayDns?: string;\n\t};\n\t/**\n\t * Monotonic catalog version. Publishers MUST bump on any\n\t * `endpoints[]` mutation (BLO-3323 catalog freshness model).\n\t */\n\tversion?: number;\n\t/**\n\t * Unix-ms timestamp after which subscribers SHOULD refetch the\n\t * catalog. Refetch-on-multicast-receive-timeout short-circuits this.\n\t */\n\tvalidUntil?: number;\n}\n\nexport function isMulticastTrackObject(obj: unknown): obj is MulticastTrackObject {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn typeof o[\"name\"] === \"string\" && typeof o[\"packetId\"] === \"number\";\n}\n\n/**\n * Wire-shape predicate: returns `true` when `obj` matches the\n * MulticastEndpoint JSON shape strictly enough to be safely cast.\n *\n * Intentionally less strict than `multicastFromConfig`'s\n * `validateEndpoint` (in `multicast-input.ts`):\n *\n * - `tracks: []` is accepted here (Array.isArray check only) but\n * rejected by `validateEndpoint` as `empty_tracks`.\n * - SSM-specific group-range and source-required checks are not\n * enforced here.\n * - Bandwidth/AmtFallback/version are not type-checked.\n *\n * That asymmetry is deliberate: this predicate guards JSON parsed off\n * the wire (already-published catalogs that downstream may have\n * trimmed), while `validateEndpoint` guards publisher input *before*\n * a catalog is composed. Subscribers are tolerant; publishers are\n * strict \u2014 the catalog format never re-validates upstream invariants.\n */\nexport function isMulticastEndpoint(obj: unknown): obj is MulticastEndpoint {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn (\n\t\tmulticastEndpointSource(o) !== undefined &&\n\t\ttypeof o[\"groupAddress\"] === \"string\" && (o[\"groupAddress\"] as string).length > 0 &&\n\t\ttypeof o[\"port\"] === \"number\" && (o[\"port\"] as number) > 0 && (o[\"port\"] as number) <= 65535 &&\n\t\tArray.isArray(o[\"tracks\"]) &&\n\t\t(o[\"tracks\"] as unknown[]).every((t: unknown) => typeof t === \"string\" || isMulticastTrackObject(t))\n\t);\n}\n\nexport function isMulticastCatalogConfig(obj: unknown): obj is MulticastCatalogConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\treturn (\n\t\tArray.isArray(o[\"endpoints\"]) &&\n\t\t(o[\"endpoints\"] as unknown[]).every((ep: unknown) => isMulticastEndpoint(ep)) &&\n\t\t// Shape only, not contents \u2014 entry validation is `multicastFromConfig`'s.\n\t\t// `networkSource` now carries the relay, so a non-array here must not\n\t\t// narrow clean: every reader of it indexes it.\n\t\t//\n\t\t// `=== undefined`, not `!(\"networkSource\" in o)`: `multicastFromConfig`\n\t\t// tests definedness, and the two must agree on what \"set\" means. A key\n\t\t// present with value `undefined` is a legal `MulticastCatalogConfig`\n\t\t// (`strict` without `exactOptionalPropertyTypes`) and is what a spread\n\t\t// of an optional field produces \u2014 `in` would narrow that valid config\n\t\t// to `false` and a consumer gating on this guard would discard a\n\t\t// working multicast block.\n\t\t(o[\"networkSource\"] === undefined || Array.isArray(o[\"networkSource\"]))\n\t);\n}\n\n// \u2500\u2500 Track Timing (mmt-01 \u00A74.4.2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Per-track group-duration signaling.\n *\n * The catalog publishes either `groupDurationMs` (integer ms) or the exact\n * `groupDurationTicks` value used by `groupIdForTicks` in align.ts. The tick\n * form is required for cases where milliseconds cannot represent the cadence\n * exactly:\n *\n * - Per-frame group durations at non-integer-ms cadences\n * (1/30 s = 33.333\u2026 ms; 1/60 s = 16.666\u2026 ms).\n * - Audio sample-rate timescales not divisible by 1000 Hz\n * (e.g. 44 100 Hz \u00D7 333 ms = 14 685.3 ticks).\n *\n * If both fields are present, `groupDurationTicks` wins (matches the\n * spec, mmt-01 \u00A74.4.2).\n *\n * @see draft-ramadan-moq-mmt-01 \u00A74.4.2\n * @see ./align.ts groupIdForTicks\n */\nexport interface TrackTimingConfig {\n\t/** Group duration in milliseconds (positive integer). Required unless an\n\t * exact groupDurationTicks value is supplied. */\n\tgroupDurationMs?: number;\n\t/** Integer-tick override (OPTIONAL, positive integer). Wins over the\n\t * ms field when both are present. Required when\n\t * `(groupDurationMs * timescale) % 1000 != 0`. */\n\tgroupDurationTicks?: number;\n}\n\nexport function isTrackTimingConfig(obj: unknown): obj is TrackTimingConfig {\n\tif (typeof obj !== \"object\" || obj === null) return false;\n\tconst o = obj as Record<string, unknown>;\n\tconst ms = o[\"groupDurationMs\"];\n\tconst ticks = o[\"groupDurationTicks\"];\n\tif (ms === undefined && ticks === undefined) return false;\n\tif (ms !== undefined && (typeof ms !== \"number\" || !Number.isInteger(ms) || ms <= 0)) return false;\n\tif (\"groupDurationTicks\" in o) {\n\t\tif (ticks === undefined) return ms !== undefined;\n\t\tif (typeof ticks !== \"number\" || !Number.isInteger(ticks) || ticks <= 0) return false;\n\t}\n\treturn true;\n}\n\n/** Resolve catalog group timing to an operational wall-clock duration.\n *\n * `groupDurationTicks` wins when both wire fields are present, matching\n * mmt-01 \u00A74.4.2. The result may be fractional because exact frame cadences\n * such as 33_333 ticks at 1 MHz cannot be represented by the wire catalog's\n * integer `groupDurationMs` field.\n */\nexport function resolveGroupDurationMs(\n\ttiming: TrackTimingConfig,\n\ttimescale: number,\n): number {\n\tif (!Number.isSafeInteger(timescale) || timescale <= 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: timescale must be a positive safe integer Hz, got ${timescale}`,\n\t\t);\n\t}\n\tif (!isTrackTimingConfig(timing)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: invalid TrackTimingConfig: ${JSON.stringify(timing)}`,\n\t\t);\n\t}\n\tif (timing.groupDurationTicks === undefined) {\n\t\treturn timing.groupDurationMs!;\n\t}\n\n\tconst durationMs = (timing.groupDurationTicks / timescale) * 1000;\n\tif (!Number.isFinite(durationMs) || durationMs <= 0 || durationMs > Number.MAX_SAFE_INTEGER) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationMs: groupDurationTicks=${timing.groupDurationTicks} ` +\n\t\t\t\t`at timescale=${timescale} cannot be represented as a positive safe millisecond duration`,\n\t\t);\n\t}\n\treturn durationMs;\n}\n\n/**\n * Optional unicast archive tier (BLO-41290).\n *\n * `origin` is scheme-and-authority only. Object paths are never advertised:\n * a consumer derives them via the v1 ContentId profile \u2014\n * `/v1/{ns}/{track}/{group}[/{object}]` under this origin\n * (`mmt-archive/src/content_id.rs`). Advertising the pair a second time would\n * be a second source of truth that can disagree with `tracks[]`.\n *\n * `{track}` is the track's `name`. `{ns}` is the track's own `namespace` when\n * it declares one, and otherwise the MoQ subscription's Track Namespace tuple,\n * of which `ContentId::from_moq` admits exactly one element. The catalog does\n * not carry that tuple, so for a track omitting `namespace` the derivation\n * needs the subscription context \u2014 the catalog alone is not sufficient. That\n * inheritance is the load-bearing half of the contract: `namespace` is\n * optional in the schema, so a catalog can advertise an archive whose object\n * paths only resolve in a subscribing consumer.\n *\n * One origin at the top level, not one per track: the `(namespace, track)` key\n * lives in the derived path rather than in the origin, so a single origin\n * already disambiguates every track however many namespaces the catalog spans.\n * A per-track origin would only buy splitting one catalog across several\n * archives, which nothing asks for.\n *\n * Absence means \"no archive advertised\". It is never a default URL.\n */\nexport interface ArchiveConfig {\n\torigin: string;\n}\n\n/**\n * The advertised archive origin, or `undefined` when none is advertised.\n *\n * Takes a catalog already through `validateCmsfMmtpCatalog`, so the returned\n * string is canonical and safe to use as a base \u2014 no string surgery on a\n * config blob, and no second place that decides what an origin may look like.\n */\nexport function archiveOrigin(catalog: CmsfCatalog): string | undefined {\n\treturn catalog.archive?.origin;\n}\n\n/**\n * Pin the archive origin to exactly one spelling per archive.\n *\n * The ContentId codec rejects percent-encoding so every identity has exactly\n * one URL path; an advertised base must not reintroduce aliasing above it.\n * `value === new URL(value).origin` is most of the rule \u2014 `URL.origin` drops\n * path, query, fragment and userinfo, lowercases scheme and host, strips the\n * default port and any trailing slash, and normalizes percent-escapes. So a\n * separate percent-encoding check would be redundant, not missing: `%6d` in a\n * host decodes and fails the comparison. `https:` is checked separately\n * because `http://host` is already its own origin and passes the comparison.\n *\n * The comparison alone admits one alias, so the trailing dot is rejected\n * explicitly: `https://host.` is its own `URL.origin` verbatim, while `host.`\n * and `host` are the same DNS name. That aliases the advertised *base*, not\n * the codec input \u2014 the origin contributes no path segments, so a trailing\n * dot never reaches `ContentId::parse_path` \u2014 but it still gives one archive\n * two cache keys, and some TLS stacks treat `host.` as a distinct SNI/CORS\n * origin.\n *\n * `additionalProperties: false` on `archive` is enforced here as well as in\n * the schema. The runtime validator is the gate `archiveOrigin` documents\n * itself against, so a second spelling of the origin smuggled in under\n * another key must be rejected by both, not just by AJV.\n */\nfunction validateArchive(catalog: Record<string, unknown>): void {\n\tconst archive = catalog[\"archive\"];\n\tif (archive === undefined) return;\n\tif (typeof archive !== \"object\" || archive === null || Array.isArray(archive)) {\n\t\tcatalogError(\"archive_object\", \"$.archive\", \"archive must be an object\");\n\t}\n\tconst unknown = Object.keys(archive as Record<string, unknown>).filter(key => key !== \"origin\");\n\tif (unknown.length > 0) {\n\t\tcatalogError(\"archive_object\", \"$.archive\", `archive has unknown field(s) ${unknown.map(key => JSON.stringify(key)).join(\", \")}; only \"origin\" is allowed`);\n\t}\n\tconst origin = (archive as Record<string, unknown>)[\"origin\"];\n\t// Absent and present-but-wrong-type are different mistakes and the schema\n\t// already distinguishes them (`required` vs `type`). Collapsing both into\n\t// \"must be a string\" tells someone hand-writing a catalog to fix a field\n\t// they never wrote. Same wording convention as `codec is required`.\n\t//\n\t// This is the one archive rule where the two gates report different fields,\n\t// deliberately: AJV raises `required` at the parent, `/archive` (its\n\t// placement, not a choice \u2014 same shape as `fec-block-profile-missing`),\n\t// while this raises at `$.archive.origin`. Pointing at the field someone has\n\t// to add is the more useful of the two, so the \"same field\" convention\n\t// documented above bends here rather than the message getting vaguer.\n\tif (origin === undefined) {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", \"origin is required when an archive block is present\");\n\t}\n\tif (typeof origin !== \"string\") {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", \"origin must be a string\");\n\t}\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(origin);\n\t} catch {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin must be an absolute URL, got ${JSON.stringify(origin)}`);\n\t}\n\tif (parsed.protocol !== \"https:\") {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin must use https, got ${JSON.stringify(parsed.protocol)}`);\n\t}\n\tif (origin !== parsed.origin) {\n\t\tcatalogError(\n\t\t\t\"archive_origin\",\n\t\t\t\"$.archive.origin\",\n\t\t\t`origin must be scheme and authority in canonical form with no path, query, fragment, userinfo or trailing slash; expected ${JSON.stringify(parsed.origin)}, got ${JSON.stringify(origin)}`,\n\t\t);\n\t}\n\tif (parsed.hostname.endsWith(\".\")) {\n\t\tcatalogError(\"archive_origin\", \"$.archive.origin\", `origin host must not end in a trailing dot; ${JSON.stringify(parsed.hostname)} and ${JSON.stringify(parsed.hostname.slice(0, -1))} are the same host`);\n\t}\n}\n\nexport type CmsfCatalog = {\n\tversion: number;\n\ttracks: Record<string, unknown>[];\n\tmulticast?: Record<string, unknown>;\n\tarchive?: ArchiveConfig;\n\t[key: string]: unknown;\n};\n\nexport type CmsfCatalogErrorCode =\n\t| \"catalog_object\"\n\t| \"catalog_version\"\n\t| \"catalog_tracks\"\n\t| \"catalog_common_track_fields\"\n\t| \"track_object\"\n\t| \"track_name\"\n\t| \"track_packaging\"\n\t| \"track_selection_params\"\n\t| \"track_is_live\"\n\t| \"track_codec\"\n\t| \"track_mmtp_mode\"\n\t| \"track_presentation_anchor\"\n\t| \"track_init_ref\"\n\t| \"catalog_init_data_list\"\n\t| \"track_timescale\"\n\t| \"track_group_duration_ms\"\n\t| \"track_group_duration_ticks\"\n\t| \"track_group_duration_exact\"\n\t| \"track_keyframe_interval\"\n\t| \"track_fec\"\n\t| \"track_fec_bounds\"\n\t| \"track_fec_block_profile\"\n\t| \"track_fec_block_profile_unimplemented\"\n\t| \"track_repair_track\"\n\t| \"track_repair_depends\"\n\t| \"track_repair_layer\"\n\t| \"track_repair_priority\"\n\t| \"track_repair_overlay\"\n\t| \"track_repair_geometry\"\n\t| \"track_switching_set\"\n\t| \"multicast_auth\"\n\t| \"multicast_endpoint\"\n\t| \"multicast_endpoint_auth\"\n\t| \"multicast_track\"\n\t| \"multicast_packet_id\"\n\t| \"multicast_relay_address\"\n\t| \"multicast_network_source\"\n\t| \"archive_object\"\n\t| \"archive_origin\";\n\nexport class CmsfCatalogValidationError extends Error {\n\tconstructor(\n\t\tpublic readonly code: CmsfCatalogErrorCode,\n\t\tpublic readonly path: string,\n\t\tmessage: string,\n\t) {\n\t\tsuper(`[cmsf:${code}] ${path}: ${message}`);\n\t\tthis.name = \"CmsfCatalogValidationError\";\n\t}\n}\n\nfunction catalogError(code: CmsfCatalogErrorCode, path: string, message: string): never {\n\tthrow new CmsfCatalogValidationError(code, path, message);\n}\n\n// draft-ramadan-moq-multicast-01 4.2.1: DRIAD supplies the relay\n// candidates, so a `discovery: \"driad\"` entry MUST omit `relay`.\nfunction validateNetworkSourceRelays(sources: unknown, path: string): void {\n\tif (!Array.isArray(sources)) return;\n\tsources.forEach((source, i) => {\n\t\tconst s = source as Record<string, unknown> | null;\n\t\tif (s?.[\"type\"] === \"amt\" && s[\"discovery\"] === \"driad\" && s[\"relay\"] !== undefined) {\n\t\t\tcatalogError(\"multicast_network_source\", `${path}[${i}].relay`, 'relay must be omitted with discovery \"driad\"');\n\t\t}\n\t});\n}\n\n/**\n * Validate the common CMSF shape and the MMTP required-field profile. It never\n * throws on a draft -02 4.1 / 7.2.6 multicast rule: pass `catalogNamespace`\n * (the namespace of the catalog track) to `admittedMulticastEndpoints` /\n * `multicastCatalogIssues` instead. The option is accepted and unused.\n */\nexport function validateCmsfMmtpCatalog(\n\tvalue: unknown,\n\t_options: { catalogNamespace?: string } = {},\n): CmsfCatalog {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tcatalogError(\"catalog_object\", \"$\", \"catalog must be an object\");\n\t}\n\tconst catalog = value as Record<string, unknown>;\n\t// IETF-draft catalogs (cast `ladder_catalog.rs`, moq-server\n\t// `catalog-synthesizer.ts`, consumed by the hang player) carry a string\n\t// version tag such as \"draft-ietf-moq-catalogformat-01\" instead of the CMSF\n\t// `version: 1` integer, and the draft-ramadan-moq-*-01 examples carry\n\t// \"draft-01\". Nothing keys behavior off the catalog version, so\n\t// normalize the tag to a number rather than rejecting the whole catalog \u2014\n\t// mirrors the relay (dual-stack-relay `bridge/catalog.rs`\n\t// `deserialize_optional_u32_lenient`, #825) and moqtail-ts\n\t// `normalizeCatalogVersion`.\n\tconst rawVersion = catalog[\"version\"];\n\tconst normalizedVersion =\n\t\ttypeof rawVersion === \"string\"\n\t\t\t? Number.isFinite(Number(rawVersion))\n\t\t\t\t? Number(rawVersion)\n\t\t\t\t: rawVersion.startsWith(\"draft-ietf-moq-catalogformat\") || rawVersion === \"draft-01\"\n\t\t\t\t\t? 1\n\t\t\t\t\t: NaN\n\t\t\t: rawVersion;\n\tif (!Number.isInteger(normalizedVersion) || (normalizedVersion as number) <= 0) {\n\t\tcatalogError(\"catalog_version\", \"$.version\", \"version must be a positive integer or a recognized format tag\");\n\t}\n\tcatalog[\"version\"] = normalizedVersion;\n\tif (!Array.isArray(catalog[\"tracks\"])) {\n\t\tcatalogError(\"catalog_tracks\", \"$.tracks\", \"tracks must be an array\");\n\t}\n\n\tif (\"commonTrackFields\" in catalog) {\n\t\tcatalogError(\n\t\t\t\"catalog_common_track_fields\",\n\t\t\t\"$.commonTrackFields\",\n\t\t\t\"commonTrackFields is retired; every track carries its own fields\",\n\t\t);\n\t}\n\t// mmt-01 4.5 / MSF 5.1.7: entries carry an id and a type; an \"inline\"\n\t// entry carries its base64 init segment in data.\n\tconst initDataList = catalog[\"initDataList\"];\n\tif (initDataList !== undefined) {\n\t\tif (!Array.isArray(initDataList)) {\n\t\t\tcatalogError(\"catalog_init_data_list\", \"$.initDataList\", \"initDataList must be an array\");\n\t\t}\n\t\tinitDataList.forEach((entry: unknown, i: number) => {\n\t\t\tconst e = entry as Record<string, unknown> | null;\n\t\t\tconst entryPath = `$.initDataList[${i}]`;\n\t\t\tif (typeof e !== \"object\" || e === null || Array.isArray(e) ||\n\t\t\t\ttypeof e[\"id\"] !== \"string\" || e[\"id\"].length === 0 ||\n\t\t\t\ttypeof e[\"type\"] !== \"string\" || e[\"type\"].length === 0) {\n\t\t\t\tcatalogError(\"catalog_init_data_list\", entryPath, \"entry needs a non-empty string id and type\");\n\t\t\t}\n\t\t\tif (e[\"type\"] === \"inline\" && (typeof e[\"data\"] !== \"string\" || e[\"data\"].length === 0)) {\n\t\t\t\tcatalogError(\"catalog_init_data_list\", `${entryPath}.data`, 'an \"inline\" entry needs non-empty data');\n\t\t\t}\n\t\t});\n\t}\n\tconst tracks = catalog[\"tracks\"] as unknown[];\n\tconst trackRecords: Record<string, unknown>[] = [];\n\tconst positiveInteger = (input: unknown): input is number => Number.isSafeInteger(input) && (input as number) > 0;\n\tconst positiveNumber = (input: unknown): input is number =>\n\t\ttypeof input === \"number\" && Number.isFinite(input) && input > 0;\n\n\tfor (let index = 0; index < tracks.length; index++) {\n\t\tconst raw = tracks[index];\n\t\tconst path = `$.tracks[${index}]`;\n\t\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n\t\t\tcatalogError(\"track_object\", path, \"track must be an object\");\n\t\t}\n\t\tconst track = raw as Record<string, unknown>;\n\t\tconst name = track[\"name\"];\n\t\tif (typeof name !== \"string\" || name.length === 0) {\n\t\t\tcatalogError(\"track_name\", `${path}.name`, \"name must be a non-empty string\");\n\t\t}\n\t\ttrackRecords.push(track);\n\n\t\tconst packaging = track[\"packaging\"];\n\t\tif (typeof packaging !== \"string\" || packaging.length === 0) {\n\t\t\tcatalogError(\"track_packaging\", `${path}.packaging`, \"packaging must be a non-empty string\");\n\t\t}\n\t\tconst hasIsLive = Object.prototype.hasOwnProperty.call(track, \"isLive\");\n\t\tconst isLive = track[\"isLive\"];\n\t\tif (hasIsLive && typeof isLive !== \"boolean\") {\n\t\t\tcatalogError(\"track_is_live\", `${path}.isLive`, \"isLive must be a boolean\");\n\t\t}\n\t\ttrack[\"isLive\"] = isLive ?? true;\n\t\tif (\"selectionParams\" in track) {\n\t\t\tcatalogError(\n\t\t\t\t\"track_selection_params\",\n\t\t\t\t`${path}.selectionParams`,\n\t\t\t\t\"selectionParams is retired; codec and the other selection fields are flat on the track\",\n\t\t\t);\n\t\t}\n\t\tconst codec = track[\"codec\"];\n\t\tif (packaging !== \"timeline\" && packaging !== \"mediatimeline\" && packaging !== \"eventtimeline\" && packaging !== \"fec-repair\" && packaging !== \"fec-source\" &&\n\t\t\t(typeof codec !== \"string\" || codec.length === 0)) {\n\t\t\tcatalogError(\"track_codec\", `${path}.codec`, `codec is required for packaging=${packaging}`);\n\t\t}\n\n\t\tconst timescale = track[\"timescale\"];\n\t\tconst groupDurationMs = track[\"groupDurationMs\"];\n\t\tconst groupDurationTicks = track[\"groupDurationTicks\"];\n\t\tconst keyframeIntervalMs = track[\"keyframeIntervalMs\"];\n\t\tconst keyframeIntervalTicks = track[\"keyframeIntervalTicks\"];\n\t\tif (groupDurationMs !== undefined && !positiveInteger(groupDurationMs)) {\n\t\t\tcatalogError(\"track_group_duration_ms\", `${path}.groupDurationMs`, \"must be a positive integer\");\n\t\t}\n\t\tif (groupDurationTicks !== undefined && !positiveInteger(groupDurationTicks)) {\n\t\t\tcatalogError(\"track_group_duration_ticks\", `${path}.groupDurationTicks`, \"must be a positive integer\");\n\t\t}\n\t\tif ((keyframeIntervalMs !== undefined && !positiveNumber(keyframeIntervalMs)) ||\n\t\t\t(keyframeIntervalTicks !== undefined && !positiveInteger(keyframeIntervalTicks))) {\n\t\t\tcatalogError(\"track_keyframe_interval\", path, \"keyframe interval fields must be positive numbers\");\n\t\t}\n\t\t// mmt-01 4.4.1: a signed integer in the track's timescale.\n\t\tconst anchor = track[\"presentationAnchorTicks\"];\n\t\tif (anchor !== undefined && !Number.isSafeInteger(anchor)) {\n\t\t\tcatalogError(\"track_presentation_anchor\", `${path}.presentationAnchorTicks`, \"must be a safe integer\");\n\t\t}\n\t\t// mmt-01 4.5: the id of a root initDataList entry.\n\t\tconst initRef = track[\"initRef\"];\n\t\tif (initRef !== undefined && (typeof initRef !== \"string\" || initRef.length === 0)) {\n\t\t\tcatalogError(\"track_init_ref\", `${path}.initRef`, \"must be a non-empty string\");\n\t\t}\n\t\t// FEC binding (draft-ramadan-moq-fec-catalog-00) is `validateFecCatalog`'s:\n\t\t// it fails closed per instance and never stops another track, so nothing\n\t\t// here throws on `fec` or on a repair or symbol track. A -01 single-object\n\t\t// `fec` therefore costs only that track's FEC (BLO-50171).\n\t\tif (packaging === \"mmtp\") {\n\t\t\t// mmt-01 12.1: MUST be \"mfu\"; a subscriber rejects anything else.\n\t\t\tif (track[\"mmtpMode\"] !== \"mfu\") {\n\t\t\t\tcatalogError(\"track_mmtp_mode\", `${path}.mmtpMode`, \"must be mfu\");\n\t\t\t}\n\t\t\tif (!positiveInteger(timescale)) {\n\t\t\t\tcatalogError(\"track_timescale\", `${path}.timescale`, \"must be a positive integer\");\n\t\t\t}\n\t\t\tif (groupDurationMs === undefined && groupDurationTicks === undefined) {\n\t\t\t\tcatalogError(\n\t\t\t\t\t\"track_group_duration_ms\",\n\t\t\t\t\tpath,\n\t\t\t\t\t\"groupDurationMs or groupDurationTicks must be a positive integer\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (groupDurationTicks === undefined &&\n\t\t\t\t(BigInt(groupDurationMs as number) * BigInt(timescale)) % 1000n !== 0n) {\n\t\t\t\tcatalogError(\"track_group_duration_exact\", `${path}.groupDurationMs`, \"does not resolve to an exact integer tick count\");\n\t\t\t}\n\t\t}\n\t}\n\n\t// mmt-01 \u00A74.4: alternates share group boundaries so a subscriber can switch\n\t// at any of them. An intra-only member (one frame per group, e.g. the demo's\n\t// forceIOnly `video/base`) has a boundary at every frame, so it switches\n\t// cleanly with any GOP peer whose group duration is a whole number of its\n\t// groups. Such a member is checked for divisibility against the first GOP\n\t// member; every GOP member must still match that anchor exactly (BLO-49202).\n\ttype SwitchingMember = { index: number; name: string; ticks: bigint; timescale: bigint; intraOnly: boolean };\n\tconst switchingSets = new Map<number, SwitchingMember[]>();\n\tfor (let index = 0; index < trackRecords.length; index++) {\n\t\tconst track = trackRecords[index]!;\n\t\tif (track[\"packaging\"] !== \"mmtp\") continue;\n\t\tconst altGroup = track[\"altGroup\"];\n\t\tif (!Number.isSafeInteger(altGroup)) continue;\n\t\tconst timescale = BigInt(track[\"timescale\"] as number);\n\t\tconst durationTicks = track[\"groupDurationTicks\"];\n\t\tconst ticks = durationTicks !== undefined\n\t\t\t? BigInt(durationTicks as number)\n\t\t\t: BigInt(track[\"groupDurationMs\"] as number) * timescale / 1000n;\n\t\tconst framerate = track[\"framerate\"];\n\t\t// Group duration == one frame period. `framerate` is a float (29.97), so\n\t\t// compare within 0.1%; a GOP track is off by a factor of >= 2.\n\t\tconst intraOnly = positiveNumber(framerate) &&\n\t\t\tMath.abs(Number(ticks) * framerate - Number(timescale)) <= Number(timescale) * 1e-3;\n\t\tconst members = switchingSets.get(altGroup as number) ?? [];\n\t\tmembers.push({ index, name: track[\"name\"] as string, ticks, timescale, intraOnly });\n\t\tswitchingSets.set(altGroup as number, members);\n\t}\n\tfor (const [altGroup, members] of switchingSets) {\n\t\tconst anchor = members.find((member) => !member.intraOnly) ?? members[0]!;\n\t\tfor (const member of members) {\n\t\t\tif (member === anchor || member.ticks * anchor.timescale === anchor.ticks * member.timescale) continue;\n\t\t\tconst nests = member.intraOnly && !anchor.intraOnly &&\n\t\t\t\t(anchor.ticks * member.timescale) % (member.ticks * anchor.timescale) === 0n;\n\t\t\tif (!nests) {\n\t\t\t\tcatalogError(\"track_switching_set\", `$.tracks[${member.index}]`, `group duration does not match ${JSON.stringify(anchor.name)} in altGroup ${altGroup}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst multicast = catalog[\"multicast\"];\n\tif (typeof multicast === \"object\" && multicast !== null && !Array.isArray(multicast)) {\n\t\tvalidateNetworkSourceRelays((multicast as Record<string, unknown>)[\"networkSource\"], \"$.multicast.networkSource\");\n\t\tconst endpoints = (multicast as Record<string, unknown>)[\"endpoints\"];\n\t\tif (Array.isArray(endpoints)) {\n\t\t\tfor (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {\n\t\t\t\tconst endpoint = endpoints[endpointIndex];\n\t\t\t\tif (typeof endpoint !== \"object\" || endpoint === null || Array.isArray(endpoint)) continue;\n\t\t\t\tvalidateNetworkSourceRelays(\n\t\t\t\t\t(endpoint as Record<string, unknown>)[\"networkSource\"],\n\t\t\t\t\t`$.multicast.endpoints[${endpointIndex}].networkSource`,\n\t\t\t\t);\n\t\t\t\t// The endpoint relay moved to `multicast.networkSource[{type:\"amt\",relay}]`.\n\t\t\t\t// Refuse rather than ignore: a dropped relay strands an AMT endpoint\n\t\t\t\t// with no tunnel and reads as a gateway failure, not a bad catalog.\n\t\t\t\tif (\"relayAddress\" in endpoint) {\n\t\t\t\t\tcatalogError(\n\t\t\t\t\t\t\"multicast_relay_address\",\n\t\t\t\t\t\t`$.multicast.endpoints[${endpointIndex}].relayAddress`,\n\t\t\t\t\t\t'relayAddress is retired; publish the relay as multicast.networkSource[{type:\"amt\",relay}]',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst refs = (endpoint as Record<string, unknown>)[\"tracks\"];\n\t\t\t\tif (!Array.isArray(refs)) continue;\n\t\t\t\tconst packetIds = new Map<number, string>();\n\t\t\t\tfor (let refIndex = 0; refIndex < refs.length; refIndex++) {\n\t\t\t\t\tconst ref = refs[refIndex];\n\t\t\t\t\tif (typeof ref !== \"object\" || ref === null || Array.isArray(ref)) continue;\n\t\t\t\t\tconst trackRef = ref as Record<string, unknown>;\n\t\t\t\t\tconst refPath = `$.multicast.endpoints[${endpointIndex}].tracks[${refIndex}]`;\n\t\t\t\t\tconst name = trackRef[\"name\"];\n\t\t\t\t\tif (typeof name !== \"string\" || !trackRecords.some((track) => track[\"name\"] === name)) {\n\t\t\t\t\t\tcatalogError(\"multicast_track\", `${refPath}.name`, `unknown catalog track ${JSON.stringify(name)}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst packetId = trackRef[\"packetId\"];\n\t\t\t\t\tif (packetId === MMTP_SIGNALING_PACKET_ID) {\n\t\t\t\t\t\tcatalogError(\n\t\t\t\t\t\t\t\"multicast_packet_id\",\n\t\t\t\t\t\t\t`${refPath}.packetId`,\n\t\t\t\t\t\t\t`packetId ${packetId} is the MMTP signaling flow and must not be advertised for a track; packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (!Number.isSafeInteger(packetId) || (packetId as number) < 1 || (packetId as number) > MMTP_PACKET_ID_MAX) {\n\t\t\t\t\t\tcatalogError(\"multicast_packet_id\", `${refPath}.packetId`, `packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst previous = packetIds.get(packetId as number);\n\t\t\t\t\tif (previous !== undefined) {\n\t\t\t\t\t\tcatalogError(\"multicast_packet_id\", `${refPath}.packetId`, `packetId is already assigned to ${JSON.stringify(previous)}`);\n\t\t\t\t\t}\n\t\t\t\t\tpacketIds.set(packetId as number, name as string);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// draft -02 4.1 / 7.2.6 (protocol and S, address literals, auth\n\t// all-or-nothing, the key set, one endpoint per (S, G, port),\n\t// foreign-namespace tracks) are never a catalog fault here: a receiver\n\t// ignores the endpoint an issue names, and a catalog-level issue makes the\n\t// multicast object yield no data (`admittedMulticastEndpoints`), while MoQ\n\t// unicast stays usable. `multicastCatalogIssues` reports them.\n\n\tvalidateArchive(catalog);\n\n\treturn catalog as CmsfCatalog;\n}\n\n/**\n * Resolve a track's group duration to integer ticks using the supplied\n * timescale (Hz). Implements mmt-01 \u00A74.4.2:\n *\n * - If `groupDurationTicks` is present, return it (override wins).\n * - Else compute `groupDurationMs * timescale / 1000`. The result MUST\n * be an exact integer; throws if `(ms * timescale) % 1000 != 0`.\n *\n * Throws on invalid timing config or non-positive timescale (CLAUDE.md:\n * error on bad init).\n */\nexport function resolveGroupDurationTicks(\n\ttiming: TrackTimingConfig,\n\ttimescale: number,\n): number {\n\tif (!Number.isFinite(timescale) || !Number.isInteger(timescale) || timescale <= 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: timescale must be a positive integer Hz, got ${timescale}`,\n\t\t);\n\t}\n\tif (!isTrackTimingConfig(timing)) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: invalid TrackTimingConfig: ${JSON.stringify(timing)}`,\n\t\t);\n\t}\n\tif (timing.groupDurationTicks !== undefined) {\n\t\treturn timing.groupDurationTicks;\n\t}\n\tif (timing.groupDurationMs === undefined) {\n\t\tthrow new Error(\n\t\t\t\"[carp-catalog] resolveGroupDurationTicks: groupDurationMs is required when groupDurationTicks is absent\",\n\t\t);\n\t}\n\tconst product = timing.groupDurationMs * timescale;\n\tif (product % 1000 !== 0) {\n\t\tthrow new Error(\n\t\t\t`[carp-catalog] resolveGroupDurationTicks: groupDurationMs=${timing.groupDurationMs} at timescale=${timescale} ` +\n\t\t\t\t`produces non-integer ticks (${product / 1000}); publisher must use groupDurationTicks override (mmt-01 \u00A74.4.2)`,\n\t\t);\n\t}\n\treturn product / 1000;\n}\n\n/**\n * Validate that every track in a switching set agrees on the same\n * wall-clock group duration (mmt-01 \u00A74.4.2). Per-track timescales may\n * differ, so raw tick counts cannot be compared directly.\n *\n * Returns `{ ok: true }` when all tracks agree (or the set is empty);\n * `{ ok: false, mismatches }` listing each track whose effective ticks\n * differ from the first track's value. Subscribers per spec SHOULD\n * reject the catalog on mismatch.\n *\n * Throws if any track's timing config is invalid (per\n * `resolveGroupDurationTicks`).\n */\nexport function validateSwitchingSetTiming(\n\ttracks: Array<{ name: string; timing: TrackTimingConfig; timescale: number }>,\n):\n\t| { ok: true }\n\t| { ok: false; expectedTicks: number; mismatches: Array<{ name: string; ticks: number }> } {\n\tif (tracks.length === 0) return { ok: true };\n\tconst ticksByTrack = tracks.map((t) => ({\n\t\tname: t.name,\n\t\tticks: resolveGroupDurationTicks(t.timing, t.timescale),\n\t\ttimescale: t.timescale,\n\t}));\n\tconst expectedTicks = ticksByTrack[0].ticks;\n\tconst expectedTimescale = ticksByTrack[0].timescale;\n\tconst mismatches = ticksByTrack\n\t\t.slice(1)\n\t\t.filter(\n\t\t\t(t) =>\n\t\t\t\tBigInt(t.ticks) * BigInt(expectedTimescale) !==\n\t\t\t\tBigInt(expectedTicks) * BigInt(t.timescale),\n\t\t)\n\t\t.map(({ name, ticks }) => ({ name, ticks }));\n\tif (mismatches.length === 0) return { ok: true };\n\treturn { ok: false, expectedTicks, mismatches };\n}\n", "// Resolved repair layer and overlay shapes (draft-ramadan-moq-fec-catalog-00,\n// fec-core-00): the output of `resolveRepairLayers`. Every base layer comes\n// from one instance's partition and differs only in P_i; layer i owns RS_IDs\n// [O_i, O_i + P_i) with O_i = sum(P_j, j < i), and ESI = K + RS_ID. A keyframe\n// overlay is a second instance with its own K, P and RS_ID space.\n//\n// Binding (which repair tracks belong to which instance) is\n// `resolveFecBindings` in mmt-base; nothing here reads a catalog.\n\n/** Lowest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MIN = 192;\n/** Highest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MAX = 255;\n/** Priority of a base layer 0 track that declares none. */\nexport const DEFAULT_REPAIR_PRIORITY = 240;\n/** RFC 6330 \u00A74.4.1.2: the largest number of source symbols in one RaptorQ source block. */\nexport const MAX_RAPTORQ_SOURCE_SYMBOLS = 56403;\n/** The 24-bit RS_ID field (\u00A77.1) bounds the repair total of one FEC instance. */\nexport const MAX_REPAIR_SYMBOLS = 0xffffff;\n/** The 24-bit ESI bounds K + totalP - 1 for one FEC instance. */\nexport const MAX_ENCODING_SYMBOL_ID = 0xffffff;\n/** Reed-Solomon block bound: K + P <= 255 (draft-ramadan-moq-fec-01 4.5, 6.3.4 rule 12). */\nexport const MAX_REED_SOLOMON_BLOCK_SYMBOLS = 255;\n\n/** A resolved base repair layer with its derived RS_ID range. */\nexport type RepairLayerParams = {\n\t/** Layer index; 0 is the track named by the source's `fec.repairTrack`. */\n\tlayer: number;\n\trepairTrackName: string;\n\t/** P_i: repair symbols per block carried by this layer. */\n\trepairSymbols: number;\n\t/** O_i = sum(P_j for j < i): the first RS_ID this layer carries. ESI = K + RS_ID. */\n\trsIdOffset: number;\n\t/** MoQT-scale priority (lower first); 240 when layer 0 declares none. */\n\tpriority: number;\n};\n\n/** A resolved keyframe overlay: a separate FEC instance over RAP fragments only. */\nexport type RepairOverlayParams = {\n\trepairTrackName: string;\n\tscope: \"keyframe\";\n\t/** K_overlay: source symbols of the overlay instance. */\n\tsourceSymbols: number;\n\t/** P_overlay: repair symbols per overlay block; also the overlay's own RSB_length. */\n\trepairSymbols: number;\n\t/** MoQT-scale priority, independent of the base layer chain. */\n\tpriority: number;\n};\n\nexport type RepairLayersResolution = {\n\tsourceTrackName: string;\n\t/** Base layers in layer order, layer 0 first. Never empty. */\n\tbaseLayers: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances, in catalog order. Empty when none is declared. */\n\t/** At most one entry: the wire carries one overlay trailer per RAP packet. */\n\toverlays: readonly RepairOverlayParams[];\n\t/** Sum of P over base layers: the wire RSB_length on every base layer packet. */\n\ttotalBaseRepairSymbols: number;\n};\n\n"],
5
+ "mappings": "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",
6
6
  "names": ["rendition"]
7
7
  }