@blockcast/mmt-container 2.1.0 → 2.2.0-main.02321582f1ca

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1302 -249
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +65 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +33 -1
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +213 -6
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +5 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +9 -0
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +17 -0
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +37 -2
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +98 -28
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +222 -45
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +97 -28
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +25 -2
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +1749 -56
  56. package/dist/mmtp-router.js.map +4 -4
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.d.ts +15 -2
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +15683 -14941
  62. package/dist/multicast-manager.js.map +4 -4
  63. package/dist/multicast-transport.d.ts +11 -3
  64. package/dist/multicast-transport.d.ts.map +1 -1
  65. package/dist/multicast-transport.js +281 -35
  66. package/dist/multicast-transport.js.map +4 -4
  67. package/dist/multicast-unwrap.js +12 -19
  68. package/dist/multicast-unwrap.js.map +2 -2
  69. package/dist/no-modules.js +215 -42
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.js.map +1 -1
  72. package/dist/repair-layer-subscriptions.d.ts +82 -0
  73. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  74. package/dist/repair-layer-subscriptions.js +192 -0
  75. package/dist/repair-layer-subscriptions.js.map +7 -0
  76. package/dist/signaling.js.map +2 -2
  77. package/dist/timestamp-codec.js +12 -19
  78. package/dist/timestamp-codec.js.map +2 -2
  79. package/dist/track-session.d.ts +1 -1
  80. package/dist/track-session.js.map +1 -1
  81. package/dist/wasm-mfu-reassembler.js.map +1 -1
  82. package/dist/wasm-mmtp-container-parser.js +97 -28
  83. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  84. package/package.json +8 -5
  85. package/schemas/catalog.schema.json +4 -0
  86. package/src/catalog-fec-params.ts +36 -1
  87. package/src/cmcd-producer.ts +194 -179
  88. package/src/fec-manager.ts +256 -6
  89. package/src/fec-worker-setup.ts +2 -2
  90. package/src/index.ts +18 -2
  91. package/src/iso-block-locator.ts +19 -0
  92. package/src/iso-bmff-timing.ts +40 -1
  93. package/src/mmtp-container-parser.ts +96 -8
  94. package/src/mmtp-fec-session.ts +115 -15
  95. package/src/mmtp-router.ts +414 -29
  96. package/src/multicast-manager.ts +26 -5
  97. package/src/multicast-transport.ts +44 -20
  98. package/src/repair-layer-subscriptions.ts +261 -0
  99. package/src/track-session.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/ip-address.ts", "../../mmt-base/src/multicast-catalog.ts", "../../mmt-base/src/fec-bindings.ts", "../src/multicast-input.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", "/**\n * IP address literals as draft-ramadan-moq-multicast-02 7.2.6 requires them of\n * `sourceAddress`, `groupAddress` and `senderAddress`: the IPv4address or\n * IPv6address form of RFC 3986 3.2.2. Dotted decimal with no leading zero in\n * any dec-octet (the ls32 tail of an IPv6 literal included); IPv6 h16 groups\n * may carry leading zeros in either hex case; no brackets, no zone identifier,\n * never a name. Draft: Blockcast/moqcast-draft@5f49172.\n *\n * Addresses compare as IP addresses, never as strings, and an address of one\n * family never matches one of the other: an IPv4-mapped IPv6 address is of the\n * IPv6 family, so it parses to 16 octets and never equals its IPv4 address.\n */\n\nconst DEC_OCTET = /^(?:0|[1-9]\\d{0,2})$/;\nconst H16 = /^[0-9A-Fa-f]{1,4}$/;\n\nfunction parseIpv4(text: string): number[] | undefined {\n\tconst fields = text.split(\".\");\n\tif (fields.length !== 4) return undefined;\n\tconst octets: number[] = [];\n\tfor (const field of fields) {\n\t\tif (!DEC_OCTET.test(field)) return undefined;\n\t\tconst value = Number(field);\n\t\tif (value > 255) return undefined;\n\t\toctets.push(value);\n\t}\n\treturn octets;\n}\n\nfunction parseIpv6(text: string): Uint8Array | undefined {\n\tconst halves = text.split(\"::\");\n\tif (halves.length > 2) return undefined;\n\tconst groupsOf = (part: string) => (part === \"\" ? [] : part.split(\":\"));\n\tconst left = groupsOf(halves[0]!);\n\tconst right = halves.length === 2 ? groupsOf(halves[1]!) : [];\n\tconst words: number[][] = [];\n\tconst all = [...left, ...right];\n\t// The ls32 dotted tail ends the literal: the last group, never before \"::\".\n\tconst last = halves.length === 2 && right.length === 0 ? -1 : all.length - 1;\n\tfor (let index = 0; index < all.length; index++) {\n\t\tconst group = all[index]!;\n\t\tif (index === last && group.includes(\".\")) {\n\t\t\tconst v4 = parseIpv4(group);\n\t\t\tif (!v4) return undefined;\n\t\t\twords.push([(v4[0]! << 8) | v4[1]!, (v4[2]! << 8) | v4[3]!]);\n\t\t} else if (H16.test(group)) {\n\t\t\twords.push([Number.parseInt(group, 16)]);\n\t\t} else {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tconst leftWords = words.slice(0, left.length).flat();\n\tconst rightWords = words.slice(left.length).flat();\n\tconst missing = 8 - leftWords.length - rightWords.length;\n\tif (halves.length === 2 ? missing < 1 : missing !== 0) return undefined;\n\tconst out = new Uint8Array(16);\n\t[...leftWords, ...new Array<number>(missing).fill(0), ...rightWords].forEach((word, index) => {\n\t\tout[index * 2] = word >> 8;\n\t\tout[index * 2 + 1] = word & 0xff;\n\t});\n\treturn out;\n}\n\n/** The octets of an RFC 3986 IPv4address (4) or IPv6address (16), or `undefined`. */\nexport function parseIpAddressLiteral(text: unknown): Uint8Array | undefined {\n\tif (typeof text !== \"string\") return undefined;\n\tif (text.includes(\":\")) return parseIpv6(text);\n\tconst v4 = parseIpv4(text);\n\treturn v4 ? Uint8Array.from(v4) : undefined;\n}\n\n/** Whether two address literals name one address of one family. Unparseable never matches. */\nexport function sameIpAddress(a: unknown, b: unknown): boolean {\n\tconst x = parseIpAddressLiteral(a);\n\tconst y = parseIpAddressLiteral(b);\n\treturn x !== undefined && y !== undefined && x.length === y.length && x.every((octet, index) => octet === y[index]);\n}\n\n/**\n * One spelling per address: dotted decimal for IPv4, RFC 5952 for IPv6\n * (lowercase, no leading zeros, the longest run of two or more zero groups,\n * leftmost on a tie, as \"::\"; an IPv4-mapped address in hex like any other).\n * `undefined` for text that is not an address literal.\n */\nexport function canonicalIpAddress(text: unknown): string | undefined {\n\tconst bytes = parseIpAddressLiteral(text);\n\tif (!bytes) return undefined;\n\tif (bytes.length === 4) return bytes.join(\".\");\n\tconst words = Array.from({ length: 8 }, (_, index) => (bytes[index * 2]! << 8) | bytes[index * 2 + 1]!);\n\tlet bestStart = -1;\n\tlet bestLength = 1;\n\tfor (let start = 0; start < 8; ) {\n\t\tlet end = start;\n\t\twhile (end < 8 && words[end] === 0) end++;\n\t\tif (end - start > bestLength) {\n\t\t\tbestStart = start;\n\t\t\tbestLength = end - start;\n\t\t}\n\t\tstart = end === start ? start + 1 : end;\n\t}\n\tconst hex = words.map(word => word.toString(16));\n\tif (bestStart < 0) return hex.join(\":\");\n\treturn `${hex.slice(0, bestStart).join(\":\")}::${hex.slice(bestStart + bestLength).join(\":\")}`;\n}\n", "/**\n * The draft-ramadan-moq-multicast-02 endpoint and `auth` rules, written once\n * for every receiver of the catalog: the receivers in `@blockcast/mmt-transport`\n * and the container resolver ignore every endpoint an issue names (all of\n * them when the issue is catalog-level, which leaves the broadcast no data).\n * `validateCmsfMmtpCatalog` reports none of them as a catalog fault, so MoQ\n * unicast stays usable. It lives here because mmt-base is the only package\n * every receiver depends on.\n *\n * Draft: Blockcast/moqcast-draft@5f49172 (draft-ramadan-moq-multicast-02 as\n * posted 2026-11-02), sections 4.1 and 7.2.6.\n */\n\nimport { parseIpAddressLiteral } from \"./ip-address.js\";\n\nexport type MulticastJoinMode = \"ssm\" | \"asm\";\n\n/** An endpoint's join mode and `S` (4.1): `sourceAddress` on SSM,\n * `auth.senderAddress` on protected ASM, absent on unprotected ASM. */\nexport interface MulticastEndpointSource {\n\tmode: MulticastJoinMode;\n\tsource?: string;\n}\n\nexport type MulticastCatalogIssueCode =\n\t| \"multicast_auth\"\n\t| \"multicast_endpoint\"\n\t| \"multicast_endpoint_auth\"\n\t| \"multicast_track\";\n\nexport interface MulticastCatalogIssue {\n\tcode: MulticastCatalogIssueCode;\n\t/** JSONPath of the offending field. */\n\tpath: string;\n\tmessage: string;\n\t/** Endpoint the issue makes a receiver ignore; absent for a catalog-level\n\t * issue, which leaves the receiver no endpoint at all (7.2.6). */\n\tendpointIndex?: number;\n}\n\n/** Canonical unpadded base64url of exactly 32 octets. */\nexport const ED25519_PUBLIC_KEY_BASE64URL = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;\n\nconst UINT32_MAX = 0xffff_ffff;\nconst AUTH_MEMBERS = new Set([\"scheme\", \"version\", \"sig\", \"keys\", \"minEpoch\"]);\nconst KEY_MEMBERS = new Set([\"kid\", \"publicKey\", \"firstEpoch\"]);\n// Draft -01's separate record port is no member here: a leftover one is an\n// unknown member, so the endpoint is ignored (libmmt policy, fails closed).\nconst ENDPOINT_AUTH_MEMBERS = new Set([\n\t\"streamId\",\n\t\"senderAddress\",\n\t\"maxAuthDelayMs\",\n\t\"manifestTimerMs\",\n\t\"manifestTrack\",\n]);\n\ntype JsonObject = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is JsonObject {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isIntegerIn(value: unknown, min: number, max: number): value is number {\n\treturn Number.isSafeInteger(value) && (value as number) >= min && (value as number) <= max;\n}\n\n/** Defaults of an endpoint's `auth.maxAuthDelayMs` and `auth.manifestTimerMs` (7.2.6). */\nexport const BCP_DEFAULT_MAX_AUTH_DELAY_MS = 200;\nexport const BCP_DEFAULT_MANIFEST_TIMER_MS = 20;\n\n/** The timing and mirror members of an endpoint's `auth` as a receiver uses them. */\nexport interface EndpointAuthTiming {\n\tmaxAuthDelayMs: number;\n\tmanifestTimerMs: number;\n\tmanifestTrack?: string;\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value);\n}\n\n/**\n * 7.2.6: a `maxAuthDelayMs` outside 50..1000 is clamped to the nearer bound;\n * one that is not a JSON number, or an in-range one that is not an integer,\n * is absent (200). A `manifestTimerMs` that is not a JSON number, or an\n * integer-free value of at least 1, is absent (20); one below 1 makes the\n * endpoint ignored, so it is returned here as is for the caller to refuse. A\n * `manifestTrack` that is not a string is absent.\n */\nexport function endpointAuthTiming(auth: unknown): EndpointAuthTiming {\n\tconst record = isObject(auth) ? auth : {};\n\tconst delay = record[\"maxAuthDelayMs\"];\n\tconst timer = record[\"manifestTimerMs\"];\n\tconst track = record[\"manifestTrack\"];\n\tconst maxAuthDelayMs = !isFiniteNumber(delay)\n\t\t? BCP_DEFAULT_MAX_AUTH_DELAY_MS\n\t\t: delay < 50 ? 50 : delay > 1000 ? 1000 : Number.isInteger(delay) ? delay : BCP_DEFAULT_MAX_AUTH_DELAY_MS;\n\tconst manifestTimerMs = !isFiniteNumber(timer)\n\t\t? BCP_DEFAULT_MANIFEST_TIMER_MS\n\t\t: timer < 1 || Number.isSafeInteger(timer) ? timer : BCP_DEFAULT_MANIFEST_TIMER_MS;\n\treturn {\n\t\tmaxAuthDelayMs,\n\t\tmanifestTimerMs,\n\t\t...(typeof track === \"string\" && track.length > 0 ? { manifestTrack: track } : {}),\n\t};\n}\n\n/**\n * Join mode and `S` of one endpoint, or `undefined` when its fields disagree\n * and a receiver must ignore it (4.1). `sourceAddress` alone selects the mode;\n * `protocol` may only restate it, so \"amt\" and \"atsc3\" (draft -00) are\n * inconsistent here: a bearer is a `networkSource`, never a protocol.\n */\nexport function multicastEndpointSource(endpoint: unknown): MulticastEndpointSource | undefined {\n\tif (!isObject(endpoint)) return undefined;\n\tconst sourceAddress = endpoint[\"sourceAddress\"];\n\tif (sourceAddress !== undefined && !parseIpAddressLiteral(sourceAddress)) return undefined;\n\tconst mode: MulticastJoinMode = sourceAddress === undefined ? \"asm\" : \"ssm\";\n\tconst protocol = endpoint[\"protocol\"];\n\tif (protocol !== undefined && protocol !== mode) return undefined;\n\tconst auth = endpoint[\"auth\"];\n\tconst senderAddress = isObject(auth) ? auth[\"senderAddress\"] : undefined;\n\tif (mode === \"ssm\") return senderAddress === undefined ? { mode, source: sourceAddress as string } : undefined;\n\tif (auth === undefined) return { mode };\n\treturn parseIpAddressLiteral(senderAddress) ? { mode, source: senderAddress as string } : undefined;\n}\n\nfunction authIssues(auth: unknown): MulticastCatalogIssue[] {\n\tconst base = \"$.multicast.auth\";\n\tconst issue = (path: string, message: string): MulticastCatalogIssue[] => [\n\t\t{ code: \"multicast_auth\", path: `${base}${path}`, message },\n\t];\n\tif (!isObject(auth)) return issue(\"\", \"auth must be an object\");\n\tconst unknown = Object.keys(auth).filter(key => !AUTH_MEMBERS.has(key));\n\tif (unknown.length > 0) {\n\t\treturn issue(\"\", `auth has unknown member(s) ${unknown.join(\", \")}; draft -02 7.2.6 defines scheme, version, sig, keys and minEpoch`);\n\t}\n\tif (auth[\"scheme\"] !== \"bc-provenance\") return issue(\".scheme\", 'scheme must be \"bc-provenance\"');\n\tif (auth[\"version\"] !== 2) return issue(\".version\", \"version must be 2 (bc-provenance version 2, draft -02 7.2)\");\n\tif (auth[\"sig\"] !== \"ed25519\") return issue(\".sig\", 'sig must be \"ed25519\"');\n\tconst keys = auth[\"keys\"];\n\tif (!Array.isArray(keys) || keys.length < 1 || keys.length > 4) {\n\t\treturn issue(\".keys\", \"keys must be an array of one to four {kid, publicKey, firstEpoch} objects\");\n\t}\n\tconst kids = new Set<number>();\n\tconst firstEpochs = new Set<number>();\n\tfor (let index = 0; index < keys.length; index++) {\n\t\tconst key = keys[index];\n\t\tconst at = `.keys[${index}]`;\n\t\tif (!isObject(key) || Object.keys(key).some(member => !KEY_MEMBERS.has(member))) {\n\t\t\treturn issue(at, \"a key is exactly {kid, publicKey, firstEpoch}\");\n\t\t}\n\t\tif (!isIntegerIn(key[\"kid\"], 0, UINT32_MAX)) return issue(`${at}.kid`, \"kid must be an unsigned 32-bit integer\");\n\t\tif (typeof key[\"publicKey\"] !== \"string\" || !ED25519_PUBLIC_KEY_BASE64URL.test(key[\"publicKey\"])) {\n\t\t\treturn issue(`${at}.publicKey`, \"publicKey must be a 32-octet Ed25519 key in canonical unpadded base64url\");\n\t\t}\n\t\tif (!isIntegerIn(key[\"firstEpoch\"], 0, UINT32_MAX)) {\n\t\t\treturn issue(`${at}.firstEpoch`, \"firstEpoch must be an unsigned 32-bit integer\");\n\t\t}\n\t\t// A repeated kid or firstEpoch leaves no key set (7.2.6): fail the whole\n\t\t// block, never fall back to unauthenticated reception.\n\t\tif (kids.has(key[\"kid\"])) return issue(`${at}.kid`, `kid ${key[\"kid\"]} is repeated; the catalog holds no key set`);\n\t\tif (firstEpochs.has(key[\"firstEpoch\"])) {\n\t\t\treturn issue(`${at}.firstEpoch`, `firstEpoch ${key[\"firstEpoch\"]} is repeated; the catalog holds no key set`);\n\t\t}\n\t\tkids.add(key[\"kid\"]);\n\t\tfirstEpochs.add(key[\"firstEpoch\"]);\n\t}\n\tif (!isIntegerIn(auth[\"minEpoch\"], 0, UINT32_MAX)) {\n\t\treturn issue(\".minEpoch\", \"minEpoch must be an unsigned 32-bit integer\");\n\t}\n\treturn [];\n}\n\nfunction endpointAuthIssue(\n\tendpoint: JsonObject,\n\tmode: MulticastJoinMode,\n\tat: string,\n): { path: string; message: string } | undefined {\n\tconst auth = endpoint[\"auth\"];\n\tif (!isObject(auth)) return { path: `${at}.auth`, message: \"auth must be an object\" };\n\tconst unknown = Object.keys(auth).filter(key => !ENDPOINT_AUTH_MEMBERS.has(key));\n\tif (unknown.length > 0) return { path: `${at}.auth`, message: `auth has unknown member(s) ${unknown.join(\", \")}` };\n\tif (!isIntegerIn(auth[\"streamId\"], 0, UINT32_MAX)) {\n\t\treturn { path: `${at}.auth.streamId`, message: \"streamId must be an unsigned 32-bit integer\" };\n\t}\n\tif (mode === \"ssm\" && auth[\"senderAddress\"] !== undefined) {\n\t\treturn { path: `${at}.auth.senderAddress`, message: \"senderAddress must not appear on an SSM endpoint; S is sourceAddress\" };\n\t}\n\tif (mode === \"asm\" && !parseIpAddressLiteral(auth[\"senderAddress\"])) {\n\t\treturn { path: `${at}.auth.senderAddress`, message: \"a protected ASM endpoint requires senderAddress, an RFC 3986 IP address literal\" };\n\t}\n\t// maxAuthDelayMs never ignores the endpoint (clamped or absent) and\n\t// manifestTrack is absent when it is not a string: endpointAuthTiming.\n\tif (endpointAuthTiming(auth).manifestTimerMs < 1) {\n\t\treturn { path: `${at}.auth.manifestTimerMs`, message: \"manifestTimerMs must be at least 1\" };\n\t}\n\tif (!isIntegerIn(endpoint[\"bandwidth\"], 1, Number.MAX_SAFE_INTEGER)) {\n\t\treturn { path: `${at}.bandwidth`, message: \"a protected endpoint requires bandwidth, which bounds the receiver's hold queues\" };\n\t}\n\treturn undefined;\n}\n\n/**\n * Where a catalog object carries the namespace of the catalog track it was\n * read from. A symbol, so it is never wire data and survives object spread;\n * `Symbol.for`, so two bundled copies of this module agree on it.\n */\nexport const CATALOG_NAMESPACE: unique symbol = Symbol.for(\"@blockcast/mmt-base/catalogNamespace\");\n\n/**\n * `catalog` tagged with the namespace of its catalog track. Every receiver\n * rule that takes the catalog (`multicastCatalogIssues`,\n * `admittedMulticastEndpoints` and the transport selectors built on them)\n * then judges the foreign-namespace rule against it, independent of track\n * order. Tag once, where the catalog track is read.\n */\nexport function withCatalogNamespace<T extends object>(catalog: T, namespace: string): T {\n\treturn { ...catalog, [CATALOG_NAMESPACE]: namespace };\n}\n\n/**\n * Every draft -02 4.1 / 7.2.6 violation in `catalog.multicast`, in document\n * order. Packet-id, group-address and track-existence rules are not here; the\n * container validator and the receivers already check those.\n *\n * `catalogNamespace` is the namespace of the catalog track, from `options` or else the\n * catalog's `withCatalogNamespace` tag. Without it the\n * foreign-namespace rule can only compare listed tracks with each other: the\n * first listed track's namespace becomes the reference, so which side of a\n * disagreement is dropped depends on order. It still fails closed; pass\n * `catalogNamespace` wherever the catalog track is known.\n */\nexport function multicastCatalogIssues(\n\tcatalog: unknown,\n\toptions: { catalogNamespace?: string } = {},\n): MulticastCatalogIssue[] {\n\tif (!isObject(catalog)) return [];\n\tconst issues: MulticastCatalogIssue[] = [];\n\tif (\"multicast.auth\" in catalog) {\n\t\t// Fail closed: a draft -00 publisher meant the flow to be protected, and\n\t\t// the key it names verifies nothing in bc-provenance version 2.\n\t\tissues.push({\n\t\t\tcode: \"multicast_auth\",\n\t\t\tpath: '$[\"multicast.auth\"]',\n\t\t\tmessage: 'the root \"multicast.auth\" key is retired; declare auth as multicast.auth (draft -02 7.2.6)',\n\t\t});\n\t}\n\tconst multicast = catalog[\"multicast\"];\n\tif (!isObject(multicast)) return issues;\n\tconst protectedCatalog = multicast[\"auth\"] !== undefined;\n\tif (protectedCatalog) issues.push(...authIssues(multicast[\"auth\"]));\n\tconst endpoints = multicast[\"endpoints\"];\n\tif (!Array.isArray(endpoints)) return issues;\n\n\tconst tracks = Array.isArray(catalog[\"tracks\"]) ? catalog[\"tracks\"] : [];\n\tconst namespaceOf = new Map<string, unknown>();\n\tfor (const track of tracks) {\n\t\tif (isObject(track) && typeof track[\"name\"] === \"string\") namespaceOf.set(track[\"name\"], track[\"namespace\"]);\n\t}\n\tconst tagged = (catalog as { [CATALOG_NAMESPACE]?: unknown })[CATALOG_NAMESPACE];\n\tlet referenceNamespace = options.catalogNamespace ?? (typeof tagged === \"string\" ? tagged : undefined);\n\n\tconst sources: Array<MulticastEndpointSource | undefined> = [];\n\tfor (let index = 0; index < endpoints.length; index++) {\n\t\tconst endpoint = endpoints[index];\n\t\tconst at = `$.multicast.endpoints[${index}]`;\n\t\tconst fail = (code: MulticastCatalogIssueCode, path: string, message: string) => {\n\t\t\tissues.push({ code, path, message, endpointIndex: index });\n\t\t};\n\t\tsources.push(undefined);\n\t\tif (!isObject(endpoint)) {\n\t\t\tfail(\"multicast_endpoint\", at, \"endpoint must be an object\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst protocol = endpoint[\"protocol\"];\n\t\tif (protocol !== undefined && protocol !== \"ssm\" && protocol !== \"asm\") {\n\t\t\tfail(\"multicast_endpoint\", `${at}.protocol`, `protocol must be \"ssm\" or \"asm\"; a bearer such as ${JSON.stringify(protocol)} is a networkSource entry (draft -02 4.2)`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst hasAuth = endpoint[\"auth\"] !== undefined;\n\t\tif (protectedCatalog !== hasAuth) {\n\t\t\tfail(\n\t\t\t\t\"multicast_endpoint_auth\",\n\t\t\t\t`${at}.auth`,\n\t\t\t\tprotectedCatalog\n\t\t\t\t\t? \"every endpoint requires auth when multicast carries auth; a receiver ignores this endpoint\"\n\t\t\t\t\t: \"auth names no key without multicast.auth; a receiver ignores this endpoint\",\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\t\t// 7.2.6: addresses are RFC 3986 IP literals, never names; 4.1: S and G\n\t\t// of one family, or the endpoint's fields are mutually inconsistent.\n\t\tconst sourceAddress = endpoint[\"sourceAddress\"];\n\t\tconst sourceBytes = sourceAddress === undefined ? undefined : parseIpAddressLiteral(sourceAddress);\n\t\tif (sourceAddress !== undefined && !sourceBytes) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.sourceAddress`, \"sourceAddress must be an RFC 3986 IPv4address or IPv6address literal\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst groupBytes = parseIpAddressLiteral(endpoint[\"groupAddress\"]);\n\t\tif (!groupBytes) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.groupAddress`, \"groupAddress must be an RFC 3986 IPv4address or IPv6address literal\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (sourceBytes && sourceBytes.length !== groupBytes.length) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.sourceAddress`, \"sourceAddress and groupAddress are of different address families\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode: MulticastJoinMode = sourceAddress === undefined ? \"asm\" : \"ssm\";\n\t\tif (protocol !== undefined && protocol !== mode) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.protocol`, `protocol ${JSON.stringify(protocol)} disagrees with sourceAddress, which selects ${mode}`);\n\t\t\tcontinue;\n\t\t}\n\t\t// The (S, G, port) rule below compares ports with ===, so only an\n\t\t// integer may reach it: \"5001\" and 5001 would otherwise both pass.\n\t\tif (!isIntegerIn(endpoint[\"port\"], 1, 65535)) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.port`, \"port must be an integer from 1 to 65535\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (hasAuth) {\n\t\t\tconst authIssue = endpointAuthIssue(endpoint, mode, at);\n\t\t\tif (authIssue) {\n\t\t\t\tfail(\"multicast_endpoint_auth\", authIssue.path, authIssue.message);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tconst source = multicastEndpointSource(endpoint);\n\t\tconst senderBytes = source?.mode === \"asm\" && source.source !== undefined ? parseIpAddressLiteral(source.source) : undefined;\n\t\tif (senderBytes && senderBytes.length !== groupBytes.length) {\n\t\t\tfail(\"multicast_endpoint_auth\", `${at}.auth.senderAddress`, \"senderAddress and groupAddress are of different address families\");\n\t\t\tcontinue;\n\t\t}\n\t\tsources[index] = source;\n\n\t\tconst refs = Array.isArray(endpoint[\"tracks\"]) ? endpoint[\"tracks\"] : [];\n\t\tfor (let refIndex = 0; refIndex < refs.length; refIndex++) {\n\t\t\tconst ref = refs[refIndex];\n\t\t\tconst name = isObject(ref) ? ref[\"name\"] : ref;\n\t\t\tif (typeof name !== \"string\") continue;\n\t\t\tconst namespace = namespaceOf.get(name);\n\t\t\tif (typeof namespace !== \"string\") continue;\n\t\t\treferenceNamespace ??= namespace;\n\t\t\tif (namespace !== referenceNamespace) {\n\t\t\t\tfail(\"multicast_track\", `${at}.tracks[${refIndex}].name`, `track ${JSON.stringify(name)} declares namespace ${JSON.stringify(namespace)}, not the catalog track's ${JSON.stringify(referenceNamespace)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// One endpoint object per (S, G, port) (4.1), and every protected endpoint\n\t// sharing its (S, G, port) with another is ignored (7.2.6), so both sides\n\t// are named. The key is the parsed addresses, never their spelling: an\n\t// address of one family never matches the other (an IPv4-mapped address\n\t// is IPv6), and \"232.0.0.1\" and \"ff3e::1\" never collide with each other.\n\tconst addressKey = (text: unknown) => {\n\t\tconst bytes = text === undefined ? undefined : parseIpAddressLiteral(text);\n\t\treturn bytes ? `${bytes.length}:${bytes.join(\".\")}` : \"-\";\n\t};\n\tconst tupleOf = (index: number) => {\n\t\tconst endpoint = endpoints[index] as JsonObject;\n\t\treturn `${addressKey(sources[index]!.source)}|${addressKey(endpoint[\"groupAddress\"])}|${endpoint[\"port\"]}`;\n\t};\n\tfor (let index = 0; index < endpoints.length; index++) {\n\t\tif (!sources[index]) continue;\n\t\tconst own = tupleOf(index);\n\t\tfor (let other = 0; other < endpoints.length; other++) {\n\t\t\tif (other === index || !sources[other] || tupleOf(other) !== own) continue;\n\t\t\tissues.push({ code: \"multicast_endpoint\", path: `$.multicast.endpoints[${index}].port`, message: `(S, G, port) is shared with endpoint ${other}; one endpoint object per multicast tuple`, endpointIndex: index });\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn issues;\n}\n\n/**\n * Indices of the endpoints a receiver may use: none when a catalog-level\n * issue leaves it no key set (7.2.6), otherwise every endpoint no issue\n * names. Other receiver checks (group address, packet ids) still apply.\n */\nexport function admittedMulticastEndpoints(\n\tcatalog: unknown,\n\toptions: { catalogNamespace?: string } = {},\n): ReadonlySet<number> {\n\tconst multicast = isObject(catalog) ? catalog[\"multicast\"] : undefined;\n\tconst endpoints = isObject(multicast) && Array.isArray(multicast[\"endpoints\"]) ? multicast[\"endpoints\"] : [];\n\tconst issues = multicastCatalogIssues(catalog, options);\n\tif (issues.some(issue => issue.endpointIndex === undefined)) return new Set();\n\tconst ignored = new Set(issues.map(issue => issue.endpointIndex));\n\treturn new Set(endpoints.map((_, index) => index).filter(index => !ignored.has(index)));\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", "// Module-local ambient: `multicastFromEnv` reads `process.env` when present\n// (Node, FFI host) and falls back to `{}` in browser contexts. The package\n// targets both, so we don't pull in `@types/node` org-wide \u2014 declaring the\n// minimal surface we touch keeps the dependency graph tight and the\n// published .d.ts free of node-isms.\ndeclare const process: { env: Record<string, string | undefined> } | undefined;\n\n/**\n * Multicast catalog input contract \u2014 validator + builder.\n *\n * Per draft-ramadan-moq-multicast-01 \u00A73 + \u00A74.1 + \u00A77.2.6\n * (Blockcast/moqcast-draft@fd962fd73d21ed281b60219c11ea61cc9e4e91c4), the publisher (FFmpeg,\n * cast bridge, GStreamer, OBS-bridge) populates `multicast.endpoints[]`\n * in the catalog when its output includes a UDP multicast destination.\n *\n * This module is the TypeScript-side validator over a versioned input\n * shape. Sister-language validators (FFmpeg C, cast Rust) target the\n * same invariants \u2014 eventually via a shared JSON Schema + fixtures\n * (BLO-3341 follow-up); today parity is enforced by code review against\n * 1:1 ports. Each publisher's integration MUST also assert that the\n * declared `(sourceAddress, groupAddress, port)` matches what it\n * actually emits \u2014 that's a separate runtime check the helper cannot\n * perform.\n *\n * Validation invariants (per codex review of BLO-3323 v2):\n * - The emitted mode is the one `sourceAddress` selects (present: SSM,\n * absent: ASM), restated as `protocol`. Input `protocol` \"ssm\" requires\n * `sourceAddress`, \"asm\" forbids it.\n * - Input `protocol` \"amt\" / \"atsc3\" names a bearer, which draft -01\n * carries as a `networkSource` entry, not a protocol: the `networkSource`\n * entries of that type MOVE onto each endpoint that named it (so they\n * no longer apply to native endpoints) and the protocol is never emitted.\n * An entry of that type is REQUIRED: an AMT one's `relay` only for\n * `discovery: \"manual\"` (\"driad\" resolves it at runtime), an ATSC3 one\n * carries the only tuning information an ATSC3 receiver has. A\n * per-endpoint `relayAddress` input is LIFTED into the AMT entry.\n * - Native \"ssm\" | \"asm\" requires no `networkSource` entry: a native join\n * describes no alternate bearer (BLO-37926).\n * - `auth` (catalog) and endpoint `auth` follow draft -02 7.2.6 through the\n * shared `multicastCatalogIssues` of `@blockcast/mmt-base`: all or\n * nothing, `senderAddress` on ASM only, bandwidth required, addresses\n * RFC 3986 IP literals, and one endpoint per `(S, G, port)` (the flow's\n * records ride in band on its `port`, 7.2.3). A receiver clamps a\n * `maxAuthDelayMs` outside 50..1000; this emitter never writes one, nor a\n * non-integer timer or a non-string `manifestTrack`.\n * - groupAddress is a valid IPv4 multicast group (224.0.0.0/4)\n * or IPv6 multicast group (ff00::/8)\n * - SSM groupAddress MUST be in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6),\n * where the IPv6 form requires P=1 + T=1 (the `3` nibble) AND the\n * 16-bit reserved-zero second hextet per RFC 3306 \u00A74 / RFC 4607.\n * - port in 1..65535\n * - tracks non-empty; per-track packetId in 1..65535\n * - packetId 0 (`MMTP_SIGNALING_PACKET_ID`) is the MMTP signaling flow\n * (FFmpeg `moq_mmt` sends the AL-FEC message 0x0203 on it into the same\n * multicast group) and is refused for a track with its own kind,\n * `signaling_packet_id`. The kind string matches cast's\n * `multicast_input.rs` so the two ports report the same reason.\n * - packetIds unique within an endpoint\n * - (S, groupAddress, port) tuple unique across endpoints, S being\n * sourceAddress (SSM), auth.senderAddress (protected ASM) or absent\n * - bandwidth (if present) > 0\n * - validUntil (if present) is a positive unix-ms timestamp\n *\n * @see draft-ramadan-moq-multicast-01 \u00A74.1, \u00A77.2.6\n * @see .planning/drafts/blo-3323-multicast-catalog-source-of-truth.md\n */\n\nimport { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID, multicastCatalogIssues } from \"@blockcast/mmt-base\";\nimport {\n\ttype AmtDiscovery,\n\ttype MulticastCatalogConfig,\n\ttype MulticastEndpoint,\n\ttype MulticastTrackObject,\n\ttype NetworkSource,\n} from \"./carp-catalog.js\";\nimport type { EndpointAuthConfig, MulticastAuthConfig } from \"./multicast-auth-catalog.js\";\n\n/**\n * Publisher input protocol. \"ssm\" / \"asm\" restate the mode `sourceAddress`\n * selects; \"amt\" / \"atsc3\" (draft -00) name a bearer and are mapped onto an\n * endpoint-level `networkSource`, never emitted.\n */\nexport type MulticastInputProtocol = \"ssm\" | \"asm\" | \"amt\" | \"atsc3\";\n\n/**\n * Strict typed input for one multicast endpoint. The publisher provides\n * this shape (env vars / CLI / config record), the helper validates and\n * lifts it into a `MulticastEndpoint` ready to embed in the catalog.\n */\nexport interface MulticastEndpointInput {\n\treadonly protocol: MulticastInputProtocol;\n\treadonly sourceAddress?: string;\n\treadonly groupAddress: string;\n\treadonly port: number;\n\t/** New emitters MUST use the object form (name + packetId). */\n\treadonly tracks: ReadonlyArray<MulticastTrackObject>;\n\t/**\n\t * AMT relay for this endpoint.\n\t *\n\t * Publisher-side input only \u2014 it is LIFTED into the catalog-level\n\t * `multicast.networkSource` and is never written onto the emitted endpoint\n\t * (the normative endpoint model carries no relay field). Because\n\t * `networkSource` is catalog-level, a relay that differs from the catalog's\n\t * AMT relay cannot be expressed and is rejected with `conflicting_amt_relay`.\n\t *\n\t * Prefer declaring `MulticastInput.networkSource` directly.\n\t */\n\treadonly relayAddress?: string;\n\t/** Aggregate bandwidth in bits per second. RECOMMENDED per draft \u00A74.1; REQUIRED with `auth`. */\n\treadonly bandwidth?: number;\n\t/** Protected-flow parameters (draft -02 7.2.6); REQUIRED exactly when `MulticastInput.auth` is set. */\n\treadonly auth?: EndpointAuthConfig;\n}\n\n/**\n * Strict typed input for the catalog-level multicast extension.\n *\n * `version` (monotonic) + `validUntil` (unix ms) support the catalog\n * freshness model: subscribers refetch before expiry, and publishers\n * MUST republish (with bumped `version`) on any endpoint mutation.\n */\nexport interface MulticastInput {\n\treadonly enabled?: boolean;\n\treadonly endpoints: ReadonlyArray<MulticastEndpointInput>;\n\t/**\n\t * Canonical relay/tuning declaration (draft-ramadan-moq-multicast \u00A74.1).\n\t * When omitted, one is derived from `amtFallback` and any endpoint\n\t * `relayAddress`; when present no derivation happens. Either way every entry\n\t * is validated, and an endpoint `relayAddress` it does not carry is\n\t * rejected (`conflicting_amt_relay`). Omit it entirely for native\n\t * multicast: it describes alternate bearers only (BLO-37926).\n\t */\n\treadonly networkSource?: ReadonlyArray<NetworkSource>;\n\t/** @deprecated Lifted into `networkSource`; no longer emitted. */\n\treadonly amtFallback?: { readonly discovery?: AmtDiscovery; readonly relayDns?: string };\n\t/** bc-provenance version 2 profile and keys (draft -01 7.2.6). */\n\treadonly auth?: MulticastAuthConfig;\n\treadonly version?: number;\n\treadonly validUntil?: number;\n}\n\nexport type MulticastInputErrorKind =\n\t| \"empty_endpoints\"\n\t/**\n\t * `endpoints` was not an array at all. Distinct from `empty_endpoints`,\n\t * which reports a well-formed array that declares nothing: a caller\n\t * handling these programmatically needs \"you passed the wrong shape\"\n\t * apart from \"you passed nothing\".\n\t */\n\t| \"invalid_endpoints\"\n\t| \"ssm_requires_source\"\n\t/** Input `protocol` \"asm\" with a `sourceAddress`, which selects SSM. */\n\t| \"protocol_source_mismatch\"\n\t/** `MulticastInput.auth` fails draft -02 7.2.6 (version, keys, a repeated kid or firstEpoch). */\n\t| \"invalid_multicast_auth\"\n\t/**\n\t * An endpoint's `auth` fails draft -02 7.2.6: present without\n\t * `MulticastInput.auth` or absent with it, `senderAddress` on SSM or\n\t * missing on ASM, a timer out of range or not an integer, a non-string\n\t * `manifestTrack`, or no `bandwidth`.\n\t */\n\t| \"invalid_endpoint_auth\"\n\t/**\n\t * Deliberately overloaded, unlike its one-site mirror\n\t * `atsc3_requires_tuning`: an AMT relay can be missing at two layers, and\n\t * both mean \"this endpoint names a relay nobody can reach\". Raised for an\n\t * `amt` endpoint with no `networkSource` entry of type `\"amt\"`, and for an\n\t * entry whose `discovery: \"manual\"` names no `relay`. Splitting them would\n\t * make callers handle two kinds for one operator-visible fault.\n\t */\n\t| \"amt_requires_relay\"\n\t/**\n\t * An `atsc3` endpoint with no `networkSource` entry of type `\"atsc3\"`. The\n\t * ATSC3 entry carries the frequency (kHz; PLP, service id and SLS URI are\n\t * optional) \u2014 the only tuning information there is \u2014 so without it the endpoint names a broadcast\n\t * nobody can tune to. The AMT mirror of this is `amt_requires_relay`; native\n\t * joins (\"ssm\", \"asm\") describe no bearer and are exempt (BLO-37926).\n\t */\n\t| \"atsc3_requires_tuning\"\n\t/**\n\t * An endpoint `relayAddress` is not the relay of any AMT `networkSource`\n\t * entry (another endpoint's relay, `amtFallback.relayDns`, or an explicit\n\t * entry won). `networkSource` is catalog-level, so per-endpoint relays\n\t * cannot be represented; fail closed rather than silently drop one.\n\t */\n\t| \"conflicting_amt_relay\"\n\t/**\n\t * A `networkSource` AMT entry's `discovery` was not `\"driad\"` or `\"manual\"`,\n\t * the schema enum. Checked on explicit and derived entries alike.\n\t */\n\t| \"invalid_amt_discovery\"\n\t/**\n\t * A `networkSource` entry the schema rejects: unknown `type`, a key outside\n\t * its `type`'s shape, a blank `relay`, or an out-of-range ATSC3 field.\n\t */\n\t| \"invalid_network_source\"\n\t| \"invalid_group_address\"\n\t| \"ssm_group_out_of_range\"\n\t| \"invalid_port\"\n\t| \"empty_tracks\"\n\t| \"invalid_track_name\"\n\t| \"invalid_packet_id\"\n\t/**\n\t * A track advertised on `MMTP_SIGNALING_PACKET_ID` (0), the MMTP signaling\n\t * flow. Distinct from `invalid_packet_id` so the reason is legible: 0 is a\n\t * well-formed 16-bit value, it is just not a track's to claim.\n\t */\n\t| \"signaling_packet_id\"\n\t| \"duplicate_packet_id\"\n\t| \"duplicate_endpoint_tuple\"\n\t| \"invalid_bandwidth\"\n\t| \"invalid_valid_until\"\n\t| \"invalid_version\"\n\t| \"invalid_env_shape\";\n\nexport class MulticastInputError extends Error {\n\tconstructor(\n\t\tpublic readonly kind: MulticastInputErrorKind,\n\t\tpublic readonly endpointIndex: number | null,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MulticastInputError\";\n\t}\n}\n\n/**\n * Validate a typed publisher input and lift it into a\n * `MulticastCatalogConfig` ready for `catalog.multicast = \u2026`.\n *\n * Throws `MulticastInputError` on any invariant violation. Fail-fast\n * at config-load time, never at runtime \u2014 silent fallbacks hide bugs\n * upstream (per the project's \"no anti-patterns\" directive).\n */\nexport function multicastFromConfig(input: MulticastInput): MulticastCatalogConfig {\n\t// Both array-valued inputs are checked here, before anything indexes them.\n\t// These helpers are consumed out-of-tree from plain JS (see\n\t// `validateNetworkSourceEntry`), so the declared types are no guarantee \u2014\n\t// and object-instead-of-array is the likely mistake on both: `amtFallback`,\n\t// which `networkSource` replaces, was a bare object. Without these a wrong\n\t// shape escapes as a raw `TypeError` from `.map`, which a caller catching\n\t// `MulticastInputError` does not catch.\n\tif (!Array.isArray(input.endpoints)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_endpoints\",\n\t\t\tnull,\n\t\t\t`endpoints must be an array of endpoint objects; got ${typeof input.endpoints}`,\n\t\t);\n\t}\n\tif (input.networkSource !== undefined && !Array.isArray(input.networkSource)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_network_source\",\n\t\t\tnull,\n\t\t\t\"networkSource must be an array of entries; got \" +\n\t\t\t\t`${typeof input.networkSource} \u2014 a single entry still goes in a one-element array`,\n\t\t);\n\t}\n\tif (input.endpoints.length === 0) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"empty_endpoints\",\n\t\t\tnull,\n\t\t\t\"endpoints[] must be non-empty \u2014 a multicast field with zero endpoints declares nothing\",\n\t\t);\n\t}\n\tif (input.version !== undefined && (!Number.isInteger(input.version) || input.version < 0)) {\n\t\tthrow new MulticastInputError(\"invalid_version\", null, `version must be a non-negative integer; got ${input.version}`);\n\t}\n\tif (input.validUntil !== undefined) {\n\t\tif (!Number.isFinite(input.validUntil) || input.validUntil <= 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_valid_until\",\n\t\t\t\tnull,\n\t\t\t\t`validUntil must be a positive unix-ms timestamp; got ${input.validUntil}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst endpoints: MulticastEndpoint[] = input.endpoints.map((ep, idx) => validateEndpoint(ep, idx));\n\n\tconst out: MulticastCatalogConfig = { endpoints };\n\tconst { entries: networkSource, relayFrom } = resolveNetworkSource(input);\n\t// Checked on the resolved array, so an explicit `input.networkSource` gets\n\t// exactly the same checks as one derived from the deprecated inputs.\n\tnetworkSource.forEach(validateNetworkSourceEntry);\n\tif (networkSource.length > 0) out.networkSource = networkSource;\n\n\t// A relay the emitted catalog would not carry fails closed, on both paths:\n\t// `networkSource` is catalog-level, so a per-endpoint relay that differs from\n\t// it (another endpoint's, `amtFallback.relayDns`, or an explicit entry) would\n\t// otherwise be dropped silently and that endpoint joins the wrong relay.\n\t// A DRIAD entry carries no relay (`validateNetworkSourceEntry`).\n\tconst amtRelays = new Set(\n\t\tnetworkSource\n\t\t\t.flatMap((s) => (s.type === \"amt\" && s.relay?.trim() ? [s.relay.trim()] : []))\n\t\t\t// Only the first, because it is the only one either reader can\n\t\t\t// deliver: `catalogAmtNetworkSource` returns on it and `findAmtRelay`\n\t\t\t// uses `.find`. Collecting every entry accepts an endpoint naming a\n\t\t\t// second manual entry, which then joins the first -- the same silent\n\t\t\t// wrong-relay join this guard exists to prevent. It also keeps the `(...)`\n\t\t\t// listing below honest. Sliced rather than `.find`ed so the `amt`\n\t\t\t// narrowing stays inside the callback.\n\t\t\t.slice(0, 1),\n\t);\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tconst relay = ep.relayAddress?.trim();\n\t\tif (!relay || amtRelays.has(relay)) return;\n\t\t// The entry is written down but undeliverable for its position: both\n\t\t// readers join the first AMT entry. Reachable only on the explicit\n\t\t// path -- `resolveNetworkSource` emits at most one entry -- so there is\n\t\t// no provenance clause to add, and saying \"is not the relay of any AMT\n\t\t// entry\" here would be plainly false: the operator can see the entry.\n\t\t// `amtRelays` holds only the first joinable relay, so reaching this\n\t\t// throw at all already means this one is not it.\n\t\tconst shadowed = networkSource.some(\n\t\t\t(s) => s.type === \"amt\" && s.relay?.trim() === relay,\n\t\t);\n\t\t// The derived and env paths never wrote `networkSource` at all -- it was\n\t\t// synthesised in `resolveNetworkSource` -- so name the input that\n\t\t// supplied the relay. Read that source from `relayFrom` rather than\n\t\t// re-inferring it: three review rounds found a local re-inference here\n\t\t// drifting from the rule above it.\n\t\tconst fromAmtFallback = \", derived from amtFallback/MOQCAST_MULTICAST_AMT_*\";\n\t\tconst fromRelay =\n\t\t\trelayFrom === \"amtFallback.relayDns\"\n\t\t\t\t? fromAmtFallback\n\t\t\t\t: relayFrom === \"endpoint.relayAddress\"\n\t\t\t\t\t? \", derived from the endpoint relayAddress values\"\n\t\t\t\t\t: \"\";\n\t\t// `resolveNetworkSource` lets `relayDns` outrank an endpoint relay, so on\n\t\t// the derived path the set can be non-empty and still not hold this\n\t\t// relay. Blaming per-endpoint representation there is wrong twice over:\n\t\t// there may be only one endpoint, and the field that actually won is\n\t\t// `relayDns`, which is the one the operator can act on.\n\t\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\t\tconst outranked = relayFrom === \"amtFallback.relayDns\" && relayDns !== relay;\n\t\tthrow new MulticastInputError(\n\t\t\t\"conflicting_amt_relay\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] relayAddress ${JSON.stringify(relay)} ` +\n\t\t\t\t(shadowed\n\t\t\t\t\t? `is carried by a multicast.networkSource AMT entry that is not the first one: ` +\n\t\t\t\t\t\t`both readers join the first AMT entry (${[...amtRelays].join(\", \")}) and ignore ` +\n\t\t\t\t\t\t\"the rest, so this relay is never joined; keep a single AMT entry, or put this one first\"\n\t\t\t\t\t: `is not the relay of any multicast.networkSource AMT entry ` +\n\t\t\t\t\t\t`(${[...amtRelays].join(\", \") || \"none\"})${fromRelay}; ` +\n\t\t\t\t\t\t(outranked\n\t\t\t\t\t\t\t? `amtFallback.relayDns/MOQCAST_MULTICAST_AMT_RELAY_DNS (${relayDns}) outranks an ` +\n\t\t\t\t\t\t\t\t\"endpoint relay when networkSource is derived -- drop one, or make them agree\"\n\t\t\t\t\t\t\t: \"networkSource is catalog-level and cannot represent per-endpoint relays\")),\n\t\t);\n\t});\n\n\t// A bearer endpoint must name its bearer. The two bearer protocols are\n\t// exactly the two `networkSource` types, so an endpoint's own protocol names\n\t// the entry it requires. Native joins (\"ssm\", \"asm\") describe no bearer and\n\t// are exempt (BLO-37926) \u2014 the endpoint tuple is the whole join.\n\t// Whether an AMT entry needs a relay string (\"manual\" does, \"driad\" does\n\t// not) is per-entry, checked above.\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tif (ep.protocol !== \"amt\" && ep.protocol !== \"atsc3\") return;\n\t\tif (networkSource.some((s) => s.type === ep.protocol)) return;\n\t\tthrow new MulticastInputError(\n\t\t\tep.protocol === \"amt\" ? \"amt_requires_relay\" : \"atsc3_requires_tuning\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=${ep.protocol} requires a multicast.networkSource entry of ` +\n\t\t\t\t`type \"${ep.protocol}\"` +\n\t\t\t\t(ep.protocol === \"atsc3\"\n\t\t\t\t\t? \" carrying at least its frequency (kHz) \u2014 an ATSC3 endpoint is not tunable without it\"\n\t\t\t\t\t: \"\"),\n\t\t);\n\t});\n\n\t// Draft -01 4.2: a bearer is a networkSource entry. An endpoint that named\n\t// one as its protocol gets that type's entries as its own networkSource.\n\t// They are copied, not moved: the catalog-level array already applied to\n\t// every endpoint under -00 readers, and still does under -01.\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tif (ep.protocol !== \"amt\" && ep.protocol !== \"atsc3\") return;\n\t\tendpoints[idx]!.networkSource = networkSource.filter((s) => s.type === ep.protocol).map((s) => ({ ...s }));\n\t});\n\n\tif (input.auth !== undefined) out.auth = input.auth;\n\tconst [issue] = multicastCatalogIssues({ multicast: out });\n\tif (issue) {\n\t\tconst kind: MulticastInputErrorKind =\n\t\t\tissue.code === \"multicast_auth\"\n\t\t\t\t? \"invalid_multicast_auth\"\n\t\t\t\t: issue.code === \"multicast_endpoint_auth\"\n\t\t\t\t\t? \"invalid_endpoint_auth\"\n\t\t\t\t\t: issue.path.endsWith(\".port\")\n\t\t\t\t\t\t? \"duplicate_endpoint_tuple\"\n\t\t\t\t\t\t: \"protocol_source_mismatch\";\n\t\tconst where = issue.endpointIndex === undefined ? \"\" : `endpoint[${issue.endpointIndex}] `;\n\t\tthrow new MulticastInputError(kind, issue.endpointIndex ?? null, `${where}${issue.path.replace(\"$.multicast.\", \"\")}: ${issue.message}`);\n\t}\n\t// What a receiver clamps or reads as absent (7.2.6) a publisher still never emits.\n\tendpoints.forEach((ep, idx) => {\n\t\tconst auth = (ep as { auth?: Record<string, unknown> }).auth;\n\t\tif (auth === undefined) return;\n\t\tconst delay = auth[\"maxAuthDelayMs\"];\n\t\tconst timer = auth[\"manifestTimerMs\"];\n\t\tconst track = auth[\"manifestTrack\"];\n\t\tconst bad =\n\t\t\tdelay !== undefined && !(Number.isSafeInteger(delay) && (delay as number) >= 50 && (delay as number) <= 1000)\n\t\t\t\t? \"maxAuthDelayMs must be an integer from 50 to 1000\"\n\t\t\t\t: timer !== undefined && !(Number.isSafeInteger(timer) && (timer as number) >= 1)\n\t\t\t\t\t? \"manifestTimerMs must be an integer of at least 1\"\n\t\t\t\t\t: track !== undefined && !(typeof track === \"string\" && track.length > 0)\n\t\t\t\t\t\t? \"manifestTrack must be a non-empty string\"\n\t\t\t\t\t\t: undefined;\n\t\tif (bad) throw new MulticastInputError(\"invalid_endpoint_auth\", idx, `endpoint[${idx}] auth: ${bad}`);\n\t});\n\n\tif (input.enabled !== undefined) out.enabled = input.enabled;\n\tif (input.version !== undefined) out.version = input.version;\n\tif (input.validUntil !== undefined) out.validUntil = input.validUntil;\n\treturn out;\n}\n\n/**\n * Build the catalog-level `multicast.networkSource` for this input.\n *\n * An explicit `input.networkSource` is authoritative and used as given.\n * Otherwise one AMT entry is derived from the deprecated inputs it replaces:\n * `amtFallback.discovery`/`relayDns` and any endpoint `relayAddress`.\n * `discovery` is copied only when given: draft-ramadan-moq-multicast-01 4.2.1\n * reads an omitted `discovery` with a hostname `relay` as `\"dns\"`, so no\n * default is written. Endpoint relays that disagree with the result\n * are rejected by `multicastFromConfig`, not resolved here.\n *\n * `relayFrom` reports which deprecated input supplied the derived relay, so\n * that rejection can name a field the operator actually set instead of\n * re-deciding the precedence below at the throw site. It is `null` on the\n * explicit path and when no relay was derived at all.\n */\nfunction resolveNetworkSource(input: MulticastInput): {\n\tentries: NetworkSource[];\n\trelayFrom: \"amtFallback.relayDns\" | \"endpoint.relayAddress\" | null;\n} {\n\tif (input.networkSource !== undefined) {\n\t\treturn { entries: input.networkSource.map((s) => ({ ...s })), relayFrom: null };\n\t}\n\n\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\tconst relay = relayDns || input.endpoints.map((ep) => ep.relayAddress?.trim()).find((r) => !!r);\n\tconst discovery = input.amtFallback?.discovery;\n\tif (discovery === undefined && !relay) return { entries: [], relayFrom: null };\n\treturn {\n\t\tentries: [{ type: \"amt\", ...(discovery ? { discovery } : {}), ...(relay ? { relay } : {}) }],\n\t\trelayFrom: !relay ? null : relayDns ? \"amtFallback.relayDns\" : \"endpoint.relayAddress\",\n\t};\n}\n\nconst AMT_SOURCE_KEYS = new Set([\"type\", \"discovery\", \"relay\", \"port\"]);\nconst ATSC3_SOURCE_KEYS = new Set([\"type\", \"frequency\", \"plpId\", \"serviceId\", \"slsUri\"]);\n\n/**\n * One `networkSource[]` entry against `catalog.schema.json` `$defs/networkSource`\n * (and moq-rs `NetworkSource`), plus the one semantic rule the schema leaves\n * out: `discovery: \"dns\"` and `\"manual\"` name their relay. These helpers are consumed\n * out-of-tree from plain JS, so the `NetworkSource` type is no guarantee.\n */\nfunction validateNetworkSourceEntry(s: NetworkSource, i: number): void {\n\tconst err = (kind: MulticastInputErrorKind, why: string) =>\n\t\tnew MulticastInputError(kind, null, `networkSource[${i}] ${why}`);\n\tconst keys = s?.type === \"amt\" ? AMT_SOURCE_KEYS : s?.type === \"atsc3\" ? ATSC3_SOURCE_KEYS : undefined;\n\tif (!keys) throw err(\"invalid_network_source\", `type must be \"amt\" or \"atsc3\"; got ${JSON.stringify(s?.type)}`);\n\t// Definedness, not presence: `isMulticastCatalogConfig` in `carp-catalog.ts`\n\t// already chose `=== undefined` over `in` for exactly this reason, and the\n\t// two must agree on what \"set\" means. A key spread in as `undefined`\n\t// (`{...entry, relayDns: undefined}`) is dropped by `JSON.stringify`, so the\n\t// emitted catalog is clean and rejecting it would fail a shape that ships\n\t// correctly.\n\tconst extra = Object.keys(s).filter((k) => !keys.has(k) && (s as Record<string, unknown>)[k] !== undefined);\n\tif (extra.length > 0) throw err(\"invalid_network_source\", `has keys the schema rejects: ${extra.join(\", \")}`);\n\n\tif (s.type === \"amt\") {\n\t\tif (s.discovery !== undefined && s.discovery !== \"dns\" && s.discovery !== \"driad\" && s.discovery !== \"manual\") {\n\t\t\tthrow err(\n\t\t\t\t\"invalid_amt_discovery\",\n\t\t\t\t`discovery must be \"dns\", \"driad\" or \"manual\" when present; got ${JSON.stringify(s.discovery)}`,\n\t\t\t);\n\t\t}\n\t\tif (s.relay !== undefined) {\n\t\t\t// Canonicalize, don't just check: the conflict guard in\n\t\t\t// `multicastFromConfig` compares `.trim()`ed values, so a padded\n\t\t\t// explicit relay would pass the guard and then ship padded. The\n\t\t\t// derived path (`resolveNetworkSource`) already trims, so trimming\n\t\t\t// here is what makes both paths emit the same canonical string.\n\t\t\t// Entries are copies by then, so this cannot mutate caller input.\n\t\t\tconst relay = typeof s.relay === \"string\" ? s.relay.trim() : \"\";\n\t\t\tif (!relay) {\n\t\t\t\tthrow err(\"invalid_network_source\", `relay must be non-blank when present; got ${JSON.stringify(s.relay)}`);\n\t\t\t}\n\t\t\ts.relay = relay;\n\t\t}\n\t\tif ((s.discovery === \"dns\" || s.discovery === \"manual\") && s.relay === undefined) {\n\t\t\tthrow err(\"amt_requires_relay\", `discovery=\"${s.discovery}\" requires a relay (\"driad\" resolves it at runtime)`);\n\t\t}\n\t\tif (s.discovery === \"driad\" && s.relay !== undefined) {\n\t\t\tthrow err(\n\t\t\t\t\"invalid_network_source\",\n\t\t\t\t`discovery=\"driad\" must omit relay: DRIAD supplies the relay candidates (draft -01 4.2.1); ` +\n\t\t\t\t\t`drop the relay (amtFallback.relayDns / endpoint relayAddress on the derived path), or use \"dns\" or \"manual\"`,\n\t\t\t);\n\t\t}\n\t\tif (s.port !== undefined && (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535)) {\n\t\t\tthrow err(\"invalid_port\", `port must be 1..65535; got ${s.port}`);\n\t\t}\n\t\treturn;\n\t}\n\tconst inRange = (v: number, min: number, max: number) => Number.isInteger(v) && v >= min && v <= max;\n\tif (!inRange(s.frequency, 1, Number.MAX_SAFE_INTEGER)) {\n\t\tthrow err(\"invalid_network_source\", `frequency must be a positive integer in kHz; got ${s.frequency}`);\n\t}\n\t// Draft -01 4.2.2: only frequency (kHz) is REQUIRED; the rest are checked when set.\n\tif (s.plpId !== undefined && !inRange(s.plpId, 0, 63)) {\n\t\tthrow err(\"invalid_network_source\", `plpId must be 0..63; got ${s.plpId}`);\n\t}\n\tif (s.serviceId !== undefined && !inRange(s.serviceId, 0, 65535)) {\n\t\tthrow err(\"invalid_network_source\", `serviceId must be 0..65535; got ${s.serviceId}`);\n\t}\n\tif (s.slsUri !== undefined && (typeof s.slsUri !== \"string\" || !isAbsoluteUri(s.slsUri))) {\n\t\tthrow err(\"invalid_network_source\", `slsUri must be an absolute URI; got ${JSON.stringify(s.slsUri)}`);\n\t}\n}\n\nfunction isAbsoluteUri(value: string): boolean {\n\ttry {\n\t\tnew URL(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction validateEndpoint(ep: MulticastEndpointInput, idx: number): MulticastEndpoint {\n\tif (ep.protocol === \"ssm\" && (!ep.sourceAddress || ep.sourceAddress.length === 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_requires_source\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires sourceAddress`,\n\t\t);\n\t}\n\tif (ep.protocol === \"asm\" && ep.sourceAddress !== undefined) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"protocol_source_mismatch\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=asm with sourceAddress \"${ep.sourceAddress}\"; sourceAddress selects SSM (draft -01 4.1)`,\n\t\t);\n\t}\n\t// NOTE: `protocol: \"amt\"` no longer requires a per-endpoint relay here. The\n\t// relay is catalog-level (`multicast.networkSource`), which this function\n\t// cannot see; `multicastFromConfig` enforces it once both halves are in hand.\n\n\tif (!isMulticastGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_group_address\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] groupAddress \"${ep.groupAddress}\" is not a valid multicast address`,\n\t\t);\n\t}\n\tif (ep.protocol === \"ssm\" && !isSsmGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_group_out_of_range\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires groupAddress in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6); got \"${ep.groupAddress}\"`,\n\t\t);\n\t}\n\n\tif (!Number.isInteger(ep.port) || ep.port < 1 || ep.port > 65535) {\n\t\tthrow new MulticastInputError(\"invalid_port\", idx, `endpoint[${idx}] port must be 1..65535; got ${ep.port}`);\n\t}\n\n\tif (ep.tracks.length === 0) {\n\t\tthrow new MulticastInputError(\"empty_tracks\", idx, `endpoint[${idx}] tracks must be non-empty`);\n\t}\n\n\tconst seenPids = new Set<number>();\n\tfor (const t of ep.tracks) {\n\t\tif (typeof t.name !== \"string\" || t.name.length === 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track name must be a non-empty string`,\n\t\t\t);\n\t\t}\n\t\tif (t.packetId === MMTP_SIGNALING_PACKET_ID) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"signaling_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId ${t.packetId} is the MMTP signaling flow ` +\n\t\t\t\t\t`and must not be advertised for a track; packetId must be 1..${MMTP_PACKET_ID_MAX}`,\n\t\t\t);\n\t\t}\n\t\tif (!Number.isInteger(t.packetId) || t.packetId < 1 || t.packetId > MMTP_PACKET_ID_MAX) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId must be 1..${MMTP_PACKET_ID_MAX}; got ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tif (seenPids.has(t.packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"duplicate_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] duplicate packetId ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tseenPids.add(t.packetId);\n\t}\n\n\tif (ep.bandwidth !== undefined && (!Number.isFinite(ep.bandwidth) || ep.bandwidth <= 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_bandwidth\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] bandwidth must be positive; got ${ep.bandwidth}`,\n\t\t);\n\t}\n\n\t// Tuple uniqueness is keyed on S, which needs `auth`; `multicastFromConfig`\n\t// checks it with the rest of draft -01 4.1 once the catalog is composed.\n\t// The emitted protocol restates the mode `sourceAddress` selects; a bearer\n\t// input protocol is never emitted.\n\tconst out: MulticastEndpoint = {\n\t\tprotocol: ep.sourceAddress === undefined ? \"asm\" : \"ssm\",\n\t\tgroupAddress: ep.groupAddress,\n\t\tport: ep.port,\n\t\ttracks: ep.tracks.map((t) => ({ name: t.name, packetId: t.packetId })),\n\t};\n\tif (ep.sourceAddress !== undefined) out.sourceAddress = ep.sourceAddress;\n\t// `relayAddress` is deliberately NOT copied: the relay is lifted to\n\t// catalog-level `multicast.networkSource` by `multicastFromConfig`.\n\tif (ep.bandwidth !== undefined) out.bandwidth = ep.bandwidth;\n\tif (ep.auth !== undefined) out.auth = { ...ep.auth };\n\treturn out;\n}\n\nfunction isMulticastGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// IPv6: ff00::/8. Prefix-only check; full IPv6 syntax validation\n\t\t// is delegated to the OS bind() call downstream.\n\t\treturn /^ff[0-9a-f]{2}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(addr);\n\tif (!m) return false;\n\tfor (let i = 1; i <= 4; i++) {\n\t\tconst v = Number(m[i]);\n\t\tif (!Number.isInteger(v) || v < 0 || v > 255) return false;\n\t}\n\tconst first = Number(m[1]);\n\treturn first >= 224 && first <= 239;\n}\n\nfunction isSsmGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// SSM IPv6: ff3x::/32 per RFC 3306 \u00A74. Requires P=1 + T=1 (the `3`\n\t\t// nibble) AND the second 16-bit hextet to be all-zero (RFC 3306's\n\t\t// \"prefix length = 0\" \u2192 next 32 bits are 0). The regex matches\n\t\t// `ff3?:0000:` collapsed forms (`ff3e::`, `ff3e:0::`, `ff3e:0:`,\n\t\t// `ff3e:0000:`) and rejects `ff3e:beef::1` etc.\n\t\treturn /^ff3[0-9a-f]:0{0,4}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\./.exec(addr);\n\treturn m !== null && Number(m[1]) === 232;\n}\n\n// Env-var helper (BLO-3337): read MOQCAST_MULTICAST_* keys and route\n// through multicastFromConfig so all per-endpoint invariants apply.\n\n/**\n * Read `MOQCAST_MULTICAST_*` env vars and return a\n * `MulticastCatalogConfig`. Returns `undefined` when no per-endpoint\n * key is set.\n *\n * Env contract \u2014 per-endpoint keys take an optional non-negative index:\n *\n * MOQCAST_MULTICAST[_<idx>]_PROTOCOL=ssm\n * MOQCAST_MULTICAST[_<idx>]_GROUP=232.1.1.1\n * MOQCAST_MULTICAST[_<idx>]_PORT=5004\n * MOQCAST_MULTICAST[_<idx>]_SOURCE=10.0.0.20 (required for ssm)\n * MOQCAST_MULTICAST[_<idx>]_RELAY=amt.example.com (required for amt)\n * MOQCAST_MULTICAST[_<idx>]_TRACKS=video:1,audio:3\n * MOQCAST_MULTICAST[_<idx>]_BANDWIDTH=8000000 (optional, bps)\n *\n * MOQCAST_MULTICAST_AMT_DISCOVERY=dns (catalog-level)\n * MOQCAST_MULTICAST_AMT_RELAY_DNS=relay.example.com (catalog-level;\n * must be unset with discovery=driad)\n *\n * The unindexed form (`MOQCAST_MULTICAST_GROUP`) is exactly index 0;\n * mixing it with explicit `_0_` keys is rejected as ambiguous. Indices\n * need not be contiguous; observed indices are sorted ascending.\n */\nexport function multicastFromEnv(\n\tenv: Record<string, string | undefined> = ((globalThis as { process?: { env?: Record<string, string | undefined> } })\n\t\t.process?.env ?? {}) as Record<string, string | undefined>,\n): MulticastCatalogConfig | undefined {\n\tconst indices = collectMulticastIndices(env);\n\t// An empty value is unset: `FOO=${BAR}` with BAR unset, or an empty\n\t// configMap value, is a templating outcome, not a setting. Normalising\n\t// here gives every test below one notion of \"set\". Stricter than SOURCE\n\t// and RELAY, which gate on truthiness alone and so keep a whitespace-only\n\t// value \u2014 deliberately, as these two decide whether AMT is declared at all.\n\tconst amtDiscovery = env[\"MOQCAST_MULTICAST_AMT_DISCOVERY\"]?.trim() || undefined;\n\tconst amtRelayDns = env[\"MOQCAST_MULTICAST_AMT_RELAY_DNS\"]?.trim() || undefined;\n\t// One predicate, both guards: a key significant enough to reject at zero\n\t// endpoints is not silently discarded once endpoints are present.\n\tconst hasCatalogAmt = amtDiscovery !== undefined || amtRelayDns !== undefined;\n\tif (indices.length === 0) {\n\t\t// Catalog-level AMT keys without per-endpoint keys is a\n\t\t// misconfiguration, not a no-op: `MOQCAST_MULTICAST_AMT_DISCOVERY`\n\t\t// only makes sense alongside endpoints to fall back FOR. Throw\n\t\t// loudly instead of silently dropping the request \u2014 a publisher\n\t\t// that meant to declare AMT fallback shouldn't have its config\n\t\t// disappear without any signal.\n\t\tif (hasCatalogAmt) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_env_shape\",\n\t\t\t\tnull,\n\t\t\t\t\"MOQCAST_MULTICAST_AMT_* env keys are set but no per-endpoint keys \" +\n\t\t\t\t\t\"(MOQCAST_MULTICAST_GROUP / _PROTOCOL / _PORT / _TRACKS) were found. \" +\n\t\t\t\t\t\"AMT discovery is a catalog-level fallback for endpoints; declaring it \" +\n\t\t\t\t\t\"with zero endpoints is a misconfiguration.\",\n\t\t\t);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tconst endpoints: MulticastEndpointInput[] = indices.map((idx) => readEndpointAt(env, idx));\n\n\tconst input: MulticastInput = {\n\t\tendpoints,\n\t\t...(hasCatalogAmt\n\t\t\t? {\n\t\t\t\t\tamtFallback: {\n\t\t\t\t\t\t// No default: an omitted discovery with a relay hostname is\n\t\t\t\t\t\t// \"dns\" (draft-ramadan-moq-multicast-01 4.2.1).\n\t\t\t\t\t\t...(amtDiscovery ? { discovery: amtDiscovery as AmtDiscovery } : {}),\n\t\t\t\t\t\t...(amtRelayDns ? { relayDns: amtRelayDns } : {}),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {}),\n\t};\n\treturn multicastFromConfig(input);\n}\n\nconst ENV_PER_FIELD = [\"PROTOCOL\", \"GROUP\", \"PORT\", \"SOURCE\", \"RELAY\", \"TRACKS\", \"BANDWIDTH\"];\nconst ENV_CATALOG_LEVEL = new Set([\"AMT_DISCOVERY\", \"AMT_RELAY_DNS\"]);\n\nfunction collectMulticastIndices(env: Record<string, string | undefined>): number[] {\n\tconst indices = new Set<number>();\n\tlet hasUnindexed = false;\n\tlet hasZeroIndexed = false;\n\n\tfor (const [key, val] of Object.entries(env)) {\n\t\tif (val === undefined) continue;\n\t\tif (!key.startsWith(\"MOQCAST_MULTICAST_\")) continue;\n\t\tconst suffix = key.slice(\"MOQCAST_MULTICAST_\".length);\n\t\tif (ENV_CATALOG_LEVEL.has(suffix)) continue;\n\n\t\tconst m = /^(\\d+)_(.+)$/.exec(suffix);\n\t\tif (m) {\n\t\t\tconst idx = Number(m[1]);\n\t\t\tconst field = m[2];\n\t\t\tif (!ENV_PER_FIELD.includes(field)) continue;\n\t\t\tif (idx === 0) hasZeroIndexed = true;\n\t\t\tindices.add(idx);\n\t\t} else if (ENV_PER_FIELD.includes(suffix)) {\n\t\t\thasUnindexed = true;\n\t\t\tindices.add(0);\n\t\t}\n\t}\n\n\tif (hasUnindexed && hasZeroIndexed) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_env_shape\",\n\t\t\tnull,\n\t\t\t\"MOQCAST_MULTICAST_* env contract: cannot mix unindexed keys (MOQCAST_MULTICAST_GROUP) \" +\n\t\t\t\t\"with explicitly-indexed keys at index 0 (MOQCAST_MULTICAST_0_GROUP) \u2014 they refer to \" +\n\t\t\t\t\"the same endpoint. Use one form consistently.\",\n\t\t);\n\t}\n\n\treturn [...indices].sort((a, b) => a - b);\n}\n\nfunction readEndpointAt(env: Record<string, string | undefined>, idx: number): MulticastEndpointInput {\n\tconst lookup = (field: string): string | undefined => {\n\t\tif (idx === 0) {\n\t\t\treturn env[`MOQCAST_MULTICAST_${field}`] ?? env[`MOQCAST_MULTICAST_0_${field}`];\n\t\t}\n\t\treturn env[`MOQCAST_MULTICAST_${idx}_${field}`];\n\t};\n\n\tconst portStr = lookup(\"PORT\");\n\tconst port = portStr !== undefined ? Number(portStr) : NaN;\n\tconst tracks = parseTracksEnv(lookup(\"TRACKS\"), idx);\n\tconst bandwidthStr = lookup(\"BANDWIDTH\");\n\tconst bandwidth = bandwidthStr !== undefined ? Number(bandwidthStr) : undefined;\n\tconst source = lookup(\"SOURCE\");\n\tconst relay = lookup(\"RELAY\");\n\n\treturn {\n\t\tprotocol: lookup(\"PROTOCOL\") as MulticastInputProtocol,\n\t\tgroupAddress: lookup(\"GROUP\") ?? \"\",\n\t\tport,\n\t\ttracks,\n\t\t...(source ? { sourceAddress: source } : {}),\n\t\t...(relay ? { relayAddress: relay } : {}),\n\t\t...(bandwidth !== undefined ? { bandwidth } : {}),\n\t};\n}\n\nfunction parseTracksEnv(raw: string | undefined, endpointIdx: number): MulticastTrackObject[] {\n\tif (!raw || raw.length === 0) return [];\n\treturn raw.split(\",\").map((entry, i) => {\n\t\tconst colon = entry.lastIndexOf(\":\");\n\t\tif (colon < 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] missing \":\" \u2014 expected \"name:packetId\", got ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\tconst name = entry.slice(0, colon).trim();\n\t\tconst idStr = entry.slice(colon + 1).trim();\n\t\tconst packetId = Number(idStr);\n\t\tif (!name || idStr.length === 0 || !Number.isFinite(packetId) || !Number.isInteger(packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] entry malformed (need \"name:integer\"): ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\treturn { name, packetId };\n\t});\n}\n"],
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", "/**\n * IP address literals as draft-ramadan-moq-multicast-02 7.2.6 requires them of\n * `sourceAddress`, `groupAddress` and `senderAddress`: the IPv4address or\n * IPv6address form of RFC 3986 3.2.2. Dotted decimal with no leading zero in\n * any dec-octet (the ls32 tail of an IPv6 literal included); IPv6 h16 groups\n * may carry leading zeros in either hex case; no brackets, no zone identifier,\n * never a name. Draft: Blockcast/moqcast-draft@5f49172.\n *\n * Addresses compare as IP addresses, never as strings, and an address of one\n * family never matches one of the other: an IPv4-mapped IPv6 address is of the\n * IPv6 family, so it parses to 16 octets and never equals its IPv4 address.\n */\n\nconst DEC_OCTET = /^(?:0|[1-9]\\d{0,2})$/;\nconst H16 = /^[0-9A-Fa-f]{1,4}$/;\n\nfunction parseIpv4(text: string): number[] | undefined {\n\tconst fields = text.split(\".\");\n\tif (fields.length !== 4) return undefined;\n\tconst octets: number[] = [];\n\tfor (const field of fields) {\n\t\tif (!DEC_OCTET.test(field)) return undefined;\n\t\tconst value = Number(field);\n\t\tif (value > 255) return undefined;\n\t\toctets.push(value);\n\t}\n\treturn octets;\n}\n\nfunction parseIpv6(text: string): Uint8Array | undefined {\n\tconst halves = text.split(\"::\");\n\tif (halves.length > 2) return undefined;\n\tconst groupsOf = (part: string) => (part === \"\" ? [] : part.split(\":\"));\n\tconst left = groupsOf(halves[0]!);\n\tconst right = halves.length === 2 ? groupsOf(halves[1]!) : [];\n\tconst words: number[][] = [];\n\tconst all = [...left, ...right];\n\t// The ls32 dotted tail ends the literal: the last group, never before \"::\".\n\tconst last = halves.length === 2 && right.length === 0 ? -1 : all.length - 1;\n\tfor (let index = 0; index < all.length; index++) {\n\t\tconst group = all[index]!;\n\t\tif (index === last && group.includes(\".\")) {\n\t\t\tconst v4 = parseIpv4(group);\n\t\t\tif (!v4) return undefined;\n\t\t\twords.push([(v4[0]! << 8) | v4[1]!, (v4[2]! << 8) | v4[3]!]);\n\t\t} else if (H16.test(group)) {\n\t\t\twords.push([Number.parseInt(group, 16)]);\n\t\t} else {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tconst leftWords = words.slice(0, left.length).flat();\n\tconst rightWords = words.slice(left.length).flat();\n\tconst missing = 8 - leftWords.length - rightWords.length;\n\tif (halves.length === 2 ? missing < 1 : missing !== 0) return undefined;\n\tconst out = new Uint8Array(16);\n\t[...leftWords, ...new Array<number>(missing).fill(0), ...rightWords].forEach((word, index) => {\n\t\tout[index * 2] = word >> 8;\n\t\tout[index * 2 + 1] = word & 0xff;\n\t});\n\treturn out;\n}\n\n/** The octets of an RFC 3986 IPv4address (4) or IPv6address (16), or `undefined`. */\nexport function parseIpAddressLiteral(text: unknown): Uint8Array | undefined {\n\tif (typeof text !== \"string\") return undefined;\n\tif (text.includes(\":\")) return parseIpv6(text);\n\tconst v4 = parseIpv4(text);\n\treturn v4 ? Uint8Array.from(v4) : undefined;\n}\n\n/** Whether two address literals name one address of one family. Unparseable never matches. */\nexport function sameIpAddress(a: unknown, b: unknown): boolean {\n\tconst x = parseIpAddressLiteral(a);\n\tconst y = parseIpAddressLiteral(b);\n\treturn x !== undefined && y !== undefined && x.length === y.length && x.every((octet, index) => octet === y[index]);\n}\n\n/**\n * One spelling per address: dotted decimal for IPv4, RFC 5952 for IPv6\n * (lowercase, no leading zeros, the longest run of two or more zero groups,\n * leftmost on a tie, as \"::\"; an IPv4-mapped address in hex like any other).\n * `undefined` for text that is not an address literal.\n */\nexport function canonicalIpAddress(text: unknown): string | undefined {\n\tconst bytes = parseIpAddressLiteral(text);\n\tif (!bytes) return undefined;\n\tif (bytes.length === 4) return bytes.join(\".\");\n\tconst words = Array.from({ length: 8 }, (_, index) => (bytes[index * 2]! << 8) | bytes[index * 2 + 1]!);\n\tlet bestStart = -1;\n\tlet bestLength = 1;\n\tfor (let start = 0; start < 8; ) {\n\t\tlet end = start;\n\t\twhile (end < 8 && words[end] === 0) end++;\n\t\tif (end - start > bestLength) {\n\t\t\tbestStart = start;\n\t\t\tbestLength = end - start;\n\t\t}\n\t\tstart = end === start ? start + 1 : end;\n\t}\n\tconst hex = words.map(word => word.toString(16));\n\tif (bestStart < 0) return hex.join(\":\");\n\treturn `${hex.slice(0, bestStart).join(\":\")}::${hex.slice(bestStart + bestLength).join(\":\")}`;\n}\n", "/**\n * The draft-ramadan-moq-multicast-02 endpoint and `auth` rules, written once\n * for every receiver of the catalog: the receivers in `@blockcast/mmt-transport`\n * and the container resolver ignore every endpoint an issue names (all of\n * them when the issue is catalog-level, which leaves the broadcast no data).\n * `validateCmsfMmtpCatalog` reports none of them as a catalog fault, so MoQ\n * unicast stays usable. It lives here because mmt-base is the only package\n * every receiver depends on.\n *\n * Draft: Blockcast/moqcast-draft@5f49172 (draft-ramadan-moq-multicast-02 as\n * posted 2026-11-02), sections 4.1 and 7.2.6.\n */\n\nimport { parseIpAddressLiteral } from \"./ip-address.js\";\n\nexport type MulticastJoinMode = \"ssm\" | \"asm\";\n\n/** An endpoint's join mode and `S` (4.1): `sourceAddress` on SSM,\n * `auth.senderAddress` on protected ASM, absent on unprotected ASM. */\nexport interface MulticastEndpointSource {\n\tmode: MulticastJoinMode;\n\tsource?: string;\n}\n\nexport type MulticastCatalogIssueCode =\n\t| \"multicast_auth\"\n\t| \"multicast_endpoint\"\n\t| \"multicast_endpoint_auth\"\n\t| \"multicast_track\";\n\nexport interface MulticastCatalogIssue {\n\tcode: MulticastCatalogIssueCode;\n\t/** JSONPath of the offending field. */\n\tpath: string;\n\tmessage: string;\n\t/** Endpoint the issue makes a receiver ignore; absent for a catalog-level\n\t * issue, which leaves the receiver no endpoint at all (7.2.6). */\n\tendpointIndex?: number;\n}\n\n/** Canonical unpadded base64url of exactly 32 octets. */\nexport const ED25519_PUBLIC_KEY_BASE64URL = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;\n\nconst UINT32_MAX = 0xffff_ffff;\nconst AUTH_MEMBERS = new Set([\"scheme\", \"version\", \"sig\", \"keys\", \"minEpoch\"]);\nconst KEY_MEMBERS = new Set([\"kid\", \"publicKey\", \"firstEpoch\"]);\n// Draft -01's separate record port is no member here: a leftover one is an\n// unknown member, so the endpoint is ignored (libmmt policy, fails closed).\nconst ENDPOINT_AUTH_MEMBERS = new Set([\n\t\"streamId\",\n\t\"senderAddress\",\n\t\"maxAuthDelayMs\",\n\t\"manifestTimerMs\",\n\t\"manifestTrack\",\n]);\n\ntype JsonObject = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is JsonObject {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isIntegerIn(value: unknown, min: number, max: number): value is number {\n\treturn Number.isSafeInteger(value) && (value as number) >= min && (value as number) <= max;\n}\n\n/** Defaults of an endpoint's `auth.maxAuthDelayMs` and `auth.manifestTimerMs` (7.2.6). */\nexport const BCP_DEFAULT_MAX_AUTH_DELAY_MS = 200;\nexport const BCP_DEFAULT_MANIFEST_TIMER_MS = 20;\n\n/** The timing and mirror members of an endpoint's `auth` as a receiver uses them. */\nexport interface EndpointAuthTiming {\n\tmaxAuthDelayMs: number;\n\tmanifestTimerMs: number;\n\tmanifestTrack?: string;\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value);\n}\n\n/**\n * 7.2.6: a `maxAuthDelayMs` outside 50..1000 is clamped to the nearer bound;\n * one that is not a JSON number, or an in-range one that is not an integer,\n * is absent (200). A `manifestTimerMs` that is not a JSON number, or an\n * integer-free value of at least 1, is absent (20); one below 1 makes the\n * endpoint ignored, so it is returned here as is for the caller to refuse. A\n * `manifestTrack` that is not a string is absent.\n */\nexport function endpointAuthTiming(auth: unknown): EndpointAuthTiming {\n\tconst record = isObject(auth) ? auth : {};\n\tconst delay = record[\"maxAuthDelayMs\"];\n\tconst timer = record[\"manifestTimerMs\"];\n\tconst track = record[\"manifestTrack\"];\n\tconst maxAuthDelayMs = !isFiniteNumber(delay)\n\t\t? BCP_DEFAULT_MAX_AUTH_DELAY_MS\n\t\t: delay < 50 ? 50 : delay > 1000 ? 1000 : Number.isInteger(delay) ? delay : BCP_DEFAULT_MAX_AUTH_DELAY_MS;\n\tconst manifestTimerMs = !isFiniteNumber(timer)\n\t\t? BCP_DEFAULT_MANIFEST_TIMER_MS\n\t\t: timer < 1 || Number.isSafeInteger(timer) ? timer : BCP_DEFAULT_MANIFEST_TIMER_MS;\n\treturn {\n\t\tmaxAuthDelayMs,\n\t\tmanifestTimerMs,\n\t\t...(typeof track === \"string\" && track.length > 0 ? { manifestTrack: track } : {}),\n\t};\n}\n\n/**\n * Join mode and `S` of one endpoint, or `undefined` when its fields disagree\n * and a receiver must ignore it (4.1). `sourceAddress` alone selects the mode;\n * `protocol` may only restate it, so \"amt\" and \"atsc3\" (draft -00) are\n * inconsistent here: a bearer is a `networkSource`, never a protocol.\n */\nexport function multicastEndpointSource(endpoint: unknown): MulticastEndpointSource | undefined {\n\tif (!isObject(endpoint)) return undefined;\n\tconst sourceAddress = endpoint[\"sourceAddress\"];\n\tif (sourceAddress !== undefined && !parseIpAddressLiteral(sourceAddress)) return undefined;\n\tconst mode: MulticastJoinMode = sourceAddress === undefined ? \"asm\" : \"ssm\";\n\tconst protocol = endpoint[\"protocol\"];\n\tif (protocol !== undefined && protocol !== mode) return undefined;\n\tconst auth = endpoint[\"auth\"];\n\tconst senderAddress = isObject(auth) ? auth[\"senderAddress\"] : undefined;\n\tif (mode === \"ssm\") return senderAddress === undefined ? { mode, source: sourceAddress as string } : undefined;\n\tif (auth === undefined) return { mode };\n\treturn parseIpAddressLiteral(senderAddress) ? { mode, source: senderAddress as string } : undefined;\n}\n\nfunction authIssues(auth: unknown): MulticastCatalogIssue[] {\n\tconst base = \"$.multicast.auth\";\n\tconst issue = (path: string, message: string): MulticastCatalogIssue[] => [\n\t\t{ code: \"multicast_auth\", path: `${base}${path}`, message },\n\t];\n\tif (!isObject(auth)) return issue(\"\", \"auth must be an object\");\n\tconst unknown = Object.keys(auth).filter(key => !AUTH_MEMBERS.has(key));\n\tif (unknown.length > 0) {\n\t\treturn issue(\"\", `auth has unknown member(s) ${unknown.join(\", \")}; draft -02 7.2.6 defines scheme, version, sig, keys and minEpoch`);\n\t}\n\tif (auth[\"scheme\"] !== \"bc-provenance\") return issue(\".scheme\", 'scheme must be \"bc-provenance\"');\n\tif (auth[\"version\"] !== 2) return issue(\".version\", \"version must be 2 (bc-provenance version 2, draft -02 7.2)\");\n\tif (auth[\"sig\"] !== \"ed25519\") return issue(\".sig\", 'sig must be \"ed25519\"');\n\tconst keys = auth[\"keys\"];\n\tif (!Array.isArray(keys) || keys.length < 1 || keys.length > 4) {\n\t\treturn issue(\".keys\", \"keys must be an array of one to four {kid, publicKey, firstEpoch} objects\");\n\t}\n\tconst kids = new Set<number>();\n\tconst firstEpochs = new Set<number>();\n\tfor (let index = 0; index < keys.length; index++) {\n\t\tconst key = keys[index];\n\t\tconst at = `.keys[${index}]`;\n\t\tif (!isObject(key) || Object.keys(key).some(member => !KEY_MEMBERS.has(member))) {\n\t\t\treturn issue(at, \"a key is exactly {kid, publicKey, firstEpoch}\");\n\t\t}\n\t\tif (!isIntegerIn(key[\"kid\"], 0, UINT32_MAX)) return issue(`${at}.kid`, \"kid must be an unsigned 32-bit integer\");\n\t\tif (typeof key[\"publicKey\"] !== \"string\" || !ED25519_PUBLIC_KEY_BASE64URL.test(key[\"publicKey\"])) {\n\t\t\treturn issue(`${at}.publicKey`, \"publicKey must be a 32-octet Ed25519 key in canonical unpadded base64url\");\n\t\t}\n\t\tif (!isIntegerIn(key[\"firstEpoch\"], 0, UINT32_MAX)) {\n\t\t\treturn issue(`${at}.firstEpoch`, \"firstEpoch must be an unsigned 32-bit integer\");\n\t\t}\n\t\t// A repeated kid or firstEpoch leaves no key set (7.2.6): fail the whole\n\t\t// block, never fall back to unauthenticated reception.\n\t\tif (kids.has(key[\"kid\"])) return issue(`${at}.kid`, `kid ${key[\"kid\"]} is repeated; the catalog holds no key set`);\n\t\tif (firstEpochs.has(key[\"firstEpoch\"])) {\n\t\t\treturn issue(`${at}.firstEpoch`, `firstEpoch ${key[\"firstEpoch\"]} is repeated; the catalog holds no key set`);\n\t\t}\n\t\tkids.add(key[\"kid\"]);\n\t\tfirstEpochs.add(key[\"firstEpoch\"]);\n\t}\n\tif (!isIntegerIn(auth[\"minEpoch\"], 0, UINT32_MAX)) {\n\t\treturn issue(\".minEpoch\", \"minEpoch must be an unsigned 32-bit integer\");\n\t}\n\treturn [];\n}\n\nfunction endpointAuthIssue(\n\tendpoint: JsonObject,\n\tmode: MulticastJoinMode,\n\tat: string,\n): { path: string; message: string } | undefined {\n\tconst auth = endpoint[\"auth\"];\n\tif (!isObject(auth)) return { path: `${at}.auth`, message: \"auth must be an object\" };\n\tconst unknown = Object.keys(auth).filter(key => !ENDPOINT_AUTH_MEMBERS.has(key));\n\tif (unknown.length > 0) return { path: `${at}.auth`, message: `auth has unknown member(s) ${unknown.join(\", \")}` };\n\tif (!isIntegerIn(auth[\"streamId\"], 0, UINT32_MAX)) {\n\t\treturn { path: `${at}.auth.streamId`, message: \"streamId must be an unsigned 32-bit integer\" };\n\t}\n\tif (mode === \"ssm\" && auth[\"senderAddress\"] !== undefined) {\n\t\treturn { path: `${at}.auth.senderAddress`, message: \"senderAddress must not appear on an SSM endpoint; S is sourceAddress\" };\n\t}\n\tif (mode === \"asm\" && !parseIpAddressLiteral(auth[\"senderAddress\"])) {\n\t\treturn { path: `${at}.auth.senderAddress`, message: \"a protected ASM endpoint requires senderAddress, an RFC 3986 IP address literal\" };\n\t}\n\t// maxAuthDelayMs never ignores the endpoint (clamped or absent) and\n\t// manifestTrack is absent when it is not a string: endpointAuthTiming.\n\tif (endpointAuthTiming(auth).manifestTimerMs < 1) {\n\t\treturn { path: `${at}.auth.manifestTimerMs`, message: \"manifestTimerMs must be at least 1\" };\n\t}\n\tif (!isIntegerIn(endpoint[\"bandwidth\"], 1, Number.MAX_SAFE_INTEGER)) {\n\t\treturn { path: `${at}.bandwidth`, message: \"a protected endpoint requires bandwidth, which bounds the receiver's hold queues\" };\n\t}\n\treturn undefined;\n}\n\n/**\n * Where a catalog object carries the namespace of the catalog track it was\n * read from. A symbol, so it is never wire data and survives object spread;\n * `Symbol.for`, so two bundled copies of this module agree on it.\n */\nexport const CATALOG_NAMESPACE: unique symbol = Symbol.for(\"@blockcast/mmt-base/catalogNamespace\");\n\n/**\n * `catalog` tagged with the namespace of its catalog track. Every receiver\n * rule that takes the catalog (`multicastCatalogIssues`,\n * `admittedMulticastEndpoints` and the transport selectors built on them)\n * then judges the foreign-namespace rule against it, independent of track\n * order. Tag once, where the catalog track is read.\n */\nexport function withCatalogNamespace<T extends object>(catalog: T, namespace: string): T {\n\treturn { ...catalog, [CATALOG_NAMESPACE]: namespace };\n}\n\n/**\n * Every draft -02 4.1 / 7.2.6 violation in `catalog.multicast`, in document\n * order. Packet-id, group-address and track-existence rules are not here; the\n * container validator and the receivers already check those.\n *\n * `catalogNamespace` is the namespace of the catalog track, from `options` or else the\n * catalog's `withCatalogNamespace` tag. Without it the\n * foreign-namespace rule can only compare listed tracks with each other: the\n * first listed track's namespace becomes the reference, so which side of a\n * disagreement is dropped depends on order. It still fails closed; pass\n * `catalogNamespace` wherever the catalog track is known.\n */\nexport function multicastCatalogIssues(\n\tcatalog: unknown,\n\toptions: { catalogNamespace?: string } = {},\n): MulticastCatalogIssue[] {\n\tif (!isObject(catalog)) return [];\n\tconst issues: MulticastCatalogIssue[] = [];\n\tif (\"multicast.auth\" in catalog) {\n\t\t// Fail closed: a draft -00 publisher meant the flow to be protected, and\n\t\t// the key it names verifies nothing in bc-provenance version 2.\n\t\tissues.push({\n\t\t\tcode: \"multicast_auth\",\n\t\t\tpath: '$[\"multicast.auth\"]',\n\t\t\tmessage: 'the root \"multicast.auth\" key is retired; declare auth as multicast.auth (draft -02 7.2.6)',\n\t\t});\n\t}\n\tconst multicast = catalog[\"multicast\"];\n\tif (!isObject(multicast)) return issues;\n\tconst protectedCatalog = multicast[\"auth\"] !== undefined;\n\tif (protectedCatalog) issues.push(...authIssues(multicast[\"auth\"]));\n\tconst endpoints = multicast[\"endpoints\"];\n\tif (!Array.isArray(endpoints)) return issues;\n\n\tconst tracks = Array.isArray(catalog[\"tracks\"]) ? catalog[\"tracks\"] : [];\n\tconst namespaceOf = new Map<string, unknown>();\n\tfor (const track of tracks) {\n\t\tif (isObject(track) && typeof track[\"name\"] === \"string\") namespaceOf.set(track[\"name\"], track[\"namespace\"]);\n\t}\n\tconst tagged = (catalog as { [CATALOG_NAMESPACE]?: unknown })[CATALOG_NAMESPACE];\n\tlet referenceNamespace = options.catalogNamespace ?? (typeof tagged === \"string\" ? tagged : undefined);\n\n\tconst sources: Array<MulticastEndpointSource | undefined> = [];\n\tfor (let index = 0; index < endpoints.length; index++) {\n\t\tconst endpoint = endpoints[index];\n\t\tconst at = `$.multicast.endpoints[${index}]`;\n\t\tconst fail = (code: MulticastCatalogIssueCode, path: string, message: string) => {\n\t\t\tissues.push({ code, path, message, endpointIndex: index });\n\t\t};\n\t\tsources.push(undefined);\n\t\tif (!isObject(endpoint)) {\n\t\t\tfail(\"multicast_endpoint\", at, \"endpoint must be an object\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst protocol = endpoint[\"protocol\"];\n\t\tif (protocol !== undefined && protocol !== \"ssm\" && protocol !== \"asm\") {\n\t\t\tfail(\"multicast_endpoint\", `${at}.protocol`, `protocol must be \"ssm\" or \"asm\"; a bearer such as ${JSON.stringify(protocol)} is a networkSource entry (draft -02 4.2)`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst hasAuth = endpoint[\"auth\"] !== undefined;\n\t\tif (protectedCatalog !== hasAuth) {\n\t\t\tfail(\n\t\t\t\t\"multicast_endpoint_auth\",\n\t\t\t\t`${at}.auth`,\n\t\t\t\tprotectedCatalog\n\t\t\t\t\t? \"every endpoint requires auth when multicast carries auth; a receiver ignores this endpoint\"\n\t\t\t\t\t: \"auth names no key without multicast.auth; a receiver ignores this endpoint\",\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\t\t// 7.2.6: addresses are RFC 3986 IP literals, never names; 4.1: S and G\n\t\t// of one family, or the endpoint's fields are mutually inconsistent.\n\t\tconst sourceAddress = endpoint[\"sourceAddress\"];\n\t\tconst sourceBytes = sourceAddress === undefined ? undefined : parseIpAddressLiteral(sourceAddress);\n\t\tif (sourceAddress !== undefined && !sourceBytes) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.sourceAddress`, \"sourceAddress must be an RFC 3986 IPv4address or IPv6address literal\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst groupBytes = parseIpAddressLiteral(endpoint[\"groupAddress\"]);\n\t\tif (!groupBytes) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.groupAddress`, \"groupAddress must be an RFC 3986 IPv4address or IPv6address literal\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (sourceBytes && sourceBytes.length !== groupBytes.length) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.sourceAddress`, \"sourceAddress and groupAddress are of different address families\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode: MulticastJoinMode = sourceAddress === undefined ? \"asm\" : \"ssm\";\n\t\tif (protocol !== undefined && protocol !== mode) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.protocol`, `protocol ${JSON.stringify(protocol)} disagrees with sourceAddress, which selects ${mode}`);\n\t\t\tcontinue;\n\t\t}\n\t\t// The (S, G, port) rule below compares ports with ===, so only an\n\t\t// integer may reach it: \"5001\" and 5001 would otherwise both pass.\n\t\tif (!isIntegerIn(endpoint[\"port\"], 1, 65535)) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.port`, \"port must be an integer from 1 to 65535\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (hasAuth) {\n\t\t\tconst authIssue = endpointAuthIssue(endpoint, mode, at);\n\t\t\tif (authIssue) {\n\t\t\t\tfail(\"multicast_endpoint_auth\", authIssue.path, authIssue.message);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tconst source = multicastEndpointSource(endpoint);\n\t\tconst senderBytes = source?.mode === \"asm\" && source.source !== undefined ? parseIpAddressLiteral(source.source) : undefined;\n\t\tif (senderBytes && senderBytes.length !== groupBytes.length) {\n\t\t\tfail(\"multicast_endpoint_auth\", `${at}.auth.senderAddress`, \"senderAddress and groupAddress are of different address families\");\n\t\t\tcontinue;\n\t\t}\n\t\tsources[index] = source;\n\n\t\tconst refs = Array.isArray(endpoint[\"tracks\"]) ? endpoint[\"tracks\"] : [];\n\t\tfor (let refIndex = 0; refIndex < refs.length; refIndex++) {\n\t\t\tconst ref = refs[refIndex];\n\t\t\tconst name = isObject(ref) ? ref[\"name\"] : ref;\n\t\t\tif (typeof name !== \"string\") continue;\n\t\t\tconst namespace = namespaceOf.get(name);\n\t\t\tif (typeof namespace !== \"string\") continue;\n\t\t\treferenceNamespace ??= namespace;\n\t\t\tif (namespace !== referenceNamespace) {\n\t\t\t\tfail(\"multicast_track\", `${at}.tracks[${refIndex}].name`, `track ${JSON.stringify(name)} declares namespace ${JSON.stringify(namespace)}, not the catalog track's ${JSON.stringify(referenceNamespace)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// One endpoint object per (S, G, port) (4.1), and every protected endpoint\n\t// sharing its (S, G, port) with another is ignored (7.2.6), so both sides\n\t// are named. The key is the parsed addresses, never their spelling: an\n\t// address of one family never matches the other (an IPv4-mapped address\n\t// is IPv6), and \"232.0.0.1\" and \"ff3e::1\" never collide with each other.\n\tconst addressKey = (text: unknown) => {\n\t\tconst bytes = text === undefined ? undefined : parseIpAddressLiteral(text);\n\t\treturn bytes ? `${bytes.length}:${bytes.join(\".\")}` : \"-\";\n\t};\n\tconst tupleOf = (index: number) => {\n\t\tconst endpoint = endpoints[index] as JsonObject;\n\t\treturn `${addressKey(sources[index]!.source)}|${addressKey(endpoint[\"groupAddress\"])}|${endpoint[\"port\"]}`;\n\t};\n\tfor (let index = 0; index < endpoints.length; index++) {\n\t\tif (!sources[index]) continue;\n\t\tconst own = tupleOf(index);\n\t\tfor (let other = 0; other < endpoints.length; other++) {\n\t\t\tif (other === index || !sources[other] || tupleOf(other) !== own) continue;\n\t\t\tissues.push({ code: \"multicast_endpoint\", path: `$.multicast.endpoints[${index}].port`, message: `(S, G, port) is shared with endpoint ${other}; one endpoint object per multicast tuple`, endpointIndex: index });\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn issues;\n}\n\n/**\n * Indices of the endpoints a receiver may use: none when a catalog-level\n * issue leaves it no key set (7.2.6), otherwise every endpoint no issue\n * names. Other receiver checks (group address, packet ids) still apply.\n */\nexport function admittedMulticastEndpoints(\n\tcatalog: unknown,\n\toptions: { catalogNamespace?: string } = {},\n): ReadonlySet<number> {\n\tconst multicast = isObject(catalog) ? catalog[\"multicast\"] : undefined;\n\tconst endpoints = isObject(multicast) && Array.isArray(multicast[\"endpoints\"]) ? multicast[\"endpoints\"] : [];\n\tconst issues = multicastCatalogIssues(catalog, options);\n\tif (issues.some(issue => issue.endpointIndex === undefined)) return new Set();\n\tconst ignored = new Set(issues.map(issue => issue.endpointIndex));\n\treturn new Set(endpoints.map((_, index) => index).filter(index => !ignored.has(index)));\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", "// Module-local ambient: `multicastFromEnv` reads `process.env` when present\n// (Node, FFI host) and falls back to `{}` in browser contexts. The package\n// targets both, so we don't pull in `@types/node` org-wide \u2014 declaring the\n// minimal surface we touch keeps the dependency graph tight and the\n// published .d.ts free of node-isms.\ndeclare const process: { env: Record<string, string | undefined> } | undefined;\n\n/**\n * Multicast catalog input contract \u2014 validator + builder.\n *\n * Per draft-ramadan-moq-multicast-01 \u00A73 + \u00A74.1 + \u00A77.2.6\n * (Blockcast/moqcast-draft@fd962fd73d21ed281b60219c11ea61cc9e4e91c4), the publisher (FFmpeg,\n * cast bridge, GStreamer, OBS-bridge) populates `multicast.endpoints[]`\n * in the catalog when its output includes a UDP multicast destination.\n *\n * This module is the TypeScript-side validator over a versioned input\n * shape. Sister-language validators (FFmpeg C, cast Rust) target the\n * same invariants \u2014 eventually via a shared JSON Schema + fixtures\n * (BLO-3341 follow-up); today parity is enforced by code review against\n * 1:1 ports. Each publisher's integration MUST also assert that the\n * declared `(sourceAddress, groupAddress, port)` matches what it\n * actually emits \u2014 that's a separate runtime check the helper cannot\n * perform.\n *\n * Validation invariants (per codex review of BLO-3323 v2):\n * - The emitted mode is the one `sourceAddress` selects (present: SSM,\n * absent: ASM), restated as `protocol`. Input `protocol` \"ssm\" requires\n * `sourceAddress`, \"asm\" forbids it.\n * - Input `protocol` \"amt\" / \"atsc3\" names a bearer, which draft -01\n * carries as a `networkSource` entry, not a protocol: the `networkSource`\n * entries of that type MOVE onto each endpoint that named it (so they\n * no longer apply to native endpoints) and the protocol is never emitted.\n * An entry of that type is REQUIRED: an AMT one's `relay` only for\n * `discovery: \"manual\"` (\"driad\" resolves it at runtime), an ATSC3 one\n * carries the only tuning information an ATSC3 receiver has. A\n * per-endpoint `relayAddress` input is LIFTED into the AMT entry.\n * - Native \"ssm\" | \"asm\" requires no `networkSource` entry: a native join\n * describes no alternate bearer (BLO-37926).\n * - `auth` (catalog) and endpoint `auth` follow draft -02 7.2.6 through the\n * shared `multicastCatalogIssues` of `@blockcast/mmt-base`: all or\n * nothing, `senderAddress` on ASM only, bandwidth required, addresses\n * RFC 3986 IP literals, and one endpoint per `(S, G, port)` (the flow's\n * records ride in band on its `port`, 7.2.3). A receiver clamps a\n * `maxAuthDelayMs` outside 50..1000; this emitter never writes one, nor a\n * non-integer timer or a non-string `manifestTrack`.\n * - groupAddress is a valid IPv4 multicast group (224.0.0.0/4)\n * or IPv6 multicast group (ff00::/8)\n * - SSM groupAddress MUST be in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6),\n * where the IPv6 form requires P=1 + T=1 (the `3` nibble) AND the\n * 16-bit reserved-zero second hextet per RFC 3306 \u00A74 / RFC 4607.\n * - port in 1..65535\n * - tracks non-empty; per-track packetId in 1..65535\n * - packetId 0 (`MMTP_SIGNALING_PACKET_ID`) is the MMTP signaling flow\n * (FFmpeg `moq_mmt` sends the AL-FEC message 0x0203 on it into the same\n * multicast group) and is refused for a track with its own kind,\n * `signaling_packet_id`. The kind string matches cast's\n * `multicast_input.rs` so the two ports report the same reason.\n * - packetIds unique within an endpoint\n * - (S, groupAddress, port) tuple unique across endpoints, S being\n * sourceAddress (SSM), auth.senderAddress (protected ASM) or absent\n * - bandwidth (if present) > 0\n * - validUntil (if present) is a positive unix-ms timestamp\n *\n * @see draft-ramadan-moq-multicast-01 \u00A74.1, \u00A77.2.6\n * @see .planning/drafts/blo-3323-multicast-catalog-source-of-truth.md\n */\n\nimport { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID, multicastCatalogIssues } from \"@blockcast/mmt-base\";\nimport {\n\ttype AmtDiscovery,\n\ttype MulticastCatalogConfig,\n\ttype MulticastEndpoint,\n\ttype MulticastTrackObject,\n\ttype NetworkSource,\n} from \"./carp-catalog.js\";\nimport type { EndpointAuthConfig, MulticastAuthConfig } from \"./multicast-auth-catalog.js\";\n\n/**\n * Publisher input protocol. \"ssm\" / \"asm\" restate the mode `sourceAddress`\n * selects; \"amt\" / \"atsc3\" (draft -00) name a bearer and are mapped onto an\n * endpoint-level `networkSource`, never emitted.\n */\nexport type MulticastInputProtocol = \"ssm\" | \"asm\" | \"amt\" | \"atsc3\";\n\n/**\n * Strict typed input for one multicast endpoint. The publisher provides\n * this shape (env vars / CLI / config record), the helper validates and\n * lifts it into a `MulticastEndpoint` ready to embed in the catalog.\n */\nexport interface MulticastEndpointInput {\n\treadonly protocol: MulticastInputProtocol;\n\treadonly sourceAddress?: string;\n\treadonly groupAddress: string;\n\treadonly port: number;\n\t/** New emitters MUST use the object form (name + packetId). */\n\treadonly tracks: ReadonlyArray<MulticastTrackObject>;\n\t/**\n\t * AMT relay for this endpoint.\n\t *\n\t * Publisher-side input only \u2014 it is LIFTED into the catalog-level\n\t * `multicast.networkSource` and is never written onto the emitted endpoint\n\t * (the normative endpoint model carries no relay field). Because\n\t * `networkSource` is catalog-level, a relay that differs from the catalog's\n\t * AMT relay cannot be expressed and is rejected with `conflicting_amt_relay`.\n\t *\n\t * Prefer declaring `MulticastInput.networkSource` directly.\n\t */\n\treadonly relayAddress?: string;\n\t/** Aggregate bandwidth in bits per second. RECOMMENDED per draft \u00A74.1; REQUIRED with `auth`. */\n\treadonly bandwidth?: number;\n\t/** Protected-flow parameters (draft -02 7.2.6); REQUIRED exactly when `MulticastInput.auth` is set. */\n\treadonly auth?: EndpointAuthConfig;\n}\n\n/**\n * Strict typed input for the catalog-level multicast extension.\n *\n * `version` (monotonic) + `validUntil` (unix ms) support the catalog\n * freshness model: subscribers refetch before expiry, and publishers\n * MUST republish (with bumped `version`) on any endpoint mutation.\n */\nexport interface MulticastInput {\n\treadonly enabled?: boolean;\n\treadonly endpoints: ReadonlyArray<MulticastEndpointInput>;\n\t/**\n\t * Canonical relay/tuning declaration (draft-ramadan-moq-multicast \u00A74.1).\n\t * When omitted, one is derived from `amtFallback` and any endpoint\n\t * `relayAddress`; when present no derivation happens. Either way every entry\n\t * is validated, and an endpoint `relayAddress` it does not carry is\n\t * rejected (`conflicting_amt_relay`). Omit it entirely for native\n\t * multicast: it describes alternate bearers only (BLO-37926).\n\t */\n\treadonly networkSource?: ReadonlyArray<NetworkSource>;\n\t/** @deprecated Lifted into `networkSource`; no longer emitted. */\n\treadonly amtFallback?: { readonly discovery?: AmtDiscovery; readonly relayDns?: string };\n\t/** bc-provenance version 2 profile and keys (draft -01 7.2.6). */\n\treadonly auth?: MulticastAuthConfig;\n\treadonly version?: number;\n\treadonly validUntil?: number;\n}\n\nexport type MulticastInputErrorKind =\n\t| \"empty_endpoints\"\n\t/**\n\t * `endpoints` was not an array at all. Distinct from `empty_endpoints`,\n\t * which reports a well-formed array that declares nothing: a caller\n\t * handling these programmatically needs \"you passed the wrong shape\"\n\t * apart from \"you passed nothing\".\n\t */\n\t| \"invalid_endpoints\"\n\t| \"ssm_requires_source\"\n\t/** Input `protocol` \"asm\" with a `sourceAddress`, which selects SSM. */\n\t| \"protocol_source_mismatch\"\n\t/** `MulticastInput.auth` fails draft -02 7.2.6 (version, keys, a repeated kid or firstEpoch). */\n\t| \"invalid_multicast_auth\"\n\t/**\n\t * An endpoint's `auth` fails draft -02 7.2.6: present without\n\t * `MulticastInput.auth` or absent with it, `senderAddress` on SSM or\n\t * missing on ASM, a timer out of range or not an integer, a non-string\n\t * `manifestTrack`, or no `bandwidth`.\n\t */\n\t| \"invalid_endpoint_auth\"\n\t/**\n\t * Deliberately overloaded, unlike its one-site mirror\n\t * `atsc3_requires_tuning`: an AMT relay can be missing at two layers, and\n\t * both mean \"this endpoint names a relay nobody can reach\". Raised for an\n\t * `amt` endpoint with no `networkSource` entry of type `\"amt\"`, and for an\n\t * entry whose `discovery: \"manual\"` names no `relay`. Splitting them would\n\t * make callers handle two kinds for one operator-visible fault.\n\t */\n\t| \"amt_requires_relay\"\n\t/**\n\t * An `atsc3` endpoint with no `networkSource` entry of type `\"atsc3\"`. The\n\t * ATSC3 entry carries the frequency (kHz; PLP, service id and SLS URI are\n\t * optional) \u2014 the only tuning information there is \u2014 so without it the endpoint names a broadcast\n\t * nobody can tune to. The AMT mirror of this is `amt_requires_relay`; native\n\t * joins (\"ssm\", \"asm\") describe no bearer and are exempt (BLO-37926).\n\t */\n\t| \"atsc3_requires_tuning\"\n\t/**\n\t * An endpoint `relayAddress` is not the relay of any AMT `networkSource`\n\t * entry (another endpoint's relay, `amtFallback.relayDns`, or an explicit\n\t * entry won). `networkSource` is catalog-level, so per-endpoint relays\n\t * cannot be represented; fail closed rather than silently drop one.\n\t */\n\t| \"conflicting_amt_relay\"\n\t/**\n\t * A `networkSource` AMT entry's `discovery` was not `\"driad\"` or `\"manual\"`,\n\t * the schema enum. Checked on explicit and derived entries alike.\n\t */\n\t| \"invalid_amt_discovery\"\n\t/**\n\t * A `networkSource` entry the schema rejects: unknown `type`, a key outside\n\t * its `type`'s shape, a blank `relay`, or an out-of-range ATSC3 field.\n\t */\n\t| \"invalid_network_source\"\n\t| \"invalid_group_address\"\n\t| \"ssm_group_out_of_range\"\n\t| \"invalid_port\"\n\t| \"empty_tracks\"\n\t| \"invalid_track_name\"\n\t| \"invalid_packet_id\"\n\t/**\n\t * A track advertised on `MMTP_SIGNALING_PACKET_ID` (0), the MMTP signaling\n\t * flow. Distinct from `invalid_packet_id` so the reason is legible: 0 is a\n\t * well-formed 16-bit value, it is just not a track's to claim.\n\t */\n\t| \"signaling_packet_id\"\n\t| \"duplicate_packet_id\"\n\t| \"duplicate_endpoint_tuple\"\n\t| \"invalid_bandwidth\"\n\t| \"invalid_valid_until\"\n\t| \"invalid_version\"\n\t| \"invalid_env_shape\";\n\nexport class MulticastInputError extends Error {\n\tconstructor(\n\t\tpublic readonly kind: MulticastInputErrorKind,\n\t\tpublic readonly endpointIndex: number | null,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MulticastInputError\";\n\t}\n}\n\n/**\n * Validate a typed publisher input and lift it into a\n * `MulticastCatalogConfig` ready for `catalog.multicast = \u2026`.\n *\n * Throws `MulticastInputError` on any invariant violation. Fail-fast\n * at config-load time, never at runtime \u2014 silent fallbacks hide bugs\n * upstream (per the project's \"no anti-patterns\" directive).\n */\nexport function multicastFromConfig(input: MulticastInput): MulticastCatalogConfig {\n\t// Both array-valued inputs are checked here, before anything indexes them.\n\t// These helpers are consumed out-of-tree from plain JS (see\n\t// `validateNetworkSourceEntry`), so the declared types are no guarantee \u2014\n\t// and object-instead-of-array is the likely mistake on both: `amtFallback`,\n\t// which `networkSource` replaces, was a bare object. Without these a wrong\n\t// shape escapes as a raw `TypeError` from `.map`, which a caller catching\n\t// `MulticastInputError` does not catch.\n\tif (!Array.isArray(input.endpoints)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_endpoints\",\n\t\t\tnull,\n\t\t\t`endpoints must be an array of endpoint objects; got ${typeof input.endpoints}`,\n\t\t);\n\t}\n\tif (input.networkSource !== undefined && !Array.isArray(input.networkSource)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_network_source\",\n\t\t\tnull,\n\t\t\t\"networkSource must be an array of entries; got \" +\n\t\t\t\t`${typeof input.networkSource} \u2014 a single entry still goes in a one-element array`,\n\t\t);\n\t}\n\tif (input.endpoints.length === 0) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"empty_endpoints\",\n\t\t\tnull,\n\t\t\t\"endpoints[] must be non-empty \u2014 a multicast field with zero endpoints declares nothing\",\n\t\t);\n\t}\n\tif (input.version !== undefined && (!Number.isInteger(input.version) || input.version < 0)) {\n\t\tthrow new MulticastInputError(\"invalid_version\", null, `version must be a non-negative integer; got ${input.version}`);\n\t}\n\tif (input.validUntil !== undefined) {\n\t\tif (!Number.isFinite(input.validUntil) || input.validUntil <= 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_valid_until\",\n\t\t\t\tnull,\n\t\t\t\t`validUntil must be a positive unix-ms timestamp; got ${input.validUntil}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst endpoints: MulticastEndpoint[] = input.endpoints.map((ep, idx) => validateEndpoint(ep, idx));\n\n\tconst out: MulticastCatalogConfig = { endpoints };\n\tconst { entries: networkSource, relayFrom } = resolveNetworkSource(input);\n\t// Checked on the resolved array, so an explicit `input.networkSource` gets\n\t// exactly the same checks as one derived from the deprecated inputs.\n\tnetworkSource.forEach(validateNetworkSourceEntry);\n\tif (networkSource.length > 0) out.networkSource = networkSource;\n\n\t// A relay the emitted catalog would not carry fails closed, on both paths:\n\t// `networkSource` is catalog-level, so a per-endpoint relay that differs from\n\t// it (another endpoint's, `amtFallback.relayDns`, or an explicit entry) would\n\t// otherwise be dropped silently and that endpoint joins the wrong relay.\n\t// A DRIAD entry carries no relay (`validateNetworkSourceEntry`).\n\tconst amtRelays = new Set(\n\t\tnetworkSource\n\t\t\t.flatMap((s) => (s.type === \"amt\" && s.relay?.trim() ? [s.relay.trim()] : []))\n\t\t\t// Only the first, because it is the only one either reader can\n\t\t\t// deliver: `catalogAmtNetworkSource` returns on it and `findAmtRelay`\n\t\t\t// uses `.find`. Collecting every entry accepts an endpoint naming a\n\t\t\t// second manual entry, which then joins the first -- the same silent\n\t\t\t// wrong-relay join this guard exists to prevent. It also keeps the `(...)`\n\t\t\t// listing below honest. Sliced rather than `.find`ed so the `amt`\n\t\t\t// narrowing stays inside the callback.\n\t\t\t.slice(0, 1),\n\t);\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tconst relay = ep.relayAddress?.trim();\n\t\tif (!relay || amtRelays.has(relay)) return;\n\t\t// The entry is written down but undeliverable for its position: both\n\t\t// readers join the first AMT entry. Reachable only on the explicit\n\t\t// path -- `resolveNetworkSource` emits at most one entry -- so there is\n\t\t// no provenance clause to add, and saying \"is not the relay of any AMT\n\t\t// entry\" here would be plainly false: the operator can see the entry.\n\t\t// `amtRelays` holds only the first joinable relay, so reaching this\n\t\t// throw at all already means this one is not it.\n\t\tconst shadowed = networkSource.some(\n\t\t\t(s) => s.type === \"amt\" && s.relay?.trim() === relay,\n\t\t);\n\t\t// The derived and env paths never wrote `networkSource` at all -- it was\n\t\t// synthesised in `resolveNetworkSource` -- so name the input that\n\t\t// supplied the relay. Read that source from `relayFrom` rather than\n\t\t// re-inferring it: three review rounds found a local re-inference here\n\t\t// drifting from the rule above it.\n\t\tconst fromAmtFallback = \", derived from amtFallback/MOQCAST_MULTICAST_AMT_*\";\n\t\tconst fromRelay =\n\t\t\trelayFrom === \"amtFallback.relayDns\"\n\t\t\t\t? fromAmtFallback\n\t\t\t\t: relayFrom === \"endpoint.relayAddress\"\n\t\t\t\t\t? \", derived from the endpoint relayAddress values\"\n\t\t\t\t\t: \"\";\n\t\t// `resolveNetworkSource` lets `relayDns` outrank an endpoint relay, so on\n\t\t// the derived path the set can be non-empty and still not hold this\n\t\t// relay. Blaming per-endpoint representation there is wrong twice over:\n\t\t// there may be only one endpoint, and the field that actually won is\n\t\t// `relayDns`, which is the one the operator can act on.\n\t\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\t\tconst outranked = relayFrom === \"amtFallback.relayDns\" && relayDns !== relay;\n\t\tthrow new MulticastInputError(\n\t\t\t\"conflicting_amt_relay\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] relayAddress ${JSON.stringify(relay)} ` +\n\t\t\t\t(shadowed\n\t\t\t\t\t? `is carried by a multicast.networkSource AMT entry that is not the first one: ` +\n\t\t\t\t\t\t`both readers join the first AMT entry (${[...amtRelays].join(\", \")}) and ignore ` +\n\t\t\t\t\t\t\"the rest, so this relay is never joined; keep a single AMT entry, or put this one first\"\n\t\t\t\t\t: `is not the relay of any multicast.networkSource AMT entry ` +\n\t\t\t\t\t\t`(${[...amtRelays].join(\", \") || \"none\"})${fromRelay}; ` +\n\t\t\t\t\t\t(outranked\n\t\t\t\t\t\t\t? `amtFallback.relayDns/MOQCAST_MULTICAST_AMT_RELAY_DNS (${relayDns}) outranks an ` +\n\t\t\t\t\t\t\t\t\"endpoint relay when networkSource is derived -- drop one, or make them agree\"\n\t\t\t\t\t\t\t: \"networkSource is catalog-level and cannot represent per-endpoint relays\")),\n\t\t);\n\t});\n\n\t// A bearer endpoint must name its bearer. The two bearer protocols are\n\t// exactly the two `networkSource` types, so an endpoint's own protocol names\n\t// the entry it requires. Native joins (\"ssm\", \"asm\") describe no bearer and\n\t// are exempt (BLO-37926) \u2014 the endpoint tuple is the whole join.\n\t// Whether an AMT entry needs a relay string (\"manual\" does, \"driad\" does\n\t// not) is per-entry, checked above.\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tif (ep.protocol !== \"amt\" && ep.protocol !== \"atsc3\") return;\n\t\tif (networkSource.some((s) => s.type === ep.protocol)) return;\n\t\tthrow new MulticastInputError(\n\t\t\tep.protocol === \"amt\" ? \"amt_requires_relay\" : \"atsc3_requires_tuning\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=${ep.protocol} requires a multicast.networkSource entry of ` +\n\t\t\t\t`type \"${ep.protocol}\"` +\n\t\t\t\t(ep.protocol === \"atsc3\"\n\t\t\t\t\t? \" carrying at least its frequency (kHz) \u2014 an ATSC3 endpoint is not tunable without it\"\n\t\t\t\t\t: \"\"),\n\t\t);\n\t});\n\n\t// Draft -01 4.2: a bearer is a networkSource entry. An endpoint that named\n\t// one as its protocol gets that type's entries as its own networkSource.\n\t// They are copied, not moved: the catalog-level array already applied to\n\t// every endpoint under -00 readers, and still does under -01.\n\tinput.endpoints.forEach((ep, idx) => {\n\t\tif (ep.protocol !== \"amt\" && ep.protocol !== \"atsc3\") return;\n\t\tendpoints[idx]!.networkSource = networkSource.filter((s) => s.type === ep.protocol).map((s) => ({ ...s }));\n\t});\n\n\tif (input.auth !== undefined) out.auth = input.auth;\n\tconst [issue] = multicastCatalogIssues({ multicast: out });\n\tif (issue) {\n\t\tconst kind: MulticastInputErrorKind =\n\t\t\tissue.code === \"multicast_auth\"\n\t\t\t\t? \"invalid_multicast_auth\"\n\t\t\t\t: issue.code === \"multicast_endpoint_auth\"\n\t\t\t\t\t? \"invalid_endpoint_auth\"\n\t\t\t\t\t: issue.path.endsWith(\".port\")\n\t\t\t\t\t\t? \"duplicate_endpoint_tuple\"\n\t\t\t\t\t\t: \"protocol_source_mismatch\";\n\t\tconst where = issue.endpointIndex === undefined ? \"\" : `endpoint[${issue.endpointIndex}] `;\n\t\tthrow new MulticastInputError(kind, issue.endpointIndex ?? null, `${where}${issue.path.replace(\"$.multicast.\", \"\")}: ${issue.message}`);\n\t}\n\t// What a receiver clamps or reads as absent (7.2.6) a publisher still never emits.\n\tendpoints.forEach((ep, idx) => {\n\t\tconst auth = (ep as { auth?: Record<string, unknown> }).auth;\n\t\tif (auth === undefined) return;\n\t\tconst delay = auth[\"maxAuthDelayMs\"];\n\t\tconst timer = auth[\"manifestTimerMs\"];\n\t\tconst track = auth[\"manifestTrack\"];\n\t\tconst bad =\n\t\t\tdelay !== undefined && !(Number.isSafeInteger(delay) && (delay as number) >= 50 && (delay as number) <= 1000)\n\t\t\t\t? \"maxAuthDelayMs must be an integer from 50 to 1000\"\n\t\t\t\t: timer !== undefined && !(Number.isSafeInteger(timer) && (timer as number) >= 1)\n\t\t\t\t\t? \"manifestTimerMs must be an integer of at least 1\"\n\t\t\t\t\t: track !== undefined && !(typeof track === \"string\" && track.length > 0)\n\t\t\t\t\t\t? \"manifestTrack must be a non-empty string\"\n\t\t\t\t\t\t: undefined;\n\t\tif (bad) throw new MulticastInputError(\"invalid_endpoint_auth\", idx, `endpoint[${idx}] auth: ${bad}`);\n\t});\n\n\tif (input.enabled !== undefined) out.enabled = input.enabled;\n\tif (input.version !== undefined) out.version = input.version;\n\tif (input.validUntil !== undefined) out.validUntil = input.validUntil;\n\treturn out;\n}\n\n/**\n * Build the catalog-level `multicast.networkSource` for this input.\n *\n * An explicit `input.networkSource` is authoritative and used as given.\n * Otherwise one AMT entry is derived from the deprecated inputs it replaces:\n * `amtFallback.discovery`/`relayDns` and any endpoint `relayAddress`.\n * `discovery` is copied only when given: draft-ramadan-moq-multicast-01 4.2.1\n * reads an omitted `discovery` with a hostname `relay` as `\"dns\"`, so no\n * default is written. Endpoint relays that disagree with the result\n * are rejected by `multicastFromConfig`, not resolved here.\n *\n * `relayFrom` reports which deprecated input supplied the derived relay, so\n * that rejection can name a field the operator actually set instead of\n * re-deciding the precedence below at the throw site. It is `null` on the\n * explicit path and when no relay was derived at all.\n */\nfunction resolveNetworkSource(input: MulticastInput): {\n\tentries: NetworkSource[];\n\trelayFrom: \"amtFallback.relayDns\" | \"endpoint.relayAddress\" | null;\n} {\n\tif (input.networkSource !== undefined) {\n\t\treturn { entries: input.networkSource.map((s) => ({ ...s })), relayFrom: null };\n\t}\n\n\tconst relayDns = input.amtFallback?.relayDns?.trim();\n\tconst relay = relayDns || input.endpoints.map((ep) => ep.relayAddress?.trim()).find((r) => !!r);\n\tconst discovery = input.amtFallback?.discovery;\n\tif (discovery === undefined && !relay) return { entries: [], relayFrom: null };\n\treturn {\n\t\tentries: [{ type: \"amt\", ...(discovery ? { discovery } : {}), ...(relay ? { relay } : {}) }],\n\t\trelayFrom: !relay ? null : relayDns ? \"amtFallback.relayDns\" : \"endpoint.relayAddress\",\n\t};\n}\n\nconst AMT_SOURCE_KEYS = new Set([\"type\", \"discovery\", \"relay\", \"port\"]);\nconst ATSC3_SOURCE_KEYS = new Set([\"type\", \"frequency\", \"plpId\", \"serviceId\", \"slsUri\"]);\n\n/**\n * One `networkSource[]` entry against `catalog.schema.json` `$defs/networkSource`\n * (and moq-rs `NetworkSource`), plus the one semantic rule the schema leaves\n * out: `discovery: \"dns\"` and `\"manual\"` name their relay. These helpers are consumed\n * out-of-tree from plain JS, so the `NetworkSource` type is no guarantee.\n */\nfunction validateNetworkSourceEntry(s: NetworkSource, i: number): void {\n\tconst err = (kind: MulticastInputErrorKind, why: string) =>\n\t\tnew MulticastInputError(kind, null, `networkSource[${i}] ${why}`);\n\tconst keys = s?.type === \"amt\" ? AMT_SOURCE_KEYS : s?.type === \"atsc3\" ? ATSC3_SOURCE_KEYS : undefined;\n\tif (!keys) throw err(\"invalid_network_source\", `type must be \"amt\" or \"atsc3\"; got ${JSON.stringify(s?.type)}`);\n\t// Definedness, not presence: `isMulticastCatalogConfig` in `carp-catalog.ts`\n\t// already chose `=== undefined` over `in` for exactly this reason, and the\n\t// two must agree on what \"set\" means. A key spread in as `undefined`\n\t// (`{...entry, relayDns: undefined}`) is dropped by `JSON.stringify`, so the\n\t// emitted catalog is clean and rejecting it would fail a shape that ships\n\t// correctly.\n\tconst extra = Object.keys(s).filter((k) => !keys.has(k) && (s as Record<string, unknown>)[k] !== undefined);\n\tif (extra.length > 0) throw err(\"invalid_network_source\", `has keys the schema rejects: ${extra.join(\", \")}`);\n\n\tif (s.type === \"amt\") {\n\t\tif (s.discovery !== undefined && s.discovery !== \"dns\" && s.discovery !== \"driad\" && s.discovery !== \"manual\") {\n\t\t\tthrow err(\n\t\t\t\t\"invalid_amt_discovery\",\n\t\t\t\t`discovery must be \"dns\", \"driad\" or \"manual\" when present; got ${JSON.stringify(s.discovery)}`,\n\t\t\t);\n\t\t}\n\t\tif (s.relay !== undefined) {\n\t\t\t// Canonicalize, don't just check: the conflict guard in\n\t\t\t// `multicastFromConfig` compares `.trim()`ed values, so a padded\n\t\t\t// explicit relay would pass the guard and then ship padded. The\n\t\t\t// derived path (`resolveNetworkSource`) already trims, so trimming\n\t\t\t// here is what makes both paths emit the same canonical string.\n\t\t\t// Entries are copies by then, so this cannot mutate caller input.\n\t\t\tconst relay = typeof s.relay === \"string\" ? s.relay.trim() : \"\";\n\t\t\tif (!relay) {\n\t\t\t\tthrow err(\"invalid_network_source\", `relay must be non-blank when present; got ${JSON.stringify(s.relay)}`);\n\t\t\t}\n\t\t\ts.relay = relay;\n\t\t}\n\t\tif ((s.discovery === \"dns\" || s.discovery === \"manual\") && s.relay === undefined) {\n\t\t\tthrow err(\"amt_requires_relay\", `discovery=\"${s.discovery}\" requires a relay (\"driad\" resolves it at runtime)`);\n\t\t}\n\t\tif (s.discovery === \"driad\" && s.relay !== undefined) {\n\t\t\tthrow err(\n\t\t\t\t\"invalid_network_source\",\n\t\t\t\t`discovery=\"driad\" must omit relay: DRIAD supplies the relay candidates (draft -01 4.2.1); ` +\n\t\t\t\t\t`drop the relay (amtFallback.relayDns / endpoint relayAddress on the derived path), or use \"dns\" or \"manual\"`,\n\t\t\t);\n\t\t}\n\t\tif (s.port !== undefined && (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535)) {\n\t\t\tthrow err(\"invalid_port\", `port must be 1..65535; got ${s.port}`);\n\t\t}\n\t\treturn;\n\t}\n\tconst inRange = (v: number, min: number, max: number) => Number.isInteger(v) && v >= min && v <= max;\n\tif (!inRange(s.frequency, 1, Number.MAX_SAFE_INTEGER)) {\n\t\tthrow err(\"invalid_network_source\", `frequency must be a positive integer in kHz; got ${s.frequency}`);\n\t}\n\t// Draft -01 4.2.2: only frequency (kHz) is REQUIRED; the rest are checked when set.\n\tif (s.plpId !== undefined && !inRange(s.plpId, 0, 63)) {\n\t\tthrow err(\"invalid_network_source\", `plpId must be 0..63; got ${s.plpId}`);\n\t}\n\tif (s.serviceId !== undefined && !inRange(s.serviceId, 0, 65535)) {\n\t\tthrow err(\"invalid_network_source\", `serviceId must be 0..65535; got ${s.serviceId}`);\n\t}\n\tif (s.slsUri !== undefined && (typeof s.slsUri !== \"string\" || !isAbsoluteUri(s.slsUri))) {\n\t\tthrow err(\"invalid_network_source\", `slsUri must be an absolute URI; got ${JSON.stringify(s.slsUri)}`);\n\t}\n}\n\nfunction isAbsoluteUri(value: string): boolean {\n\ttry {\n\t\tnew URL(value);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction validateEndpoint(ep: MulticastEndpointInput, idx: number): MulticastEndpoint {\n\tif (ep.protocol === \"ssm\" && (!ep.sourceAddress || ep.sourceAddress.length === 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_requires_source\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires sourceAddress`,\n\t\t);\n\t}\n\tif (ep.protocol === \"asm\" && ep.sourceAddress !== undefined) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"protocol_source_mismatch\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=asm with sourceAddress \"${ep.sourceAddress}\"; sourceAddress selects SSM (draft -01 4.1)`,\n\t\t);\n\t}\n\t// NOTE: `protocol: \"amt\"` no longer requires a per-endpoint relay here. The\n\t// relay is catalog-level (`multicast.networkSource`), which this function\n\t// cannot see; `multicastFromConfig` enforces it once both halves are in hand.\n\n\tif (!isMulticastGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_group_address\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] groupAddress \"${ep.groupAddress}\" is not a valid multicast address`,\n\t\t);\n\t}\n\tif (ep.protocol === \"ssm\" && !isSsmGroupAddress(ep.groupAddress)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"ssm_group_out_of_range\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] protocol=ssm requires groupAddress in 232.0.0.0/8 (IPv4) or ff3x::/32 (IPv6); got \"${ep.groupAddress}\"`,\n\t\t);\n\t}\n\n\tif (!Number.isInteger(ep.port) || ep.port < 1 || ep.port > 65535) {\n\t\tthrow new MulticastInputError(\"invalid_port\", idx, `endpoint[${idx}] port must be 1..65535; got ${ep.port}`);\n\t}\n\n\tif (ep.tracks.length === 0) {\n\t\tthrow new MulticastInputError(\"empty_tracks\", idx, `endpoint[${idx}] tracks must be non-empty`);\n\t}\n\n\tconst seenPids = new Set<number>();\n\tfor (const t of ep.tracks) {\n\t\tif (typeof t.name !== \"string\" || t.name.length === 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track name must be a non-empty string`,\n\t\t\t);\n\t\t}\n\t\tif (t.packetId === MMTP_SIGNALING_PACKET_ID) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"signaling_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId ${t.packetId} is the MMTP signaling flow ` +\n\t\t\t\t\t`and must not be advertised for a track; packetId must be 1..${MMTP_PACKET_ID_MAX}`,\n\t\t\t);\n\t\t}\n\t\tif (!Number.isInteger(t.packetId) || t.packetId < 1 || t.packetId > MMTP_PACKET_ID_MAX) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] track \"${t.name}\" packetId must be 1..${MMTP_PACKET_ID_MAX}; got ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tif (seenPids.has(t.packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"duplicate_packet_id\",\n\t\t\t\tidx,\n\t\t\t\t`endpoint[${idx}] duplicate packetId ${t.packetId}`,\n\t\t\t);\n\t\t}\n\t\tseenPids.add(t.packetId);\n\t}\n\n\tif (ep.bandwidth !== undefined && (!Number.isFinite(ep.bandwidth) || ep.bandwidth <= 0)) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_bandwidth\",\n\t\t\tidx,\n\t\t\t`endpoint[${idx}] bandwidth must be positive; got ${ep.bandwidth}`,\n\t\t);\n\t}\n\n\t// Tuple uniqueness is keyed on S, which needs `auth`; `multicastFromConfig`\n\t// checks it with the rest of draft -01 4.1 once the catalog is composed.\n\t// The emitted protocol restates the mode `sourceAddress` selects; a bearer\n\t// input protocol is never emitted.\n\tconst out: MulticastEndpoint = {\n\t\tprotocol: ep.sourceAddress === undefined ? \"asm\" : \"ssm\",\n\t\tgroupAddress: ep.groupAddress,\n\t\tport: ep.port,\n\t\ttracks: ep.tracks.map((t) => ({ name: t.name, packetId: t.packetId })),\n\t};\n\tif (ep.sourceAddress !== undefined) out.sourceAddress = ep.sourceAddress;\n\t// `relayAddress` is deliberately NOT copied: the relay is lifted to\n\t// catalog-level `multicast.networkSource` by `multicastFromConfig`.\n\tif (ep.bandwidth !== undefined) out.bandwidth = ep.bandwidth;\n\tif (ep.auth !== undefined) out.auth = { ...ep.auth };\n\treturn out;\n}\n\nfunction isMulticastGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// IPv6: ff00::/8. Prefix-only check; full IPv6 syntax validation\n\t\t// is delegated to the OS bind() call downstream.\n\t\treturn /^ff[0-9a-f]{2}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(addr);\n\tif (!m) return false;\n\tfor (let i = 1; i <= 4; i++) {\n\t\tconst v = Number(m[i]);\n\t\tif (!Number.isInteger(v) || v < 0 || v > 255) return false;\n\t}\n\tconst first = Number(m[1]);\n\treturn first >= 224 && first <= 239;\n}\n\nfunction isSsmGroupAddress(addr: string): boolean {\n\tif (addr.includes(\":\")) {\n\t\t// SSM IPv6: ff3x::/32 per RFC 3306 \u00A74. Requires P=1 + T=1 (the `3`\n\t\t// nibble) AND the second 16-bit hextet to be all-zero (RFC 3306's\n\t\t// \"prefix length = 0\" \u2192 next 32 bits are 0). The regex matches\n\t\t// `ff3?:0000:` collapsed forms (`ff3e::`, `ff3e:0::`, `ff3e:0:`,\n\t\t// `ff3e:0000:`) and rejects `ff3e:beef::1` etc.\n\t\treturn /^ff3[0-9a-f]:0{0,4}:/i.test(addr);\n\t}\n\tconst m = /^(\\d{1,3})\\./.exec(addr);\n\treturn m !== null && Number(m[1]) === 232;\n}\n\n// Env-var helper (BLO-3337): read MOQCAST_MULTICAST_* keys and route\n// through multicastFromConfig so all per-endpoint invariants apply.\n\n/**\n * Read `MOQCAST_MULTICAST_*` env vars and return a\n * `MulticastCatalogConfig`. Returns `undefined` when no per-endpoint\n * key is set.\n *\n * Env contract \u2014 per-endpoint keys take an optional non-negative index:\n *\n * MOQCAST_MULTICAST[_<idx>]_PROTOCOL=ssm\n * MOQCAST_MULTICAST[_<idx>]_GROUP=232.1.1.1\n * MOQCAST_MULTICAST[_<idx>]_PORT=5004\n * MOQCAST_MULTICAST[_<idx>]_SOURCE=10.0.0.20 (required for ssm)\n * MOQCAST_MULTICAST[_<idx>]_RELAY=amt.example.com (required for amt)\n * MOQCAST_MULTICAST[_<idx>]_TRACKS=video:1,audio:3\n * MOQCAST_MULTICAST[_<idx>]_BANDWIDTH=8000000 (optional, bps)\n *\n * MOQCAST_MULTICAST_AMT_DISCOVERY=dns (catalog-level)\n * MOQCAST_MULTICAST_AMT_RELAY_DNS=relay.example.com (catalog-level;\n * must be unset with discovery=driad)\n *\n * The unindexed form (`MOQCAST_MULTICAST_GROUP`) is exactly index 0;\n * mixing it with explicit `_0_` keys is rejected as ambiguous. Indices\n * need not be contiguous; observed indices are sorted ascending.\n */\nexport function multicastFromEnv(\n\tenv: Record<string, string | undefined> = ((globalThis as { process?: { env?: Record<string, string | undefined> } })\n\t\t.process?.env ?? {}) as Record<string, string | undefined>,\n): MulticastCatalogConfig | undefined {\n\tconst indices = collectMulticastIndices(env);\n\t// An empty value is unset: `FOO=${BAR}` with BAR unset, or an empty\n\t// configMap value, is a templating outcome, not a setting. Normalising\n\t// here gives every test below one notion of \"set\". Stricter than SOURCE\n\t// and RELAY, which gate on truthiness alone and so keep a whitespace-only\n\t// value \u2014 deliberately, as these two decide whether AMT is declared at all.\n\tconst amtDiscovery = env[\"MOQCAST_MULTICAST_AMT_DISCOVERY\"]?.trim() || undefined;\n\tconst amtRelayDns = env[\"MOQCAST_MULTICAST_AMT_RELAY_DNS\"]?.trim() || undefined;\n\t// One predicate, both guards: a key significant enough to reject at zero\n\t// endpoints is not silently discarded once endpoints are present.\n\tconst hasCatalogAmt = amtDiscovery !== undefined || amtRelayDns !== undefined;\n\tif (indices.length === 0) {\n\t\t// Catalog-level AMT keys without per-endpoint keys is a\n\t\t// misconfiguration, not a no-op: `MOQCAST_MULTICAST_AMT_DISCOVERY`\n\t\t// only makes sense alongside endpoints to fall back FOR. Throw\n\t\t// loudly instead of silently dropping the request \u2014 a publisher\n\t\t// that meant to declare AMT fallback shouldn't have its config\n\t\t// disappear without any signal.\n\t\tif (hasCatalogAmt) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_env_shape\",\n\t\t\t\tnull,\n\t\t\t\t\"MOQCAST_MULTICAST_AMT_* env keys are set but no per-endpoint keys \" +\n\t\t\t\t\t\"(MOQCAST_MULTICAST_GROUP / _PROTOCOL / _PORT / _TRACKS) were found. \" +\n\t\t\t\t\t\"AMT discovery is a catalog-level fallback for endpoints; declaring it \" +\n\t\t\t\t\t\"with zero endpoints is a misconfiguration.\",\n\t\t\t);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tconst endpoints: MulticastEndpointInput[] = indices.map((idx) => readEndpointAt(env, idx));\n\n\tconst input: MulticastInput = {\n\t\tendpoints,\n\t\t...(hasCatalogAmt\n\t\t\t? {\n\t\t\t\t\tamtFallback: {\n\t\t\t\t\t\t// No default: an omitted discovery with a relay hostname is\n\t\t\t\t\t\t// \"dns\" (draft-ramadan-moq-multicast-01 4.2.1).\n\t\t\t\t\t\t...(amtDiscovery ? { discovery: amtDiscovery as AmtDiscovery } : {}),\n\t\t\t\t\t\t...(amtRelayDns ? { relayDns: amtRelayDns } : {}),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {}),\n\t};\n\treturn multicastFromConfig(input);\n}\n\nconst ENV_PER_FIELD = [\"PROTOCOL\", \"GROUP\", \"PORT\", \"SOURCE\", \"RELAY\", \"TRACKS\", \"BANDWIDTH\"];\nconst ENV_CATALOG_LEVEL = new Set([\"AMT_DISCOVERY\", \"AMT_RELAY_DNS\"]);\n\nfunction collectMulticastIndices(env: Record<string, string | undefined>): number[] {\n\tconst indices = new Set<number>();\n\tlet hasUnindexed = false;\n\tlet hasZeroIndexed = false;\n\n\tfor (const [key, val] of Object.entries(env)) {\n\t\tif (val === undefined) continue;\n\t\tif (!key.startsWith(\"MOQCAST_MULTICAST_\")) continue;\n\t\tconst suffix = key.slice(\"MOQCAST_MULTICAST_\".length);\n\t\tif (ENV_CATALOG_LEVEL.has(suffix)) continue;\n\n\t\tconst m = /^(\\d+)_(.+)$/.exec(suffix);\n\t\tif (m) {\n\t\t\tconst idx = Number(m[1]);\n\t\t\tconst field = m[2];\n\t\t\tif (!ENV_PER_FIELD.includes(field)) continue;\n\t\t\tif (idx === 0) hasZeroIndexed = true;\n\t\t\tindices.add(idx);\n\t\t} else if (ENV_PER_FIELD.includes(suffix)) {\n\t\t\thasUnindexed = true;\n\t\t\tindices.add(0);\n\t\t}\n\t}\n\n\tif (hasUnindexed && hasZeroIndexed) {\n\t\tthrow new MulticastInputError(\n\t\t\t\"invalid_env_shape\",\n\t\t\tnull,\n\t\t\t\"MOQCAST_MULTICAST_* env contract: cannot mix unindexed keys (MOQCAST_MULTICAST_GROUP) \" +\n\t\t\t\t\"with explicitly-indexed keys at index 0 (MOQCAST_MULTICAST_0_GROUP) \u2014 they refer to \" +\n\t\t\t\t\"the same endpoint. Use one form consistently.\",\n\t\t);\n\t}\n\n\treturn [...indices].sort((a, b) => a - b);\n}\n\nfunction readEndpointAt(env: Record<string, string | undefined>, idx: number): MulticastEndpointInput {\n\tconst lookup = (field: string): string | undefined => {\n\t\tif (idx === 0) {\n\t\t\treturn env[`MOQCAST_MULTICAST_${field}`] ?? env[`MOQCAST_MULTICAST_0_${field}`];\n\t\t}\n\t\treturn env[`MOQCAST_MULTICAST_${idx}_${field}`];\n\t};\n\n\tconst portStr = lookup(\"PORT\");\n\tconst port = portStr !== undefined ? Number(portStr) : NaN;\n\tconst tracks = parseTracksEnv(lookup(\"TRACKS\"), idx);\n\tconst bandwidthStr = lookup(\"BANDWIDTH\");\n\tconst bandwidth = bandwidthStr !== undefined ? Number(bandwidthStr) : undefined;\n\tconst source = lookup(\"SOURCE\");\n\tconst relay = lookup(\"RELAY\");\n\n\treturn {\n\t\tprotocol: lookup(\"PROTOCOL\") as MulticastInputProtocol,\n\t\tgroupAddress: lookup(\"GROUP\") ?? \"\",\n\t\tport,\n\t\ttracks,\n\t\t...(source ? { sourceAddress: source } : {}),\n\t\t...(relay ? { relayAddress: relay } : {}),\n\t\t...(bandwidth !== undefined ? { bandwidth } : {}),\n\t};\n}\n\nfunction parseTracksEnv(raw: string | undefined, endpointIdx: number): MulticastTrackObject[] {\n\tif (!raw || raw.length === 0) return [];\n\treturn raw.split(\",\").map((entry, i) => {\n\t\tconst colon = entry.lastIndexOf(\":\");\n\t\tif (colon < 0) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_track_name\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] missing \":\" \u2014 expected \"name:packetId\", got ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\tconst name = entry.slice(0, colon).trim();\n\t\tconst idStr = entry.slice(colon + 1).trim();\n\t\tconst packetId = Number(idStr);\n\t\tif (!name || idStr.length === 0 || !Number.isFinite(packetId) || !Number.isInteger(packetId)) {\n\t\t\tthrow new MulticastInputError(\n\t\t\t\t\"invalid_packet_id\",\n\t\t\t\tendpointIdx,\n\t\t\t\t`endpoint[${endpointIdx}] TRACKS[${i}] entry malformed (need \"name:integer\"): ${JSON.stringify(entry)}`,\n\t\t\t);\n\t\t}\n\t\treturn { name, packetId };\n\t});\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }