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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +29 -17
  2. package/dist/browser-runtime.js +1949 -374
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.js.map +1 -1
  5. package/dist/catalog-fec-params.d.ts +17 -1
  6. package/dist/catalog-fec-params.d.ts.map +1 -1
  7. package/dist/catalog-fec-params.js +15 -0
  8. package/dist/catalog-fec-params.js.map +2 -2
  9. package/dist/catalog-network-source.js.map +1 -1
  10. package/dist/cmcd-producer.d.ts +65 -47
  11. package/dist/cmcd-producer.d.ts.map +1 -1
  12. package/dist/cmcd-producer.js +138 -109
  13. package/dist/cmcd-producer.js.map +2 -2
  14. package/dist/delivery-catalog.js +26 -16
  15. package/dist/delivery-catalog.js.map +2 -2
  16. package/dist/delta-fallback-config.js.map +1 -1
  17. package/dist/fec-manager.d.ts +70 -12
  18. package/dist/fec-manager.d.ts.map +1 -1
  19. package/dist/fec-manager.js +403 -58
  20. package/dist/fec-manager.js.map +2 -2
  21. package/dist/fec-state.js.map +1 -1
  22. package/dist/fec-worker-setup.d.ts +1 -1
  23. package/dist/fec-worker-setup.js +1 -1
  24. package/dist/fec-worker-setup.js.map +1 -1
  25. package/dist/held-layer-admission.js.map +1 -1
  26. package/dist/index.d.ts +6 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/iso-block-locator.d.ts +48 -13
  31. package/dist/iso-block-locator.d.ts.map +1 -1
  32. package/dist/iso-block-locator.js +97 -25
  33. package/dist/iso-block-locator.js.map +2 -2
  34. package/dist/iso-bmff-timing.d.ts +10 -1
  35. package/dist/iso-bmff-timing.d.ts.map +1 -1
  36. package/dist/iso-bmff-timing.js +36 -2
  37. package/dist/iso-bmff-timing.js.map +2 -2
  38. package/dist/media-router.js.map +1 -1
  39. package/dist/mfu-reassembler.js +12 -19
  40. package/dist/mfu-reassembler.js.map +2 -2
  41. package/dist/mmtp-container-parser.d.ts +79 -10
  42. package/dist/mmtp-container-parser.d.ts.map +1 -1
  43. package/dist/mmtp-container-parser.js +219 -45
  44. package/dist/mmtp-container-parser.js.map +2 -2
  45. package/dist/mmtp-container-parser.no-modules.js +342 -63
  46. package/dist/mmtp-container-parser.no-modules.js.map +3 -3
  47. package/dist/mmtp-decode-pipeline.js +218 -45
  48. package/dist/mmtp-decode-pipeline.js.map +2 -2
  49. package/dist/mmtp-fec-session.d.ts +41 -3
  50. package/dist/mmtp-fec-session.d.ts.map +1 -1
  51. package/dist/mmtp-fec-session.js +118 -13
  52. package/dist/mmtp-fec-session.js.map +3 -3
  53. package/dist/mmtp-router.d.ts +43 -9
  54. package/dist/mmtp-router.d.ts.map +1 -1
  55. package/dist/mmtp-router.js +2006 -95
  56. package/dist/mmtp-router.js.map +4 -4
  57. package/dist/multicast-auth-catalog.js.map +1 -1
  58. package/dist/multicast-input.js.map +1 -1
  59. package/dist/multicast-manager.d.ts +28 -5
  60. package/dist/multicast-manager.d.ts.map +1 -1
  61. package/dist/multicast-manager.js +16116 -15005
  62. package/dist/multicast-manager.js.map +4 -4
  63. package/dist/multicast-transport.d.ts +11 -3
  64. package/dist/multicast-transport.d.ts.map +1 -1
  65. package/dist/multicast-transport.js +281 -35
  66. package/dist/multicast-transport.js.map +4 -4
  67. package/dist/multicast-unwrap.js +12 -19
  68. package/dist/multicast-unwrap.js.map +2 -2
  69. package/dist/no-modules.js +336 -59
  70. package/dist/no-modules.js.map +3 -3
  71. package/dist/repair-fetch.d.ts +36 -8
  72. package/dist/repair-fetch.d.ts.map +1 -1
  73. package/dist/repair-fetch.js +285 -26
  74. package/dist/repair-fetch.js.map +3 -3
  75. package/dist/repair-layer-subscriptions.d.ts +82 -0
  76. package/dist/repair-layer-subscriptions.d.ts.map +1 -0
  77. package/dist/repair-layer-subscriptions.js +192 -0
  78. package/dist/repair-layer-subscriptions.js.map +7 -0
  79. package/dist/signaling.js.map +2 -2
  80. package/dist/timestamp-codec.js +12 -19
  81. package/dist/timestamp-codec.js.map +2 -2
  82. package/dist/track-session.d.ts +1 -1
  83. package/dist/track-session.d.ts.map +1 -1
  84. package/dist/track-session.js +1 -0
  85. package/dist/track-session.js.map +2 -2
  86. package/dist/wasm-mfu-reassembler.js.map +1 -1
  87. package/dist/wasm-mmtp-container-parser.js +218 -45
  88. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  89. package/package.json +8 -5
  90. package/schemas/catalog.schema.json +4 -0
  91. package/src/catalog-fec-params.ts +36 -1
  92. package/src/cmcd-producer.ts +194 -179
  93. package/src/fec-manager.ts +433 -32
  94. package/src/fec-worker-setup.ts +2 -2
  95. package/src/index.ts +20 -2
  96. package/src/iso-block-locator.ts +133 -31
  97. package/src/iso-bmff-timing.ts +40 -1
  98. package/src/mmtp-container-parser.ts +306 -29
  99. package/src/mmtp-fec-session.ts +115 -15
  100. package/src/mmtp-router.ts +629 -54
  101. package/src/multicast-manager.ts +70 -11
  102. package/src/multicast-transport.ts +44 -20
  103. package/src/repair-fetch.ts +363 -49
  104. package/src/repair-layer-subscriptions.ts +261 -0
  105. package/src/track-session.ts +4 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/fec-bindings.ts", "../src/repair-fetch.ts", "../src/block-policy.ts", "../src/repair-layers.ts"],
4
- "sourcesContent": ["/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nexport const SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// fec_code_id_for_repair_flow code points (ISO/IEC 23008-10 Table 1;\n// draft-ramadan-moq-mmt-01 Section 8.3).\nexport const FEC_CODE_REED_SOLOMON = 1\nexport const FEC_CODE_RAPTORQ = 3\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n hasOverlayFecId: boolean\n}\n\n/** Options for extractMFUPayload and explainMFURejection. */\nexport interface MFUExtractOptions {\n /**\n * When true, do NOT subtract the Source FEC Payload ID from the payload\n * boundary, even if fecType >= 1. For FEC-recovered symbols whose FEC PID\n * was already stripped before WASM decoding.\n */\n skipFecId?: boolean\n /**\n * The catalog declares a keyframe overlay (`scope: \"keyframe\"` repair track)\n * for this packet's source. A RAP packet then carries two Source FEC Payload\n * IDs, base first and overlay last (BLO-32719 Clarification 7). This is a\n * catalog fact, never inferred: the header has no overlay bit, and a payload\n * four bytes shorter is byte-identical to a missing overlay. Mirrors\n * mmt-core `split_payload_and_source_fec_trailers(overlay_declared)`.\n */\n overlayDeclared?: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: MFUExtractOptions,\n rapFlag = false,\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false, hasOverlayFecId: false }\n : null\n }\n\n const overlay = options?.overlayDeclared === true && rapFlag\n if (fecType === 1) {\n const fecStart = payload.length - sourceFecTrailerSize(overlay)\n if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true, hasOverlayFecId: overlay }\n }\n // An overlay ID rides behind a base ID the header says is absent.\n if (fecType !== 0 || overlay) return null\n\n if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false, hasOverlayFecId: false }\n }\n\n return null\n}\n\nfunction sourceFecTrailerSize(overlay: boolean): number {\n return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n /** Keyframe-overlay Source FEC Payload ID: RAP packets with `overlayDeclared` only. */\n overlayFecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. The legacy\n * authenticator suffix and the Source FEC trailer are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-01 \u00A73.1).\n * @param options.overlayDeclared The catalog declares a keyframe overlay for this\n * source, so a RAP packet carries an 8-byte trailer (see MFUExtractOptions).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n header.rapFlag,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag)\n if (observed !== expected) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${expected} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact legacy authenticator trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const trailerStart =\n mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId)\n const ssIdAt = (offset: number) => ({\n ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false),\n })\n const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null\n const overlayFecPayloadId = trailerLayout.hasOverlayFecId\n ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE)\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n checkRsId = true,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.ssbLength === 0 ||\n (checkRsId && repairId.rsId >= repairId.rsbLength) ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n/**\n * Why a repair object of an mmtp track is discarded on admission:\n * `repairLength` -- the object is not exactly 12 + 13 + T bytes with a\n * 12-byte MMTP header (C = 0, X = 0; draft-ramadan-moq-mmt-02 7.3, judged\n * before any Repair FEC Payload ID field is read); `repairRsId` -- RS_ID is not\n * below RSB_length, under every scheme; `repairSchemeBound` -- the object\n * breaks its scheme's bound: reed-solomon SSB_length + RSB_length > 255, which\n * also caps RS_ID at 253, or raptorq SSB_length + RSB_length - 1 > 2^24 - 1\n * (draft-ramadan-moq-fec-core-00 7.5 and 10.3, -block-codes-00 7).\n */\nexport type RepairObjectReject = 'repairLength' | 'repairRsId' | 'repairSchemeBound'\n\n/**\n * Admission of one mmtp repair object under its instance's T and scheme. The\n * caller names the instance (by the repair track it subscribed); nothing here\n * infers it from the fields. `repairRsId` and `repairSchemeBound` carry the\n * frame so the caller can first route an RSB_length below a held layer's\n * O_i + P_i to the 7.3.2 stop, which takes precedence over the discard.\n * `null`: not an iso repair packet, or SSB_length 0.\n */\nexport function admitMmtpRepairObject(\n packet: Uint8Array,\n header: MMTPHeader,\n symbolSize: number,\n algorithm: string | undefined,\n): { frame: MoqRepairFrame; reject?: Exclude<RepairObjectReject, 'repairLength'> } | { reject: 'repairLength' } | null {\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n if (header.headerLength !== MMTP_HEADER_SIZE || packet.length !== MMTP_HEADER_SIZE + REPAIR_FEC_PID_SIZE + symbolSize) {\n return { reject: 'repairLength' }\n }\n const frame = moqRepairFrameFrom(packet, readRepairFecPayloadIdFields(packet, header), false)\n if (!frame) return null\n if (frame.rsId >= frame.rsbLength) return { frame, reject: 'repairRsId' }\n const blockSymbols = frame.ssbLength + frame.rsbLength\n if (algorithm === 'reed-solomon' ? blockSymbols > 255 : algorithm === 'raptorq' && blockSymbols - 1 > 0xffffff) {\n return { frame, reject: 'repairSchemeBound' }\n }\n return { frame }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "/**\n * Runtime FEC bindings (BLO-50171): what each source track's FEC resolves to\n * for this library's receiver, read from `validateFecCatalog`'s per-instance\n * result and never re-derived.\n *\n * Every runtime reader (the container's `resolveCatalogTrackFec`,\n * `resolveRepairLayers`, `MulticastManager.configureMultiTrack`, transport's\n * `catalogRepairTracks`) goes through `resolveFecBindings`, so they cannot\n * disagree with the validator or with each other about which tracks have FEC\n * and which repair tracks belong to them.\n *\n * The draft-ramadan-moq-fec-catalog-00 array is the only shape. Both catalog\n * dialects bind through it: an MSF `tracks[]` entry directly, and a hang\n * `video`/`audio` rendition (`fec` the same array) with its `repair.renditions`\n * entries (`source` + `instanceId`), mapped onto a `tracks[]` view so the same\n * validator runs. A hang rendition is authoritative over a `tracks[]` entry of\n * the same name.\n *\n * Fail closed per instance: a source whose base instance is not `ok` resolves\n * to `unavailable` with the validator's code and path, and costs no other\n * track anything. A draft-01 object `fec` is `fec_member` at that track's\n * `fec`: the track plays without FEC.\n */\nimport { validateFecCatalog, type FecCatalogCode, type FecCatalogReceiver } from './fec-catalog.js'\n\n/** The receiver this runtime is: it decodes `iso-ssbg1-v1` only. */\nexport const RUNTIME_FEC_RECEIVER: FecCatalogReceiver = Object.freeze({\n schemes: Object.freeze(['raptorq', 'reed-solomon']),\n blockProfiles: Object.freeze(['iso-ssbg1-v1']),\n overlays: true,\n})\n\n/** Repair track priority when the track states none (MoQT scale, 192..255). */\nexport const DEFAULT_FEC_REPAIR_PRIORITY = 240\n\n/** One `ok` instance's geometry. */\nexport interface FecInstanceGeometry {\n readonly instanceId: number\n readonly scheme: 'raptorq' | 'reed-solomon'\n /** `iso-ssbg1-v1` under `RUNTIME_FEC_RECEIVER`. */\n readonly blockProfile: string\n /** K. */\n readonly sourceSymbols: number\n /** P_0 for a base instance; the overlay's own P for an overlay. */\n readonly repairSymbols: number\n /** T. */\n readonly symbolSize: number\n readonly interleaveDepthMs?: number\n readonly reorderToleranceMs?: number\n readonly repairTrack: string\n readonly symbolTrack?: string\n}\n\nexport interface FecLayerBinding {\n readonly layer: number\n readonly repairTrackName: string\n readonly repairSymbols: number\n /** O_i = sum(P_j, j < i). */\n readonly rsIdOffset: number\n readonly priority: number\n}\n\nexport interface FecOverlayBinding {\n readonly repairTrackName: string\n readonly scope: 'keyframe'\n readonly sourceSymbols: number\n readonly repairSymbols: number\n readonly priority: number\n readonly geometry: FecInstanceGeometry\n}\n\nexport interface FecBindingUnavailable {\n /** The validator's code, or `fec_base_instances` for a track with no base\n * instance or more than one `ok` one (the MMTP wire carries one base Source FEC Payload\n * ID, mmt-02 7.4.1). */\n readonly code: FecCatalogCode | 'fec_base_instances'\n /** JSON Pointer into the catalog (a hang path starts `/video` or `/audio`). */\n readonly path: string\n readonly outcome: 'unavailable' | 'violation'\n}\n\nexport type FecSourceBinding =\n | {\n readonly status: 'ok'\n readonly track: string\n /** JSON Pointer of the source track (`/tracks/<i>`, `/video/renditions/<name>`). */\n readonly pointer: string\n readonly base: FecInstanceGeometry\n /** Base layers in order, layer 0 first. */\n readonly layers: readonly FecLayerBinding[]\n /** Sum of P over the base layers: the wire RSB_length. */\n readonly totalBaseRepairSymbols: number\n readonly overlay?: FecOverlayBinding\n /** Why a declared overlay is not used, when one is not. */\n readonly overlayUnavailable?: FecBindingUnavailable\n }\n | {\n readonly status: 'unavailable'\n readonly track: string\n readonly pointer: string\n readonly reason: FecBindingUnavailable\n }\n\ntype Obj = Record<string, unknown>\nconst recordOf = (v: unknown): Obj | undefined =>\n typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as Obj) : undefined\nconst escape = (s: string) => s.replace(/~/g, '~0').replace(/\\//g, '~1')\n\n/**\n * The `tracks[]` view of a catalog in either dialect, with each view track's\n * JSON Pointer in the original catalog.\n */\nexport function fecCatalogTracks(catalog: unknown): { tracks: unknown[]; pointers: string[] } {\n const root = recordOf(catalog) ?? {}\n const tracks: unknown[] = []\n const pointers: string[] = []\n const hangNames = new Set<string>()\n for (const role of ['video', 'audio'] as const) {\n for (const [name, r] of Object.entries(recordOf(recordOf(root[role])?.['renditions']) ?? {})) {\n const entry = recordOf(r)\n if (!entry) continue\n hangNames.add(name)\n const kind = recordOf(entry['container'])?.['kind']\n // A hang rendition carries no `packaging`; its container kind is it.\n // `mmtp-fec` is hang's kind for MMTP that carries FEC (the FFmpeg\n // publisher's `moq_publish_set_video_packaging`): MMTP packaging.\n const view: Obj = { name, packaging: kind === undefined || kind === 'mmtp-fec' ? 'mmtp' : kind }\n if (entry['fec'] !== undefined) view['fec'] = entry['fec']\n tracks.push(view)\n pointers.push(`/${role}/renditions/${escape(name)}`)\n }\n }\n const hangRenditions = tracks.length\n for (const [name, r] of Object.entries(recordOf(recordOf(root['repair'])?.['renditions']) ?? {})) {\n hangNames.add(name)\n tracks.push({ packaging: 'fec-repair', ...recordOf(r), name })\n pointers.push(`/repair/renditions/${escape(name)}`)\n }\n const raw = Array.isArray(root['tracks']) ? root['tracks'] : []\n const named = new Set(hangNames)\n raw.forEach((t, i) => {\n const name = recordOf(t)?.['name']\n if (typeof name === 'string') named.add(name)\n if (typeof name === 'string' && hangNames.has(name)) return\n tracks.push(t)\n pointers.push(`/tracks/${i}`)\n })\n // hang layer 0 needs no `repair.renditions` entry: the rendition's instance\n // names it, and a hang Root (hang's toHang) carries no repair block. Such a\n // name stands for a repair track of that instance.\n tracks.slice(0, hangRenditions).forEach((t, ti) => {\n const view = t as Obj\n if (!Array.isArray(view['fec'])) return\n view['fec'].forEach((o, ii) => {\n const repairTrack = recordOf(o)?.['repairTrack']\n if (typeof repairTrack !== 'string' || named.has(repairTrack)) return\n named.add(repairTrack)\n const instanceId = recordOf(o)?.['instanceId']\n tracks.push({ name: repairTrack, packaging: 'fec-repair', source: view['name'], ...(instanceId === undefined ? {} : { instanceId }) })\n pointers.push(`${pointers[ti]}/fec/${ii}/repairTrack`)\n })\n })\n return { tracks, pointers }\n}\n\n/**\n * Every source track's runtime FEC binding, by track name. A track with no\n * `fec`, or an empty `fec` array, declares no FEC and has no entry.\n */\nexport function resolveFecBindings(\n catalog: unknown,\n receiver: FecCatalogReceiver = RUNTIME_FEC_RECEIVER,\n): ReadonlyMap<string, FecSourceBinding> {\n const { tracks, pointers } = fecCatalogTracks(catalog)\n const report = validateFecCatalog({ tracks }, receiver)\n const at = (path: string) => {\n const m = /^\\/tracks\\/(\\d+)(.*)$/.exec(path)\n return m ? pointers[Number(m[1])] + m[2] : path\n }\n const trackAt = (i: number) => recordOf(tracks[i]) ?? {}\n const nameAt = (i: number | null) => (i === null ? undefined : (trackAt(i)['name'] as string))\n const why = (i: { outcome: string; issues: readonly { code: FecCatalogCode; path: string }[]; path: string }) =>\n Object.freeze({\n code: i.issues[0]?.code ?? 'fec_member',\n path: at(i.issues[0]?.path ?? i.path),\n outcome: i.outcome === 'unavailable' ? ('unavailable' as const) : ('violation' as const),\n })\n\n const out = new Map<string, FecSourceBinding>()\n tracks.forEach((t, ti) => {\n const track = recordOf(t)\n const name = track?.['name']\n if (!track || typeof name !== 'string' || out.has(name)) return\n if (track['packaging'] === 'fec-repair' || track['packaging'] === 'fec-source') return\n const fec = track['fec']\n if (fec === undefined) return\n if (!Array.isArray(fec)) {\n const reason = { code: 'fec_member' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const }\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason: Object.freeze(reason) }))\n return\n }\n if (fec.length === 0) return\n const insts = report.instances.filter((i) => i.trackIndex === ti)\n const isOverlay = (i: (typeof insts)[number]) => Object.hasOwn(recordOf(fec[Number(i.path.split('/').pop())]) ?? {}, 'scope')\n const bases = insts.filter((i) => !isOverlay(i))\n const okBases = bases.filter((i) => i.outcome === 'ok')\n if (okBases.length !== 1) {\n const reason =\n // No base instance at all (overlay-only `fec`) is a count of 0, not\n // the overlay's own issue.\n okBases.length > 1 || bases.length === 0\n ? Object.freeze({ code: 'fec_base_instances' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const })\n : why(bases[0] ?? insts[0]!)\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason }))\n return\n }\n const b = okBases[0]!\n const base = geometry(recordOf(fec[Number(b.path.split('/').pop())])!, b.instanceId!, nameAt(b.symbolTrack))\n const priorityOf = (i: number | null) => {\n const p = i === null ? undefined : trackAt(i)['priority']\n return typeof p === 'number' ? p : DEFAULT_FEC_REPAIR_PRIORITY\n }\n const layers: FecLayerBinding[] = [\n { layer: 0, repairTrackName: base.repairTrack, repairSymbols: base.repairSymbols, rsIdOffset: 0, priority: priorityOf(b.repairTrack) },\n ]\n const more = b.boundRepairTracks\n .map((tj) => ({ tj, layer: trackAt(tj)['repairLayer'] as number }))\n .sort((x, y) => x.layer - y.layer)\n let offset = base.repairSymbols\n for (const { tj, layer } of more) {\n const repairSymbols = trackAt(tj)['repairSymbols'] as number\n layers.push({ layer, repairTrackName: nameAt(tj)!, repairSymbols, rsIdOffset: offset, priority: priorityOf(tj) })\n offset += repairSymbols\n }\n let overlay: FecOverlayBinding | undefined\n let overlayUnavailable: FecBindingUnavailable | undefined\n const ov = insts.find(isOverlay)\n if (ov?.outcome === 'ok') {\n const g = geometry(recordOf(fec[Number(ov.path.split('/').pop())])!, ov.instanceId!, nameAt(ov.symbolTrack))\n overlay = Object.freeze({\n repairTrackName: g.repairTrack,\n scope: 'keyframe' as const,\n sourceSymbols: g.sourceSymbols,\n repairSymbols: g.repairSymbols,\n priority: priorityOf(ov.repairTrack),\n geometry: g,\n })\n } else if (ov) overlayUnavailable = why(ov)\n out.set(\n name,\n Object.freeze({\n status: 'ok' as const,\n track: name,\n pointer: pointers[ti]!,\n base,\n layers: Object.freeze(layers.map((l) => Object.freeze(l))),\n totalBaseRepairSymbols: offset,\n ...(overlay ? { overlay } : {}),\n ...(overlayUnavailable ? { overlayUnavailable } : {}),\n }),\n )\n })\n return out\n}\n\n// The validator passed this instance for this receiver, so every member read\n// here is present and in range.\nfunction geometry(o: Obj, instanceId: number, symbolTrack: string | undefined): FecInstanceGeometry {\n return Object.freeze({\n instanceId,\n scheme: o['scheme'] as 'raptorq' | 'reed-solomon',\n blockProfile: o['blockProfile'] as string,\n sourceSymbols: o['sourceSymbols'] as number,\n repairSymbols: o['repairSymbols'] as number,\n symbolSize: o['symbolSize'] as number,\n ...(typeof o['interleaveDepthMs'] === 'number' ? { interleaveDepthMs: o['interleaveDepthMs'] } : {}),\n ...(typeof o['reorderToleranceMs'] === 'number' ? { reorderToleranceMs: o['reorderToleranceMs'] } : {}),\n repairTrack: o['repairTrack'] as string,\n ...(symbolTrack !== undefined ? { symbolTrack } : {}),\n })\n}\n", "/**\n * D-28 shared repair-fetch scheduler.\n *\n * Player-agnostic orchestration for per-block on-demand MoQ FEC repair\n * backfill. Each player (moqtail client, hang watch, Shaka MSF bridge)\n * supplies only its protocol verb via {@link RepairFetchTransport}, fetching\n * one block's repair group. Blocks are `iso-ssbg1-v1` (ISO/IEC 23008-1\n * ssbg_mode1, the only FEC block profile), so that group's MoQ id is the\n * 62-bit wrap-epoch-extended SS_Start, `extSsStart(wrapEpoch, ssStart)` from\n * `@blockcast/mmt-fec` -- never the 32-bit block key itself (see\n * `RepairFetchRequest.sbn`):\n *\n * - moqtail: standalone FETCH (FetchType.Standalone) of that one group\n * - moq-lite/hang: Subscribe with startGroup = endGroup = that group (the\n * relay propagates startGroup upstream to pull the past group from the\n * origin's producer cache)\n *\n * Everything else \u2014 registration, fire-time repair-track resolution,\n * keyframe-first admission, preemption, feeding symbols, and in-flight slot\n * release \u2014 is identical across players and lives here. The Player owns the\n * scheduler: one per `wireRepairFetch` call, and its disposer aborts\n * everything.\n *\n * Priority (from the block's class, see `repairPriorityForClass`):\n * - P0 (keyframe / unknown) dispatches as soon as it is enqueued, with no\n * cap. Each has its own fail deadline, and a cap would turn a\n * recoverable protected block into a stall; their number is already\n * bounded by the blocks one fail window can hold. A P0 block that lies\n * below a RAP the receiver already holds complete is not requested at\n * all (`moot-rap`), and a P0 FETCH still running when such a RAP\n * arrives is cancelled (`repair-stopped`, `moot`): the receiver resumes\n * from that RAP (draft-ramadan-moq-fec \u00A711.4.2). Only a repair that\n * could not land by the block's playout deadline (it relies on the\n * keyframe budget's extension) is moot: one that can stalls nothing.\n * - P1 (audio) dispatches, uncapped, while no P0 is queued or in flight;\n * P2 (delta) only while no P0/P1 is, earliest absolute fail deadline\n * first (\u00A711.4.5 item 3).\n * - Only a P0 FETCH the scheduler actually sends aborts the in-flight P1\n * and P2 FETCHes (`repair-preempted`), which return to the queue while\n * their blocks are unresolved: a keyframe request that is refused, moot\n * or otherwise never sent cancels nothing (\u00A711.4.5 item 3), and an audio\n * request never preempts. A P2 that never ran before its block resolved\n * is reported as `repair-deferred`.\n * - A block reclassified to a higher class while its P2 fetch is in flight\n * is re-issued at once under the new class's wire priority: the running\n * FETCH is aborted (FETCH_CANCEL) and a new one sent.\n *\n * Wire priority (draft-ramadan-moq-fec \u00A711.4.5): every request carries the\n * MoQ Transport SUBSCRIBER_PRIORITY its FETCH must use, derived from the two\n * catalog priorities `ResolveRepairTarget` returns (see\n * `repairFetchSubscriberPriority`): a keyframe-bearing (or unknown) block's\n * FETCH is sent in the Source Media range at no more than the source\n * track's priority, so it is delivered ahead of every repair track and not\n * shed with repair data; a delta or audio FETCH is sent at no less than the\n * fetched repair track's priority, so it is shed first.\n *\n * Deadline and admission (BLO-35597): a repair FETCH rides the reliable path,\n * so its deadline is the block's PRESENTATION deadline, not the multicast\n * reassembly deadline. The player states how long after a block's first\n * symbol its media can still be presented (`presentationDelayMs`); from the\n * block's receiver-local anchor that gives an absolute deadline, never earlier\n * than the multicast wait and never later than the release gate's hold ceiling\n * (`terminalRetentionMsForPacket`), which bounds how long any successor frame\n * may wait behind the block. A FETCH is sent only when the measured\n * reliable-path RTT (`reliableRttMs`) is less than the time left to that\n * deadline -- a request that cannot come back in time is never sent, rather\n * than sent and cancelled -- and an admitted FETCH holds the block open to\n * that deadline, so the multicast span elapsing never aborts it.\n *\n * Keyframe late-repair budget: a block that may hold a RAP may be given a\n * longer deadline than the playout budget (`keyframeRepairBudgetMs`). Losing\n * such a block costs every frame up to the next keyframe, so holding it open\n * for a FETCH that lands within the budget trades a short hold for a GOP-long\n * freeze. At a small playout budget (a low-latency target) the presentation\n * deadline alone leaves less than one round trip by the time the multicast\n * wait gives up on the block, so every keyframe request would be refused\n * (`rtt-exceeds-budget`). Eligible (`keyframeBudgetEligible`): a `keyframe`\n * block (in-band RAP evidence), and an `unknown` block only while its\n * stream's classifier is `classifying`, where `unknown` means the window rule\n * could not exclude a RAP. Under `all-protected` every block reads `unknown`\n * whatever it holds, so there only RAP evidence earns the budget. Never\n * audio (P1) or delta (P2), and never a block its repair group cannot\n * complete (received sources + RSB_length < K'). The hold ends when its\n * FETCH ends without making the block decodable, not at the budget.\n *\n * Every deadline is capped by the release gate's hold ceiling measured the\n * way the gate measures it: from the earliest frame it holds for the stream\n * (a surviving frame of the block itself is routed before a repair symbol\n * states the block, so before its anchor), never later than from the anchor.\n * A FETCH landing after the gate forced that frame out would recover media\n * the gate then drops as stale.\n *\n * The scheduler arms no timers for block requests. A fetch ends when its\n * transport completes or throws, when it is preempted, when any later\n * scheduling event finds it past its presentation deadline, when its block\n * resolves (recovered, or failed at that deadline), on track reset, or on\n * dispose. A range request (\u00A77.5) is for a hole, not a tracked block, so no\n * FEC deadline resolves it: it arms one timer at its deadline, cleared when\n * it retires, so a hung range FETCH cannot hold the hole (and the release\n * gate behind it) past that deadline.\n */\n\nimport { parseFecBlockProfile, type FecBlockProfile } from '@blockcast/mmt-base'\nimport { extendSsId, repairFetchRange, type RepairFetchRange } from '@blockcast/mmt-fec'\nimport {\n repairPriorityForClass,\n type BlockClass,\n type ClassifierMode,\n type RepairPriority,\n} from './block-policy.js'\nimport type { UndelimitedIsoLoss } from './fec-manager.js'\nimport type { RepairLifecycleObserver } from './multicast-manager.js'\nimport { REPAIR_PRIORITY_MIN } from './repair-layers.js'\n\n/** The MoQ Transport priority scale: 0..255, a lower value is delivered first. */\nconst MOQT_PRIORITY_MAX = 255\n\n/**\n * The highest value in the Source Media priority range of\n * draft-ramadan-moq-fec \u00A710 (64..191): one below the repair range, which\n * starts at `REPAIR_PRIORITY_MIN` (192).\n */\nexport const SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1\n\n/** What `ResolveRepairTarget` returns for one repair-bearing stream. */\nexport interface RepairTarget {\n /** The repair track to FETCH (base layer 0 of the block's source). */\n readonly repairTrack: string\n /**\n * The catalog `priority` of the SOURCE track the block belongs to, on the\n * MoQ Transport scale (0..255, lower first), or null when the catalog\n * carries none. Never a player constant: a keyframe repair must not be\n * delivered behind the source it repairs, and only the catalog says what\n * that source's priority is.\n */\n readonly sourcePriority: number | null\n /**\n * The catalog priority of `repairTrack` (MoQ Transport scale). Layer 0\n * that declares none has the value draft-ramadan-moq-fec \u00A710 defines\n * (240); `resolveRepairLayers` / `catalogRepairTarget` already apply it.\n * Null only when the resolver cannot read the repair track's entry.\n */\n readonly repairPriority: number | null\n}\n\n/** Resolve a packetId to its repair track and priorities at FIRE TIME.\n *\n * Called on every repair request, never cached by the shell: ABR ladder\n * rungs can share a packetId (e.g. video/720p and video/360p both p:1), so\n * the mapping must reflect the track active when the request fires, not\n * when the shell was wired. Return null when the packetId has no\n * repair-bearing track \u2014 the request is released without a fetch.\n * `catalogRepairTarget` builds the answer from a catalog. */\nexport type ResolveRepairTarget = (\n packetId: number,\n catalogTrackName?: string,\n) => RepairTarget | null\n\n/** One on-demand repair FETCH, as the scheduler hands it to the transport. */\nexport interface RepairFetchRequest {\n /** Repair track whose block `sbn` is fetched. */\n readonly repairTrack: string\n /**\n * The block's key: its 32-bit SS_Start (ISO/IEC 23008-1 C.5.3). The field\n * keeps its historical name, but ssbg_mode1 blocks have no source block\n * number (BLO-35597 D2a). The repair group's MoQ id is the 62-bit\n * `extSsStart(wrapEpoch, sbn)`; a receiver that knows only a lost SS_ID\n * fetches `repairFetchRange(lostSsId, kMax, anchor)`, where `anchor` is\n * `{ previousBlockStart }` (the extended start of the last block delimited\n * before it) or `{ reference }` (an epoch reference). Never use it as a\n * group id directly: SS_Start wraps at 2^32, a MoQ group id never goes\n * backwards.\n */\n readonly sbn: number\n /**\n * The block profile `sbn` is keyed by: the catalog `fec.blockProfile`,\n * which is always `iso-ssbg1-v1`. Required, and validated with\n * `parseFecBlockProfile` when the request is queued, so a stream configured\n * with any other token (the retired `k-padded-v1` included) never reaches\n * the transport.\n */\n readonly blockProfile: FecBlockProfile\n /** The source packetId the block belongs to. */\n readonly packetId: number\n /**\n * Aborts when the scheduler no longer wants the group: preemption by\n * keyframe work, reprioritisation, block resolution, track reset, dispose.\n */\n readonly signal: AbortSignal\n /**\n * The SUBSCRIBER_PRIORITY parameter the FETCH MUST carry, on the MoQ\n * Transport scale (0..255, lower delivered first), as\n * draft-ramadan-moq-fec \u00A711.4.5 requires. `undefined` means OMIT the\n * parameter: the catalog carries no priority to derive it from, and the\n * publisher then uses the value draft-ietf-moq-transport-16 \u00A79.2.2.3\n * defines for an omitted parameter (128, inside the Source Media range).\n * A transport never substitutes a value of its own. A transport whose API\n * uses another scale (moq-lite's higher-wins weight, say) maps this one\n * without reordering it against the source and repair subscriptions.\n */\n readonly subscriberPriority: number | undefined\n}\n\n/** Protocol-specific fetch of one FEC block's repair group.\n *\n * Yield each repair object payload (MMTP packet bytes, ISO 23008-1 \u00A7C.5.3\n * Repair FEC Payload ID framing). Return when the group ends.\n *\n * Put `request.subscriberPriority` on the FETCH (see RepairFetchRequest).\n *\n * When `request.signal` aborts, the transport MUST cancel the FETCH ON THE\n * WIRE, not merely stop reading: send FETCH_CANCEL for its Request ID\n * (draft-ietf-moq-transport-16 \u00A79.18; STOP_SENDING on an open data stream\n * as well, \u00A75.2), and do it from the abort handler itself, before it\n * returns. A relay that is never told keeps sending the group, so a\n * preempted delta repair keeps competing with the keyframe repair that\n * preempted it. The scheduler may issue the next request for the same block\n * (reprioritisation) as soon as it aborts one, and \u00A711.4.5 allows at most\n * one repair FETCH outstanding per block and repair layer, so the cancel\n * must already be queued ahead of that request. A FETCH whose Request ID\n * is not known yet is cancelled as soon as FETCH_OK assigns it. */\nexport type RepairFetchTransport = (request: RepairFetchRequest) => AsyncIterable<Uint8Array>\n\n/**\n * One range repair FETCH for a lost SS_ID in an undelimited span\n * (draft-ramadan-moq-fec \u00A77.5): the absolute, inclusive repair-track Group\n * range `repairFetchRange` gives. The last Group returned is the block holding\n * `lostSsId`; earlier ones are other blocks, admitted like any repair object.\n */\nexport interface RepairRangeFetchRequest extends RepairFetchRange {\n readonly repairTrack: string\n /** The lost SS_ID x the range was computed for. */\n readonly lostSsId: number\n readonly blockProfile: FecBlockProfile\n readonly packetId: number\n /** Same contract as `RepairFetchRequest.signal`: cancel on the wire. */\n readonly signal: AbortSignal\n /** Same contract as `RepairFetchRequest.subscriberPriority`. */\n readonly subscriberPriority: number | undefined\n}\n\n/**\n * Protocol-specific range FETCH (e.g. MoQT LOCATION_FILTER 0x03 Group range).\n * Yield each repair object payload; return when the range ends. The abort\n * rules of {@link RepairFetchTransport} apply unchanged.\n */\nexport type RepairRangeFetchTransport = (request: RepairRangeFetchRequest) => AsyncIterable<Uint8Array>\n\n/** Why a request could not carry a catalog-derived priority. */\nexport type RepairPriorityReason =\n | 'source-priority-absent'\n | 'source-priority-invalid'\n | 'repair-priority-absent'\n | 'repair-priority-invalid'\n\n/**\n * The wire priority of one repair request (draft-ramadan-moq-fec \u00A711.4.5).\n *\n * - `carry`: send the FETCH with `subscriberPriority`.\n * - `omit`: send it without SUBSCRIBER_PRIORITY. Only a keyframe-bearing or\n * unknown block's request is ever sent this way: the publisher then uses\n * 128 (draft-ietf-moq-transport-16 \u00A79.2.2.3), which lies in the Source\n * Media range, and without a catalog source priority there is no bound\n * below it to honour.\n * - `decline`: send no FETCH. A delta or audio FETCH MUST carry a priority\n * at least its repair track's, so at least 192, and the omitted value (128)\n * would put best-effort repair ahead of every repair track. Unicast repair\n * is optional for those classes (\u00A711.4.3, \u00A711.4.4); in-band FEC and\n * concealment still apply.\n */\nexport type RepairFetchPriority =\n | { readonly kind: 'carry'; readonly subscriberPriority: number }\n | { readonly kind: 'omit'; readonly reason: RepairPriorityReason }\n | { readonly kind: 'decline'; readonly reason: RepairPriorityReason }\n\nfunction isMoqtPriority(value: number): boolean {\n return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX\n}\n\n/**\n * Derive the SUBSCRIBER_PRIORITY of a repair FETCH from the block's class and\n * the catalog priorities of its source and repair tracks\n * (draft-ramadan-moq-fec \u00A711.4.5 and the \u00A710 ranges):\n *\n * - keyframe / unknown: in the Source Media range and no greater than the\n * source track's priority, i.e. min(source, 191); omitted when the catalog\n * carries no source priority.\n * - delta / audio: no lower than the fetched repair track's priority and at\n * least 192, i.e. max(repair, 192); declined when it carries none.\n */\nexport function repairFetchSubscriberPriority(\n blockClass: BlockClass,\n target: Pick<RepairTarget, 'sourcePriority' | 'repairPriority'>,\n): RepairFetchPriority {\n if (repairPriorityForClass(blockClass) === 0) {\n const source = target.sourcePriority\n if (source === null) return { kind: 'omit', reason: 'source-priority-absent' }\n if (!isMoqtPriority(source)) return { kind: 'omit', reason: 'source-priority-invalid' }\n return { kind: 'carry', subscriberPriority: Math.min(source, SOURCE_MEDIA_PRIORITY_MAX) }\n }\n const repair = target.repairPriority\n if (repair === null) return { kind: 'decline', reason: 'repair-priority-absent' }\n if (!isMoqtPriority(repair)) return { kind: 'decline', reason: 'repair-priority-invalid' }\n return { kind: 'carry', subscriberPriority: Math.max(repair, REPAIR_PRIORITY_MIN) }\n}\n\n/** What `RepairFetchHost.fecManager.blockSymbolCounts` reports for one open block. */\nexport interface BlockSymbolCounts {\n readonly sources: number\n readonly repairs: number\n readonly authoredLength: number | undefined\n readonly repairGroupLength: number | undefined\n}\n\n/**\n * Whether a block of `blockClass` may get the keyframe late-repair budget:\n * a `keyframe` block always (in-band RAP evidence), an `unknown` block only\n * while its stream's classifier is `classifying` (the window rule could not\n * exclude a RAP). An `all-protected` stream reads `unknown` for every block,\n * delta-only ones included, so there `unknown` is no evidence of a RAP and\n * earns no hold. Audio and delta never do.\n */\nexport function keyframeBudgetEligible(\n blockClass: BlockClass,\n classifierMode: ClassifierMode | undefined,\n): boolean {\n if (blockClass === 'keyframe') return true\n return blockClass === 'unknown' && classifierMode === 'classifying'\n}\n\nexport interface WireRepairFetchOptions {\n resolveTarget: ResolveRepairTarget\n transport: RepairFetchTransport\n /**\n * Maximum transport attempts per block (required). A retry happens only\n * when the transport rejects with an error `shouldRetry` classifies as\n * retryable, before any symbol was fed, while the block's deadline remains.\n */\n attemptLimit: number\n /** Classify a transport error as a transient publisher/relay retention race. */\n shouldRetry?: (error: unknown) => boolean\n /**\n * The measured round-trip time of the reliable path the transport fetches\n * over, in ms (required). Read at every admission, so it tracks the live\n * session. A value that is not finite and positive, or a throw, means the\n * path offers no measurement: nothing can show the FETCH would come back in\n * time, so none is sent (`rtt-unmeasured`). Never a default.\n */\n reliableRttMs: () => number\n /**\n * How long after a block's first symbol arrived its media can still be\n * presented, in ms (required): the player's own playout budget for the\n * stream. Same fail-closed rule as `reliableRttMs`\n * (`presentation-delay-unavailable`).\n */\n presentationDelayMs: (packetId: number, catalogTrackName?: string) => number\n /**\n * How long after the first symbol of a block that may hold a RAP a repair\n * FETCH of it may still land, in ms (optional). Such a block\n * (`keyframeBudgetEligible`) gets the larger of this and\n * `presentationDelayMs`, still never later than the release gate's hold\n * ceiling and never earlier than the multicast wait; RTT admission is\n * unchanged. Losing a keyframe block costs the rest of its GOP, so a player\n * may hold the block (and the frames released after it) while its FETCH is\n * in flight rather than freeze to the next keyframe. Absent, throwing, or\n * not finite and positive: every block gets the presentation deadline\n * alone. Never applied to audio, delta, or an `unknown` block of an\n * `all-protected` stream.\n */\n keyframeRepairBudgetMs?: (packetId: number, catalogTrackName?: string) => number\n /**\n * Range repair for a lost SS_ID whose block no repair delimited\n * (draft-ramadan-moq-fec \u00A77.5), optional. Given, the host keeps such a gap\n * as a pending hole and the scheduler FETCHes `repairFetchRange(x, K,\n * anchor)` for it under the same admission, priority and abort rules as a\n * block request (class `unknown`, P0). Requires `repairEpochReference`.\n */\n rangeTransport?: RepairRangeFetchTransport\n /**\n * The epoch reference r of `repairTrack` on the live session\n * (`RepairEpochReferences.reference`), or undefined while none is held.\n * The \u00A77.5 anchor is ext(S_prev) extended from it when a stated block ends\n * where the hole starts, else r itself; without r no range request is sent\n * (`range-no-anchor`).\n */\n repairEpochReference?: (repairTrack: string) => bigint | undefined\n}\n\n/** Why the D-28 layer asked for a repair: the block's class at its deadline. */\nexport type RepairSubscribeCallback = (\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n blockClass: BlockClass,\n) => void\n\n/** Why the scheduler sent no FETCH for a block it was asked to repair. */\nexport type RepairSkipReason =\n /** The reliable path offered no RTT measurement; arrival cannot be shown. */\n | 'rtt-unmeasured'\n /** The measured RTT is not less than the time left to the deadline. */\n | 'rtt-exceeds-budget'\n /** The player gave no usable presentation delay for the block's stream. */\n | 'presentation-delay-unavailable'\n /** A lost SS_ID in an undelimited span had no \u00A77.5 anchor to extend it from. */\n | 'range-no-anchor'\n /**\n * The block lost more sources than its repair group carries symbols\n * (sources + RSB_length < K'): no FETCH of the group can complete it, and\n * when multicast already delivered the whole group the relay could serve\n * only duplicates (BLO-50082).\n */\n | 'group-cannot-complete'\n /**\n * The keyframe-bearing block lies below a RAP the receiver already holds\n * complete, so its repair would be moot (draft-ramadan-moq-fec \u00A711.4.2).\n */\n | 'moot-rap'\n\n/**\n * Which term set a FETCH's deadline (BLO-33597): the multicast wait, the\n * player's presentation delay past the block anchor, the keyframe budget past\n * the anchor (when it granted more than the presentation delay), or the\n * release gate's retention ceiling.\n */\nexport type RepairDeadlineBound = 'wait' | 'presentation' | 'keyframe' | 'retention'\n\n/** The admission inputs of a FETCH the scheduler sent (BLO-33597). */\nexport interface RepairSendMeasurement {\n /** The reliable-path RTT admission compared against `budgetMs`. */\n readonly rttMs: number\n /** Time left to the entry's deadline when the FETCH was sent. */\n readonly budgetMs: number\n /**\n * Delay past the block anchor the deadline was computed from; null when\n * the block had no anchor (its deadline is then the multicast wait).\n */\n readonly delayMs: number | null\n /** Time since the block anchor; null when the block had no anchor. */\n readonly blockAgeMs: number | null\n readonly bound: RepairDeadlineBound\n /** True for a range repair FETCH (\u00A77.5 lost SS_ID); absent for a block FETCH. */\n readonly range?: true\n}\n\n/** The subset of MulticastManager the scheduler drives. */\nexport interface RepairFetchHost {\n readonly fecManager: {\n blockProfileForPacket(packetId: number): FecBlockProfile | undefined\n repairFetchTimeoutMsForBlock(sbn: number, packetId: number, catalogTrackName?: string): number | undefined\n /** Receiver-local time of the block's first accepted symbol, if any. */\n blockAnchorAtMs(sbn: number, packetId: number, catalogTrackName?: string): number | undefined\n /** The stream's classifier mode (optional; absent reads as unknown). */\n classifierModeFor?(packetId: number, catalogTrackName?: string): { readonly mode: ClassifierMode }\n /**\n * An open block's received sources and distinct repair symbols, its K'\n * (SSB_length) and RSB_length (optional). The repair group carries\n * RSB_length symbols, so it can complete the block only when\n * sources + RSB_length >= K'; a FETCH that ended leaving fewer than K'\n * distinct symbols cannot be decoding it.\n */\n blockSymbolCounts?(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): BlockSymbolCounts | undefined\n /** How long a terminal block stays authoritative: the release-gate hold ceiling. */\n terminalRetentionMsForPacket(packetId: number): number | undefined\n /** Keep the block open to `untilAtMs` for an admitted FETCH. */\n holdBlockForRepair(\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n untilAtMs: number,\n ): boolean\n /** The admitted FETCH ended without delivering: back to the multicast wait. */\n releaseRepairHold(sbn: number, packetId: number, catalogTrackName?: string): void\n }\n onRepairSubscribe(cb: RepairSubscribeCallback): void\n /**\n * Arrival time of the earliest frame the in-order release gate holds for\n * the stream, or undefined (optional; absent, the block's anchor bounds the\n * ceiling alone). The gate releases that frame `terminalRetentionMsForPacket`\n * after it, whatever its blocks are doing.\n */\n earliestGatedFrameAtMs?(packetId: number, catalogTrackName?: string): number | undefined\n /** The keyframe budget extended a request's deadline by `extensionMs` (optional). */\n noteKeyframeBudgetGranted?(\n extensionMs: number,\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n addRepairLifecycleObserver(observer: RepairLifecycleObserver): () => void\n addTrackResetObserver(observer: (packetId: number, catalogTrackName: string) => void): () => void\n /** Register for lost SS_IDs in undelimited spans (required with `rangeTransport`). */\n onUndelimitedLoss?(cb: (loss: UndelimitedIsoLoss) => void): void\n /** A hole's range repair ended, or none is sent: feed what was not stated. */\n closeIsoHoles?(packetId: number, catalogTrackName: string | undefined, start: number, length: number): void\n clearRepairForBlock(sbn: number, packetId: number, catalogTrackName?: string): void\n processRepairFromMoQ(packet: Uint8Array, ownerPacketId?: number, catalogTrackName?: string): void\n /** This scheduler can request unicast repair: protected flows keep pending entries for it (optional). */\n setUnicastRepair?(enabled: boolean): void\n noteRepairScheduling(\n type: 'repair-preempted' | 'repair-deferred',\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /** A request whose wire priority the catalog could not supply. */\n noteRepairPriority(\n action: 'omitted' | 'declined',\n reason: RepairPriorityReason,\n blockClass: BlockClass,\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /**\n * A request the scheduler did not send (see RepairSkipReason).\n * `measurement.priority` is the request's repair priority (0 keyframe or\n * unknown, 1 audio, 2 delta).\n */\n noteRepairSkipped(\n reason: RepairSkipReason,\n measurement: { readonly rttMs: number | null; readonly budgetMs: number; readonly priority?: RepairPriority },\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /** A running FETCH the scheduler stopped, and why (optional). */\n noteRepairStopped?(\n reason: RepairStopReason,\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /**\n * Whether a complete RAP above SS_ID `ssId` already reached the stream\n * (optional; absent, none ever has). Under the bc-provenance profile only\n * an admitted RAP counts (multicast-01 \u00A77.2.7).\n */\n rapCompleteAbove?(packetId: number, catalogTrackName: string | undefined, ssId: number): boolean\n /** Observe each complete RAP reaching a stream (optional; called synchronously). */\n addRapCompleteObserver?(\n observer: (packetId: number, catalogTrackName: string | undefined, ssId: number) => void,\n ): () => void\n /**\n * Observe each base block (`sbn`) whose RAP fragments the keyframe overlay\n * recovered with the keyframe's FT=0 and FT=1 held (optional; called\n * synchronously). Its keyframe FETCH is then stopped (fec-multicast-00 \u00A76.6).\n */\n addKeyframeOverlayRecoveredObserver?(\n observer: (packetId: number, catalogTrackName: string | undefined, sbn: number) => void,\n ): () => void\n /**\n * A FETCH the scheduler admitted and handed to the transport (optional):\n * every attempt, a retry or a reprioritised restart included.\n */\n noteRepairFetchSent?(\n priority: RepairPriority,\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n measurement: RepairSendMeasurement,\n ): void\n}\n\n/**\n * Why the scheduler ended a running FETCH (the transport cancels it on the\n * wire): preempted by a sent keyframe FETCH, reissued at a new class's\n * priority, past its deadline, its block resolved, track reset or a newer\n * request for the block, dispose, `moot` (a RAP above the block arrived\n * complete first, draft-ramadan-moq-fec \u00A711.4.2), or `overlay-recovered`\n * (the keyframe overlay recovered the block's RAP, fec-multicast-00 \u00A76.6).\n */\nexport type RepairStopReason =\n | 'preempted'\n | 'reprioritized'\n | 'expired'\n | 'resolved'\n | 'reset'\n | 'disposed'\n | 'moot'\n | 'overlay-recovered'\ntype StopReason = RepairStopReason\n\ninterface FetchRun {\n readonly controller: AbortController\n /** When this FETCH went out: it lands around `sentAt + rtt`. */\n readonly sentAt: number\n stopped: StopReason | null\n}\n\ninterface RepairEntry {\n readonly key: string\n readonly sbn: number\n readonly packetId: number\n readonly catalogTrackName?: string\n readonly repairTrack: string\n readonly target: RepairTarget\n /** The block profile `sbn` is keyed by, resolved before the entry is queued. */\n readonly blockProfile: FecBlockProfile\n priority: RepairPriority\n /** SUBSCRIBER_PRIORITY for this entry's class; undefined = omit it. */\n subscriberPriority: number | undefined\n /**\n * Absolute `performance.now()` millisecond by which a FETCH must land: the\n * block's presentation deadline (see the module note). Only ever moves\n * later: a block reclassified from delta can gain the keyframe budget.\n */\n deadlineAt: number\n /**\n * `deadlineAt` without the keyframe budget's extension: the block's playout\n * deadline. A FETCH landing by it costs no stall, so it is never moot.\n */\n playoutDeadlineAt: number\n /** Inputs `deadlineAt` was computed from; they move with it. */\n anchorAt: number | null\n delayMs: number | null\n bound: RepairDeadlineBound\n /** Monotonic enqueue order, the EDF tie-break. */\n readonly order: number\n attempts: number\n running: FetchRun | null\n /** A range request for a pending hole `[sbn, sbn + holeLength)`. */\n readonly range?: RepairFetchRange & { readonly lostSsId: number; readonly holeLength: number }\n /** A range request's deadline timer (see the module note). */\n expiry?: ReturnType<typeof setTimeout>\n}\n\ninterface ResolvedDeadline {\n readonly deadlineAt: number\n readonly extensionMs: number\n readonly anchorAt: number | null\n readonly delayMs: number | null\n readonly bound: RepairDeadlineBound\n}\n\n/**\n * Wire the D-28 repair-fetch scheduler onto a MulticastManager.\n *\n * Call AFTER the manager has a repair-bearing FEC configuration (players gate\n * this on the first fec-setup that carries a repairTrack) \u2014 registration arms\n * the per-block repair deadlines in the FEC layer.\n *\n * Returns a dispose function that aborts outstanding fetches and stops new\n * ones from being issued.\n */\nexport function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOptions): () => void {\n if (!Number.isSafeInteger(opts.attemptLimit) || opts.attemptLimit < 1) {\n throw new Error(\n `[RepairFetch] attemptLimit=${String(opts.attemptLimit)} must be a positive integer`,\n )\n }\n if (typeof opts.reliableRttMs !== 'function') {\n throw new Error('[RepairFetch] reliableRttMs is required: repair admission needs the measured reliable-path RTT')\n }\n if (typeof opts.presentationDelayMs !== 'function') {\n throw new Error('[RepairFetch] presentationDelayMs is required: a repair FETCH is due by the block\\'s presentation deadline')\n }\n if (\n opts.rangeTransport !== undefined &&\n (typeof opts.repairEpochReference !== 'function' || !mc.onUndelimitedLoss || !mc.closeIsoHoles)\n ) {\n throw new Error('[RepairFetch] rangeTransport needs repairEpochReference and a host that reports undelimited losses')\n }\n mc.setUnicastRepair?.(true)\n const entries = new Map<string, RepairEntry>()\n const warnedNoTarget = new Set<string>()\n let disposed = false\n let enqueueOrder = 0\n\n const requestKey = (sbn: number, packetId: number, catalogTrackName?: string): string =>\n JSON.stringify([packetId, catalogTrackName ?? null, sbn])\n const noTargetKey = (packetId: number, catalogTrackName?: string): string =>\n JSON.stringify([packetId, catalogTrackName ?? null])\n\n const clearRepair = (sbn: number, packetId: number, catalogTrackName?: string): void => {\n if (catalogTrackName === undefined) mc.clearRepairForBlock(sbn, packetId)\n else mc.clearRepairForBlock(sbn, packetId, catalogTrackName)\n }\n const note = (type: 'repair-preempted' | 'repair-deferred', entry: RepairEntry): void => {\n mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n /** Whatever ends an entry: a block's repair slot, or a hole's pending state. */\n const clearEntry = (entry: RepairEntry): void => {\n if (entry.range) mc.closeIsoHoles!(entry.packetId, entry.catalogTrackName, entry.sbn, entry.range.holeLength)\n else clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n const releaseHold = (entry: RepairEntry): void => {\n // A hole is no tracked block: nothing holds it but the hole itself.\n if (entry.range) return\n if (entry.catalogTrackName === undefined) mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId)\n else mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n const stop = (entry: RepairEntry, reason: StopReason): void => {\n const run = entry.running\n if (!run) return\n entry.running = null\n run.stopped = reason\n run.controller.abort()\n // A preempted request returns to the queue; until it runs again nothing is\n // in flight that could land, so the block goes back to its multicast wait.\n if (reason === 'preempted') releaseHold(entry)\n mc.noteRepairStopped?.(reason, entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n /** The last SS_ID the entry covers, or undefined while its block is undelimited. */\n const lastSsId = (entry: RepairEntry): number | undefined => {\n if (entry.range) return entry.sbn + entry.range.holeLength - 1\n const length = symbolCounts(entry.sbn, entry.packetId, entry.catalogTrackName)?.authoredLength\n return length === undefined ? undefined : entry.sbn + length - 1\n }\n\n /**\n * A keyframe request whose whole block lies below a RAP already held\n * complete, and whose repair can only land on the keyframe budget's\n * extension. One that lands by the playout deadline stalls nothing, and\n * skipping it only throws away the block's frames (BLO-51635).\n */\n const moot = (entry: RepairEntry): boolean => {\n if (entry.priority !== 0) return false\n const rttMs = readRtt()\n // A running FETCH lands relative to when it was sent, not to now.\n const sentAt = entry.running?.sentAt ?? performance.now()\n if (rttMs !== null && sentAt + rttMs < entry.playoutDeadlineAt) return false\n const last = lastSsId(entry)\n if (last === undefined) return false\n try {\n return mc.rapCompleteAbove?.(entry.packetId, entry.catalogTrackName, last) === true\n } catch {\n return false\n }\n }\n\n /** Drop a request no FETCH will be sent for, and say why. */\n const skip = (entry: RepairEntry, reason: RepairSkipReason, rttMs: number | null, budgetMs: number): void => {\n retire(entry)\n releaseHold(entry)\n clearEntry(entry)\n mc.noteRepairSkipped(\n reason,\n { rttMs, budgetMs, priority: entry.priority },\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n )\n }\n\n /** The keyframe budget for a P0 block, or null: absent, throwing or not positive. */\n const readKeyframeBudget = (packetId: number, catalogTrackName: string | undefined): number | null => {\n const reader = opts.keyframeRepairBudgetMs\n if (reader === undefined) return null\n try {\n const budgetMs = reader(packetId, catalogTrackName)\n return Number.isFinite(budgetMs) && budgetMs > 0 ? budgetMs : null\n } catch {\n return null\n }\n }\n\n const readRtt = (): number | null => {\n try {\n const rtt = opts.reliableRttMs()\n return Number.isFinite(rtt) && rtt > 0 ? rtt : null\n } catch {\n return null\n }\n }\n\n /**\n * Admission: send only a FETCH whose repair group can complete the block\n * and that the measured round trip says can land before the entry's\n * deadline. A refused entry is released, not queued: the\n * deadline only gets closer. Returns the admitting RTT, or null when refused.\n */\n const admit = (entry: RepairEntry, now: number): number | null => {\n const budgetMs = entry.deadlineAt - now\n const rttMs = readRtt()\n const reason: RepairSkipReason | null =\n !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName)\n ? 'group-cannot-complete'\n : rttMs === null\n ? 'rtt-unmeasured'\n : rttMs < budgetMs\n ? null\n : 'rtt-exceeds-budget'\n if (reason === null) return rttMs\n skip(entry, reason, rttMs, budgetMs)\n return null\n }\n\n const retire = (entry: RepairEntry): void => {\n if (entries.get(entry.key) === entry) entries.delete(entry.key)\n if (entry.expiry !== undefined) clearTimeout(entry.expiry)\n }\n\n /** Whether any entry of priority below `priority` is queued or in flight. */\n const outstandingBelow = (priority: RepairPriority): boolean => {\n for (const entry of entries.values()) {\n if (entry.priority < priority) return true\n }\n return false\n }\n\n /**\n * A keyframe FETCH is being sent: cancel every in-flight audio and delta\n * FETCH (\u00A711.4.5 item 3). Called only for a FETCH actually sent.\n */\n const preemptBestEffort = (): void => {\n for (const entry of entries.values()) {\n if (entry.priority === 0 || !entry.running) continue\n stop(entry, 'preempted')\n note('repair-preempted', entry)\n }\n }\n\n /**\n * The wire priority for `blockClass`, or null when no FETCH may be sent.\n * A priority the catalog cannot supply is reported once per request.\n */\n const wirePriority = (\n entry: Pick<RepairEntry, 'target' | 'sbn' | 'packetId' | 'catalogTrackName'>,\n blockClass: BlockClass,\n ): { subscriberPriority: number | undefined } | null => {\n const derived = repairFetchSubscriberPriority(blockClass, entry.target)\n if (derived.kind === 'carry') return { subscriberPriority: derived.subscriberPriority }\n mc.noteRepairPriority(\n derived.kind === 'omit' ? 'omitted' : 'declined',\n derived.reason,\n blockClass,\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n )\n return derived.kind === 'omit' ? { subscriberPriority: undefined } : null\n }\n\n /** `counted` is false for a reprioritisation restart, which is no retry. */\n const start = (entry: RepairEntry, counted = true): void => {\n const now = performance.now()\n if (moot(entry)) {\n skip(entry, 'moot-rap', readRtt(), entry.deadlineAt - now)\n return\n }\n const rttMs = admit(entry, now)\n if (rttMs === null) return\n // Before the transport sees this request, so the cancels go out first.\n if (entry.priority === 0) preemptBestEffort()\n if (counted) entry.attempts++\n mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {\n rttMs,\n budgetMs: entry.deadlineAt - now,\n delayMs: entry.delayMs,\n blockAgeMs: entry.anchorAt === null ? null : Math.max(0, now - entry.anchorAt),\n bound: entry.bound,\n ...(entry.range ? { range: true as const } : {}),\n })\n const run: FetchRun = { controller: new AbortController(), sentAt: now, stopped: null }\n entry.running = run\n // Hold the block open to the presentation deadline while this FETCH can\n // still land: the multicast span elapsing must not abort it.\n if (!entry.range) {\n mc.fecManager.holdBlockForRepair(\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n entry.deadlineAt,\n )\n }\n void execute(entry, run)\n }\n\n const dispatch = (): void => {\n if (disposed) return\n const now = performance.now()\n const queued: RepairEntry[] = []\n for (const entry of [...entries.values()]) {\n if (entry.running) {\n // A fetch still running at its block's fail bound can no longer land\n // in time. Cancel it here rather than trust the transport or the FEC\n // layer's resolution to end it: a hung P0/P1 left running would keep\n // outstandingBelow() true and gate every P1/P2 for the session.\n if (entry.deadlineAt <= now) {\n stop(entry, 'expired')\n retire(entry)\n clearEntry(entry)\n }\n continue\n }\n if (entry.deadlineAt <= now) {\n // The block's fail bound has passed; its resolution is imminent and\n // no fetch can land in time.\n retire(entry)\n if (entry.priority === 2 && entry.attempts === 0) note('repair-deferred', entry)\n clearEntry(entry)\n continue\n }\n queued.push(entry)\n }\n for (const entry of queued) {\n if (entry.priority === 0) start(entry)\n }\n if (outstandingBelow(1)) return\n for (const entry of queued) {\n if (entry.priority === 1) start(entry)\n }\n if (outstandingBelow(2)) return\n queued\n .filter((entry) => entry.priority === 2)\n .sort((a, b) => a.deadlineAt - b.deadlineAt || a.order - b.order)\n .forEach((entry) => start(entry))\n }\n\n /**\n * Whether the block may still be decoding from what it holds: false only\n * when the host shows fewer distinct symbols than its K'. A FETCH that\n * ended leaving it short delivered all it could (its group carries no more\n * symbols), so holding the block, and every frame behind it, longer buys\n * nothing: it goes back to its multicast wait. Unknown counts read as\n * decodable (the FEC layer resolves the block).\n */\n const symbolCounts = (\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n ): BlockSymbolCounts | undefined => {\n try {\n return mc.fecManager.blockSymbolCounts?.(sbn, packetId, catalogTrackName)\n } catch {\n return undefined\n }\n }\n const decodable = (entry: RepairEntry): boolean => {\n const counts = symbolCounts(entry.sbn, entry.packetId, entry.catalogTrackName)\n if (counts === undefined || counts.authoredLength === undefined) return true\n return counts.sources + counts.repairs >= counts.authoredLength\n }\n /**\n * Whether the block's repair group can complete it: its received sources\n * plus every symbol the group carries reach K'. A loss of more sources than\n * the group has repair symbols (every source-only loss among them: the\n * block then already holds the whole group) gets no FETCH-length hold. True\n * while the counts are not known (no repair stated the block yet).\n */\n const groupCanComplete = (sbn: number, packetId: number, catalogTrackName: string | undefined): boolean => {\n const counts = symbolCounts(sbn, packetId, catalogTrackName)\n if (counts === undefined || counts.authoredLength === undefined || counts.repairGroupLength === undefined) {\n return true\n }\n return counts.sources + counts.repairGroupLength >= counts.authoredLength\n }\n\n const finish = (entry: RepairEntry, run: FetchRun, fed: number, error: unknown): void => {\n if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {\n return\n }\n entry.running = null\n if (error === undefined) {\n retire(entry)\n // A fetch that ended without feeding anything cannot resolve the block;\n // release the slot and the hold so failure accounting is not wedged. A\n // fetch that fed symbols leaves resolution to the FEC layer.\n // A range FETCH placed whatever it stated as it arrived; the rest of\n // its hole is now known to have no repair coming.\n if (entry.range || fed === 0 || !decodable(entry)) {\n releaseHold(entry)\n clearEntry(entry)\n }\n dispatch()\n return\n }\n const retryable =\n fed === 0 &&\n entry.attempts < opts.attemptLimit &&\n entry.deadlineAt > performance.now() &&\n opts.shouldRetry?.(error) === true\n if (retryable) {\n // Back through the same admission rule: a P2 retry waits for P0/P1.\n dispatch()\n return\n }\n retire(entry)\n if (fed === 0) releaseHold(entry)\n clearEntry(entry)\n console.warn(\n `[RepairFetch] fetch failed pid=${entry.packetId} sbn=${entry.sbn} track=${entry.repairTrack}:`,\n error,\n )\n dispatch()\n }\n\n // The profile the block key is in; a stream reaching repair was configured\n // from its catalog, so an absent profile is a wiring fault, not a default.\n // Any token other than `iso-ssbg1-v1` (the retired `k-padded-v1` included)\n // is one too: `parseFecBlockProfile` refuses it, whatever an untyped host\n // returns. Resolved when the request arrives (onRepairSubscribe below),\n // never inside execute: there its throw would read as an ordinary fetch\n // failure, warned once per block while the stream silently lost unicast\n // repair.\n const blockProfileOf = (packetId: number): FecBlockProfile => {\n const profile: unknown = mc.fecManager.blockProfileForPacket(packetId)\n if (profile === undefined) {\n throw new Error(`[RepairFetch] packetId=${packetId} has no configured fec.blockProfile`)\n }\n return parseFecBlockProfile(profile, `[RepairFetch] packetId=${packetId} fec.blockProfile`)\n }\n\n const execute = async (entry: RepairEntry, run: FetchRun): Promise<void> => {\n let fed = 0\n const common = {\n repairTrack: entry.repairTrack,\n blockProfile: entry.blockProfile,\n packetId: entry.packetId,\n signal: run.controller.signal,\n subscriberPriority: entry.subscriberPriority,\n }\n try {\n const packets = entry.range\n ? opts.rangeTransport!({\n ...common,\n startGroup: entry.range.startGroup,\n endGroup: entry.range.endGroup,\n lostSsId: entry.range.lostSsId,\n })\n : opts.transport({ ...common, sbn: entry.sbn })\n for await (const packet of packets) {\n if (run.stopped !== null || entry.running !== run) break\n fed++\n if (entry.catalogTrackName === undefined) {\n mc.processRepairFromMoQ(packet, entry.packetId)\n } else {\n mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName)\n }\n }\n finish(entry, run, fed, undefined)\n } catch (error) {\n finish(entry, run, fed, error ?? new Error('repair transport rejected'))\n }\n }\n\n const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {\n for (const entry of [...entries.values()]) {\n if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue\n retire(entry)\n stop(entry, 'reset')\n if (entry.range) clearEntry(entry)\n }\n // A reset is when the packetId -> repair-track mapping can change, so\n // re-arm the once-per-track warn for the new epoch.\n warnedNoTarget.delete(noTargetKey(packetId, catalogTrackName))\n dispatch()\n })\n\n const removeLifecycleObserver = mc.addRepairLifecycleObserver((event) => {\n if (disposed) return\n if (event.type === 'reclassified') {\n const entry = entries.get(\n requestKey(event.block.sbn, event.block.packetId, event.block.catalogTrackName ?? undefined),\n )\n if (!entry || entry.priority !== 2) return\n const promoted = repairPriorityForClass(event.to)\n if (promoted >= entry.priority) return\n const wire = wirePriority(entry, event.to)\n // A block leaves P2 only for P0, whose request is never declined.\n if (wire === null) return\n entry.priority = promoted\n entry.subscriberPriority = wire.subscriberPriority\n // A block that turned out to carry a keyframe gets the keyframe budget\n // its request would have had (never a shorter deadline than it had).\n const promotedDeadline = presentationDeadlineAt(\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n performance.now(),\n event.to,\n )\n if (promotedDeadline !== null && promotedDeadline.deadlineAt > entry.deadlineAt) {\n entry.deadlineAt = promotedDeadline.deadlineAt\n entry.playoutDeadlineAt = Math.max(\n entry.playoutDeadlineAt,\n promotedDeadline.deadlineAt - promotedDeadline.extensionMs,\n )\n entry.anchorAt = promotedDeadline.anchorAt\n entry.delayMs = promotedDeadline.delayMs\n entry.bound = promotedDeadline.bound\n noteBudget(entry, promotedDeadline.extensionMs)\n }\n if (entry.running) {\n // The running FETCH carries the delta priority. Cancel it and send\n // the block's repair again under its new class (\u00A711.4.5): a keyframe\n // repair must not stay queued behind source media.\n stop(entry, 'reprioritized')\n start(entry, false)\n }\n dispatch()\n return\n }\n if (event.type !== 'recovered' && event.type !== 'failed') return\n const entry = entries.get(\n requestKey(event.block.sbn, event.block.packetId, event.block.catalogTrackName ?? undefined),\n )\n if (!entry) return\n retire(entry)\n if (entry.running) stop(entry, 'resolved')\n else if (entry.priority === 2 && entry.attempts === 0) note('repair-deferred', entry)\n dispatch()\n })\n\n /**\n * The block's presentation deadline, or null when the player gives none.\n *\n * anchor + presentationDelay, clamped below by the multicast wait (the block\n * is open that long whatever happens, so a FETCH landing inside it still\n * counts) and above by anchor + the release gate's hold ceiling (a successor\n * frame may not wait behind a block longer than that). A block no symbol\n * reached has no anchor; its deadline is the multicast wait. The ceiling is\n * measured from the earlier of the anchor and the earliest frame the gate\n * holds for the stream (see the module note). A block that may hold a RAP\n * (`keyframeBudgetEligible`) uses the larger of the presentation delay and\n * the keyframe budget; `extensionMs` is how much later that made it.\n */\n const presentationDeadlineAt = (\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n now: number,\n blockClass: BlockClass,\n ): ResolvedDeadline | null => {\n // Undefined: the packetId is no longer configured (released), so it has\n // no repair budget.\n const waitMs = mc.fecManager.repairFetchTimeoutMsForBlock(sbn, packetId, catalogTrackName)\n if (waitMs === undefined) return null\n const waitDeadlineAt = now + waitMs\n const anchor = mc.fecManager.blockAnchorAtMs(sbn, packetId, catalogTrackName)\n if (anchor === undefined) {\n return { deadlineAt: waitDeadlineAt, extensionMs: 0, anchorAt: null, delayMs: null, bound: 'wait' }\n }\n let delayMs: number\n try {\n delayMs = opts.presentationDelayMs(packetId, catalogTrackName)\n } catch {\n return null\n }\n if (!(Number.isFinite(delayMs) && delayMs > 0)) return null\n const retentionMs = mc.fecManager.terminalRetentionMsForPacket(packetId)\n if (retentionMs === undefined) return null\n const ceilingAt = Math.min(anchor, earliestGatedFrameAt(packetId, catalogTrackName) ?? anchor) + retentionMs\n const at = (\n delay: number,\n delayBound: 'presentation' | 'keyframe',\n ): { readonly deadlineAt: number; readonly bound: RepairDeadlineBound } => {\n const presentAt = Math.min(anchor + delay, ceilingAt)\n if (waitDeadlineAt >= presentAt) return { deadlineAt: waitDeadlineAt, bound: 'wait' }\n return { deadlineAt: presentAt, bound: anchor + delay <= ceilingAt ? delayBound : 'retention' }\n }\n const base = at(delayMs, 'presentation')\n const budgetMs =\n keyframeBudgetEligible(blockClass, classifierMode(packetId, catalogTrackName)) &&\n groupCanComplete(sbn, packetId, catalogTrackName)\n ? readKeyframeBudget(packetId, catalogTrackName)\n : null\n if (budgetMs === null || budgetMs <= delayMs) return { ...base, extensionMs: 0, anchorAt: anchor, delayMs }\n const extended = at(budgetMs, 'keyframe')\n return {\n ...extended,\n extensionMs: extended.deadlineAt - base.deadlineAt,\n anchorAt: anchor,\n delayMs: budgetMs,\n }\n }\n\n const earliestGatedFrameAt = (packetId: number, catalogTrackName: string | undefined): number | undefined => {\n try {\n const at = mc.earliestGatedFrameAtMs?.(packetId, catalogTrackName)\n return at !== undefined && Number.isFinite(at) ? at : undefined\n } catch {\n return undefined\n }\n }\n\n const classifierMode = (packetId: number, catalogTrackName: string | undefined): ClassifierMode | undefined => {\n try {\n return mc.fecManager.classifierModeFor?.(packetId, catalogTrackName).mode\n } catch {\n return undefined\n }\n }\n\n const noteBudget = (entry: Pick<RepairEntry, 'sbn' | 'packetId' | 'catalogTrackName'>, extensionMs: number): void => {\n if (extensionMs > 0) mc.noteKeyframeBudgetGranted?.(extensionMs, entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n mc.onRepairSubscribe((sbn, packetId, catalogTrackName, blockClass) => {\n if (disposed) {\n clearRepair(sbn, packetId, catalogTrackName)\n return\n }\n // Throws on a wiring fault, here and synchronously, before anything is\n // queued (see blockProfileOf).\n const blockProfile = blockProfileOf(packetId)\n\n // Fire-time resolution \u2014 see ResolveRepairTarget.\n const target = opts.resolveTarget(packetId, catalogTrackName)\n if (!target) {\n // A starved block with no repair target can only resolve via the fail\n // bound. Say so, once per track: a null target is also the documented\n // answer for a rung that carries no repair, and the typed lifecycle\n // events already enumerate every block.\n const warnKey = noTargetKey(packetId, catalogTrackName)\n if (!warnedNoTarget.has(warnKey)) {\n warnedNoTarget.add(warnKey)\n console.warn(\n `[RepairFetch] no repair target for pid=${packetId} sbn=${sbn}` +\n `${catalogTrackName === undefined ? '' : ` track=${catalogTrackName}`}; releasing without a fetch`,\n )\n }\n clearRepair(sbn, packetId, catalogTrackName)\n return\n }\n\n const key = requestKey(sbn, packetId, catalogTrackName)\n const previous = entries.get(key)\n if (previous) {\n retire(previous)\n stop(previous, 'reset')\n }\n const identity = {\n target,\n sbn,\n packetId,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n }\n const wire = wirePriority(identity, blockClass)\n if (wire === null) {\n // No conformant FETCH exists for this block (see RepairFetchPriority).\n clearRepair(sbn, packetId, catalogTrackName)\n return\n }\n const now = performance.now()\n const priority = repairPriorityForClass(blockClass)\n const deadline = presentationDeadlineAt(sbn, packetId, catalogTrackName, now, blockClass)\n if (deadline === null) {\n clearRepair(sbn, packetId, catalogTrackName)\n mc.noteRepairSkipped(\n 'presentation-delay-unavailable',\n { rttMs: readRtt(), budgetMs: 0, priority },\n sbn,\n packetId,\n catalogTrackName,\n )\n return\n }\n const entry: RepairEntry = {\n key,\n ...identity,\n repairTrack: target.repairTrack,\n blockProfile,\n priority,\n subscriberPriority: wire.subscriberPriority,\n deadlineAt: deadline.deadlineAt,\n playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,\n anchorAt: deadline.anchorAt,\n delayMs: deadline.delayMs,\n bound: deadline.bound,\n order: enqueueOrder++,\n attempts: 0,\n running: null,\n }\n entries.set(key, entry)\n noteBudget(entry, deadline.extensionMs)\n dispatch()\n })\n\n /**\n * \u00A77.5 range for a lost SS_ID: from ext(S_prev) when a stated block ends\n * where the hole starts, else from the epoch reference r. Null when there\n * is no r, or the lost SS_ID cannot be extended from it.\n */\n const rangeFor = (loss: UndelimitedIsoLoss, repairTrack: string): RepairFetchRange | null => {\n let reference: bigint | undefined\n try {\n reference = opts.repairEpochReference!(repairTrack)\n } catch {\n return null\n }\n if (reference === undefined) return null\n try {\n const anchor = loss.previousBlockStart === undefined\n ? { reference }\n : { previousBlockStart: extendSsId(reference, loss.previousBlockStart) }\n return repairFetchRange(loss.lostSsId, loss.kMax, anchor)\n } catch (error) {\n if (error instanceof RangeError) return null\n throw error\n }\n }\n\n if (opts.rangeTransport !== undefined) {\n mc.onUndelimitedLoss!((loss) => {\n const { packetId, catalogTrackName, holeStart, holeLength } = loss\n const close = (): void => mc.closeIsoHoles!(packetId, catalogTrackName, holeStart, holeLength)\n if (disposed) return close()\n const blockProfile = blockProfileOf(packetId)\n const target = opts.resolveTarget(packetId, catalogTrackName)\n if (!target) return close()\n const priority = repairPriorityForClass('unknown')\n const range = rangeFor(loss, target.repairTrack)\n if (range === null) {\n close()\n mc.noteRepairSkipped('range-no-anchor', { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName)\n return\n }\n const identity = {\n target,\n sbn: holeStart,\n packetId,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n }\n // `unknown` is P0, whose request is never declined (omitted at worst).\n const wire = wirePriority(identity, 'unknown')!\n // The hole is no tracked block: no anchor, so this is the multicast\n // wait (`bound: 'wait'`), by intent. A hole holds the release gate, so it\n // must not wait out a presentation delay on top.\n const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, performance.now(), 'unknown')\n if (deadline === null) {\n close()\n mc.noteRepairSkipped(\n 'presentation-delay-unavailable',\n { rttMs: readRtt(), budgetMs: 0, priority },\n holeStart,\n packetId,\n catalogTrackName,\n )\n return\n }\n const entry: RepairEntry = {\n key: JSON.stringify(['range', packetId, catalogTrackName ?? null, holeStart]),\n ...identity,\n repairTrack: target.repairTrack,\n blockProfile,\n priority,\n subscriberPriority: wire.subscriberPriority,\n deadlineAt: deadline.deadlineAt,\n playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,\n anchorAt: deadline.anchorAt,\n delayMs: deadline.delayMs,\n bound: deadline.bound,\n order: enqueueOrder++,\n attempts: 0,\n running: null,\n range: { ...range, lostSsId: loss.lostSsId, holeLength },\n }\n const previous = entries.get(entry.key)\n if (previous) {\n retire(previous)\n stop(previous, 'reset')\n }\n entries.set(entry.key, entry)\n entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1)\n dispatch()\n })\n }\n\n // A RAP arrived complete: a keyframe FETCH below it is moot (\u00A711.4.2). The\n // observer runs inside the frame push, so the cancel waits for a microtask\n // rather than re-entering the FEC layer through the hold release.\n const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {\n queueMicrotask(() => {\n if (disposed) return\n for (const entry of [...entries.values()]) {\n if (entry.packetId !== packetId || !entry.running || !moot(entry)) continue\n stop(entry, 'moot')\n retire(entry)\n releaseHold(entry)\n clearEntry(entry)\n }\n dispatch()\n })\n })\n\n // fec-multicast-00 \u00A76.6: once the overlay recovered a block's RAP (its FT=0\n // and FT=1 held), the rest of the block is concealed as a non-keyframe\n // block, so its keyframe FETCH is stopped whatever its deadline. Deferred\n // as the RAP observer is: this fires from inside the recovered-block route.\n const removeOverlayObserver = mc.addKeyframeOverlayRecoveredObserver?.((packetId, catalogTrackName, sbn) => {\n queueMicrotask(() => {\n if (disposed) return\n for (const entry of [...entries.values()]) {\n if (entry.range || entry.priority !== 0 || entry.packetId !== packetId || entry.sbn !== sbn) continue\n if (catalogTrackName !== undefined && entry.catalogTrackName !== catalogTrackName) continue\n stop(entry, 'overlay-recovered')\n retire(entry)\n releaseHold(entry)\n clearEntry(entry)\n }\n dispatch()\n })\n })\n\n return () => {\n disposed = true\n mc.setUnicastRepair?.(false)\n removeResetObserver()\n removeLifecycleObserver()\n removeRapObserver?.()\n removeOverlayObserver?.()\n for (const entry of [...entries.values()]) {\n clearEntry(entry)\n if (entry.running) releaseHold(entry)\n stop(entry, 'disposed')\n if (entry.expiry !== undefined) clearTimeout(entry.expiry)\n }\n entries.clear()\n }\n}\n", "/**\n * Keyframe-protected FEC repair policy.\n *\n * Every FEC block still gets in-band FEC decode. What this module decides is\n * (1) which blocks may hold a keyframe (RAP), so a block short of K at its\n * repair deadline can be repaired over QUIC with the right priority, and\n * (2) what a sink must do with the first frame released after a gap: stall\n * until the next RAP, or conceal and keep playing.\n *\n * The two decisions are deliberately independent. Classification only sets\n * repair priority. The stall/conceal decision is made again per released gap\n * from the media that is actually missing, so a wrong classification can only\n * lower a block's repair priority; it can never make playback wrong.\n *\n * Everything here is derived from catalog-authored timing and in-band MMTP\n * evidence. There are no timers, no cadence heuristics, and no defaults: a\n * track whose geometry cannot be derived exactly runs in the defined\n * fail-closed mode `all-protected`, where every block is treated as possibly\n * keyframe-bearing.\n *\n * All three players (moqtail, hang, Shaka) consume this one implementation.\n */\n\nimport { deriveFecInterleaveDepth } from '@blockcast/mmt-fec'\n\n// \u2500\u2500\u2500 Classes and priorities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Repair class of one FEC block.\n *\n * - `keyframe`: the block holds (or held) a RAP / MPU start.\n * - `delta`: the block provably holds only non-RAP video frames.\n * - `unknown`: neither could be proven; treated exactly like `keyframe`.\n * - `audio`: an audio block.\n */\nexport type BlockClass = 'keyframe' | 'delta' | 'unknown' | 'audio'\n\n/** Repair priority. Lower is more urgent. */\nexport type RepairPriority = 0 | 1 | 2\n\nexport type TrackKind = 'video' | 'audio'\n\n/** P0 = keyframe/unknown (stall-preventing), P1 = audio, P2 = delta. */\nexport function repairPriorityForClass(blockClass: BlockClass): RepairPriority {\n switch (blockClass) {\n case 'keyframe':\n case 'unknown':\n return 0\n case 'audio':\n return 1\n case 'delta':\n return 2\n }\n}\n\n// \u2500\u2500\u2500 Geometry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ClassifierMode = 'classifying' | 'all-protected'\n\n/** Why a track runs `all-protected`. */\nexport type ClassifierReason =\n // Geometry (catalog) reasons.\n | 'framerate-absent'\n | 'keyframe-cadence-absent'\n | 'keyframe-interval-not-integral'\n | 'block-spans-gop'\n /**\n * The catalog keyframe interval is not an exact integer number of ticks\n * in the media track's own timescale (ISO-BMFF `mdhd`), so released RAP\n * spacing cannot be verified and hold-last cannot predict the next RAP.\n * Nothing is wrong with the stream; the cadence is simply unprovable.\n */\n | 'keyframe-interval-not-media-integral'\n /**\n * Another stream on this packetId was configured with its own geometry\n * and this one was not, so no geometry provably belongs to it (ABR rungs\n * sharing a packetId).\n */\n | 'stream-geometry-absent'\n // In-band invariant violations (positive evidence only).\n | 'rap-not-mpu-start'\n | 'mpu-longer-than-gop'\n | 'block-not-contiguous'\n | 'rap-spacing-mismatch'\n | 'mpu-multiple-fragments'\n | 'block-wider-than-interleave'\n\n/** Raw catalog timing for one track. Never a resolved cadence. */\nexport interface BlockClassifierGeometryInput {\n readonly trackKind: TrackKind\n /** Catalog `framerate` (frames per second). */\n readonly framerate?: number\n /** Catalog `fec.interleaveDepthMs`: the full FEC block span (\u00A75.1). */\n readonly interleaveDepthMs: number\n /** Catalog track `timescale` for `keyframeIntervalTicks`. */\n readonly timescale?: number\n /** Catalog `keyframeIntervalTicks` (preferred when the timescale is present). */\n readonly keyframeIntervalTicks?: number\n /** Catalog `keyframeIntervalMs`. */\n readonly keyframeIntervalMs?: number\n}\n\n/** Exact keyframe interval as a rational number of seconds: ticks / timescale. */\nexport interface KeyframeInterval {\n readonly ticks: number\n readonly timescale: number\n}\n\nexport type BlockClassifierGeometry =\n | { readonly trackKind: 'audio' }\n | {\n readonly trackKind: 'video'\n readonly mode: 'classifying'\n /** F: one frame's duration in milliseconds (1000 / framerate). */\n readonly frameDurationMs: number\n /** D: media units per FEC block (deriveFecInterleaveDepth). */\n readonly interleaveUnits: number\n /** G: frames per GOP. */\n readonly gopFrames: number\n readonly keyframeInterval: KeyframeInterval\n }\n | {\n readonly trackKind: 'video'\n readonly mode: 'all-protected'\n readonly reason: ClassifierReason\n }\n\n/**\n * Integer value of `numerator / denominator` when it is one, else null.\n *\n * Catalog framerates are JSON numbers, so an NTSC rate arrives as the double\n * nearest 30000/1001. One multiply and one divide each add at most half an\n * ulp of relative error (IEEE-754), so a quotient within two ulps of an\n * integer IS that integer; anything further is genuinely non-integral.\n */\nfunction exactInteger(numerator: number, denominator: number): number | null {\n if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0) {\n return null\n }\n const quotient = numerator / denominator\n const rounded = Math.round(quotient)\n if (rounded < 1) return null\n const representationError = 2 * Number.EPSILON * Math.abs(quotient)\n return Math.abs(quotient - rounded) <= representationError ? rounded : null\n}\n\nfunction isPositiveFinite(value: number | undefined): value is number {\n return value !== undefined && Number.isFinite(value) && value > 0\n}\n\n/**\n * Derive the per-track classifier geometry from raw catalog fields.\n *\n * `all-protected` is the policy's defined fail-closed state, not a default:\n * it is returned with the exact reason. The only throw is for a value the\n * catalog validator already rejects (a non-positive block span).\n */\nexport function deriveBlockClassifierGeometry(\n input: BlockClassifierGeometryInput,\n): BlockClassifierGeometry {\n if (!isPositiveFinite(input.interleaveDepthMs)) {\n throw new Error(\n `[BlockPolicy] interleaveDepthMs=${input.interleaveDepthMs} is not a positive block span; ` +\n 'the catalog validator rejects this',\n )\n }\n if (input.trackKind === 'audio') return { trackKind: 'audio' }\n\n if (!isPositiveFinite(input.framerate)) {\n return { trackKind: 'video', mode: 'all-protected', reason: 'framerate-absent' }\n }\n const framerate = input.framerate\n\n let keyframeInterval: KeyframeInterval\n if (isPositiveFinite(input.keyframeIntervalTicks) && isPositiveFinite(input.timescale)) {\n keyframeInterval = { ticks: input.keyframeIntervalTicks, timescale: input.timescale }\n } else if (isPositiveFinite(input.keyframeIntervalMs)) {\n keyframeInterval = { ticks: input.keyframeIntervalMs, timescale: 1000 }\n } else {\n return { trackKind: 'video', mode: 'all-protected', reason: 'keyframe-cadence-absent' }\n }\n\n const gopFrames = exactInteger(keyframeInterval.ticks * framerate, keyframeInterval.timescale)\n if (gopFrames === null) {\n return {\n trackKind: 'video',\n mode: 'all-protected',\n reason: 'keyframe-interval-not-integral',\n }\n }\n\n const frameDurationMs = 1000 / framerate\n const interleaveUnits = deriveFecInterleaveDepth(input.interleaveDepthMs, frameDurationMs)\n if (interleaveUnits >= gopFrames) {\n return { trackKind: 'video', mode: 'all-protected', reason: 'block-spans-gop' }\n }\n\n return {\n trackKind: 'video',\n mode: 'classifying',\n frameDurationMs,\n interleaveUnits,\n gopFrames,\n keyframeInterval,\n }\n}\n\n/**\n * The keyframe interval expressed in a media track's timescale, or null when\n * the geometry is not `classifying` or the interval is not an exact integer\n * number of media ticks.\n */\nexport function keyframeIntervalMediaTicks(\n geometry: BlockClassifierGeometry,\n mediaTimescale: number,\n): bigint | null {\n if (geometry.trackKind !== 'video' || geometry.mode !== 'classifying') return null\n if (!Number.isSafeInteger(mediaTimescale) || mediaTimescale <= 0) return null\n const { ticks, timescale } = geometry.keyframeInterval\n if (!Number.isSafeInteger(ticks) || !Number.isSafeInteger(timescale)) return null\n const scaled = BigInt(ticks) * BigInt(mediaTimescale)\n const divisor = BigInt(timescale)\n if (scaled % divisor !== 0n) return null\n return scaled / divisor\n}\n\n// \u2500\u2500\u2500 Evidence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * In-band evidence from ONE first-arrival source symbol received from the\n * network. Recovered symbols and repair symbols never contribute, and the\n * MFU `priority > 0x80` hint is not evidence.\n */\nexport interface SourceSymbolEvidence {\n /** The MMTP header R (RAP) bit. */\n readonly rap: boolean\n /** MPU_sequence_number, when parsed from this packet. */\n readonly mpu?: number\n /**\n * 0-based sample position within the MPU: the MFU DU-header\n * `sample_number` minus one (ISO/IEC 23008-1 numbers samples from 1, and\n * the parser rejects `sample_number` 0). Present only when this packet\n * carried a DU header.\n */\n readonly idx?: number\n /** movie_fragment_sequence_number from the DU header, when present. */\n readonly movieFragment?: number\n /**\n * DU-header `offset`: the data unit's authored byte position within its\n * sample (0 for a whole-sample data unit, the partition boundary for a\n * subsample data unit). Present only with `idx`; absent reads as 0.\n */\n readonly offset?: number\n /**\n * The MMTP header packet_sequence_number (contiguous per packet_id). Not\n * classification evidence: FECManager uses it to bound how many blocks the\n * producer can have started between two source symbols, so an outage is\n * never mistaken for a source restart.\n */\n readonly packetSequenceNumber?: number\n}\n\nexport type ReclassificationEvidence = 'rap' | 'mpu-span' | 'window' | 'classifier-disabled'\n\nexport type ClassifierChange =\n | {\n readonly kind: 'reclassified'\n readonly sbn: number\n readonly from: BlockClass\n readonly to: BlockClass\n readonly evidence: ReclassificationEvidence\n }\n | {\n readonly kind: 'mode'\n readonly mode: 'all-protected'\n readonly reason: ClassifierReason\n }\n\ninterface BlockEvidence {\n rapSeen: boolean\n mpus: Set<number>\n idxMin?: number\n idxMax?: number\n cls: BlockClass\n}\n\n/** A data-unit start (a packet carrying a DU header) and the block it is in. */\ninterface DataUnitStart {\n readonly movieFragment: number\n readonly idx: number\n readonly offset: number\n readonly sbn: number\n}\n\n/** Authored order of two data-unit starts of one MPU. */\nfunction compareDataUnitStarts(\n a: Omit<DataUnitStart, 'sbn'>,\n b: Omit<DataUnitStart, 'sbn'>,\n): number {\n return a.movieFragment - b.movieFragment || a.idx - b.idx || a.offset - b.offset\n}\n\n/**\n * Per-stream block classifier.\n *\n * Classes only move toward more protection or narrower evidence:\n * `unknown\u2192delta` and `unknown\u2192keyframe` / `delta\u2192keyframe` on evidence, and\n * `delta\u2192unknown` when an invariant disables classification. `keyframe` is\n * terminal.\n *\n * The delta window rule rests on three premises, each guarded by an\n * invariant that fires on positive evidence only:\n *\n * - P1: the source packets of one block are one contiguous run of the\n * stream's authored order, and blocks follow each other in SBN order\n * (`block-not-contiguous`). The producer assigns SS_IDs in authored order\n * and a block is the SS_ID range [SBN*K, SBN*K + K), so this holds for a\n * block that closes at K symbols in the middle of a frame: that frame's\n * fragments then land in two ADJACENT blocks, which is still one run each.\n * A frame split across adjacent blocks is not a violation.\n * - P2: that run touches at most D media units, counting a frame continued\n * from the previous block (`block-wider-than-interleave`). The producer\n * counts a media-unit boundary only once the open block holds a symbol.\n * - P3: an MPU starts with its only RAP and holds at most G frames\n * (`rap-not-mpu-start`, `mpu-longer-than-gop`, `rap-spacing-mismatch`), in\n * one movie fragment (`mpu-multiple-fragments`).\n *\n * P1 and P2 are what make a block's frames contiguous and lie within\n * [idxMax \u2212 D + 1, idxMin + D \u2212 1] of its MPU. What refutes P1 is a pair of\n * source packets whose wire-stated authored order and SBN order disagree:\n * a later MPU in an earlier block, a later data unit of one MPU (by\n * movie_fragment_sequence_number, sample_number, then DU offset) in an\n * earlier block, or one data unit in two blocks. That is the signature of a\n * round-robin interleave, which scatters one run over several concurrent\n * blocks. Non-adjacency alone is not: a sample larger than K packets\n * legitimately spans blocks SBN..SBN+n.\n */\nexport class BlockClassifier {\n #geometry: BlockClassifierGeometry\n #mode: ClassifierMode\n #reason: ClassifierReason | null\n /**\n * False once the stream showed that an MPU boundary does not bound its\n * RAPs (a RAP inside an MPU, or an MPU longer than a GOP). Recorded in\n * every mode: the stall/conceal decision relies on it even when the\n * catalog geometry already runs the classifier `all-protected`.\n */\n #mpuBoundsRaps = true\n #blocks = new Map<number, BlockEvidence>()\n /**\n * P1 ledger, kept for the MPUs within the classification horizon (the\n * highest MPU seen and the one before it): the SBN span of every source\n * packet of each MPU, and each MPU's data-unit starts in authored order.\n * Valid only within one continuous SBN sequence; a source discontinuity\n * clears it (`noteSourceDiscontinuity`).\n */\n #mpuSbnSpans = new Map<number, { min: number; max: number }>()\n #dataUnitStarts = new Map<number, DataUnitStart[]>()\n #highestMpu: number | null = null\n /** MPU \u2192 movie_fragment_sequence_number observed in it. */\n #mpuFragments = new Map<number, number>()\n #lastRap: { mpu: number; presentationTime: bigint; mediaTimescale: number } | null = null\n\n constructor(geometry: BlockClassifierGeometry) {\n this.#geometry = geometry\n this.#mode = 'classifying'\n this.#reason = null\n this.reset(geometry)\n }\n\n get geometry(): BlockClassifierGeometry {\n return this.#geometry\n }\n\n get mode(): ClassifierMode {\n return this.#mode\n }\n\n get reason(): ClassifierReason | null {\n return this.#reason\n }\n\n /**\n * Whether \"same MPU on both sides of a gap\" still proves the gap held no\n * RAP. It stops holding for the rest of the epoch once a RAP was received\n * inside an MPU or an MPU ran longer than a GOP.\n */\n get mpuBoundsRaps(): boolean {\n return this.#mpuBoundsRaps\n }\n\n /** Begin a fresh epoch (catalog change or track reset). */\n reset(geometry: BlockClassifierGeometry = this.#geometry): void {\n this.#geometry = geometry\n this.#blocks.clear()\n this.#clearContiguityLedger()\n this.#mpuFragments.clear()\n this.#lastRap = null\n this.#mpuBoundsRaps = true\n if (geometry.trackKind === 'video' && geometry.mode === 'all-protected') {\n this.#mode = 'all-protected'\n this.#reason = geometry.reason\n } else {\n this.#mode = 'classifying'\n this.#reason = null\n }\n }\n\n /**\n * Retire this classifier (its stream moves to a new catalog epoch). Every\n * block it had classified `delta` loses the evidence behind that class and\n * reads `unknown` from now on; the returned changes say so.\n */\n retire(): ClassifierChange[] {\n const changes: ClassifierChange[] = []\n for (const [sbn, block] of this.#blocks) {\n if (block.cls !== 'delta') continue\n block.cls = 'unknown'\n changes.push({\n kind: 'reclassified',\n sbn,\n from: 'delta',\n to: 'unknown',\n evidence: 'classifier-disabled',\n })\n }\n return changes\n }\n\n /**\n * The stream's SBN sequence restarted (FECManager source discontinuity:\n * a publisher restart, or the SS_ID space wrapped). Authored order and SBN\n * order are comparable only within one continuous sequence, so the P1\n * ledger starts again.\n */\n noteSourceDiscontinuity(): void {\n this.#clearContiguityLedger()\n }\n\n /** Register a block that exists without evidence (a vanished block). */\n touch(sbn: number): void {\n this.#blockFor(sbn)\n }\n\n /** Forget a resolved block. */\n forget(sbn: number): void {\n this.#blocks.delete(sbn)\n }\n\n classOf(sbn: number): BlockClass {\n if (this.#geometry.trackKind === 'audio') return 'audio'\n return this.#blocks.get(sbn)?.cls ?? 'unknown'\n }\n\n /** Record one first-arrival source symbol's evidence for block `sbn`. */\n observe(sbn: number, evidence: SourceSymbolEvidence): ClassifierChange[] {\n if (this.#geometry.trackKind === 'audio') return []\n const changes: ClassifierChange[] = []\n const block = this.#blockFor(sbn)\n\n if (evidence.rap) block.rapSeen = true\n if (evidence.mpu !== undefined) block.mpus.add(evidence.mpu)\n if (evidence.idx !== undefined) {\n block.idxMin = block.idxMin === undefined ? evidence.idx : Math.min(block.idxMin, evidence.idx)\n block.idxMax = block.idxMax === undefined ? evidence.idx : Math.max(block.idxMax, evidence.idx)\n }\n\n // A RAP inside an MPU refutes \"an MPU boundary bounds RAPs\" whatever the\n // classifier mode, because gap decisions rely on it in every mode.\n if (evidence.rap && evidence.idx !== undefined && evidence.idx !== 0) {\n this.#mpuBoundsRaps = false\n }\n\n const violation = this.#checkInvariants(sbn, block, evidence)\n if (violation !== null) changes.push(...this.#disable(violation))\n\n const next = this.#classify(block)\n if (next !== block.cls && this.#allowed(block.cls, next)) {\n changes.push({\n kind: 'reclassified',\n sbn,\n from: block.cls,\n to: next,\n evidence: this.#evidenceFor(block, next),\n })\n block.cls = next\n }\n return changes\n }\n\n /**\n * Record a RAP frame released in order. Released MPU-start RAPs of MPUs m\n * and m+n must be exactly n keyframe intervals apart in the media\n * timescale, compared as integers.\n *\n * Comparing only consecutive MPUs would miss exactly the case the window\n * rule cannot survive: a short MPU whose successor's RAP was lost (that\n * RAP-bearing block is the one a wrong G calls delta), because the next\n * released RAP then belongs to m+2. Every MPU is one interval long, so the\n * span to any later released RAP proves or refutes all MPUs in between.\n */\n noteReleasedRap(\n mpu: number,\n presentationTime: bigint,\n mediaTimescale: number,\n ): ClassifierChange[] {\n if (this.#geometry.trackKind === 'audio' || this.#mode === 'all-protected') {\n return []\n }\n // Verification compares integers in the media timescale. When the\n // catalog interval is not an exact tick count there, spacing is\n // unprovable: fail closed, naming the cause, rather than blaming the\n // encoder with rap-spacing-mismatch on a perfectly spaced RAP.\n const interval = keyframeIntervalMediaTicks(this.#geometry, mediaTimescale)\n if (interval === null) return this.#disable('keyframe-interval-not-media-integral')\n const previous = this.#lastRap\n // A further RAP inside an already-anchored MPU is not an MPU start; the\n // MPU keeps its first RAP as the spacing anchor (rap-not-mpu-start owns\n // mid-MPU RAPs).\n if (previous !== null && previous.mpu === mpu && previous.mediaTimescale === mediaTimescale) {\n return []\n }\n this.#lastRap = { mpu, presentationTime, mediaTimescale }\n if (\n previous === null ||\n mpu < previous.mpu ||\n previous.mediaTimescale !== mediaTimescale\n ) {\n return []\n }\n if (\n presentationTime - previous.presentationTime === interval * BigInt(mpu - previous.mpu)\n ) {\n return []\n }\n return this.#disable('rap-spacing-mismatch')\n }\n\n #blockFor(sbn: number): BlockEvidence {\n let block = this.#blocks.get(sbn)\n if (!block) {\n block = { rapSeen: false, mpus: new Set(), cls: 'unknown' }\n this.#blocks.set(sbn, block)\n }\n return block\n }\n\n #checkInvariants(\n sbn: number,\n block: BlockEvidence,\n evidence: SourceSymbolEvidence,\n ): ClassifierReason | null {\n if (this.#mode === 'all-protected' || this.#geometry.trackKind !== 'video') return null\n const geometry = this.#geometry\n if (geometry.mode !== 'classifying') return null\n if (evidence.rap && evidence.idx !== undefined && evidence.idx !== 0) {\n return 'rap-not-mpu-start'\n }\n if (evidence.idx !== undefined && evidence.idx >= geometry.gopFrames) {\n return 'mpu-longer-than-gop'\n }\n // A block closes at the latest after D media units, so the DU headers it\n // carries from one MPU lie within D consecutive positions. A wider span\n // means the producer closes blocks some other way (at K symbols, or\n // round-robin across concurrent blocks) and the window rule is void.\n if (\n block.mpus.size === 1 &&\n block.idxMin !== undefined &&\n block.idxMax !== undefined &&\n block.idxMax - block.idxMin >= geometry.interleaveUnits\n ) {\n return 'block-wider-than-interleave'\n }\n if (evidence.mpu !== undefined && evidence.movieFragment !== undefined) {\n const fragment = this.#mpuFragments.get(evidence.mpu)\n if (fragment === undefined) {\n this.#mpuFragments.set(evidence.mpu, evidence.movieFragment)\n // One entry per MPU still inside the classification horizon.\n for (const mpu of this.#mpuFragments.keys()) {\n if (mpu + 1 < evidence.mpu) this.#mpuFragments.delete(mpu)\n }\n } else if (fragment !== evidence.movieFragment) {\n // sample_number is then a position within a movie fragment, not\n // within the MPU, and the window rule's geometry does not hold.\n return 'mpu-multiple-fragments'\n }\n }\n if (evidence.mpu !== undefined && this.#contradictsAuthoredOrder(sbn, evidence.mpu, evidence)) {\n return 'block-not-contiguous'\n }\n return null\n }\n\n #clearContiguityLedger(): void {\n this.#mpuSbnSpans.clear()\n this.#dataUnitStarts.clear()\n this.#highestMpu = null\n }\n\n /**\n * P1 check for one source packet of MPU `mpu` in block `sbn`, then record\n * it. True when some recorded packet's authored order and SBN order\n * disagree with this one's (see the class comment). Only positions the\n * wire states are compared: MPU order for every packet, and data-unit\n * order for packets that carry a DU header. A packet of one data unit\n * that continues into the next block carries no DU header and so only\n * takes part through its MPU, which is exactly what makes an adjacent\n * straddle consistent.\n */\n #contradictsAuthoredOrder(sbn: number, mpu: number, evidence: SourceSymbolEvidence): boolean {\n // Every packet of MPU m precedes every packet of MPU m' > m.\n for (const [other, span] of this.#mpuSbnSpans) {\n if (other < mpu && span.max > sbn) return true\n if (other > mpu && span.min < sbn) return true\n }\n const inHorizon = this.#highestMpu === null || mpu + 1 >= this.#highestMpu\n let insertAt = -1\n let starts: DataUnitStart[] | undefined\n let start: DataUnitStart | undefined\n if (evidence.idx !== undefined) {\n start = {\n movieFragment: evidence.movieFragment ?? 0,\n idx: evidence.idx,\n offset: evidence.offset ?? 0,\n sbn,\n }\n starts = this.#dataUnitStarts.get(mpu)\n if (starts !== undefined) {\n let lo = 0\n let hi = starts.length\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (compareDataUnitStarts(starts[mid]!, start) < 0) lo = mid + 1\n else hi = mid\n }\n const at = starts[lo]\n if (at !== undefined && compareDataUnitStarts(at, start) === 0) {\n // One data unit has one first packet: seen in another block, the\n // SBN-to-authored-order map is not a function.\n if (at.sbn !== sbn) return true\n } else {\n const before = starts[lo - 1]\n if (before !== undefined && before.sbn > sbn) return true\n if (at !== undefined && at.sbn < sbn) return true\n insertAt = lo\n }\n }\n }\n if (!inHorizon) return false\n const span = this.#mpuSbnSpans.get(mpu)\n if (span === undefined) this.#mpuSbnSpans.set(mpu, { min: sbn, max: sbn })\n else {\n if (sbn < span.min) span.min = sbn\n if (sbn > span.max) span.max = sbn\n }\n if (start !== undefined) {\n if (starts === undefined) this.#dataUnitStarts.set(mpu, [start])\n else if (insertAt >= 0) starts.splice(insertAt, 0, start)\n }\n if (this.#highestMpu === null || mpu > this.#highestMpu) {\n this.#highestMpu = mpu\n for (const other of [...this.#mpuSbnSpans.keys()]) {\n if (other + 1 < mpu) {\n this.#mpuSbnSpans.delete(other)\n this.#dataUnitStarts.delete(other)\n }\n }\n }\n return false\n }\n\n #disable(reason: ClassifierReason): ClassifierChange[] {\n if (reason === 'rap-not-mpu-start' || reason === 'mpu-longer-than-gop') {\n this.#mpuBoundsRaps = false\n }\n if (this.#mode === 'all-protected') return []\n this.#mode = 'all-protected'\n this.#reason = reason\n const changes: ClassifierChange[] = [{ kind: 'mode', mode: 'all-protected', reason }]\n for (const [sbn, block] of this.#blocks) {\n if (block.cls !== 'delta') continue\n block.cls = 'unknown'\n changes.push({\n kind: 'reclassified',\n sbn,\n from: 'delta',\n to: 'unknown',\n evidence: 'classifier-disabled',\n })\n }\n return changes\n }\n\n #classify(block: BlockEvidence): BlockClass {\n if (block.rapSeen || block.mpus.size >= 2) return 'keyframe'\n if (this.#mode === 'all-protected') return 'unknown'\n const geometry = this.#geometry\n if (geometry.trackKind !== 'video' || geometry.mode !== 'classifying') return 'unknown'\n if (\n block.mpus.size === 1 &&\n block.idxMin !== undefined &&\n block.idxMax !== undefined\n ) {\n // A block closes every D media units, so its frames are contiguous and\n // lie in [idxMax \u2212 D + 1, idxMin + D \u2212 1] of MPU m. Index 0 is the only\n // RAP of an MPU and G bounds its length.\n const span = geometry.interleaveUnits - 1\n if (block.idxMax - span >= 1 && block.idxMin + span <= geometry.gopFrames - 1) {\n return 'delta'\n }\n }\n return 'unknown'\n }\n\n #allowed(from: BlockClass, to: BlockClass): boolean {\n if (from === 'keyframe') return false\n if (from === 'unknown') return to === 'delta' || to === 'keyframe'\n if (from === 'delta') return to === 'keyframe'\n return false\n }\n\n #evidenceFor(block: BlockEvidence, to: BlockClass): ReclassificationEvidence {\n if (to === 'delta') return 'window'\n return block.rapSeen ? 'rap' : 'mpu-span'\n }\n}\n\n// \u2500\u2500\u2500 Stall vs conceal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GapStreamIdentity {\n readonly packetId: number\n /** Non-empty catalog identity, or null when the producer has no track name. */\n readonly catalogTrackName: string | null\n}\n\n/** What the sink knows about one side of a released gap. */\nexport interface GapFrameEvidence {\n /**\n * Codec-aware RAP check: `MmtpContainerParser.isKeyframe(data, rapFlag,\n * codec)`, bitstream-only for recovered frames. Never the priority hint.\n */\n readonly rap: boolean\n /**\n * MPU_sequence_number of the frame. Omit it once the stream showed that\n * an MPU boundary does not bound its RAPs (`BlockClassifier.mpuBoundsRaps`):\n * same-MPU then no longer proves a gap lost only deltas.\n */\n readonly mpu?: number\n /** 0-based sample position within the MPU, when the geometry verifies it. */\n readonly idx?: number\n /** Authored presentation time in `timescale` ticks. */\n readonly pts?: bigint\n /** Authored sample duration in `timescale` ticks. */\n readonly durationTicks?: number\n /** The media timescale of `pts` and `durationTicks`. */\n readonly timescale?: number\n}\n\nexport type FrameGapAction = 'stall' | 'conceal'\n\nexport type FrameGapReason =\n | 'rap-lost'\n | 'rap-unknown'\n | 'discontinuity'\n | 'delta-lost'\n | 'rap-next'\n | 'audio-gap'\n | 'audio-discontinuity'\n\n/**\n * The decision attached to the first frame released after a gap\n * (`RoutedFrameMeta.gapBefore`). It is the same frozen object the manager\n * emits as its `gap` repair lifecycle event, so a sink that acts on the frame\n * and an observer that counts events see one fact.\n */\nexport interface FrameGapDecision {\n readonly version: 2\n readonly type: 'gap'\n readonly stream: GapStreamIdentity\n readonly trackKind: TrackKind\n readonly action: FrameGapAction\n readonly reason: FrameGapReason\n /** FEC blocks in the gap whose media was lost (failed or vanished). */\n readonly blocks: readonly number[]\n /** Missing video frames or audio access units, when exactly known. */\n readonly missingFrames: number | null\n /** Authored duration of the missing media in milliseconds, when known. */\n readonly missingMs: number | null\n /** Manager epoch of the stream when the decision was made. */\n readonly epoch: number\n}\n\nexport interface DecideGapInput {\n readonly stream: GapStreamIdentity\n readonly trackKind: TrackKind\n /** Last frame released before the gap. */\n readonly before: GapFrameEvidence\n /** First frame released after the gap. */\n readonly after: GapFrameEvidence\n /** The stream restarted (FEC SBN went backwards or the publisher reset). */\n readonly discontinuity: boolean\n readonly blocks: readonly number[]\n /**\n * The most media units the gap can hold: the FEC blocks spanned from the\n * frame before to the frame after, times D (media units per block,\n * ceil(interleaveDepthMs / unit duration), \u00A75.1). A block touches at most D\n * units whether it closes at D or at K symbols. Null when the span or D is\n * unknown. An audio PTS delta that implies more AUs than this is a clock\n * jump, not loss, and re-anchors instead of concealing every slot.\n */\n readonly maxMissingUnits: number | null\n readonly epoch: number\n}\n\nfunction durationMs(units: number, before: GapFrameEvidence): number | null {\n if (\n before.durationTicks === undefined ||\n before.timescale === undefined ||\n before.timescale <= 0\n ) {\n return null\n }\n return (units * before.durationTicks * 1000) / before.timescale\n}\n\n/**\n * Pure stall/conceal decision for the first frame released after a gap.\n *\n * Video stalls only when a RAP may be inside the gap: the frame after it is\n * not a RAP and either sits in another MPU (`rap-lost`) or the MPU cannot be\n * proven on both sides (`rap-unknown`), or the stream restarted\n * (`discontinuity`). A gap inside one MPU lost only deltas (`delta-lost`) and\n * a RAP after the gap resynchronises by itself (`rap-next`). Audio never\n * stalls: an exact gap is concealed per access unit (`audio-gap`), anything\n * else re-anchors at the next frame (`audio-discontinuity`).\n */\nexport function decideGap(input: DecideGapInput): FrameGapDecision {\n const { before, after } = input\n let action: FrameGapAction\n let reason: FrameGapReason\n let missingFrames: number | null = null\n\n if (input.trackKind === 'audio') {\n action = 'conceal'\n reason = 'audio-discontinuity'\n if (\n !input.discontinuity &&\n before.pts !== undefined &&\n after.pts !== undefined &&\n before.durationTicks !== undefined &&\n Number.isSafeInteger(before.durationTicks) &&\n before.durationTicks > 0\n ) {\n const auDuration = BigInt(before.durationTicks)\n const missingTicks = after.pts - before.pts - auDuration\n const bound = input.maxMissingUnits\n if (\n missingTicks > 0n &&\n missingTicks % auDuration === 0n &&\n bound !== null &&\n missingTicks / auDuration <= BigInt(bound)\n ) {\n reason = 'audio-gap'\n missingFrames = Number(missingTicks / auDuration)\n }\n }\n } else if (input.discontinuity) {\n action = 'stall'\n reason = 'discontinuity'\n } else if (after.rap) {\n action = 'conceal'\n reason = 'rap-next'\n } else if (before.mpu === undefined || after.mpu === undefined) {\n action = 'stall'\n reason = 'rap-unknown'\n } else if (before.mpu !== after.mpu) {\n action = 'stall'\n reason = 'rap-lost'\n } else {\n action = 'conceal'\n reason = 'delta-lost'\n if (before.idx !== undefined && after.idx !== undefined) {\n const missing = after.idx - before.idx - 1\n missingFrames = missing > 0 ? missing : null\n }\n }\n\n return Object.freeze({\n version: 2 as const,\n type: 'gap' as const,\n stream: Object.freeze({\n packetId: input.stream.packetId,\n catalogTrackName: input.stream.catalogTrackName,\n }),\n trackKind: input.trackKind,\n action,\n reason,\n blocks: Object.freeze([...input.blocks]),\n missingFrames,\n missingMs: missingFrames === null ? null : durationMs(missingFrames, before),\n epoch: input.epoch,\n })\n}\n", "// Resolved repair layer and overlay shapes (draft-ramadan-moq-fec-catalog-00,\n// fec-core-00): the output of `resolveRepairLayers`. Every base layer comes\n// from one instance's partition and differs only in P_i; layer i owns RS_IDs\n// [O_i, O_i + P_i) with O_i = sum(P_j, j < i), and ESI = K + RS_ID. A keyframe\n// overlay is a second instance with its own K, P and RS_ID space.\n//\n// Binding (which repair tracks belong to which instance) is\n// `resolveFecBindings` in mmt-base; nothing here reads a catalog.\n\n/** Lowest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MIN = 192;\n/** Highest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MAX = 255;\n/** Priority of a base layer 0 track that declares none. */\nexport const DEFAULT_REPAIR_PRIORITY = 240;\n/** RFC 6330 \u00A74.4.1.2: the largest number of source symbols in one RaptorQ source block. */\nexport const MAX_RAPTORQ_SOURCE_SYMBOLS = 56403;\n/** The 24-bit RS_ID field (\u00A77.1) bounds the repair total of one FEC instance. */\nexport const MAX_REPAIR_SYMBOLS = 0xffffff;\n/** The 24-bit ESI bounds K + totalP - 1 for one FEC instance. */\nexport const MAX_ENCODING_SYMBOL_ID = 0xffffff;\n/** Reed-Solomon block bound: K + P <= 255 (draft-ramadan-moq-fec-01 4.5, 6.3.4 rule 12). */\nexport const MAX_REED_SOLOMON_BLOCK_SYMBOLS = 255;\n\n/** A resolved base repair layer with its derived RS_ID range. */\nexport type RepairLayerParams = {\n\t/** Layer index; 0 is the track named by the source's `fec.repairTrack`. */\n\tlayer: number;\n\trepairTrackName: string;\n\t/** P_i: repair symbols per block carried by this layer. */\n\trepairSymbols: number;\n\t/** O_i = sum(P_j for j < i): the first RS_ID this layer carries. ESI = K + RS_ID. */\n\trsIdOffset: number;\n\t/** MoQT-scale priority (lower first); 240 when layer 0 declares none. */\n\tpriority: number;\n};\n\n/** A resolved keyframe overlay: a separate FEC instance over RAP fragments only. */\nexport type RepairOverlayParams = {\n\trepairTrackName: string;\n\tscope: \"keyframe\";\n\t/** K_overlay: source symbols of the overlay instance. */\n\tsourceSymbols: number;\n\t/** P_overlay: repair symbols per overlay block; also the overlay's own RSB_length. */\n\trepairSymbols: number;\n\t/** MoQT-scale priority, independent of the base layer chain. */\n\tpriority: number;\n};\n\nexport type RepairLayersResolution = {\n\tsourceTrackName: string;\n\t/** Base layers in layer order, layer 0 first. Never empty. */\n\tbaseLayers: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances, in catalog order. Empty when none is declared. */\n\t/** At most one entry: the wire carries one overlay trailer per RAP packet. */\n\toverlays: readonly RepairOverlayParams[];\n\t/** Sum of P over base layers: the wire RSB_length on every base layer packet. */\n\ttotalBaseRepairSymbols: number;\n};\n\n"],
5
- "mappings": "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",
3
+ "sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/serial-number.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/fec-bindings.ts", "../src/repair-fetch.ts", "../src/block-policy.ts", "../src/repair-layers.ts"],
4
+ "sourcesContent": ["/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nexport const SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// fec_code_id_for_repair_flow code points (ISO/IEC 23008-10 Table 1;\n// draft-ramadan-moq-mmt-01 Section 8.3).\nexport const FEC_CODE_REED_SOLOMON = 1\nexport const FEC_CODE_RAPTORQ = 3\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n}\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 * RFC 1982 serial-number arithmetic for the 32-bit MMTP AL-FEC symbol\n * counters (ISO/IEC 23008-1:2023 C.5.2 SS_ID, C.5.3 SS_Start).\n *\n * SS_ID is a 32-bit field, so a stream that runs long enough wraps it to 0.\n * Ordering two SS_IDs (or two blocks by their first SS_ID) with `<` reads\n * every post-wrap value as older than every pre-wrap one. Serial order\n * agrees with `<` whenever the two values are less than 2^31 apart, so for\n * every stream that has not reached the wrap the result is unchanged; only\n * the order across the wrap point differs (BLO-35597 iso:5).\n */\n\n/** 2^SERIAL_BITS for SERIAL_BITS = 32. */\nexport const SERIAL32_SPAN = 0x1_0000_0000\n\n/** RFC 1982 \u00A73.2's 2^(SERIAL_BITS - 1). */\nexport const SERIAL32_HALF = 0x8000_0000\n\nfunction assertUint32(value: number, name: string): void {\n if (!Number.isInteger(value) || value < 0 || value >= SERIAL32_SPAN) {\n throw new RangeError(`${name} must be a uint32 (got ${value})`)\n }\n}\n\n/**\n * RFC 1982 \u00A73.1 addition, extended to negative `n` for the matching\n * subtraction: `(s + n) mod 2^32`. `n` must lie in (-2^31, 2^31).\n */\nexport function serial32Add(s: number, n: number): number {\n assertUint32(s, 's')\n if (!Number.isInteger(n) || n <= -SERIAL32_HALF || n >= SERIAL32_HALF) {\n throw new RangeError(`serial addend must lie in (-2^31, 2^31) (got ${n})`)\n }\n return (s + n + SERIAL32_SPAN) % SERIAL32_SPAN\n}\n\n/** Forward distance from `from` to `to`: `(to - from) mod 2^32`. */\nexport function serial32Distance(from: number, to: number): number {\n return (to - from) >>> 0\n}\n\n/**\n * RFC 1982 \u00A73.2: `a` precedes `b`. Values exactly 2^31 apart are unordered\n * (neither precedes the other).\n */\nexport function serial32Precedes(a: number, b: number): boolean {\n const distance = serial32Distance(a, b)\n return distance !== 0 && distance < SERIAL32_HALF\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "/**\n * Runtime FEC bindings (BLO-50171): what each source track's FEC resolves to\n * for this library's receiver, read from `validateFecCatalog`'s per-instance\n * result and never re-derived.\n *\n * Every runtime reader (the container's `resolveCatalogTrackFec`,\n * `resolveRepairLayers`, `MulticastManager.configureMultiTrack`, transport's\n * `catalogRepairTracks`) goes through `resolveFecBindings`, so they cannot\n * disagree with the validator or with each other about which tracks have FEC\n * and which repair tracks belong to them.\n *\n * The draft-ramadan-moq-fec-catalog-00 array is the only shape. Both catalog\n * dialects bind through it: an MSF `tracks[]` entry directly, and a hang\n * `video`/`audio` rendition (`fec` the same array) with its `repair.renditions`\n * entries (`source` + `instanceId`), mapped onto a `tracks[]` view so the same\n * validator runs. A hang rendition is authoritative over a `tracks[]` entry of\n * the same name.\n *\n * Fail closed per instance: a source whose base instance is not `ok` resolves\n * to `unavailable` with the validator's code and path, and costs no other\n * track anything. A draft-01 object `fec` is `fec_member` at that track's\n * `fec`: the track plays without FEC.\n */\nimport { validateFecCatalog, type FecCatalogCode, type FecCatalogReceiver } from './fec-catalog.js'\n\n/** The receiver this runtime is: it decodes `iso-ssbg1-v1` only. */\nexport const RUNTIME_FEC_RECEIVER: FecCatalogReceiver = Object.freeze({\n schemes: Object.freeze(['raptorq', 'reed-solomon']),\n blockProfiles: Object.freeze(['iso-ssbg1-v1']),\n overlays: true,\n})\n\n/** Repair track priority when the track states none (MoQT scale, 192..255). */\nexport const DEFAULT_FEC_REPAIR_PRIORITY = 240\n\n/** One `ok` instance's geometry. */\nexport interface FecInstanceGeometry {\n readonly instanceId: number\n readonly scheme: 'raptorq' | 'reed-solomon'\n /** `iso-ssbg1-v1` under `RUNTIME_FEC_RECEIVER`. */\n readonly blockProfile: string\n /** K. */\n readonly sourceSymbols: number\n /** P_0 for a base instance; the overlay's own P for an overlay. */\n readonly repairSymbols: number\n /** T. */\n readonly symbolSize: number\n readonly interleaveDepthMs?: number\n readonly reorderToleranceMs?: number\n readonly repairTrack: string\n readonly symbolTrack?: string\n}\n\nexport interface FecLayerBinding {\n readonly layer: number\n readonly repairTrackName: string\n readonly repairSymbols: number\n /** O_i = sum(P_j, j < i). */\n readonly rsIdOffset: number\n readonly priority: number\n}\n\nexport interface FecOverlayBinding {\n readonly repairTrackName: string\n readonly scope: 'keyframe'\n readonly sourceSymbols: number\n readonly repairSymbols: number\n readonly priority: number\n readonly geometry: FecInstanceGeometry\n}\n\nexport interface FecBindingUnavailable {\n /** The validator's code, or `fec_base_instances` for a track with no base\n * instance or more than one `ok` one (the MMTP wire carries one base Source FEC Payload\n * ID, mmt-02 7.4.1). */\n readonly code: FecCatalogCode | 'fec_base_instances'\n /** JSON Pointer into the catalog (a hang path starts `/video` or `/audio`). */\n readonly path: string\n readonly outcome: 'unavailable' | 'violation'\n}\n\nexport type FecSourceBinding =\n | {\n readonly status: 'ok'\n readonly track: string\n /** JSON Pointer of the source track (`/tracks/<i>`, `/video/renditions/<name>`). */\n readonly pointer: string\n readonly base: FecInstanceGeometry\n /** Base layers in order, layer 0 first. */\n readonly layers: readonly FecLayerBinding[]\n /** Sum of P over the base layers: the wire RSB_length. */\n readonly totalBaseRepairSymbols: number\n readonly overlay?: FecOverlayBinding\n /** Why a declared overlay is not used, when one is not. */\n readonly overlayUnavailable?: FecBindingUnavailable\n }\n | {\n readonly status: 'unavailable'\n readonly track: string\n readonly pointer: string\n readonly reason: FecBindingUnavailable\n }\n\ntype Obj = Record<string, unknown>\nconst recordOf = (v: unknown): Obj | undefined =>\n typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as Obj) : undefined\nconst escape = (s: string) => s.replace(/~/g, '~0').replace(/\\//g, '~1')\n\n/**\n * The `tracks[]` view of a catalog in either dialect, with each view track's\n * JSON Pointer in the original catalog.\n */\nexport function fecCatalogTracks(catalog: unknown): { tracks: unknown[]; pointers: string[] } {\n const root = recordOf(catalog) ?? {}\n const tracks: unknown[] = []\n const pointers: string[] = []\n const hangNames = new Set<string>()\n for (const role of ['video', 'audio'] as const) {\n for (const [name, r] of Object.entries(recordOf(recordOf(root[role])?.['renditions']) ?? {})) {\n const entry = recordOf(r)\n if (!entry) continue\n hangNames.add(name)\n const kind = recordOf(entry['container'])?.['kind']\n // A hang rendition carries no `packaging`; its container kind is it.\n // `mmtp-fec` is hang's kind for MMTP that carries FEC (the FFmpeg\n // publisher's `moq_publish_set_video_packaging`): MMTP packaging.\n const view: Obj = { name, packaging: kind === undefined || kind === 'mmtp-fec' ? 'mmtp' : kind }\n if (entry['fec'] !== undefined) view['fec'] = entry['fec']\n tracks.push(view)\n pointers.push(`/${role}/renditions/${escape(name)}`)\n }\n }\n const hangRenditions = tracks.length\n for (const [name, r] of Object.entries(recordOf(recordOf(root['repair'])?.['renditions']) ?? {})) {\n hangNames.add(name)\n tracks.push({ packaging: 'fec-repair', ...recordOf(r), name })\n pointers.push(`/repair/renditions/${escape(name)}`)\n }\n const raw = Array.isArray(root['tracks']) ? root['tracks'] : []\n const named = new Set(hangNames)\n raw.forEach((t, i) => {\n const name = recordOf(t)?.['name']\n if (typeof name === 'string') named.add(name)\n if (typeof name === 'string' && hangNames.has(name)) return\n tracks.push(t)\n pointers.push(`/tracks/${i}`)\n })\n // hang layer 0 needs no `repair.renditions` entry: the rendition's instance\n // names it, and a hang Root (hang's toHang) carries no repair block. Such a\n // name stands for a repair track of that instance.\n tracks.slice(0, hangRenditions).forEach((t, ti) => {\n const view = t as Obj\n if (!Array.isArray(view['fec'])) return\n view['fec'].forEach((o, ii) => {\n const repairTrack = recordOf(o)?.['repairTrack']\n if (typeof repairTrack !== 'string' || named.has(repairTrack)) return\n named.add(repairTrack)\n const instanceId = recordOf(o)?.['instanceId']\n tracks.push({ name: repairTrack, packaging: 'fec-repair', source: view['name'], ...(instanceId === undefined ? {} : { instanceId }) })\n pointers.push(`${pointers[ti]}/fec/${ii}/repairTrack`)\n })\n })\n return { tracks, pointers }\n}\n\n/**\n * Every source track's runtime FEC binding, by track name. A track with no\n * `fec`, or an empty `fec` array, declares no FEC and has no entry.\n */\nexport function resolveFecBindings(\n catalog: unknown,\n receiver: FecCatalogReceiver = RUNTIME_FEC_RECEIVER,\n): ReadonlyMap<string, FecSourceBinding> {\n const { tracks, pointers } = fecCatalogTracks(catalog)\n const report = validateFecCatalog({ tracks }, receiver)\n const at = (path: string) => {\n const m = /^\\/tracks\\/(\\d+)(.*)$/.exec(path)\n return m ? pointers[Number(m[1])] + m[2] : path\n }\n const trackAt = (i: number) => recordOf(tracks[i]) ?? {}\n const nameAt = (i: number | null) => (i === null ? undefined : (trackAt(i)['name'] as string))\n const why = (i: { outcome: string; issues: readonly { code: FecCatalogCode; path: string }[]; path: string }) =>\n Object.freeze({\n code: i.issues[0]?.code ?? 'fec_member',\n path: at(i.issues[0]?.path ?? i.path),\n outcome: i.outcome === 'unavailable' ? ('unavailable' as const) : ('violation' as const),\n })\n\n const out = new Map<string, FecSourceBinding>()\n tracks.forEach((t, ti) => {\n const track = recordOf(t)\n const name = track?.['name']\n if (!track || typeof name !== 'string' || out.has(name)) return\n if (track['packaging'] === 'fec-repair' || track['packaging'] === 'fec-source') return\n const fec = track['fec']\n if (fec === undefined) return\n if (!Array.isArray(fec)) {\n const reason = { code: 'fec_member' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const }\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason: Object.freeze(reason) }))\n return\n }\n if (fec.length === 0) return\n const insts = report.instances.filter((i) => i.trackIndex === ti)\n const isOverlay = (i: (typeof insts)[number]) => Object.hasOwn(recordOf(fec[Number(i.path.split('/').pop())]) ?? {}, 'scope')\n const bases = insts.filter((i) => !isOverlay(i))\n const okBases = bases.filter((i) => i.outcome === 'ok')\n if (okBases.length !== 1) {\n const reason =\n // No base instance at all (overlay-only `fec`) is a count of 0, not\n // the overlay's own issue.\n okBases.length > 1 || bases.length === 0\n ? Object.freeze({ code: 'fec_base_instances' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const })\n : why(bases[0] ?? insts[0]!)\n out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason }))\n return\n }\n const b = okBases[0]!\n const base = geometry(recordOf(fec[Number(b.path.split('/').pop())])!, b.instanceId!, nameAt(b.symbolTrack))\n const priorityOf = (i: number | null) => {\n const p = i === null ? undefined : trackAt(i)['priority']\n return typeof p === 'number' ? p : DEFAULT_FEC_REPAIR_PRIORITY\n }\n const layers: FecLayerBinding[] = [\n { layer: 0, repairTrackName: base.repairTrack, repairSymbols: base.repairSymbols, rsIdOffset: 0, priority: priorityOf(b.repairTrack) },\n ]\n const more = b.boundRepairTracks\n .map((tj) => ({ tj, layer: trackAt(tj)['repairLayer'] as number }))\n .sort((x, y) => x.layer - y.layer)\n let offset = base.repairSymbols\n for (const { tj, layer } of more) {\n const repairSymbols = trackAt(tj)['repairSymbols'] as number\n layers.push({ layer, repairTrackName: nameAt(tj)!, repairSymbols, rsIdOffset: offset, priority: priorityOf(tj) })\n offset += repairSymbols\n }\n let overlay: FecOverlayBinding | undefined\n let overlayUnavailable: FecBindingUnavailable | undefined\n const ov = insts.find(isOverlay)\n if (ov?.outcome === 'ok') {\n const g = geometry(recordOf(fec[Number(ov.path.split('/').pop())])!, ov.instanceId!, nameAt(ov.symbolTrack))\n overlay = Object.freeze({\n repairTrackName: g.repairTrack,\n scope: 'keyframe' as const,\n sourceSymbols: g.sourceSymbols,\n repairSymbols: g.repairSymbols,\n priority: priorityOf(ov.repairTrack),\n geometry: g,\n })\n } else if (ov) overlayUnavailable = why(ov)\n out.set(\n name,\n Object.freeze({\n status: 'ok' as const,\n track: name,\n pointer: pointers[ti]!,\n base,\n layers: Object.freeze(layers.map((l) => Object.freeze(l))),\n totalBaseRepairSymbols: offset,\n ...(overlay ? { overlay } : {}),\n ...(overlayUnavailable ? { overlayUnavailable } : {}),\n }),\n )\n })\n return out\n}\n\n// The validator passed this instance for this receiver, so every member read\n// here is present and in range.\nfunction geometry(o: Obj, instanceId: number, symbolTrack: string | undefined): FecInstanceGeometry {\n return Object.freeze({\n instanceId,\n scheme: o['scheme'] as 'raptorq' | 'reed-solomon',\n blockProfile: o['blockProfile'] as string,\n sourceSymbols: o['sourceSymbols'] as number,\n repairSymbols: o['repairSymbols'] as number,\n symbolSize: o['symbolSize'] as number,\n ...(typeof o['interleaveDepthMs'] === 'number' ? { interleaveDepthMs: o['interleaveDepthMs'] } : {}),\n ...(typeof o['reorderToleranceMs'] === 'number' ? { reorderToleranceMs: o['reorderToleranceMs'] } : {}),\n repairTrack: o['repairTrack'] as string,\n ...(symbolTrack !== undefined ? { symbolTrack } : {}),\n })\n}\n", "/**\n * D-28 shared repair-fetch scheduler.\n *\n * Player-agnostic orchestration for per-block on-demand MoQ FEC repair\n * backfill. Each player (moqtail client, hang watch, Shaka MSF bridge)\n * supplies only its protocol verb via {@link RepairFetchTransport}, fetching\n * one block's repair group. Blocks are `iso-ssbg1-v1` (ISO/IEC 23008-1\n * ssbg_mode1, the only FEC block profile), so that group's MoQ id is the\n * 62-bit wrap-epoch-extended SS_Start, `extSsStart(wrapEpoch, ssStart)` from\n * `@blockcast/mmt-fec` -- never the 32-bit block key itself (see\n * `RepairFetchRequest.sbn`):\n *\n * - moqtail: standalone FETCH (FetchType.Standalone) of that one group\n * - moq-lite/hang: Subscribe with startGroup = endGroup = that group (the\n * relay propagates startGroup upstream to pull the past group from the\n * origin's producer cache)\n *\n * Everything else \u2014 registration, fire-time repair-track resolution,\n * keyframe-first admission, preemption, feeding symbols, and in-flight slot\n * release \u2014 is identical across players and lives here. The Player owns the\n * scheduler: one per `wireRepairFetch` call, and its disposer aborts\n * everything.\n *\n * Priority (from the block's class, see `repairPriorityForClass`):\n * - P0 (keyframe / unknown) dispatches as soon as it is enqueued, with no\n * cap. Each has its own fail deadline, and a cap would turn a\n * recoverable protected block into a stall; their number is already\n * bounded by the blocks one fail window can hold. A P0 block that lies\n * below a RAP the receiver already holds complete is not requested at\n * all (`moot-rap`), and a P0 FETCH still running when such a RAP\n * arrives is cancelled (`repair-stopped`, `moot`): the receiver resumes\n * from that RAP (draft-ramadan-moq-fec \u00A711.4.2). Only a repair that\n * could not land by the block's playout deadline (it relies on the\n * keyframe budget's extension) is moot: one that can stalls nothing.\n * - P1 (audio) dispatches, uncapped, while no P0 is queued or in flight;\n * P2 (delta) only while no P0/P1 is, earliest absolute fail deadline\n * first (\u00A711.4.5 item 3).\n * - Only a P0 FETCH the scheduler actually sends aborts the in-flight P1\n * and P2 FETCHes (`repair-preempted`), which return to the queue while\n * their blocks are unresolved: a keyframe request that is refused, moot\n * or otherwise never sent cancels nothing (\u00A711.4.5 item 3), and an audio\n * request never preempts. A P2 that never ran before its block resolved\n * is reported as `repair-deferred`.\n * - A hole walked over the one-Group transport (no `rangeTransport`) is\n * repair the run gets before its FEC deadline (`walkHole`). On a video\n * track it is keyframe repair (class `unknown`): P0, and no audio or\n * delta FETCH is issued while one of its Group FETCHes is outstanding\n * (fec-multicast-00 rule 3). It reaches no FEC deadline, so it preempts\n * nothing and holds back no audio or delta request: one that comes due\n * cancels the walk's Group FETCH and goes out at once, and the walk asks\n * for that Group again after it (`yieldVideoWalks`). On an audio track\n * it is audio repair: P1 at the audio wire priority, one Group per FETCH,\n * and like any audio request it waits while keyframe repair is\n * outstanding, stopping before its next Group, and is preempted by a\n * keyframe FETCH sent.\n * - A block reclassified to a higher class while its P2 fetch is in flight\n * is re-issued at once under the new class's wire priority: the running\n * FETCH is aborted (FETCH_CANCEL) and a new one sent.\n *\n * Wire priority (draft-ramadan-moq-fec \u00A711.4.5): every request carries the\n * MoQ Transport SUBSCRIBER_PRIORITY its FETCH must use, derived from the two\n * catalog priorities `ResolveRepairTarget` returns (see\n * `repairFetchSubscriberPriority`): a keyframe-bearing (or unknown) block's\n * FETCH is sent in the Source Media range at no more than the source\n * track's priority, so it is delivered ahead of every repair track and not\n * shed with repair data; a delta or audio FETCH is sent at no less than the\n * fetched repair track's priority, so it is shed first.\n *\n * Deadline and admission (BLO-35597): a repair FETCH rides the reliable path,\n * so its deadline is the block's PRESENTATION deadline, not the multicast\n * reassembly deadline. The player states how long after a block's first\n * symbol its media can still be presented (`presentationDelayMs`); from the\n * block's receiver-local anchor that gives an absolute deadline, never earlier\n * than the multicast wait and never later than the release gate's hold ceiling\n * (`terminalRetentionMsForPacket`), which bounds how long any successor frame\n * may wait behind the block. A FETCH is sent only when the measured\n * reliable-path RTT (`reliableRttMs`) is less than the time left to that\n * deadline -- a request that cannot come back in time is never sent, rather\n * than sent and cancelled -- and an admitted FETCH holds the block open to\n * that deadline, so the multicast span elapsing never aborts it.\n *\n * Keyframe late-repair budget: a block that may hold a RAP may be given a\n * longer deadline than the playout budget (`keyframeRepairBudgetMs`). Losing\n * such a block costs every frame up to the next keyframe, so holding it open\n * for a FETCH that lands within the budget trades a short hold for a GOP-long\n * freeze. At a small playout budget (a low-latency target) the presentation\n * deadline alone leaves less than one round trip by the time the multicast\n * wait gives up on the block, so every keyframe request would be refused\n * (`rtt-exceeds-budget`). Eligible (`keyframeBudgetEligible`): a `keyframe`\n * block (in-band RAP evidence), and an `unknown` block only while its\n * stream's classifier is `classifying`, where `unknown` means the window rule\n * could not exclude a RAP. Under `all-protected` every block reads `unknown`\n * whatever it holds, so there only RAP evidence earns the budget. Never\n * audio (P1) or delta (P2), and never a block its repair group cannot\n * complete (received sources + RSB_length < K'). The hold ends when its\n * FETCH ends without making the block decodable, not at the budget.\n *\n * Every deadline is capped by the release gate's hold ceiling measured the\n * way the gate measures it: from the earliest frame it holds for the stream\n * (a surviving frame of the block itself is routed before a repair symbol\n * states the block, so before its anchor), never later than from the anchor.\n * A FETCH landing after the gate forced that frame out would recover media\n * the gate then drops as stale.\n *\n * The scheduler arms no timers for block requests. A fetch ends when its\n * transport completes or throws, when it is preempted, when any later\n * scheduling event finds it past its presentation deadline, when its block\n * resolves (recovered, or failed at that deadline), on track reset, or on\n * dispose. A range request (\u00A77.5) is for a hole, not a tracked block, so no\n * FEC deadline resolves it: it arms one timer at its deadline, cleared when\n * it retires, so a hung range FETCH cannot hold the hole (and the release\n * gate behind it) past that deadline.\n */\n\nimport {\n extractRepairFecPayloadId,\n parseFecBlockProfile,\n serial32Add,\n serial32Distance,\n type FecBlockProfile,\n} from '@blockcast/mmt-base'\nimport { extendSsId, repairFetchRange, type RepairFetchRange } from '@blockcast/mmt-fec'\nimport {\n repairPriorityForClass,\n type BlockClass,\n type ClassifierMode,\n type RepairPriority,\n type TrackKind,\n} from './block-policy.js'\nimport type { ExpiredIsoHole, UndelimitedIsoLoss } from './fec-manager.js'\nimport type { RepairLifecycleObserver } from './multicast-manager.js'\nimport { REPAIR_PRIORITY_MIN } from './repair-layers.js'\n\n/** The MoQ Transport priority scale: 0..255, a lower value is delivered first. */\nconst MOQT_PRIORITY_MAX = 255\n\n/**\n * How long before the span a walked hole leaves asks for its repair (D-28)\n * the walk ends: past the deadline timer's 1 ms slack and one timer tick, so\n * the span's request timer is armed before it is due (walkDeadlineAt).\n */\nconst WALK_LEAD_MS = 3\n\n/**\n * The highest value in the Source Media priority range of\n * draft-ramadan-moq-fec \u00A710 (64..191): one below the repair range, which\n * starts at `REPAIR_PRIORITY_MIN` (192).\n */\nexport const SOURCE_MEDIA_PRIORITY_MAX = REPAIR_PRIORITY_MIN - 1\n\n/** What `ResolveRepairTarget` returns for one repair-bearing stream. */\nexport interface RepairTarget {\n /** The repair track to FETCH (base layer 0 of the block's source). */\n readonly repairTrack: string\n /**\n * The catalog `priority` of the SOURCE track the block belongs to, on the\n * MoQ Transport scale (0..255, lower first), or null when the catalog\n * carries none. Never a player constant: a keyframe repair must not be\n * delivered behind the source it repairs, and only the catalog says what\n * that source's priority is.\n */\n readonly sourcePriority: number | null\n /**\n * The catalog priority of `repairTrack` (MoQ Transport scale). Layer 0\n * that declares none has the value draft-ramadan-moq-fec \u00A710 defines\n * (240); `resolveRepairLayers` / `catalogRepairTarget` already apply it.\n * Null only when the resolver cannot read the repair track's entry.\n */\n readonly repairPriority: number | null\n}\n\n/** Resolve a packetId to its repair track and priorities at FIRE TIME.\n *\n * Called on every repair request, never cached by the shell: ABR ladder\n * rungs can share a packetId (e.g. video/720p and video/360p both p:1), so\n * the mapping must reflect the track active when the request fires, not\n * when the shell was wired. Return null when the packetId has no\n * repair-bearing track \u2014 the request is released without a fetch.\n * `catalogRepairTarget` builds the answer from a catalog. */\nexport type ResolveRepairTarget = (\n packetId: number,\n catalogTrackName?: string,\n) => RepairTarget | null\n\n/** One on-demand repair FETCH, as the scheduler hands it to the transport. */\nexport interface RepairFetchRequest {\n /** Repair track whose block `sbn` is fetched. */\n readonly repairTrack: string\n /**\n * The block's key: its 32-bit SS_Start (ISO/IEC 23008-1 C.5.3). The field\n * keeps its historical name, but ssbg_mode1 blocks have no source block\n * number (BLO-35597 D2a). The repair group's MoQ id is the 62-bit\n * `extSsStart(wrapEpoch, sbn)`; a receiver that knows only a lost SS_ID\n * fetches `repairFetchRange(lostSsId, kMax, anchor)`, where `anchor` is\n * `{ previousBlockStart }` (the extended start of the last block delimited\n * before it) or `{ reference }` (an epoch reference). Never use it as a\n * group id directly: SS_Start wraps at 2^32, a MoQ group id never goes\n * backwards.\n */\n readonly sbn: number\n /**\n * The block profile `sbn` is keyed by: the catalog `fec.blockProfile`,\n * which is always `iso-ssbg1-v1`. Required, and validated with\n * `parseFecBlockProfile` when the request is queued, so a stream configured\n * with any other token (the retired `k-padded-v1` included) never reaches\n * the transport.\n */\n readonly blockProfile: FecBlockProfile\n /** The source packetId the block belongs to. */\n readonly packetId: number\n /**\n * Aborts when the scheduler no longer wants the group: preemption by\n * keyframe work, reprioritisation, block resolution, track reset, dispose.\n */\n readonly signal: AbortSignal\n /**\n * The SUBSCRIBER_PRIORITY parameter the FETCH MUST carry, on the MoQ\n * Transport scale (0..255, lower delivered first), as\n * draft-ramadan-moq-fec \u00A711.4.5 requires. `undefined` means OMIT the\n * parameter: the catalog carries no priority to derive it from, and the\n * publisher then uses the value draft-ietf-moq-transport-16 \u00A79.2.2.3\n * defines for an omitted parameter (128, inside the Source Media range).\n * A transport never substitutes a value of its own. A transport whose API\n * uses another scale (moq-lite's higher-wins weight, say) maps this one\n * without reordering it against the source and repair subscriptions.\n */\n readonly subscriberPriority: number | undefined\n}\n\n/** Protocol-specific fetch of one FEC block's repair group.\n *\n * Yield each repair object payload (MMTP packet bytes, ISO 23008-1 \u00A7C.5.3\n * Repair FEC Payload ID framing). Return when the group ends.\n *\n * Put `request.subscriberPriority` on the FETCH (see RepairFetchRequest).\n *\n * When `request.signal` aborts, the transport MUST cancel the FETCH ON THE\n * WIRE, not merely stop reading: send FETCH_CANCEL for its Request ID\n * (draft-ietf-moq-transport-16 \u00A79.18; STOP_SENDING on an open data stream\n * as well, \u00A75.2), and do it from the abort handler itself, before it\n * returns. A relay that is never told keeps sending the group, so a\n * preempted delta repair keeps competing with the keyframe repair that\n * preempted it. The scheduler may issue the next request for the same block\n * (reprioritisation) as soon as it aborts one, and \u00A711.4.5 allows at most\n * one repair FETCH outstanding per block and repair layer, so the cancel\n * must already be queued ahead of that request. A FETCH whose Request ID\n * is not known yet is cancelled as soon as FETCH_OK assigns it. */\nexport type RepairFetchTransport = (request: RepairFetchRequest) => AsyncIterable<Uint8Array>\n\n/**\n * One range repair FETCH for a lost SS_ID in an undelimited span\n * (draft-ramadan-moq-fec \u00A77.5): the absolute, inclusive repair-track Group\n * range `repairFetchRange` gives. The last Group returned is the block holding\n * `lostSsId`; earlier ones are other blocks, admitted like any repair object.\n */\nexport interface RepairRangeFetchRequest extends RepairFetchRange {\n readonly repairTrack: string\n /** The lost SS_ID x the range was computed for. */\n readonly lostSsId: number\n readonly blockProfile: FecBlockProfile\n readonly packetId: number\n /** Same contract as `RepairFetchRequest.signal`: cancel on the wire. */\n readonly signal: AbortSignal\n /** Same contract as `RepairFetchRequest.subscriberPriority`. */\n readonly subscriberPriority: number | undefined\n}\n\n/**\n * Protocol-specific range FETCH (e.g. MoQT LOCATION_FILTER 0x03 Group range).\n * Yield each repair object payload; return when the range ends. The abort\n * rules of {@link RepairFetchTransport} apply unchanged.\n */\nexport type RepairRangeFetchTransport = (request: RepairRangeFetchRequest) => AsyncIterable<Uint8Array>\n\n/** Why a request could not carry a catalog-derived priority. */\nexport type RepairPriorityReason =\n | 'source-priority-absent'\n | 'source-priority-invalid'\n | 'repair-priority-absent'\n | 'repair-priority-invalid'\n\n/**\n * The wire priority of one repair request (draft-ramadan-moq-fec \u00A711.4.5).\n *\n * - `carry`: send the FETCH with `subscriberPriority`.\n * - `omit`: send it without SUBSCRIBER_PRIORITY. Only a keyframe-bearing or\n * unknown block's request is ever sent this way: the publisher then uses\n * 128 (draft-ietf-moq-transport-16 \u00A79.2.2.3), which lies in the Source\n * Media range, and without a catalog source priority there is no bound\n * below it to honour.\n * - `decline`: send no FETCH. A delta or audio FETCH MUST carry a priority\n * at least its repair track's, so at least 192, and the omitted value (128)\n * would put best-effort repair ahead of every repair track. Unicast repair\n * is optional for those classes (\u00A711.4.3, \u00A711.4.4); in-band FEC and\n * concealment still apply.\n */\nexport type RepairFetchPriority =\n | { readonly kind: 'carry'; readonly subscriberPriority: number }\n | { readonly kind: 'omit'; readonly reason: RepairPriorityReason }\n | { readonly kind: 'decline'; readonly reason: RepairPriorityReason }\n\nfunction isMoqtPriority(value: number): boolean {\n return Number.isSafeInteger(value) && value >= 0 && value <= MOQT_PRIORITY_MAX\n}\n\n/**\n * Derive the SUBSCRIBER_PRIORITY of a repair FETCH from the block's class and\n * the catalog priorities of its source and repair tracks\n * (draft-ramadan-moq-fec \u00A711.4.5 and the \u00A710 ranges):\n *\n * - keyframe / unknown: in the Source Media range and no greater than the\n * source track's priority, i.e. min(source, 191); omitted when the catalog\n * carries no source priority.\n * - delta / audio: no lower than the fetched repair track's priority and at\n * least 192, i.e. max(repair, 192); declined when it carries none.\n */\nexport function repairFetchSubscriberPriority(\n blockClass: BlockClass,\n target: Pick<RepairTarget, 'sourcePriority' | 'repairPriority'>,\n): RepairFetchPriority {\n if (repairPriorityForClass(blockClass) === 0) {\n const source = target.sourcePriority\n if (source === null) return { kind: 'omit', reason: 'source-priority-absent' }\n if (!isMoqtPriority(source)) return { kind: 'omit', reason: 'source-priority-invalid' }\n return { kind: 'carry', subscriberPriority: Math.min(source, SOURCE_MEDIA_PRIORITY_MAX) }\n }\n const repair = target.repairPriority\n if (repair === null) return { kind: 'decline', reason: 'repair-priority-absent' }\n if (!isMoqtPriority(repair)) return { kind: 'decline', reason: 'repair-priority-invalid' }\n return { kind: 'carry', subscriberPriority: Math.max(repair, REPAIR_PRIORITY_MIN) }\n}\n\n/** What `RepairFetchHost.fecManager.blockSymbolCounts` reports for one open block. */\nexport interface BlockSymbolCounts {\n readonly sources: number\n readonly repairs: number\n readonly authoredLength: number | undefined\n readonly repairGroupLength: number | undefined\n}\n\n/**\n * Whether a block of `blockClass` may get the keyframe late-repair budget:\n * a `keyframe` block always (in-band RAP evidence), an `unknown` block only\n * while its stream's classifier is `classifying` (the window rule could not\n * exclude a RAP). An `all-protected` stream reads `unknown` for every block,\n * delta-only ones included, so there `unknown` is no evidence of a RAP and\n * earns no hold. Audio and delta never do.\n */\nexport function keyframeBudgetEligible(\n blockClass: BlockClass,\n classifierMode: ClassifierMode | undefined,\n): boolean {\n if (blockClass === 'keyframe') return true\n return blockClass === 'unknown' && classifierMode === 'classifying'\n}\n\nexport interface WireRepairFetchOptions {\n resolveTarget: ResolveRepairTarget\n transport: RepairFetchTransport\n /**\n * Maximum transport attempts per block (required). A retry happens only\n * when the transport rejects with an error `shouldRetry` classifies as\n * retryable, before any symbol was fed, while the block's deadline remains.\n */\n attemptLimit: number\n /** Classify a transport error as a transient publisher/relay retention race. */\n shouldRetry?: (error: unknown) => boolean\n /**\n * The measured round-trip time of the reliable path the transport fetches\n * over, in ms (required). Read at every admission, so it tracks the live\n * session. A value that is not finite and positive, or a throw, means the\n * path offers no measurement: nothing can show the FETCH would come back in\n * time, so none is sent (`rtt-unmeasured`). Never a default.\n */\n reliableRttMs: () => number\n /**\n * How long after a block's first symbol arrived its media can still be\n * presented, in ms (required): the player's own playout budget for the\n * stream. Same fail-closed rule as `reliableRttMs`\n * (`presentation-delay-unavailable`).\n */\n presentationDelayMs: (packetId: number, catalogTrackName?: string) => number\n /**\n * How long after the first symbol of a block that may hold a RAP a repair\n * FETCH of it may still land, in ms (optional). Such a block\n * (`keyframeBudgetEligible`) gets the larger of this and\n * `presentationDelayMs`, still never later than the release gate's hold\n * ceiling and never earlier than the multicast wait; RTT admission is\n * unchanged. Losing a keyframe block costs the rest of its GOP, so a player\n * may hold the block (and the frames released after it) while its FETCH is\n * in flight rather than freeze to the next keyframe. Absent, throwing, or\n * not finite and positive: every block gets the presentation deadline\n * alone. Never applied to audio, delta, or an `unknown` block of an\n * `all-protected` stream.\n */\n keyframeRepairBudgetMs?: (packetId: number, catalogTrackName?: string) => number\n /**\n * Range repair for a lost SS_ID whose block no repair delimited\n * (draft-ramadan-moq-fec \u00A77.5), optional. A host that can keep such a gap as\n * a pending hole gets range repair either way, under the same admission,\n * priority and abort rules as a block request (class `unknown`, P0). Given,\n * the scheduler FETCHes `repairFetchRange(x, K, anchor)` over it, and\n * requires `repairEpochReference`. Absent, it walks the hole over\n * `transport` one Group at a time from the hole's start, whose Group is a\n * block's, each repair stating where the next block starts (BLO-52085),\n * under the class of the hole's track (see the module note).\n */\n rangeTransport?: RepairRangeFetchTransport\n /**\n * The epoch reference r of `repairTrack` on the live session\n * (`RepairEpochReferences.reference`), or undefined while none is held.\n * The \u00A77.5 anchor is ext(S_prev) extended from it when a stated block ends\n * where the hole starts, else r itself; without r no range request is sent\n * (`range-no-anchor`).\n */\n repairEpochReference?: (repairTrack: string) => bigint | undefined\n}\n\n/** Why the D-28 layer asked for a repair: the block's class at its deadline. */\nexport type RepairSubscribeCallback = (\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n blockClass: BlockClass,\n) => void\n\n/** Why the scheduler sent no FETCH for a block it was asked to repair. */\nexport type RepairSkipReason =\n /** The reliable path offered no RTT measurement; arrival cannot be shown. */\n | 'rtt-unmeasured'\n /** The measured RTT is not less than the time left to the deadline. */\n | 'rtt-exceeds-budget'\n /** The player gave no usable presentation delay for the block's stream. */\n | 'presentation-delay-unavailable'\n /** A lost SS_ID in an undelimited span had no \u00A77.5 anchor to extend it from. */\n | 'range-no-anchor'\n /**\n * The block lost more sources than its repair group carries symbols\n * (sources + RSB_length < K'): no FETCH of the group can complete it, and\n * when multicast already delivered the whole group the relay could serve\n * only duplicates (BLO-50082).\n */\n | 'group-cannot-complete'\n /**\n * The keyframe-bearing block lies below a RAP the receiver already holds\n * complete, so its repair would be moot (draft-ramadan-moq-fec \u00A711.4.2).\n */\n | 'moot-rap'\n\n/**\n * Which term set a FETCH's deadline (BLO-33597): the multicast wait, the\n * player's presentation delay past the block anchor, the keyframe budget past\n * the anchor (when it granted more than the presentation delay), or the\n * release gate's retention ceiling.\n */\nexport type RepairDeadlineBound = 'wait' | 'presentation' | 'keyframe' | 'retention'\n\n/** The admission inputs of a FETCH the scheduler sent (BLO-33597). */\nexport interface RepairSendMeasurement {\n /** The reliable-path RTT admission compared against `budgetMs`. */\n readonly rttMs: number\n /** Time left to the entry's deadline when the FETCH was sent. */\n readonly budgetMs: number\n /**\n * Delay past the block anchor the deadline was computed from; null when\n * the block had no anchor (its deadline is then the multicast wait).\n */\n readonly delayMs: number | null\n /** Time since the block anchor; null when the block had no anchor. */\n readonly blockAgeMs: number | null\n readonly bound: RepairDeadlineBound\n /** True for a range repair FETCH (\u00A77.5 lost SS_ID); absent for a block FETCH. */\n readonly range?: true\n}\n\n/** The subset of MulticastManager the scheduler drives. */\nexport interface RepairFetchHost {\n readonly fecManager: {\n blockProfileForPacket(packetId: number): FecBlockProfile | undefined\n repairFetchTimeoutMsForBlock(sbn: number, packetId: number, catalogTrackName?: string): number | undefined\n /** Receiver-local time of the block's first accepted symbol, if any. */\n blockAnchorAtMs(sbn: number, packetId: number, catalogTrackName?: string): number | undefined\n /** The stream's classifier mode (optional; absent reads as unknown). */\n classifierModeFor?(packetId: number, catalogTrackName?: string): { readonly mode: ClassifierMode }\n /**\n * An open block's received sources and distinct repair symbols, its K'\n * (SSB_length) and RSB_length (optional). The repair group carries\n * RSB_length symbols, so it can complete the block only when\n * sources + RSB_length >= K'; a FETCH that ended leaving fewer than K'\n * distinct symbols cannot be decoding it.\n */\n blockSymbolCounts?(\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): BlockSymbolCounts | undefined\n /** How long a terminal block stays authoritative: the release-gate hold ceiling. */\n terminalRetentionMsForPacket(packetId: number): number | undefined\n /** Keep the block open to `untilAtMs` for an admitted FETCH. */\n holdBlockForRepair(\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n untilAtMs: number,\n ): boolean\n /** The admitted FETCH ended without delivering: back to the multicast wait. */\n releaseRepairHold(sbn: number, packetId: number, catalogTrackName?: string): void\n /** Run `fn` with `receivedAtMs` as the receipt time of what it feeds (optional). */\n withReceiptTime?(receivedAtMs: number, fn: () => void): void\n /** The configured media kind of a packetId (optional; absent reads as video). */\n trackKindFor?(packetId: number): TrackKind | undefined\n /** The stream's D-28 repair deadline: how long after a block's sources it asks for repair (optional). */\n repairDeadlineMsForPacket?(packetId: number): number | undefined\n }\n onRepairSubscribe(cb: RepairSubscribeCallback): void\n /**\n * Arrival time of the earliest frame the in-order release gate holds for\n * the stream, or undefined (optional; absent, the block's anchor bounds the\n * ceiling alone). The gate releases that frame `terminalRetentionMsForPacket`\n * after it, whatever its blocks are doing.\n */\n earliestGatedFrameAtMs?(packetId: number, catalogTrackName?: string): number | undefined\n /** The keyframe budget extended a request's deadline by `extensionMs` (optional). */\n noteKeyframeBudgetGranted?(\n extensionMs: number,\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n addRepairLifecycleObserver(observer: RepairLifecycleObserver): () => void\n addTrackResetObserver(observer: (packetId: number, catalogTrackName: string) => void): () => void\n /**\n * Register for lost SS_IDs in undelimited spans (required with\n * `rangeTransport`). A host that offers it and `closeIsoHoles` keeps such a\n * gap as a pending hole for the scheduler to repair. `onExpired` is told of\n * a pending hole the stream ran past the locator's horizon; returning true,\n * the scheduler closes it (`closeIsoHoles`), otherwise the host discards it.\n */\n onUndelimitedLoss?(\n cb: (loss: UndelimitedIsoLoss) => void,\n onExpired?: (hole: ExpiredIsoHole) => boolean,\n ): void\n /** A hole's range repair ended, or none is sent: feed what was not stated. */\n closeIsoHoles?(packetId: number, catalogTrackName: string | undefined, start: number, length: number): void\n clearRepairForBlock(sbn: number, packetId: number, catalogTrackName?: string): void\n processRepairFromMoQ(packet: Uint8Array, ownerPacketId?: number, catalogTrackName?: string): void\n /** This scheduler can request unicast repair: protected flows keep pending entries for it (optional). */\n setUnicastRepair?(enabled: boolean): void\n noteRepairScheduling(\n type: 'repair-preempted' | 'repair-deferred',\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /** A request whose wire priority the catalog could not supply. */\n noteRepairPriority(\n action: 'omitted' | 'declined',\n reason: RepairPriorityReason,\n blockClass: BlockClass,\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /**\n * A request the scheduler did not send (see RepairSkipReason).\n * `measurement.priority` is the request's repair priority (0 keyframe or\n * unknown, 1 audio, 2 delta).\n */\n noteRepairSkipped(\n reason: RepairSkipReason,\n measurement: { readonly rttMs: number | null; readonly budgetMs: number; readonly priority?: RepairPriority },\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /** A running FETCH the scheduler stopped, and why (optional). */\n noteRepairStopped?(\n reason: RepairStopReason,\n sbn: number,\n packetId: number,\n catalogTrackName?: string,\n ): void\n /**\n * Whether a complete RAP above SS_ID `ssId` already reached the stream\n * (optional; absent, none ever has). Under the bc-provenance profile only\n * an admitted RAP counts (multicast-01 \u00A77.2.7).\n */\n rapCompleteAbove?(packetId: number, catalogTrackName: string | undefined, ssId: number): boolean\n /** Observe each complete RAP reaching a stream (optional; called synchronously). */\n addRapCompleteObserver?(\n observer: (packetId: number, catalogTrackName: string | undefined, ssId: number) => void,\n ): () => void\n /**\n * Observe each base block (`sbn`) whose RAP fragments the keyframe overlay\n * recovered with the keyframe's FT=0 and FT=1 held (optional; called\n * synchronously). Its keyframe FETCH is then stopped (fec-multicast-00 \u00A76.6).\n */\n addKeyframeOverlayRecoveredObserver?(\n observer: (packetId: number, catalogTrackName: string | undefined, sbn: number) => void,\n ): () => void\n /**\n * A FETCH the scheduler admitted and handed to the transport (optional):\n * every attempt, a retry or a reprioritised restart included.\n */\n noteRepairFetchSent?(\n priority: RepairPriority,\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n measurement: RepairSendMeasurement,\n ): void\n}\n\n/**\n * Why the scheduler ended a running FETCH (the transport cancels it on the\n * wire): preempted by a sent keyframe FETCH, reissued at a new class's\n * priority, past its deadline, its block resolved, track reset or a newer\n * request for the block, dispose, `moot` (a RAP above the block arrived\n * complete first, draft-ramadan-moq-fec \u00A711.4.2), or `overlay-recovered`\n * (the keyframe overlay recovered the block's RAP, fec-multicast-00 \u00A76.6).\n */\nexport type RepairStopReason =\n | 'preempted'\n | 'reprioritized'\n | 'expired'\n | 'resolved'\n | 'reset'\n | 'disposed'\n | 'moot'\n | 'overlay-recovered'\ntype StopReason = RepairStopReason\n\ninterface FetchRun {\n readonly controller: AbortController\n /** When this FETCH went out: it lands around `sentAt + rtt`. */\n readonly sentAt: number\n stopped: StopReason | null\n}\n\ninterface RepairEntry {\n readonly key: string\n readonly sbn: number\n readonly packetId: number\n readonly catalogTrackName?: string\n readonly repairTrack: string\n readonly target: RepairTarget\n /** The block profile `sbn` is keyed by, resolved before the entry is queued. */\n readonly blockProfile: FecBlockProfile\n priority: RepairPriority\n /** SUBSCRIBER_PRIORITY for this entry's class; undefined = omit it. */\n subscriberPriority: number | undefined\n /**\n * Absolute `performance.now()` millisecond by which a FETCH must land: the\n * block's presentation deadline (see the module note). Only ever moves\n * later: a block reclassified from delta can gain the keyframe budget.\n */\n deadlineAt: number\n /**\n * `deadlineAt` without the keyframe budget's extension: the block's playout\n * deadline. A FETCH landing by it costs no stall, so it is never moot.\n */\n playoutDeadlineAt: number\n /** Inputs `deadlineAt` was computed from; they move with it. */\n anchorAt: number | null\n delayMs: number | null\n bound: RepairDeadlineBound\n /** Monotonic enqueue order, the EDF tie-break. */\n readonly order: number\n attempts: number\n running: FetchRun | null\n /** A range request for a pending hole `[sbn, sbn + holeLength)`. */\n readonly range?: {\n readonly lostSsId: number\n readonly holeLength: number\n /** The \u00A77.7 Group range for `rangeTransport`; null walks the hole over `transport`. */\n readonly groups: RepairFetchRange | null\n /** When the hole was reported: what it states keeps the deadline a span opened then would have. */\n readonly reportedAt: number\n }\n /** A range request's deadline timer (see the module note). */\n expiry?: ReturnType<typeof setTimeout>\n /**\n * A hole walked over `transport` (walkHole): the Group its next FETCH asks\n * for, and the Group FETCH it has outstanding, if any.\n */\n walk?: {\n next: number\n group: { readonly sbn: number; readonly controller: AbortController } | null\n /** Its Group FETCH was cancelled for best-effort repair: ask for that Group again. */\n yielded: boolean\n }\n}\n\ninterface ResolvedDeadline {\n readonly deadlineAt: number\n readonly extensionMs: number\n readonly anchorAt: number | null\n readonly delayMs: number | null\n readonly bound: RepairDeadlineBound\n}\n\n/**\n * Wire the D-28 repair-fetch scheduler onto a MulticastManager.\n *\n * Call AFTER the manager has a repair-bearing FEC configuration (players gate\n * this on the first fec-setup that carries a repairTrack) \u2014 registration arms\n * the per-block repair deadlines in the FEC layer.\n *\n * Returns a dispose function that aborts outstanding fetches and stops new\n * ones from being issued.\n */\nexport function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOptions): () => void {\n if (!Number.isSafeInteger(opts.attemptLimit) || opts.attemptLimit < 1) {\n throw new Error(\n `[RepairFetch] attemptLimit=${String(opts.attemptLimit)} must be a positive integer`,\n )\n }\n if (typeof opts.reliableRttMs !== 'function') {\n throw new Error('[RepairFetch] reliableRttMs is required: repair admission needs the measured reliable-path RTT')\n }\n if (typeof opts.presentationDelayMs !== 'function') {\n throw new Error('[RepairFetch] presentationDelayMs is required: a repair FETCH is due by the block\\'s presentation deadline')\n }\n if (\n opts.rangeTransport !== undefined &&\n (typeof opts.repairEpochReference !== 'function' || !mc.onUndelimitedLoss || !mc.closeIsoHoles)\n ) {\n throw new Error('[RepairFetch] rangeTransport needs repairEpochReference and a host that reports undelimited losses')\n }\n mc.setUnicastRepair?.(true)\n const entries = new Map<string, RepairEntry>()\n const warnedNoTarget = new Set<string>()\n let disposed = false\n let enqueueOrder = 0\n\n const requestKey = (sbn: number, packetId: number, catalogTrackName?: string): string =>\n JSON.stringify([packetId, catalogTrackName ?? null, sbn])\n const noTargetKey = (packetId: number, catalogTrackName?: string): string =>\n JSON.stringify([packetId, catalogTrackName ?? null])\n\n const clearRepair = (sbn: number, packetId: number, catalogTrackName?: string): void => {\n if (catalogTrackName === undefined) mc.clearRepairForBlock(sbn, packetId)\n else mc.clearRepairForBlock(sbn, packetId, catalogTrackName)\n }\n const note = (type: 'repair-preempted' | 'repair-deferred', entry: RepairEntry): void => {\n mc.noteRepairScheduling(type, entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n /** Whatever ends an entry: a block's repair slot, or a hole's pending state. */\n const clearEntry = (entry: RepairEntry): void => {\n const { range } = entry\n if (range) {\n atReport(range, () => mc.closeIsoHoles!(entry.packetId, entry.catalogTrackName, entry.sbn, range.holeLength))\n } else {\n clearRepair(entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n }\n\n /**\n * Run what a hole's walk states, or the rest of the hole it leaves as\n * spans, at the time the hole was reported: the blocks start their\n * deadlines from there, as the one repair-less span a host without holes\n * opened then would have, so the walk never holds a frame later than that\n * span did. A \u00A77.7 range FETCH feeds at its own receipt time, as a host\n * with `rangeTransport` always has.\n */\n const atReport = (range: NonNullable<RepairEntry['range']>, work: () => void): void => {\n const fec = mc.fecManager\n if (range.groups !== null || fec.withReceiptTime === undefined) work()\n else fec.withReceiptTime(range.reportedAt, work)\n }\n\n const releaseHold = (entry: RepairEntry): void => {\n // A hole is no tracked block: nothing holds it but the hole itself.\n if (entry.range) return\n if (entry.catalogTrackName === undefined) mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId)\n else mc.fecManager.releaseRepairHold(entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n const stop = (entry: RepairEntry, reason: StopReason): void => {\n const run = entry.running\n if (!run) return\n entry.running = null\n run.stopped = reason\n run.controller.abort()\n // A preempted request returns to the queue; until it runs again nothing is\n // in flight that could land, so the block goes back to its multicast wait.\n if (reason === 'preempted') releaseHold(entry)\n mc.noteRepairStopped?.(reason, entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n /** The last SS_ID the entry covers, or undefined while its block is undelimited. */\n const lastSsId = (entry: RepairEntry): number | undefined => {\n if (entry.range) return entry.sbn + entry.range.holeLength - 1\n const length = symbolCounts(entry.sbn, entry.packetId, entry.catalogTrackName)?.authoredLength\n return length === undefined ? undefined : entry.sbn + length - 1\n }\n\n /**\n * A keyframe request whose whole block lies below a RAP already held\n * complete, and whose repair can only land on the keyframe budget's\n * extension. One that lands by the playout deadline stalls nothing, and\n * skipping it only throws away the block's frames (BLO-51635).\n */\n const moot = (entry: RepairEntry): boolean => {\n if (entry.priority !== 0) return false\n const rttMs = readRtt()\n // A running FETCH lands relative to when it was sent, not to now.\n const sentAt = entry.running?.sentAt ?? performance.now()\n if (rttMs !== null && sentAt + rttMs < entry.playoutDeadlineAt) return false\n const last = lastSsId(entry)\n if (last === undefined) return false\n try {\n return mc.rapCompleteAbove?.(entry.packetId, entry.catalogTrackName, last) === true\n } catch {\n return false\n }\n }\n\n /** Drop a request no FETCH will be sent for, and say why. */\n const skip = (entry: RepairEntry, reason: RepairSkipReason, rttMs: number | null, budgetMs: number): void => {\n retire(entry)\n releaseHold(entry)\n clearEntry(entry)\n mc.noteRepairSkipped(\n reason,\n { rttMs, budgetMs, priority: entry.priority },\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n )\n }\n\n /** The keyframe budget for a P0 block, or null: absent, throwing or not positive. */\n const readKeyframeBudget = (packetId: number, catalogTrackName: string | undefined): number | null => {\n const reader = opts.keyframeRepairBudgetMs\n if (reader === undefined) return null\n try {\n const budgetMs = reader(packetId, catalogTrackName)\n return Number.isFinite(budgetMs) && budgetMs > 0 ? budgetMs : null\n } catch {\n return null\n }\n }\n\n const readRtt = (): number | null => {\n try {\n const rtt = opts.reliableRttMs()\n return Number.isFinite(rtt) && rtt > 0 ? rtt : null\n } catch {\n return null\n }\n }\n\n /**\n * Admission: send only a FETCH whose repair group can complete the block\n * and that the measured round trip says can land before the entry's\n * deadline. A refused entry is released, not queued: the\n * deadline only gets closer. Returns the admitting RTT, or null when refused.\n */\n const admit = (entry: RepairEntry, now: number): number | null => {\n const budgetMs = entry.deadlineAt - now\n const rttMs = readRtt()\n const reason = refusal(entry, now, rttMs)\n if (reason === null) return rttMs\n skip(entry, reason, rttMs, budgetMs)\n return null\n }\n\n /** Why admission refuses `entry` at `now` with round trip `rttMs`, or null when it admits it. */\n const refusal = (entry: RepairEntry, now: number, rttMs: number | null): RepairSkipReason | null =>\n !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName)\n ? 'group-cannot-complete'\n : rttMs === null\n ? 'rtt-unmeasured'\n : rttMs < entry.deadlineAt - now\n ? null\n : 'rtt-exceeds-budget'\n\n const retire = (entry: RepairEntry): void => {\n if (entries.get(entry.key) === entry) entries.delete(entry.key)\n if (entry.expiry !== undefined) clearTimeout(entry.expiry)\n }\n\n /**\n * Whether `entry` holds back a request of `priority`: no audio or delta\n * repair FETCH is issued while a keyframe repair FETCH is outstanding\n * (fec-multicast-00 rule 3), and no delta one while audio repair is. A\n * walk (walkHole) is repair a run gets before it reaches its FEC deadline:\n * on a video track it is keyframe repair while one of its Group FETCHes is\n * outstanding and between two Groups holds nothing back; on an audio track\n * it holds nothing back (BLO-52085). `videoWalks` false leaves video walks\n * out: they yield to a request that is no walk (`yieldVideoWalks`).\n */\n const holdsBack = (entry: RepairEntry, priority: RepairPriority, videoWalks = true): boolean => {\n if (entry.priority >= priority) return false\n const { walk } = entry\n if (walk === undefined) return true\n if (entry.priority !== 0 || !videoWalks) return false\n return entry.running === null || (walk.group !== null && !walk.group.controller.signal.aborted)\n }\n\n /** Whether any entry holds back a request of `priority` (see `holdsBack`). */\n const outstandingBelow = (priority: RepairPriority, videoWalks = true): boolean => {\n for (const entry of entries.values()) {\n if (holdsBack(entry, priority, videoWalks)) return true\n }\n return false\n }\n\n /**\n * A video walk reaches no FEC deadline, so it holds back no audio or delta\n * request that came due (one of `queued` that is no walk), that nothing\n * else holds back and that admission takes (`admit`): it cancels its outstanding Group FETCH first, so no\n * keyframe repair FETCH is outstanding when that request is issued\n * (fec-multicast-00 rule 3), and asks for that Group again after it\n * (walkHole). Audio and delta repair so goes out when it would with no\n * walk at all (BLO-52085).\n */\n const yieldVideoWalks = (queued: readonly RepairEntry[]): void => {\n const now = performance.now()\n const rttMs = readRtt()\n const due = queued.some((entry) =>\n entry.priority !== 0 && entry.walk === undefined && !outstandingBelow(entry.priority, false) &&\n refusal(entry, now, rttMs) === null)\n if (!due) return\n for (const entry of entries.values()) {\n const walk = entry.walk\n if (entry.priority !== 0 || walk === undefined || walk.group === null) continue\n if (walk.group.controller.signal.aborted) continue\n walk.yielded = true\n walk.group.controller.abort()\n }\n }\n\n /**\n * A keyframe FETCH is being sent: cancel every in-flight audio and delta\n * FETCH (\u00A711.4.5 item 3). Called only for a FETCH actually sent.\n */\n const preemptBestEffort = (): void => {\n for (const entry of entries.values()) {\n if (entry.priority === 0 || !entry.running) continue\n stop(entry, 'preempted')\n note('repair-preempted', entry)\n }\n }\n\n /**\n * The wire priority for `blockClass`, or null when no FETCH may be sent.\n * A priority the catalog cannot supply is reported once per request.\n */\n const wirePriority = (\n entry: Pick<RepairEntry, 'target' | 'sbn' | 'packetId' | 'catalogTrackName'>,\n blockClass: BlockClass,\n ): { subscriberPriority: number | undefined } | null => {\n const derived = repairFetchSubscriberPriority(blockClass, entry.target)\n if (derived.kind === 'carry') return { subscriberPriority: derived.subscriberPriority }\n mc.noteRepairPriority(\n derived.kind === 'omit' ? 'omitted' : 'declined',\n derived.reason,\n blockClass,\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n )\n return derived.kind === 'omit' ? { subscriberPriority: undefined } : null\n }\n\n /** `counted` is false for a reprioritisation restart, which is no retry. */\n const start = (entry: RepairEntry, counted = true): void => {\n const now = performance.now()\n if (moot(entry)) {\n skip(entry, 'moot-rap', readRtt(), entry.deadlineAt - now)\n return\n }\n const rttMs = admit(entry, now)\n if (rttMs === null) return\n // Before the transport sees this request, so the cancels go out first. A\n // walk reaches no FEC deadline, so it cancels nothing (rule 3).\n if (entry.priority === 0 && entry.walk === undefined) preemptBestEffort()\n // fec-multicast-00 rule 1: one FETCH per block and repair layer. A walk\n // reading this block's Group leaves it to this request.\n if (entry.range === undefined) leaveWalkedGroup(entry)\n if (counted) entry.attempts++\n mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {\n rttMs,\n budgetMs: entry.deadlineAt - now,\n delayMs: entry.delayMs,\n blockAgeMs: entry.anchorAt === null ? null : Math.max(0, now - entry.anchorAt),\n bound: entry.bound,\n ...(entry.range ? { range: true as const } : {}),\n })\n const run: FetchRun = { controller: new AbortController(), sentAt: now, stopped: null }\n entry.running = run\n // Hold the block open to the presentation deadline while this FETCH can\n // still land: the multicast span elapsing must not abort it.\n if (!entry.range) {\n mc.fecManager.holdBlockForRepair(\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n entry.deadlineAt,\n )\n }\n void execute(entry, run)\n }\n\n /** End any walk's FETCH of `block`'s Group, cancelling it, before the block's own goes out. */\n const leaveWalkedGroup = (block: RepairEntry): void => {\n for (const entry of entries.values()) {\n const group = entry.walk?.group\n if (\n group?.sbn === block.sbn &&\n entry.packetId === block.packetId &&\n entry.catalogTrackName === block.catalogTrackName\n ) {\n group.controller.abort()\n }\n }\n }\n\n const dispatch = (): void => {\n if (disposed) return\n const now = performance.now()\n const queued: RepairEntry[] = []\n for (const entry of [...entries.values()]) {\n if (entry.running) {\n // A fetch still running at its block's fail bound can no longer land\n // in time. Cancel it here rather than trust the transport or the FEC\n // layer's resolution to end it: a hung P0/P1 left running would keep\n // outstandingBelow() true and gate every P1/P2 for the session.\n if (entry.deadlineAt <= now) {\n stop(entry, 'expired')\n retire(entry)\n clearEntry(entry)\n }\n continue\n }\n if (entry.deadlineAt <= now) {\n // The block's fail bound has passed; its resolution is imminent and\n // no fetch can land in time.\n retire(entry)\n if (entry.priority === 2 && entry.attempts === 0) note('repair-deferred', entry)\n clearEntry(entry)\n continue\n }\n queued.push(entry)\n }\n for (const entry of queued) {\n if (entry.priority === 0) start(entry)\n }\n yieldVideoWalks(queued)\n if (outstandingBelow(1)) return\n for (const entry of queued) {\n if (entry.priority === 1) start(entry)\n }\n if (outstandingBelow(2)) return\n queued\n .filter((entry) => entry.priority === 2)\n .sort((a, b) => a.deadlineAt - b.deadlineAt || a.order - b.order)\n .forEach((entry) => start(entry))\n }\n\n /**\n * Whether the block may still be decoding from what it holds: false only\n * when the host shows fewer distinct symbols than its K'. A FETCH that\n * ended leaving it short delivered all it could (its group carries no more\n * symbols), so holding the block, and every frame behind it, longer buys\n * nothing: it goes back to its multicast wait. Unknown counts read as\n * decodable (the FEC layer resolves the block).\n */\n const symbolCounts = (\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n ): BlockSymbolCounts | undefined => {\n try {\n return mc.fecManager.blockSymbolCounts?.(sbn, packetId, catalogTrackName)\n } catch {\n return undefined\n }\n }\n const decodable = (entry: RepairEntry): boolean => {\n const counts = symbolCounts(entry.sbn, entry.packetId, entry.catalogTrackName)\n if (counts === undefined || counts.authoredLength === undefined) return true\n return counts.sources + counts.repairs >= counts.authoredLength\n }\n /**\n * Whether the block's repair group can complete it: its received sources\n * plus every symbol the group carries reach K'. A loss of more sources than\n * the group has repair symbols (every source-only loss among them: the\n * block then already holds the whole group) gets no FETCH-length hold. True\n * while the counts are not known (no repair stated the block yet).\n */\n const groupCanComplete = (sbn: number, packetId: number, catalogTrackName: string | undefined): boolean => {\n const counts = symbolCounts(sbn, packetId, catalogTrackName)\n if (counts === undefined || counts.authoredLength === undefined || counts.repairGroupLength === undefined) {\n return true\n }\n return counts.sources + counts.repairGroupLength >= counts.authoredLength\n }\n /**\n * Whether the block a walked Group states can use none of the Group's other\n * repair symbols: the host holds as many of its symbols as its K' (complete,\n * or decoding), its group cannot complete it, or it no longer tracks it\n * (resolved). A relay may hold only part of a Group, or keep its FETCH open,\n * so the walk does not wait for the rest. False when the host reports no\n * counts: the walk then takes the whole Group.\n */\n const groupServed = (sbn: number, packetId: number, catalogTrackName: string | undefined): boolean => {\n if (mc.fecManager.blockSymbolCounts === undefined) return false\n const counts = symbolCounts(sbn, packetId, catalogTrackName)\n if (counts === undefined || counts.authoredLength === undefined) return true\n return counts.sources + counts.repairs >= counts.authoredLength || !groupCanComplete(sbn, packetId, catalogTrackName)\n }\n /** Where the next block starts after the open block `sbn`, when a repair stated it. */\n const statedEnd = (sbn: number, packetId: number, catalogTrackName: string | undefined): number | undefined => {\n const length = symbolCounts(sbn, packetId, catalogTrackName)?.authoredLength\n return length === undefined ? undefined : serial32Add(sbn, length)\n }\n\n const finish = (entry: RepairEntry, run: FetchRun, fed: number, error: unknown): void => {\n if (run.stopped !== null || entry.running !== run || entries.get(entry.key) !== entry) {\n return\n }\n entry.running = null\n if (error === undefined) {\n retire(entry)\n // A fetch that ended without feeding anything cannot resolve the block;\n // release the slot and the hold so failure accounting is not wedged. A\n // fetch that fed symbols leaves resolution to the FEC layer.\n // A range FETCH placed whatever it stated as it arrived; the rest of\n // its hole is now known to have no repair coming.\n if (entry.range || fed === 0 || !decodable(entry)) {\n releaseHold(entry)\n clearEntry(entry)\n }\n dispatch()\n return\n }\n const retryable =\n fed === 0 &&\n entry.attempts < opts.attemptLimit &&\n entry.deadlineAt > performance.now() &&\n opts.shouldRetry?.(error) === true\n if (retryable) {\n // Back through the same admission rule: a P2 retry waits for P0/P1.\n dispatch()\n return\n }\n retire(entry)\n if (fed === 0) releaseHold(entry)\n clearEntry(entry)\n console.warn(\n `[RepairFetch] fetch failed pid=${entry.packetId} sbn=${entry.sbn} track=${entry.repairTrack}:`,\n error,\n )\n dispatch()\n }\n\n // The profile the block key is in; a stream reaching repair was configured\n // from its catalog, so an absent profile is a wiring fault, not a default.\n // Any token other than `iso-ssbg1-v1` (the retired `k-padded-v1` included)\n // is one too: `parseFecBlockProfile` refuses it, whatever an untyped host\n // returns. Resolved when the request arrives (onRepairSubscribe below),\n // never inside execute: there its throw would read as an ordinary fetch\n // failure, warned once per block while the stream silently lost unicast\n // repair.\n const blockProfileOf = (packetId: number): FecBlockProfile => {\n const profile: unknown = mc.fecManager.blockProfileForPacket(packetId)\n if (profile === undefined) {\n throw new Error(`[RepairFetch] packetId=${packetId} has no configured fec.blockProfile`)\n }\n return parseFecBlockProfile(profile, `[RepairFetch] packetId=${packetId} fec.blockProfile`)\n }\n\n const execute = async (entry: RepairEntry, run: FetchRun): Promise<void> => {\n let fed = 0\n const common = {\n repairTrack: entry.repairTrack,\n blockProfile: entry.blockProfile,\n packetId: entry.packetId,\n signal: run.controller.signal,\n subscriberPriority: entry.subscriberPriority,\n }\n const { range } = entry\n const feed = (packet: Uint8Array): void => {\n if (entry.catalogTrackName === undefined) {\n mc.processRepairFromMoQ(packet, entry.packetId)\n } else {\n mc.processRepairFromMoQ(packet, entry.packetId, entry.catalogTrackName)\n }\n }\n try {\n const packets = range === undefined\n ? opts.transport({ ...common, sbn: entry.sbn })\n : entry.walk !== undefined\n ? walkHole(entry, entry.walk, range, common)\n : opts.rangeTransport!({\n ...common,\n startGroup: range.groups!.startGroup,\n endGroup: range.groups!.endGroup,\n lostSsId: range.lostSsId,\n })\n for await (const packet of packets) {\n if (run.stopped !== null || entry.running !== run) break\n fed++\n if (range === undefined) feed(packet)\n else atReport(range, () => feed(packet))\n }\n finish(entry, run, fed, undefined)\n } catch (error) {\n finish(entry, run, fed, error ?? new Error('repair transport rejected'))\n }\n }\n\n /**\n * The \u00A77.7 range FETCH of a hole over a transport that FETCHes one Group.\n * A hole starts where a stated block ends, so ext(hole start) is a block's\n * Group; the repair of each Group states its block, and that block's end is\n * the next block's Group. Walks to the block holding the hole's highest\n * lost SS_ID (everything after it arrived) and stops early when a Group\n * names no block and none is stated there. A Group is done when it ends,\n * when it held its RSB_length repair symbols, or once the block it states\n * can use none of the rest (`groupServed`): a relay may keep the FETCH open\n * past its last object (BLO-52085 D2 80601), or hold only part of the\n * Group. Each Group is its own FETCH with its own signal, aborted once the\n * walk leaves it, so the transport cancels it on the wire, and never one\n * the block's own FETCH reads (fec-multicast-00 rule 1: `leaveWalkedGroup`\n * takes it over). Between two Groups the walk honours rule 3 (see\n * `holdsBack`): an audio walk that finds keyframe repair outstanding goes\n * back to the queue and resumes at `walk.next`; a video walk first\n * dispatches what came due, and asks again for a Group whose FETCH it\n * cancelled for audio or delta repair (`yieldVideoWalks`). Every packet is\n * fed before the walk reads the host's counts: the consumer feeds what it\n * yields before asking for more.\n */\n async function* walkHole(\n entry: RepairEntry,\n walk: NonNullable<RepairEntry['walk']>,\n hole: NonNullable<RepairEntry['range']>,\n common: Omit<RepairFetchRequest, 'sbn'>,\n ): AsyncGenerator<Uint8Array> {\n const lostAt = serial32Distance(entry.sbn, hole.lostSsId)\n for (let first = true; !common.signal.aborted; first = false) {\n if (!first && entry.priority !== 0 && outstandingBelow(1)) {\n stop(entry, 'preempted')\n note('repair-preempted', entry)\n return\n }\n if (!first && entry.priority === 0) {\n dispatch()\n if (common.signal.aborted) return\n }\n const sbn = walk.next\n let next: number | undefined\n if (entries.get(requestKey(sbn, entry.packetId, entry.catalogTrackName))?.running == null) {\n const rsIds = new Set<number>()\n const group = new AbortController()\n const cancel = (): void => group.abort()\n common.signal.addEventListener('abort', cancel, { once: true })\n walk.group = { sbn, controller: group }\n try {\n for await (const packet of opts.transport({ ...common, sbn, signal: group.signal })) {\n yield packet\n const id = extractRepairFecPayloadId(packet)\n if (id === null || id.ssStart !== sbn || id.ssbLength === 0) continue\n next ??= serial32Add(sbn, id.ssbLength)\n rsIds.add(id.rsId)\n // Aborted before the loop lets go of the transport, whose own\n // cleanup may stop listening for the abort.\n if (rsIds.size >= id.rsbLength || groupServed(sbn, entry.packetId, entry.catalogTrackName)) {\n group.abort()\n break\n }\n }\n } catch (error) {\n // The Group was taken over or yielded: the walk goes on.\n if (!group.signal.aborted || common.signal.aborted) throw error\n } finally {\n common.signal.removeEventListener('abort', cancel)\n walk.group = null\n }\n if (walk.yielded) {\n walk.yielded = false\n continue\n }\n }\n next ??= statedEnd(sbn, entry.packetId, entry.catalogTrackName)\n if (next === undefined) return\n // Past the highest lost SS_ID, which lies in the hole: done.\n if (serial32Distance(entry.sbn, next) > lostAt) return\n walk.next = next\n }\n }\n\n const removeResetObserver = mc.addTrackResetObserver((packetId, catalogTrackName) => {\n for (const entry of [...entries.values()]) {\n if (entry.packetId !== packetId || entry.catalogTrackName !== catalogTrackName) continue\n retire(entry)\n stop(entry, 'reset')\n if (entry.range) clearEntry(entry)\n }\n // A reset is when the packetId -> repair-track mapping can change, so\n // re-arm the once-per-track warn for the new epoch.\n warnedNoTarget.delete(noTargetKey(packetId, catalogTrackName))\n dispatch()\n })\n\n const removeLifecycleObserver = mc.addRepairLifecycleObserver((event) => {\n if (disposed) return\n if (event.type === 'reclassified') {\n const entry = entries.get(\n requestKey(event.block.sbn, event.block.packetId, event.block.catalogTrackName ?? undefined),\n )\n if (!entry || entry.priority !== 2) return\n const promoted = repairPriorityForClass(event.to)\n if (promoted >= entry.priority) return\n const wire = wirePriority(entry, event.to)\n // A block leaves P2 only for P0, whose request is never declined.\n if (wire === null) return\n entry.priority = promoted\n entry.subscriberPriority = wire.subscriberPriority\n // A block that turned out to carry a keyframe gets the keyframe budget\n // its request would have had (never a shorter deadline than it had).\n const promotedDeadline = presentationDeadlineAt(\n entry.sbn,\n entry.packetId,\n entry.catalogTrackName,\n performance.now(),\n event.to,\n )\n if (promotedDeadline !== null && promotedDeadline.deadlineAt > entry.deadlineAt) {\n entry.deadlineAt = promotedDeadline.deadlineAt\n entry.playoutDeadlineAt = Math.max(\n entry.playoutDeadlineAt,\n promotedDeadline.deadlineAt - promotedDeadline.extensionMs,\n )\n entry.anchorAt = promotedDeadline.anchorAt\n entry.delayMs = promotedDeadline.delayMs\n entry.bound = promotedDeadline.bound\n noteBudget(entry, promotedDeadline.extensionMs)\n }\n if (entry.running) {\n // The running FETCH carries the delta priority. Cancel it and send\n // the block's repair again under its new class (\u00A711.4.5): a keyframe\n // repair must not stay queued behind source media.\n stop(entry, 'reprioritized')\n start(entry, false)\n }\n dispatch()\n return\n }\n if (event.type !== 'recovered' && event.type !== 'failed') return\n const entry = entries.get(\n requestKey(event.block.sbn, event.block.packetId, event.block.catalogTrackName ?? undefined),\n )\n if (!entry) return\n retire(entry)\n if (entry.running) stop(entry, 'resolved')\n else if (entry.priority === 2 && entry.attempts === 0) note('repair-deferred', entry)\n dispatch()\n })\n\n /**\n * The block's presentation deadline, or null when the player gives none.\n *\n * anchor + presentationDelay, clamped below by the multicast wait (the block\n * is open that long whatever happens, so a FETCH landing inside it still\n * counts) and above by anchor + the release gate's hold ceiling (a successor\n * frame may not wait behind a block longer than that). A block no symbol\n * reached has no anchor; its deadline is the multicast wait. The ceiling is\n * measured from the earlier of the anchor and the earliest frame the gate\n * holds for the stream (see the module note). A block that may hold a RAP\n * (`keyframeBudgetEligible`) uses the larger of the presentation delay and\n * the keyframe budget; `extensionMs` is how much later that made it.\n */\n const presentationDeadlineAt = (\n sbn: number,\n packetId: number,\n catalogTrackName: string | undefined,\n now: number,\n blockClass: BlockClass,\n ): ResolvedDeadline | null => {\n // Undefined: the packetId is no longer configured (released), so it has\n // no repair budget.\n const waitMs = mc.fecManager.repairFetchTimeoutMsForBlock(sbn, packetId, catalogTrackName)\n if (waitMs === undefined) return null\n const waitDeadlineAt = now + waitMs\n const anchor = mc.fecManager.blockAnchorAtMs(sbn, packetId, catalogTrackName)\n if (anchor === undefined) {\n return { deadlineAt: waitDeadlineAt, extensionMs: 0, anchorAt: null, delayMs: null, bound: 'wait' }\n }\n let delayMs: number\n try {\n delayMs = opts.presentationDelayMs(packetId, catalogTrackName)\n } catch {\n return null\n }\n if (!(Number.isFinite(delayMs) && delayMs > 0)) return null\n const retentionMs = mc.fecManager.terminalRetentionMsForPacket(packetId)\n if (retentionMs === undefined) return null\n const ceilingAt = Math.min(anchor, earliestGatedFrameAt(packetId, catalogTrackName) ?? anchor) + retentionMs\n const at = (\n delay: number,\n delayBound: 'presentation' | 'keyframe',\n ): { readonly deadlineAt: number; readonly bound: RepairDeadlineBound } => {\n const presentAt = Math.min(anchor + delay, ceilingAt)\n if (waitDeadlineAt >= presentAt) return { deadlineAt: waitDeadlineAt, bound: 'wait' }\n return { deadlineAt: presentAt, bound: anchor + delay <= ceilingAt ? delayBound : 'retention' }\n }\n const base = at(delayMs, 'presentation')\n const budgetMs =\n keyframeBudgetEligible(blockClass, classifierMode(packetId, catalogTrackName)) &&\n groupCanComplete(sbn, packetId, catalogTrackName)\n ? readKeyframeBudget(packetId, catalogTrackName)\n : null\n if (budgetMs === null || budgetMs <= delayMs) return { ...base, extensionMs: 0, anchorAt: anchor, delayMs }\n const extended = at(budgetMs, 'keyframe')\n return {\n ...extended,\n extensionMs: extended.deadlineAt - base.deadlineAt,\n anchorAt: anchor,\n delayMs: budgetMs,\n }\n }\n\n const earliestGatedFrameAt = (packetId: number, catalogTrackName: string | undefined): number | undefined => {\n try {\n const at = mc.earliestGatedFrameAtMs?.(packetId, catalogTrackName)\n return at !== undefined && Number.isFinite(at) ? at : undefined\n } catch {\n return undefined\n }\n }\n\n const classifierMode = (packetId: number, catalogTrackName: string | undefined): ClassifierMode | undefined => {\n try {\n return mc.fecManager.classifierModeFor?.(packetId, catalogTrackName).mode\n } catch {\n return undefined\n }\n }\n\n const noteBudget = (entry: Pick<RepairEntry, 'sbn' | 'packetId' | 'catalogTrackName'>, extensionMs: number): void => {\n if (extensionMs > 0) mc.noteKeyframeBudgetGranted?.(extensionMs, entry.sbn, entry.packetId, entry.catalogTrackName)\n }\n\n mc.onRepairSubscribe((sbn, packetId, catalogTrackName, blockClass) => {\n if (disposed) {\n clearRepair(sbn, packetId, catalogTrackName)\n return\n }\n // Throws on a wiring fault, here and synchronously, before anything is\n // queued (see blockProfileOf).\n const blockProfile = blockProfileOf(packetId)\n\n // Fire-time resolution \u2014 see ResolveRepairTarget.\n const target = opts.resolveTarget(packetId, catalogTrackName)\n if (!target) {\n // A starved block with no repair target can only resolve via the fail\n // bound. Say so, once per track: a null target is also the documented\n // answer for a rung that carries no repair, and the typed lifecycle\n // events already enumerate every block.\n const warnKey = noTargetKey(packetId, catalogTrackName)\n if (!warnedNoTarget.has(warnKey)) {\n warnedNoTarget.add(warnKey)\n console.warn(\n `[RepairFetch] no repair target for pid=${packetId} sbn=${sbn}` +\n `${catalogTrackName === undefined ? '' : ` track=${catalogTrackName}`}; releasing without a fetch`,\n )\n }\n clearRepair(sbn, packetId, catalogTrackName)\n return\n }\n\n const key = requestKey(sbn, packetId, catalogTrackName)\n const previous = entries.get(key)\n if (previous) {\n retire(previous)\n stop(previous, 'reset')\n }\n const identity = {\n target,\n sbn,\n packetId,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n }\n const wire = wirePriority(identity, blockClass)\n if (wire === null) {\n // No conformant FETCH exists for this block (see RepairFetchPriority).\n clearRepair(sbn, packetId, catalogTrackName)\n return\n }\n const now = performance.now()\n const priority = repairPriorityForClass(blockClass)\n const deadline = presentationDeadlineAt(sbn, packetId, catalogTrackName, now, blockClass)\n if (deadline === null) {\n clearRepair(sbn, packetId, catalogTrackName)\n mc.noteRepairSkipped(\n 'presentation-delay-unavailable',\n { rttMs: readRtt(), budgetMs: 0, priority },\n sbn,\n packetId,\n catalogTrackName,\n )\n return\n }\n const entry: RepairEntry = {\n key,\n ...identity,\n repairTrack: target.repairTrack,\n blockProfile,\n priority,\n subscriberPriority: wire.subscriberPriority,\n deadlineAt: deadline.deadlineAt,\n playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,\n anchorAt: deadline.anchorAt,\n delayMs: deadline.delayMs,\n bound: deadline.bound,\n order: enqueueOrder++,\n attempts: 0,\n running: null,\n }\n entries.set(key, entry)\n noteBudget(entry, deadline.extensionMs)\n dispatch()\n })\n\n /**\n * \u00A77.5 range for a lost SS_ID: from ext(S_prev) when a stated block ends\n * where the hole starts, else from the epoch reference r. Null when there\n * is no r, or the lost SS_ID cannot be extended from it.\n */\n const rangeFor = (loss: UndelimitedIsoLoss, repairTrack: string): RepairFetchRange | null => {\n let reference: bigint | undefined\n try {\n reference = opts.repairEpochReference!(repairTrack)\n } catch {\n return null\n }\n if (reference === undefined) return null\n try {\n const anchor = loss.previousBlockStart === undefined\n ? { reference }\n : { previousBlockStart: extendSsId(reference, loss.previousBlockStart) }\n return repairFetchRange(loss.lostSsId, loss.kMax, anchor)\n } catch (error) {\n if (error instanceof RangeError) return null\n throw error\n }\n }\n\n const trackKind = (packetId: number): TrackKind | undefined => {\n try {\n return mc.fecManager.trackKindFor?.(packetId)\n } catch {\n return undefined\n }\n }\n\n /**\n * Where a walk of a hole reported `now` ends: WALK_LEAD_MS before the span\n * it leaves asks for its repair (D-28 from the report, as the span a host\n * without holes opened then did), so that span's request is armed before\n * it is due and goes out at the same time; never later than `deadlineAt`.\n */\n const walkDeadlineAt = (packetId: number, now: number, deadlineAt: number): number => {\n let repairDeadlineMs: number | undefined\n try {\n repairDeadlineMs = mc.fecManager.repairDeadlineMsForPacket?.(packetId)\n } catch {\n repairDeadlineMs = undefined\n }\n if (repairDeadlineMs === undefined || !Number.isFinite(repairDeadlineMs)) return deadlineAt\n return Math.min(deadlineAt, now + repairDeadlineMs - WALK_LEAD_MS)\n }\n\n /**\n * The stream ran past the locator's horizon behind a hole: no repair can\n * state its blocks any more. A walking hole's walk ends and the hole closes\n * as at its deadline, so the span it leaves fails, and is reported, as the\n * span opened at its report would have (BLO-52085); the host discards any\n * other. Called from inside the stream's packet handling, so the walk ends\n * once that returns, as for a RAP (below).\n */\n const onHoleExpired = (hole: ExpiredIsoHole): boolean => {\n if (disposed) return false\n const entry = [...entries.values()].find((candidate) =>\n candidate.walk !== undefined &&\n candidate.packetId === hole.packetId &&\n candidate.catalogTrackName === hole.catalogTrackName &&\n serial32Distance(candidate.sbn, hole.holeStart) < candidate.range!.holeLength)\n if (entry === undefined) return false\n queueMicrotask(() => {\n if (entries.get(entry.key) !== entry) return\n retire(entry)\n stop(entry, 'expired')\n clearEntry(entry)\n dispatch()\n })\n return true\n }\n\n // A host that can keep a hole gets range repair: the \u00A77.7 range over\n // `rangeTransport`, or else a walk of the hole's Groups over `transport`.\n if (mc.onUndelimitedLoss !== undefined && mc.closeIsoHoles !== undefined) {\n mc.onUndelimitedLoss((loss) => {\n const { packetId, catalogTrackName, holeStart, holeLength } = loss\n const close = (): void => mc.closeIsoHoles!(packetId, catalogTrackName, holeStart, holeLength)\n if (disposed) return close()\n const blockProfile = blockProfileOf(packetId)\n const target = opts.resolveTarget(packetId, catalogTrackName)\n if (!target) return close()\n // The run's class (fec-multicast-00 Block Classification, Audio\n // Blocks): walked, a hole on an audio track is audio repair, one Group\n // per FETCH; any other is of unknown class, so keyframe repair.\n const walked = opts.rangeTransport === undefined\n const holeClass: BlockClass = walked && trackKind(packetId) === 'audio' ? 'audio' : 'unknown'\n const priority = repairPriorityForClass(holeClass)\n const groups = walked ? null : rangeFor(loss, target.repairTrack)\n if (!walked && groups === null) {\n close()\n mc.noteRepairSkipped('range-no-anchor', { rttMs: readRtt(), budgetMs: 0, priority }, holeStart, packetId, catalogTrackName)\n return\n }\n const identity = {\n target,\n sbn: holeStart,\n packetId,\n ...(catalogTrackName === undefined ? {} : { catalogTrackName }),\n }\n // `unknown` is P0, whose request is never declined (omitted at worst);\n // an audio FETCH the catalog gives no priority for is not sent.\n const wire = wirePriority(identity, holeClass)\n if (wire === null) return close()\n // The hole is no tracked block: no anchor, so this is the multicast\n // wait (`bound: 'wait'`), by intent. A hole holds the release gate, so it\n // must not wait out a presentation delay on top.\n const now = performance.now()\n const deadline = presentationDeadlineAt(holeStart, packetId, catalogTrackName, now, holeClass)\n if (deadline === null) {\n close()\n mc.noteRepairSkipped(\n 'presentation-delay-unavailable',\n { rttMs: readRtt(), budgetMs: 0, priority },\n holeStart,\n packetId,\n catalogTrackName,\n )\n return\n }\n const deadlineAt = walked ? walkDeadlineAt(packetId, now, deadline.deadlineAt) : deadline.deadlineAt\n const entry: RepairEntry = {\n key: JSON.stringify(['range', packetId, catalogTrackName ?? null, holeStart]),\n ...identity,\n repairTrack: target.repairTrack,\n blockProfile,\n priority,\n subscriberPriority: wire.subscriberPriority,\n deadlineAt,\n playoutDeadlineAt: deadlineAt - deadline.extensionMs,\n anchorAt: deadline.anchorAt,\n delayMs: deadline.delayMs,\n bound: deadline.bound,\n order: enqueueOrder++,\n attempts: 0,\n running: null,\n range: { lostSsId: loss.lostSsId, holeLength, groups, reportedAt: now },\n ...(walked ? { walk: { next: holeStart, group: null, yielded: false } } : {}),\n }\n const previous = entries.get(entry.key)\n if (previous) {\n retire(previous)\n stop(previous, 'reset')\n }\n entries.set(entry.key, entry)\n entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1)\n dispatch()\n }, onHoleExpired)\n }\n\n // A RAP arrived complete: a keyframe FETCH below it is moot (\u00A711.4.2). The\n // observer runs inside the frame push, so the cancel waits for a microtask\n // rather than re-entering the FEC layer through the hold release.\n const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {\n queueMicrotask(() => {\n if (disposed) return\n for (const entry of [...entries.values()]) {\n if (entry.packetId !== packetId || !entry.running || !moot(entry)) continue\n stop(entry, 'moot')\n retire(entry)\n releaseHold(entry)\n clearEntry(entry)\n }\n dispatch()\n })\n })\n\n // fec-multicast-00 \u00A76.6: once the overlay recovered a block's RAP (its FT=0\n // and FT=1 held), the rest of the block is concealed as a non-keyframe\n // block, so its keyframe FETCH is stopped whatever its deadline. Deferred\n // as the RAP observer is: this fires from inside the recovered-block route.\n const removeOverlayObserver = mc.addKeyframeOverlayRecoveredObserver?.((packetId, catalogTrackName, sbn) => {\n queueMicrotask(() => {\n if (disposed) return\n for (const entry of [...entries.values()]) {\n if (entry.range || entry.priority !== 0 || entry.packetId !== packetId || entry.sbn !== sbn) continue\n if (catalogTrackName !== undefined && entry.catalogTrackName !== catalogTrackName) continue\n stop(entry, 'overlay-recovered')\n retire(entry)\n releaseHold(entry)\n clearEntry(entry)\n }\n dispatch()\n })\n })\n\n return () => {\n disposed = true\n mc.setUnicastRepair?.(false)\n removeResetObserver()\n removeLifecycleObserver()\n removeRapObserver?.()\n removeOverlayObserver?.()\n for (const entry of [...entries.values()]) {\n clearEntry(entry)\n if (entry.running) releaseHold(entry)\n stop(entry, 'disposed')\n if (entry.expiry !== undefined) clearTimeout(entry.expiry)\n }\n entries.clear()\n }\n}\n", "/**\n * Keyframe-protected FEC repair policy.\n *\n * Every FEC block still gets in-band FEC decode. What this module decides is\n * (1) which blocks may hold a keyframe (RAP), so a block short of K at its\n * repair deadline can be repaired over QUIC with the right priority, and\n * (2) what a sink must do with the first frame released after a gap: stall\n * until the next RAP, or conceal and keep playing.\n *\n * The two decisions are deliberately independent. Classification only sets\n * repair priority. The stall/conceal decision is made again per released gap\n * from the media that is actually missing, so a wrong classification can only\n * lower a block's repair priority; it can never make playback wrong.\n *\n * Everything here is derived from catalog-authored timing and in-band MMTP\n * evidence. There are no timers, no cadence heuristics, and no defaults: a\n * track whose geometry cannot be derived exactly runs in the defined\n * fail-closed mode `all-protected`, where every block is treated as possibly\n * keyframe-bearing.\n *\n * All three players (moqtail, hang, Shaka) consume this one implementation.\n */\n\nimport { deriveFecInterleaveDepth } from '@blockcast/mmt-fec'\n\n// \u2500\u2500\u2500 Classes and priorities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Repair class of one FEC block.\n *\n * - `keyframe`: the block holds (or held) a RAP / MPU start.\n * - `delta`: the block provably holds only non-RAP video frames.\n * - `unknown`: neither could be proven; treated exactly like `keyframe`.\n * - `audio`: an audio block.\n */\nexport type BlockClass = 'keyframe' | 'delta' | 'unknown' | 'audio'\n\n/** Repair priority. Lower is more urgent. */\nexport type RepairPriority = 0 | 1 | 2\n\nexport type TrackKind = 'video' | 'audio'\n\n/** P0 = keyframe/unknown (stall-preventing), P1 = audio, P2 = delta. */\nexport function repairPriorityForClass(blockClass: BlockClass): RepairPriority {\n switch (blockClass) {\n case 'keyframe':\n case 'unknown':\n return 0\n case 'audio':\n return 1\n case 'delta':\n return 2\n }\n}\n\n// \u2500\u2500\u2500 Geometry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ClassifierMode = 'classifying' | 'all-protected'\n\n/** Why a track runs `all-protected`. */\nexport type ClassifierReason =\n // Geometry (catalog) reasons.\n | 'framerate-absent'\n | 'keyframe-cadence-absent'\n | 'keyframe-interval-not-integral'\n | 'block-spans-gop'\n /**\n * The catalog keyframe interval is not an exact integer number of ticks\n * in the media track's own timescale (ISO-BMFF `mdhd`), so released RAP\n * spacing cannot be verified and hold-last cannot predict the next RAP.\n * Nothing is wrong with the stream; the cadence is simply unprovable.\n */\n | 'keyframe-interval-not-media-integral'\n /**\n * Another stream on this packetId was configured with its own geometry\n * and this one was not, so no geometry provably belongs to it (ABR rungs\n * sharing a packetId).\n */\n | 'stream-geometry-absent'\n // In-band invariant violations (positive evidence only).\n | 'rap-not-mpu-start'\n | 'mpu-longer-than-gop'\n | 'block-not-contiguous'\n | 'rap-spacing-mismatch'\n | 'mpu-multiple-fragments'\n | 'block-wider-than-interleave'\n\n/** Raw catalog timing for one track. Never a resolved cadence. */\nexport interface BlockClassifierGeometryInput {\n readonly trackKind: TrackKind\n /** Catalog `framerate` (frames per second). */\n readonly framerate?: number\n /** Catalog `fec.interleaveDepthMs`: the full FEC block span (\u00A75.1). */\n readonly interleaveDepthMs: number\n /** Catalog track `timescale` for `keyframeIntervalTicks`. */\n readonly timescale?: number\n /** Catalog `keyframeIntervalTicks` (preferred when the timescale is present). */\n readonly keyframeIntervalTicks?: number\n /** Catalog `keyframeIntervalMs`. */\n readonly keyframeIntervalMs?: number\n}\n\n/** Exact keyframe interval as a rational number of seconds: ticks / timescale. */\nexport interface KeyframeInterval {\n readonly ticks: number\n readonly timescale: number\n}\n\nexport type BlockClassifierGeometry =\n | { readonly trackKind: 'audio' }\n | {\n readonly trackKind: 'video'\n readonly mode: 'classifying'\n /** F: one frame's duration in milliseconds (1000 / framerate). */\n readonly frameDurationMs: number\n /** D: media units per FEC block (deriveFecInterleaveDepth). */\n readonly interleaveUnits: number\n /** G: frames per GOP. */\n readonly gopFrames: number\n readonly keyframeInterval: KeyframeInterval\n }\n | {\n readonly trackKind: 'video'\n readonly mode: 'all-protected'\n readonly reason: ClassifierReason\n }\n\n/**\n * Integer value of `numerator / denominator` when it is one, else null.\n *\n * Catalog framerates are JSON numbers, so an NTSC rate arrives as the double\n * nearest 30000/1001. One multiply and one divide each add at most half an\n * ulp of relative error (IEEE-754), so a quotient within two ulps of an\n * integer IS that integer; anything further is genuinely non-integral.\n */\nfunction exactInteger(numerator: number, denominator: number): number | null {\n if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0) {\n return null\n }\n const quotient = numerator / denominator\n const rounded = Math.round(quotient)\n if (rounded < 1) return null\n const representationError = 2 * Number.EPSILON * Math.abs(quotient)\n return Math.abs(quotient - rounded) <= representationError ? rounded : null\n}\n\nfunction isPositiveFinite(value: number | undefined): value is number {\n return value !== undefined && Number.isFinite(value) && value > 0\n}\n\n/**\n * Derive the per-track classifier geometry from raw catalog fields.\n *\n * `all-protected` is the policy's defined fail-closed state, not a default:\n * it is returned with the exact reason. The only throw is for a value the\n * catalog validator already rejects (a non-positive block span).\n */\nexport function deriveBlockClassifierGeometry(\n input: BlockClassifierGeometryInput,\n): BlockClassifierGeometry {\n if (!isPositiveFinite(input.interleaveDepthMs)) {\n throw new Error(\n `[BlockPolicy] interleaveDepthMs=${input.interleaveDepthMs} is not a positive block span; ` +\n 'the catalog validator rejects this',\n )\n }\n if (input.trackKind === 'audio') return { trackKind: 'audio' }\n\n if (!isPositiveFinite(input.framerate)) {\n return { trackKind: 'video', mode: 'all-protected', reason: 'framerate-absent' }\n }\n const framerate = input.framerate\n\n let keyframeInterval: KeyframeInterval\n if (isPositiveFinite(input.keyframeIntervalTicks) && isPositiveFinite(input.timescale)) {\n keyframeInterval = { ticks: input.keyframeIntervalTicks, timescale: input.timescale }\n } else if (isPositiveFinite(input.keyframeIntervalMs)) {\n keyframeInterval = { ticks: input.keyframeIntervalMs, timescale: 1000 }\n } else {\n return { trackKind: 'video', mode: 'all-protected', reason: 'keyframe-cadence-absent' }\n }\n\n const gopFrames = exactInteger(keyframeInterval.ticks * framerate, keyframeInterval.timescale)\n if (gopFrames === null) {\n return {\n trackKind: 'video',\n mode: 'all-protected',\n reason: 'keyframe-interval-not-integral',\n }\n }\n\n const frameDurationMs = 1000 / framerate\n const interleaveUnits = deriveFecInterleaveDepth(input.interleaveDepthMs, frameDurationMs)\n if (interleaveUnits >= gopFrames) {\n return { trackKind: 'video', mode: 'all-protected', reason: 'block-spans-gop' }\n }\n\n return {\n trackKind: 'video',\n mode: 'classifying',\n frameDurationMs,\n interleaveUnits,\n gopFrames,\n keyframeInterval,\n }\n}\n\n/**\n * The keyframe interval expressed in a media track's timescale, or null when\n * the geometry is not `classifying` or the interval is not an exact integer\n * number of media ticks.\n */\nexport function keyframeIntervalMediaTicks(\n geometry: BlockClassifierGeometry,\n mediaTimescale: number,\n): bigint | null {\n if (geometry.trackKind !== 'video' || geometry.mode !== 'classifying') return null\n if (!Number.isSafeInteger(mediaTimescale) || mediaTimescale <= 0) return null\n const { ticks, timescale } = geometry.keyframeInterval\n if (!Number.isSafeInteger(ticks) || !Number.isSafeInteger(timescale)) return null\n const scaled = BigInt(ticks) * BigInt(mediaTimescale)\n const divisor = BigInt(timescale)\n if (scaled % divisor !== 0n) return null\n return scaled / divisor\n}\n\n// \u2500\u2500\u2500 Evidence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * In-band evidence from ONE first-arrival source symbol received from the\n * network. Recovered symbols and repair symbols never contribute, and the\n * MFU `priority > 0x80` hint is not evidence.\n */\nexport interface SourceSymbolEvidence {\n /** The MMTP header R (RAP) bit. */\n readonly rap: boolean\n /** MPU_sequence_number, when parsed from this packet. */\n readonly mpu?: number\n /**\n * 0-based sample position within the MPU: the MFU DU-header\n * `sample_number` minus one (ISO/IEC 23008-1 numbers samples from 1, and\n * the parser rejects `sample_number` 0). Present only when this packet\n * carried a DU header.\n */\n readonly idx?: number\n /** movie_fragment_sequence_number from the DU header, when present. */\n readonly movieFragment?: number\n /**\n * DU-header `offset`: the data unit's authored byte position within its\n * sample (0 for a whole-sample data unit, the partition boundary for a\n * subsample data unit). Present only with `idx`; absent reads as 0.\n */\n readonly offset?: number\n /**\n * The MMTP header packet_sequence_number (contiguous per packet_id). Not\n * classification evidence: FECManager uses it to bound how many blocks the\n * producer can have started between two source symbols, so an outage is\n * never mistaken for a source restart.\n */\n readonly packetSequenceNumber?: number\n}\n\nexport type ReclassificationEvidence = 'rap' | 'mpu-span' | 'window' | 'classifier-disabled'\n\nexport type ClassifierChange =\n | {\n readonly kind: 'reclassified'\n readonly sbn: number\n readonly from: BlockClass\n readonly to: BlockClass\n readonly evidence: ReclassificationEvidence\n }\n | {\n readonly kind: 'mode'\n readonly mode: 'all-protected'\n readonly reason: ClassifierReason\n }\n\ninterface BlockEvidence {\n rapSeen: boolean\n mpus: Set<number>\n idxMin?: number\n idxMax?: number\n cls: BlockClass\n}\n\n/** A data-unit start (a packet carrying a DU header) and the block it is in. */\ninterface DataUnitStart {\n readonly movieFragment: number\n readonly idx: number\n readonly offset: number\n readonly sbn: number\n}\n\n/** Authored order of two data-unit starts of one MPU. */\nfunction compareDataUnitStarts(\n a: Omit<DataUnitStart, 'sbn'>,\n b: Omit<DataUnitStart, 'sbn'>,\n): number {\n return a.movieFragment - b.movieFragment || a.idx - b.idx || a.offset - b.offset\n}\n\n/**\n * Per-stream block classifier.\n *\n * Classes only move toward more protection or narrower evidence:\n * `unknown\u2192delta` and `unknown\u2192keyframe` / `delta\u2192keyframe` on evidence, and\n * `delta\u2192unknown` when an invariant disables classification. `keyframe` is\n * terminal.\n *\n * The delta window rule rests on three premises, each guarded by an\n * invariant that fires on positive evidence only:\n *\n * - P1: the source packets of one block are one contiguous run of the\n * stream's authored order, and blocks follow each other in SBN order\n * (`block-not-contiguous`). The producer assigns SS_IDs in authored order\n * and a block is the SS_ID range [SBN*K, SBN*K + K), so this holds for a\n * block that closes at K symbols in the middle of a frame: that frame's\n * fragments then land in two ADJACENT blocks, which is still one run each.\n * A frame split across adjacent blocks is not a violation.\n * - P2: that run touches at most D media units, counting a frame continued\n * from the previous block (`block-wider-than-interleave`). The producer\n * counts a media-unit boundary only once the open block holds a symbol.\n * - P3: an MPU starts with its only RAP and holds at most G frames\n * (`rap-not-mpu-start`, `mpu-longer-than-gop`, `rap-spacing-mismatch`), in\n * one movie fragment (`mpu-multiple-fragments`).\n *\n * P1 and P2 are what make a block's frames contiguous and lie within\n * [idxMax \u2212 D + 1, idxMin + D \u2212 1] of its MPU. What refutes P1 is a pair of\n * source packets whose wire-stated authored order and SBN order disagree:\n * a later MPU in an earlier block, a later data unit of one MPU (by\n * movie_fragment_sequence_number, sample_number, then DU offset) in an\n * earlier block, or one data unit in two blocks. That is the signature of a\n * round-robin interleave, which scatters one run over several concurrent\n * blocks. Non-adjacency alone is not: a sample larger than K packets\n * legitimately spans blocks SBN..SBN+n.\n */\nexport class BlockClassifier {\n #geometry: BlockClassifierGeometry\n #mode: ClassifierMode\n #reason: ClassifierReason | null\n /**\n * False once the stream showed that an MPU boundary does not bound its\n * RAPs (a RAP inside an MPU, or an MPU longer than a GOP). Recorded in\n * every mode: the stall/conceal decision relies on it even when the\n * catalog geometry already runs the classifier `all-protected`.\n */\n #mpuBoundsRaps = true\n #blocks = new Map<number, BlockEvidence>()\n /**\n * P1 ledger, kept for the MPUs within the classification horizon (the\n * highest MPU seen and the one before it): the SBN span of every source\n * packet of each MPU, and each MPU's data-unit starts in authored order.\n * Valid only within one continuous SBN sequence; a source discontinuity\n * clears it (`noteSourceDiscontinuity`).\n */\n #mpuSbnSpans = new Map<number, { min: number; max: number }>()\n #dataUnitStarts = new Map<number, DataUnitStart[]>()\n #highestMpu: number | null = null\n /** MPU \u2192 movie_fragment_sequence_number observed in it. */\n #mpuFragments = new Map<number, number>()\n #lastRap: { mpu: number; presentationTime: bigint; mediaTimescale: number } | null = null\n\n constructor(geometry: BlockClassifierGeometry) {\n this.#geometry = geometry\n this.#mode = 'classifying'\n this.#reason = null\n this.reset(geometry)\n }\n\n get geometry(): BlockClassifierGeometry {\n return this.#geometry\n }\n\n get mode(): ClassifierMode {\n return this.#mode\n }\n\n get reason(): ClassifierReason | null {\n return this.#reason\n }\n\n /**\n * Whether \"same MPU on both sides of a gap\" still proves the gap held no\n * RAP. It stops holding for the rest of the epoch once a RAP was received\n * inside an MPU or an MPU ran longer than a GOP.\n */\n get mpuBoundsRaps(): boolean {\n return this.#mpuBoundsRaps\n }\n\n /** Begin a fresh epoch (catalog change or track reset). */\n reset(geometry: BlockClassifierGeometry = this.#geometry): void {\n this.#geometry = geometry\n this.#blocks.clear()\n this.#clearContiguityLedger()\n this.#mpuFragments.clear()\n this.#lastRap = null\n this.#mpuBoundsRaps = true\n if (geometry.trackKind === 'video' && geometry.mode === 'all-protected') {\n this.#mode = 'all-protected'\n this.#reason = geometry.reason\n } else {\n this.#mode = 'classifying'\n this.#reason = null\n }\n }\n\n /**\n * Retire this classifier (its stream moves to a new catalog epoch). Every\n * block it had classified `delta` loses the evidence behind that class and\n * reads `unknown` from now on; the returned changes say so.\n */\n retire(): ClassifierChange[] {\n const changes: ClassifierChange[] = []\n for (const [sbn, block] of this.#blocks) {\n if (block.cls !== 'delta') continue\n block.cls = 'unknown'\n changes.push({\n kind: 'reclassified',\n sbn,\n from: 'delta',\n to: 'unknown',\n evidence: 'classifier-disabled',\n })\n }\n return changes\n }\n\n /**\n * The stream's SBN sequence restarted (FECManager source discontinuity:\n * a publisher restart, or the SS_ID space wrapped). Authored order and SBN\n * order are comparable only within one continuous sequence, so the P1\n * ledger starts again.\n */\n noteSourceDiscontinuity(): void {\n this.#clearContiguityLedger()\n }\n\n /** Register a block that exists without evidence (a vanished block). */\n touch(sbn: number): void {\n this.#blockFor(sbn)\n }\n\n /** Forget a resolved block. */\n forget(sbn: number): void {\n this.#blocks.delete(sbn)\n }\n\n classOf(sbn: number): BlockClass {\n if (this.#geometry.trackKind === 'audio') return 'audio'\n return this.#blocks.get(sbn)?.cls ?? 'unknown'\n }\n\n /** Record one first-arrival source symbol's evidence for block `sbn`. */\n observe(sbn: number, evidence: SourceSymbolEvidence): ClassifierChange[] {\n if (this.#geometry.trackKind === 'audio') return []\n const changes: ClassifierChange[] = []\n const block = this.#blockFor(sbn)\n\n if (evidence.rap) block.rapSeen = true\n if (evidence.mpu !== undefined) block.mpus.add(evidence.mpu)\n if (evidence.idx !== undefined) {\n block.idxMin = block.idxMin === undefined ? evidence.idx : Math.min(block.idxMin, evidence.idx)\n block.idxMax = block.idxMax === undefined ? evidence.idx : Math.max(block.idxMax, evidence.idx)\n }\n\n // A RAP inside an MPU refutes \"an MPU boundary bounds RAPs\" whatever the\n // classifier mode, because gap decisions rely on it in every mode.\n if (evidence.rap && evidence.idx !== undefined && evidence.idx !== 0) {\n this.#mpuBoundsRaps = false\n }\n\n const violation = this.#checkInvariants(sbn, block, evidence)\n if (violation !== null) changes.push(...this.#disable(violation))\n\n const next = this.#classify(block)\n if (next !== block.cls && this.#allowed(block.cls, next)) {\n changes.push({\n kind: 'reclassified',\n sbn,\n from: block.cls,\n to: next,\n evidence: this.#evidenceFor(block, next),\n })\n block.cls = next\n }\n return changes\n }\n\n /**\n * Record a RAP frame released in order. Released MPU-start RAPs of MPUs m\n * and m+n must be exactly n keyframe intervals apart in the media\n * timescale, compared as integers.\n *\n * Comparing only consecutive MPUs would miss exactly the case the window\n * rule cannot survive: a short MPU whose successor's RAP was lost (that\n * RAP-bearing block is the one a wrong G calls delta), because the next\n * released RAP then belongs to m+2. Every MPU is one interval long, so the\n * span to any later released RAP proves or refutes all MPUs in between.\n */\n noteReleasedRap(\n mpu: number,\n presentationTime: bigint,\n mediaTimescale: number,\n ): ClassifierChange[] {\n if (this.#geometry.trackKind === 'audio' || this.#mode === 'all-protected') {\n return []\n }\n // Verification compares integers in the media timescale. When the\n // catalog interval is not an exact tick count there, spacing is\n // unprovable: fail closed, naming the cause, rather than blaming the\n // encoder with rap-spacing-mismatch on a perfectly spaced RAP.\n const interval = keyframeIntervalMediaTicks(this.#geometry, mediaTimescale)\n if (interval === null) return this.#disable('keyframe-interval-not-media-integral')\n const previous = this.#lastRap\n // A further RAP inside an already-anchored MPU is not an MPU start; the\n // MPU keeps its first RAP as the spacing anchor (rap-not-mpu-start owns\n // mid-MPU RAPs).\n if (previous !== null && previous.mpu === mpu && previous.mediaTimescale === mediaTimescale) {\n return []\n }\n this.#lastRap = { mpu, presentationTime, mediaTimescale }\n if (\n previous === null ||\n mpu < previous.mpu ||\n previous.mediaTimescale !== mediaTimescale\n ) {\n return []\n }\n if (\n presentationTime - previous.presentationTime === interval * BigInt(mpu - previous.mpu)\n ) {\n return []\n }\n return this.#disable('rap-spacing-mismatch')\n }\n\n #blockFor(sbn: number): BlockEvidence {\n let block = this.#blocks.get(sbn)\n if (!block) {\n block = { rapSeen: false, mpus: new Set(), cls: 'unknown' }\n this.#blocks.set(sbn, block)\n }\n return block\n }\n\n #checkInvariants(\n sbn: number,\n block: BlockEvidence,\n evidence: SourceSymbolEvidence,\n ): ClassifierReason | null {\n if (this.#mode === 'all-protected' || this.#geometry.trackKind !== 'video') return null\n const geometry = this.#geometry\n if (geometry.mode !== 'classifying') return null\n if (evidence.rap && evidence.idx !== undefined && evidence.idx !== 0) {\n return 'rap-not-mpu-start'\n }\n if (evidence.idx !== undefined && evidence.idx >= geometry.gopFrames) {\n return 'mpu-longer-than-gop'\n }\n // A block closes at the latest after D media units, so the DU headers it\n // carries from one MPU lie within D consecutive positions. A wider span\n // means the producer closes blocks some other way (at K symbols, or\n // round-robin across concurrent blocks) and the window rule is void.\n if (\n block.mpus.size === 1 &&\n block.idxMin !== undefined &&\n block.idxMax !== undefined &&\n block.idxMax - block.idxMin >= geometry.interleaveUnits\n ) {\n return 'block-wider-than-interleave'\n }\n if (evidence.mpu !== undefined && evidence.movieFragment !== undefined) {\n const fragment = this.#mpuFragments.get(evidence.mpu)\n if (fragment === undefined) {\n this.#mpuFragments.set(evidence.mpu, evidence.movieFragment)\n // One entry per MPU still inside the classification horizon.\n for (const mpu of this.#mpuFragments.keys()) {\n if (mpu + 1 < evidence.mpu) this.#mpuFragments.delete(mpu)\n }\n } else if (fragment !== evidence.movieFragment) {\n // sample_number is then a position within a movie fragment, not\n // within the MPU, and the window rule's geometry does not hold.\n return 'mpu-multiple-fragments'\n }\n }\n if (evidence.mpu !== undefined && this.#contradictsAuthoredOrder(sbn, evidence.mpu, evidence)) {\n return 'block-not-contiguous'\n }\n return null\n }\n\n #clearContiguityLedger(): void {\n this.#mpuSbnSpans.clear()\n this.#dataUnitStarts.clear()\n this.#highestMpu = null\n }\n\n /**\n * P1 check for one source packet of MPU `mpu` in block `sbn`, then record\n * it. True when some recorded packet's authored order and SBN order\n * disagree with this one's (see the class comment). Only positions the\n * wire states are compared: MPU order for every packet, and data-unit\n * order for packets that carry a DU header. A packet of one data unit\n * that continues into the next block carries no DU header and so only\n * takes part through its MPU, which is exactly what makes an adjacent\n * straddle consistent.\n */\n #contradictsAuthoredOrder(sbn: number, mpu: number, evidence: SourceSymbolEvidence): boolean {\n // Every packet of MPU m precedes every packet of MPU m' > m.\n for (const [other, span] of this.#mpuSbnSpans) {\n if (other < mpu && span.max > sbn) return true\n if (other > mpu && span.min < sbn) return true\n }\n const inHorizon = this.#highestMpu === null || mpu + 1 >= this.#highestMpu\n let insertAt = -1\n let starts: DataUnitStart[] | undefined\n let start: DataUnitStart | undefined\n if (evidence.idx !== undefined) {\n start = {\n movieFragment: evidence.movieFragment ?? 0,\n idx: evidence.idx,\n offset: evidence.offset ?? 0,\n sbn,\n }\n starts = this.#dataUnitStarts.get(mpu)\n if (starts !== undefined) {\n let lo = 0\n let hi = starts.length\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (compareDataUnitStarts(starts[mid]!, start) < 0) lo = mid + 1\n else hi = mid\n }\n const at = starts[lo]\n if (at !== undefined && compareDataUnitStarts(at, start) === 0) {\n // One data unit has one first packet: seen in another block, the\n // SBN-to-authored-order map is not a function.\n if (at.sbn !== sbn) return true\n } else {\n const before = starts[lo - 1]\n if (before !== undefined && before.sbn > sbn) return true\n if (at !== undefined && at.sbn < sbn) return true\n insertAt = lo\n }\n }\n }\n if (!inHorizon) return false\n const span = this.#mpuSbnSpans.get(mpu)\n if (span === undefined) this.#mpuSbnSpans.set(mpu, { min: sbn, max: sbn })\n else {\n if (sbn < span.min) span.min = sbn\n if (sbn > span.max) span.max = sbn\n }\n if (start !== undefined) {\n if (starts === undefined) this.#dataUnitStarts.set(mpu, [start])\n else if (insertAt >= 0) starts.splice(insertAt, 0, start)\n }\n if (this.#highestMpu === null || mpu > this.#highestMpu) {\n this.#highestMpu = mpu\n for (const other of [...this.#mpuSbnSpans.keys()]) {\n if (other + 1 < mpu) {\n this.#mpuSbnSpans.delete(other)\n this.#dataUnitStarts.delete(other)\n }\n }\n }\n return false\n }\n\n #disable(reason: ClassifierReason): ClassifierChange[] {\n if (reason === 'rap-not-mpu-start' || reason === 'mpu-longer-than-gop') {\n this.#mpuBoundsRaps = false\n }\n if (this.#mode === 'all-protected') return []\n this.#mode = 'all-protected'\n this.#reason = reason\n const changes: ClassifierChange[] = [{ kind: 'mode', mode: 'all-protected', reason }]\n for (const [sbn, block] of this.#blocks) {\n if (block.cls !== 'delta') continue\n block.cls = 'unknown'\n changes.push({\n kind: 'reclassified',\n sbn,\n from: 'delta',\n to: 'unknown',\n evidence: 'classifier-disabled',\n })\n }\n return changes\n }\n\n #classify(block: BlockEvidence): BlockClass {\n if (block.rapSeen || block.mpus.size >= 2) return 'keyframe'\n if (this.#mode === 'all-protected') return 'unknown'\n const geometry = this.#geometry\n if (geometry.trackKind !== 'video' || geometry.mode !== 'classifying') return 'unknown'\n if (\n block.mpus.size === 1 &&\n block.idxMin !== undefined &&\n block.idxMax !== undefined\n ) {\n // A block closes every D media units, so its frames are contiguous and\n // lie in [idxMax \u2212 D + 1, idxMin + D \u2212 1] of MPU m. Index 0 is the only\n // RAP of an MPU and G bounds its length.\n const span = geometry.interleaveUnits - 1\n if (block.idxMax - span >= 1 && block.idxMin + span <= geometry.gopFrames - 1) {\n return 'delta'\n }\n }\n return 'unknown'\n }\n\n #allowed(from: BlockClass, to: BlockClass): boolean {\n if (from === 'keyframe') return false\n if (from === 'unknown') return to === 'delta' || to === 'keyframe'\n if (from === 'delta') return to === 'keyframe'\n return false\n }\n\n #evidenceFor(block: BlockEvidence, to: BlockClass): ReclassificationEvidence {\n if (to === 'delta') return 'window'\n return block.rapSeen ? 'rap' : 'mpu-span'\n }\n}\n\n// \u2500\u2500\u2500 Stall vs conceal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GapStreamIdentity {\n readonly packetId: number\n /** Non-empty catalog identity, or null when the producer has no track name. */\n readonly catalogTrackName: string | null\n}\n\n/** What the sink knows about one side of a released gap. */\nexport interface GapFrameEvidence {\n /**\n * Codec-aware RAP check: `MmtpContainerParser.isKeyframe(data, rapFlag,\n * codec)`, bitstream-only for recovered frames. Never the priority hint.\n */\n readonly rap: boolean\n /**\n * MPU_sequence_number of the frame. Omit it once the stream showed that\n * an MPU boundary does not bound its RAPs (`BlockClassifier.mpuBoundsRaps`):\n * same-MPU then no longer proves a gap lost only deltas.\n */\n readonly mpu?: number\n /** 0-based sample position within the MPU, when the geometry verifies it. */\n readonly idx?: number\n /** Authored presentation time in `timescale` ticks. */\n readonly pts?: bigint\n /** Authored sample duration in `timescale` ticks. */\n readonly durationTicks?: number\n /** The media timescale of `pts` and `durationTicks`. */\n readonly timescale?: number\n}\n\nexport type FrameGapAction = 'stall' | 'conceal'\n\nexport type FrameGapReason =\n | 'rap-lost'\n | 'rap-unknown'\n | 'discontinuity'\n | 'delta-lost'\n | 'rap-next'\n | 'audio-gap'\n | 'audio-discontinuity'\n\n/**\n * The decision attached to the first frame released after a gap\n * (`RoutedFrameMeta.gapBefore`). It is the same frozen object the manager\n * emits as its `gap` repair lifecycle event, so a sink that acts on the frame\n * and an observer that counts events see one fact.\n */\nexport interface FrameGapDecision {\n readonly version: 2\n readonly type: 'gap'\n readonly stream: GapStreamIdentity\n readonly trackKind: TrackKind\n readonly action: FrameGapAction\n readonly reason: FrameGapReason\n /** FEC blocks in the gap whose media was lost (failed or vanished). */\n readonly blocks: readonly number[]\n /** Missing video frames or audio access units, when exactly known. */\n readonly missingFrames: number | null\n /** Authored duration of the missing media in milliseconds, when known. */\n readonly missingMs: number | null\n /** Manager epoch of the stream when the decision was made. */\n readonly epoch: number\n}\n\nexport interface DecideGapInput {\n readonly stream: GapStreamIdentity\n readonly trackKind: TrackKind\n /** Last frame released before the gap. */\n readonly before: GapFrameEvidence\n /** First frame released after the gap. */\n readonly after: GapFrameEvidence\n /** The stream restarted (FEC SBN went backwards or the publisher reset). */\n readonly discontinuity: boolean\n readonly blocks: readonly number[]\n /**\n * The most media units the gap can hold: the FEC blocks spanned from the\n * frame before to the frame after, times D (media units per block,\n * ceil(interleaveDepthMs / unit duration), \u00A75.1). A block touches at most D\n * units whether it closes at D or at K symbols. Null when the span or D is\n * unknown. An audio PTS delta that implies more AUs than this is a clock\n * jump, not loss, and re-anchors instead of concealing every slot.\n */\n readonly maxMissingUnits: number | null\n readonly epoch: number\n}\n\nfunction durationMs(units: number, before: GapFrameEvidence): number | null {\n if (\n before.durationTicks === undefined ||\n before.timescale === undefined ||\n before.timescale <= 0\n ) {\n return null\n }\n return (units * before.durationTicks * 1000) / before.timescale\n}\n\n/**\n * Pure stall/conceal decision for the first frame released after a gap.\n *\n * Video stalls only when a RAP may be inside the gap: the frame after it is\n * not a RAP and either sits in another MPU (`rap-lost`) or the MPU cannot be\n * proven on both sides (`rap-unknown`), or the stream restarted\n * (`discontinuity`). A gap inside one MPU lost only deltas (`delta-lost`) and\n * a RAP after the gap resynchronises by itself (`rap-next`). Audio never\n * stalls: an exact gap is concealed per access unit (`audio-gap`), anything\n * else re-anchors at the next frame (`audio-discontinuity`).\n */\nexport function decideGap(input: DecideGapInput): FrameGapDecision {\n const { before, after } = input\n let action: FrameGapAction\n let reason: FrameGapReason\n let missingFrames: number | null = null\n\n if (input.trackKind === 'audio') {\n action = 'conceal'\n reason = 'audio-discontinuity'\n if (\n !input.discontinuity &&\n before.pts !== undefined &&\n after.pts !== undefined &&\n before.durationTicks !== undefined &&\n Number.isSafeInteger(before.durationTicks) &&\n before.durationTicks > 0\n ) {\n const auDuration = BigInt(before.durationTicks)\n const missingTicks = after.pts - before.pts - auDuration\n const bound = input.maxMissingUnits\n if (\n missingTicks > 0n &&\n missingTicks % auDuration === 0n &&\n bound !== null &&\n missingTicks / auDuration <= BigInt(bound)\n ) {\n reason = 'audio-gap'\n missingFrames = Number(missingTicks / auDuration)\n }\n }\n } else if (input.discontinuity) {\n action = 'stall'\n reason = 'discontinuity'\n } else if (after.rap) {\n action = 'conceal'\n reason = 'rap-next'\n } else if (before.mpu === undefined || after.mpu === undefined) {\n action = 'stall'\n reason = 'rap-unknown'\n } else if (before.mpu !== after.mpu) {\n action = 'stall'\n reason = 'rap-lost'\n } else {\n action = 'conceal'\n reason = 'delta-lost'\n if (before.idx !== undefined && after.idx !== undefined) {\n const missing = after.idx - before.idx - 1\n missingFrames = missing > 0 ? missing : null\n }\n }\n\n return Object.freeze({\n version: 2 as const,\n type: 'gap' as const,\n stream: Object.freeze({\n packetId: input.stream.packetId,\n catalogTrackName: input.stream.catalogTrackName,\n }),\n trackKind: input.trackKind,\n action,\n reason,\n blocks: Object.freeze([...input.blocks]),\n missingFrames,\n missingMs: missingFrames === null ? null : durationMs(missingFrames, before),\n epoch: input.epoch,\n })\n}\n", "// Resolved repair layer and overlay shapes (draft-ramadan-moq-fec-catalog-00,\n// fec-core-00): the output of `resolveRepairLayers`. Every base layer comes\n// from one instance's partition and differs only in P_i; layer i owns RS_IDs\n// [O_i, O_i + P_i) with O_i = sum(P_j, j < i), and ESI = K + RS_ID. A keyframe\n// overlay is a second instance with its own K, P and RS_ID space.\n//\n// Binding (which repair tracks belong to which instance) is\n// `resolveFecBindings` in mmt-base; nothing here reads a catalog.\n\n/** Lowest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MIN = 192;\n/** Highest MoQT-scale priority a repair track may declare. */\nexport const REPAIR_PRIORITY_MAX = 255;\n/** Priority of a base layer 0 track that declares none. */\nexport const DEFAULT_REPAIR_PRIORITY = 240;\n/** RFC 6330 \u00A74.4.1.2: the largest number of source symbols in one RaptorQ source block. */\nexport const MAX_RAPTORQ_SOURCE_SYMBOLS = 56403;\n/** The 24-bit RS_ID field (\u00A77.1) bounds the repair total of one FEC instance. */\nexport const MAX_REPAIR_SYMBOLS = 0xffffff;\n/** The 24-bit ESI bounds K + totalP - 1 for one FEC instance. */\nexport const MAX_ENCODING_SYMBOL_ID = 0xffffff;\n/** Reed-Solomon block bound: K + P <= 255 (draft-ramadan-moq-fec-01 4.5, 6.3.4 rule 12). */\nexport const MAX_REED_SOLOMON_BLOCK_SYMBOLS = 255;\n\n/** A resolved base repair layer with its derived RS_ID range. */\nexport type RepairLayerParams = {\n\t/** Layer index; 0 is the track named by the source's `fec.repairTrack`. */\n\tlayer: number;\n\trepairTrackName: string;\n\t/** P_i: repair symbols per block carried by this layer. */\n\trepairSymbols: number;\n\t/** O_i = sum(P_j for j < i): the first RS_ID this layer carries. ESI = K + RS_ID. */\n\trsIdOffset: number;\n\t/** MoQT-scale priority (lower first); 240 when layer 0 declares none. */\n\tpriority: number;\n};\n\n/** A resolved keyframe overlay: a separate FEC instance over RAP fragments only. */\nexport type RepairOverlayParams = {\n\trepairTrackName: string;\n\tscope: \"keyframe\";\n\t/** K_overlay: source symbols of the overlay instance. */\n\tsourceSymbols: number;\n\t/** P_overlay: repair symbols per overlay block; also the overlay's own RSB_length. */\n\trepairSymbols: number;\n\t/** MoQT-scale priority, independent of the base layer chain. */\n\tpriority: number;\n};\n\nexport type RepairLayersResolution = {\n\tsourceTrackName: string;\n\t/** Base layers in layer order, layer 0 first. Never empty. */\n\tbaseLayers: readonly RepairLayerParams[];\n\t/** Keyframe overlay instances, in catalog order. Empty when none is declared. */\n\t/** At most one entry: the wire carries one overlay trailer per RAP packet. */\n\toverlays: readonly RepairOverlayParams[];\n\t/** Sum of P over base layers: the wire RSB_length on every base layer packet. */\n\ttotalBaseRepairSymbols: number;\n};\n\n"],
5
+ "mappings": "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",
6
6
  "names": ["entry"]
7
7
  }