@blockcast/mmt-container 2.0.0 → 2.0.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/fec-bindings.ts", "../node_modules/@blockcast/mmt-transport/src/multicast-tier.ts", "../node_modules/@blockcast/mmt-transport/src/abr-controller.ts", "../node_modules/@blockcast/mmt-base/src/mmtp.ts", "../node_modules/@blockcast/mmt-base/src/net-constants.ts", "../node_modules/@blockcast/mmt-base/src/ntp-short.ts", "../node_modules/@blockcast/mmt-base/src/fec-block-profile.ts", "../node_modules/@blockcast/mmt-base/src/fec-bindings.ts", "../node_modules/@blockcast/mmt-transport/src/multicast-orchestrator.ts", "../node_modules/@blockcast/mmt-transport/src/endpoint-config.ts", "../node_modules/@blockcast/mmt-transport/src/packet-batch.ts", "../node_modules/@blockcast/mmt-transport/src/multicast-subscription-pool.ts", "../node_modules/@blockcast/mmt-transport/src/transport-manager.ts", "../node_modules/@blockcast/mmt-transport/src/moqt-priority.ts", "../node_modules/@blockcast/mmt-transport/src/amt-gateway.ts", "../src/delivery-catalog.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", "export interface MulticastTierEndpoint {\n\tsourceAddress?: string;\n\tgroupAddress: string;\n\tport: number;\n\ttracks: { name: string; packetId?: number }[];\n\tbandwidth?: number;\n\tprotocol?: string;\n\t/** False when every track on this endpoint is a `fec-repair` track, so the\n\t * endpoint carries repair symbols only and no media. A repair-layer\n\t * endpoint is joined by the loss-driven layer policy, never by the\n\t * bandwidth ladder: selecting one as a tier would leave the receiver with\n\t * no media at all. Set by the catalog policy, which resolves repair-track\n\t * membership from `depends`/`fec.repairTrack` rather than from names.\n\t * Undefined means \"not classified\" and is treated as carrying media, so a\n\t * caller that builds endpoints without the policy is unaffected. */\n\tcarriesMedia?: boolean;\n}\n\nexport function endpointKey(endpoint: MulticastTierEndpoint): string {\n\treturn `${endpoint.sourceAddress ?? \"\"}|${endpoint.groupAddress}|${endpoint.port}`;\n}\n\n/**\n * Whether this endpoint may be selected as a media tier.\n *\n * Every path that picks a tier must apply this, not only `selectMulticastTier`:\n * the orchestrator's headroom up-selection ranks endpoints itself, because it\n * advances one adjacent rung rather than fitting a bandwidth budget. Exported\n * so both rankings share one definition of \"carries media\" instead of\n * repeating the comparison and drifting apart.\n */\nexport function carriesMedia(endpoint: MulticastTierEndpoint): boolean {\n\treturn endpoint.carriesMedia !== false;\n}\n\nexport function selectMulticastTier(\n\tendpoints: readonly MulticastTierEndpoint[],\n\testimate: number | undefined,\n): MulticastTierEndpoint | undefined {\n\t// Repair-only endpoints are never a media tier. Filtering once here covers\n\t// every return path below, including the two early returns and the fallback:\n\t// a ladder that has only repair-layer endpoints returns undefined rather\n\t// than joining one and receiving no media.\n\tconst media = endpoints.filter(carriesMedia);\n\tif (media.length <= 1) return media[0];\n\n\tconst ranked = media\n\t\t.filter((endpoint) => typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > 0)\n\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!);\n\tif (ranked.length < 2) return media[0];\n\tif (estimate === undefined) return ranked[0];\n\n\tconst budget = estimate * 0.8;\n\tlet selected: MulticastTierEndpoint | undefined;\n\tfor (const endpoint of ranked) {\n\t\tif (endpoint.bandwidth! <= budget) selected = endpoint;\n\t}\n\treturn selected ?? ranked[0];\n}\n", "/**\n * @blockcast/transport - Adaptive Bitrate Controller\n *\n * Progressive adaptive bitrate increase based on network conditions.\n * Monitors received bandwidth and packet loss to adjust quality levels.\n *\n * When FEC is enabled, ABR decisions use the **effective loss rate** after FEC\n * recovery, not the raw transport-level loss rate, so FEC can mask burst losses\n * without unnecessarily downgrading quality. This is libmmt policy; no draft\n * specifies it.\n */\n\nimport type { QualityLevel } from \"./types.js\";\nimport { selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\n/**\n * FEC statistics for ABR decisions\n */\nexport interface FecStatsForAbr {\n\t/** Raw loss rate before FEC recovery (0.0-1.0) */\n\trawLossRate: number;\n\t/** Effective loss rate after FEC recovery (0.0-1.0) */\n\teffectiveLossRate: number;\n\t/** FEC recovery rate (percentage of lost packets recovered) */\n\trecoveryRate: number;\n\t/** Whether FEC is enabled */\n\tfecEnabled: boolean;\n}\n\n/**\n * A generic 240p..4K ladder, kept as public API only.\n *\n * @deprecated Not a stream's ladder and never a default: the controller\n * adapts over the ladder its caller supplies (`AdaptiveBitrateControllerOptions.ladder`,\n * e.g. the catalog's video renditions). Walking this list is how a player on a\n * 360p/480p/720p catalog logged upgrades to 1080p and 1440p it could never\n * switch to (BLO-35597).\n */\nexport const QUALITY_LEVELS: readonly QualityLevel[] = Object.freeze([\n\t{ name: \"240p\", bitrate: 300_000, width: 426, height: 240, fps: 15 },\n\t{ name: \"360p\", bitrate: 500_000, width: 640, height: 360, fps: 24 },\n\t{ name: \"480p\", bitrate: 1_000_000, width: 854, height: 480, fps: 30 },\n\t{ name: \"720p\", bitrate: 2_500_000, width: 1280, height: 720, fps: 30 },\n\t{ name: \"1080p\", bitrate: 5_000_000, width: 1920, height: 1080, fps: 30 },\n\t{ name: \"1440p\", bitrate: 8_000_000, width: 2560, height: 1440, fps: 30 },\n\t{ name: \"4K\", bitrate: 15_000_000, width: 3840, height: 2160, fps: 30 },\n].map((level) => Object.freeze(level)));\n\n/** What the controller adapts over and where it starts; both from the caller. */\nexport interface AdaptiveBitrateControllerOptions {\n\t/**\n\t * The renditions this stream carries (e.g. the catalog's video tracks), in\n\t * any order: a unique name and a bitrate (bps) each, plus dimensions and\n\t * fps when the catalog states them. Renditions may share a bitrate.\n\t */\n\tladder: readonly QualityLevel[];\n\t/** Name of the ladder rendition presented at construction; the lowest\n\t * rung when omitted. A name that matches no rung is refused. */\n\tinitialQuality?: string;\n}\n\nfunction positiveFinite(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\n/**\n * Validate a caller ladder and return it frozen, ascending by bitrate. Throws\n * on an empty ladder, a rung without a name or a positive finite bitrate, a\n * width, height or fps that is present but not positive finite, or a name that\n * appears twice: a name is how `setQuality` and the initial rendition find a\n * rung. A shared bitrate is legal (e.g. 720p30 and 720p60, or two codecs);\n * `Array.prototype.sort` is stable, so equal bitrates keep the caller's order.\n */\nexport function validateQualityLadder(ladder: readonly QualityLevel[]): readonly QualityLevel[] {\n\tif (!Array.isArray(ladder) || ladder.length === 0) {\n\t\tthrow new TypeError(\"[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)\");\n\t}\n\tconst names = new Set<string>();\n\tfor (const level of ladder) {\n\t\tif (typeof level?.name !== \"string\" || level.name.length === 0) {\n\t\t\tthrow new TypeError(\"[ABR] every ladder rendition needs a name\");\n\t\t}\n\t\tif (!positiveFinite(level.bitrate)) {\n\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);\n\t\t}\n\t\tfor (const field of [\"width\", \"height\", \"fps\"] as const) {\n\t\t\tif (level[field] !== undefined && !positiveFinite(level[field])) {\n\t\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);\n\t\t\t}\n\t\t}\n\t\tif (names.has(level.name)) throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);\n\t\tnames.add(level.name);\n\t}\n\treturn Object.freeze(\n\t\t[...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level })),\n\t);\n}\n\n/**\n * Progressive Adaptive Bitrate Controller\n *\n * Features:\n * - Monitors bandwidth via received bytes over time window\n * - Tracks packet loss rate (raw and effective after FEC)\n * - Uses effective loss rate (after FEC) for ABR decisions\n * - Progressive quality upgrades (waits for stability)\n * - Fast quality downgrades on congestion\n */\nexport class AdaptiveBitrateController {\n\t/** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */\n\treadonly ladder: readonly QualityLevel[];\n\tprivate currentQualityIndex: number;\n\tprivate history: { timestamp: number; bytesReceived: number; packetsLost: number }[] = [];\n\tprivate readonly historyWindow = 10_000; // 10 second window\n\tprivate readonly upgradeThreshold = 0.8; // 80% of target bitrate sustained\n\tprivate readonly downgradeThreshold = 0.5; // 50% of target bitrate triggers downgrade\n\tprivate readonly stabilityPeriod = 5_000; // Wait 5s before upgrading\n\n\tprivate lastQualityChange = 0;\n\tprivate onQualityChange?: (quality: QualityLevel) => void;\n\n\t// FEC stats for effective loss rate calculation\n\tprivate fecStats: FecStatsForAbr = {\n\t\trawLossRate: 0,\n\t\teffectiveLossRate: 0,\n\t\trecoveryRate: 1,\n\t\tfecEnabled: false,\n\t};\n\n\tconstructor(options: AdaptiveBitrateControllerOptions) {\n\t\tif (!options || typeof options !== \"object\") {\n\t\t\tthrow new TypeError(\"[ABR] AdaptiveBitrateController needs { ladder, initialQuality }\");\n\t\t}\n\t\tthis.ladder = validateQualityLadder(options.ladder);\n\t\tthis.currentQualityIndex =\n\t\t\toptions.initialQuality === undefined ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);\n\t\tif (this.currentQualityIndex < 0) {\n\t\t\tthrow new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);\n\t\t}\n\t}\n\n\tsetCallback(callback: (quality: QualityLevel) => void) {\n\t\tthis.onQualityChange = callback;\n\t}\n\n\tgetCurrentQuality(): QualityLevel {\n\t\treturn this.ladder[this.currentQualityIndex];\n\t}\n\n\t/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */\n\tselectMulticastTier(\n\t\tendpoints: readonly MulticastTierEndpoint[],\n\t\testimate: number | undefined = this.getMetrics().measuredBitrate || undefined,\n\t): MulticastTierEndpoint | undefined {\n\t\treturn selectMulticastTier(endpoints, estimate);\n\t}\n\n\t/**\n\t * Record network sample for bitrate adaptation\n\t *\n\t * @param bytesReceived - Bytes received in this sample\n\t * @param packetsLost - Packets lost (raw, before FEC recovery)\n\t */\n\trecordSample(bytesReceived: number, packetsLost: number) {\n\t\tconst now = Date.now();\n\n\t\tthis.history.push({ timestamp: now, bytesReceived, packetsLost });\n\n\t\t// Prune old samples\n\t\tthis.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);\n\n\t\t// Calculate metrics\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\tconst currentQuality = this.getCurrentQuality();\n\n\t\t// Use effective loss rate after FEC for ABR decisions\n\t\t// This allows FEC to mask burst losses without triggering unnecessary downgrades\n\t\tconst effectiveLossRate = this.fecStats.fecEnabled\n\t\t\t? this.fecStats.effectiveLossRate\n\t\t\t: rawLossRate;\n\n\t\t// Decision logic\n\t\tif (now - this.lastQualityChange < this.stabilityPeriod) {\n\t\t\treturn; // Wait for stability\n\t\t}\n\n\t\t// Check for downgrade - use effective loss rate (post-FEC)\n\t\t// Only downgrade if FEC cannot recover the losses\n\t\tif (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {\n\t\t\tthis.downgrade();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for upgrade (progressive increase) - use effective loss rate\n\t\tif (\n\t\t\tmeasuredBitrate > currentQuality.bitrate * this.upgradeThreshold &&\n\t\t\teffectiveLossRate < 0.01 &&\n\t\t\tthis.currentQualityIndex < this.ladder.length - 1\n\t\t) {\n\t\t\tthis.upgrade();\n\t\t}\n\t}\n\n\t/**\n\t * Update FEC statistics for ABR decisions\n\t *\n\t * ABR uses the effective loss rate (after FEC recovery) rather than the\n\t * raw transport loss rate.\n\t *\n\t * @param stats - FEC statistics from the FEC decoder\n\t */\n\tsetFecStats(stats: FecStatsForAbr) {\n\t\tthis.fecStats = stats;\n\t}\n\n\t/**\n\t * Get current FEC statistics\n\t */\n\tgetFecStats(): FecStatsForAbr {\n\t\treturn { ...this.fecStats };\n\t}\n\n\tprivate calculateMetrics() {\n\t\tif (this.history.length < 2) {\n\t\t\treturn { measuredBitrate: 0, lossRate: 0 };\n\t\t}\n\n\t\tconst totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);\n\t\tconst totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);\n\t\tconst duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1000;\n\n\t\tconst measuredBitrate = duration > 0 ? (totalBytes * 8) / duration : 0;\n\t\tconst lossRate = totalLost / (this.history.length + totalLost);\n\n\t\treturn { measuredBitrate, lossRate };\n\t}\n\n\tprivate upgrade() {\n\t\tif (this.currentQualityIndex >= this.ladder.length - 1) return;\n\n\t\tthis.currentQualityIndex++;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\tprivate downgrade() {\n\t\tif (this.currentQualityIndex <= 0) return;\n\n\t\tthis.currentQualityIndex--;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\t/**\n\t * Select a ladder rendition: the caller switched to it (a manual override,\n\t * or a switch the caller made). Throws for a name the ladder does not carry.\n\t */\n\tsetQuality(name: string) {\n\t\tconst index = this.ladder.findIndex((q) => q.name === name);\n\t\tif (index < 0) throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);\n\t\tthis.currentQualityIndex = index;\n\t\tthis.lastQualityChange = Date.now();\n\t\tthis.onQualityChange?.(this.getCurrentQuality());\n\t}\n\n\t/**\n\t * Get current metrics including both raw and effective loss rates\n\t */\n\tgetMetrics(): {\n\t\tmeasuredBitrate: number;\n\t\trawLossRate: number;\n\t\teffectiveLossRate: number;\n\t\tfecEnabled: boolean;\n\t\tsampleCount: number;\n\t} {\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\treturn {\n\t\t\tmeasuredBitrate,\n\t\t\trawLossRate,\n\t\t\teffectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,\n\t\t\tfecEnabled: this.fecStats.fecEnabled,\n\t\t\tsampleCount: this.history.length,\n\t\t};\n\t}\n\n\t/**\n\t * Reset history and start fresh\n\t */\n\treset() {\n\t\tthis.history = [];\n\t\tthis.lastQualityChange = 0;\n\t}\n}\n", "/**\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 * AMT well-known UDP port (RFC 7450 \u00A74).\n *\n * The single definition for the whole repo. It lives here, in the one package\n * both `@blockcast/mmt-transport` and `@blockcast/mmt-container` already\n * depend on, because the two apply it on opposite sides of a boundary that\n * does not otherwise share code:\n *\n * - container resolves it where `networkSource` semantics live, so a catalog\n * relay that names no port is reachable instead of handing a join a\n * portless relay that fails.\n * - transport applies it when reading a catalog, dialling a gateway, and\n * answering DRIAD \u2014 but deliberately not at every boundary. Which call\n * sites do and do not take the default is transport's business; the\n * reasoning lives beside those call sites, not here.\n */\nexport const DEFAULT_AMT_RELAY_PORT = 2268;\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", "import { compareNtpShort, isNtpShort } from \"@blockcast/mmt-base\";\nimport { carriesMedia, endpointKey, selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\nexport type OrchestratorState = \"stable\" | \"joining\";\nexport type OrchestratorCommand =\n\t| { kind: \"join-target\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint }\n\t| { kind: \"commit-swap\"; target: MulticastTierEndpoint; old: MulticastTierEndpoint }\n\t| { kind: \"abandon-join\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint };\n\nexport interface MulticastTierPacket {\n\tendpointKey: string;\n\tkeyframe: boolean;\n\t/**\n\t * The packet's MMTP header `timestamp`: its UTC sending instant in NTP short\n\t * format (ISO/IEC 23008-1 \u00A79.2.2), which wraps every 2^16 s (~18.2 h). The\n\t * orchestrator orders these only with RFC 1982 serial arithmetic\n\t * (`compareNtpShort`), never with `<` or `Math.max`.\n\t *\n\t * A non-finite value means the host could not read a header; that packet\n\t * neither raises the high-water mark nor commits. Any other value that is\n\t * not NTP short is a host bug and throws a RangeError.\n\t */\n\ttimestamp: number;\n\thasValidCodecConfig: boolean;\n}\n\nexport interface OrchestratorOptions {\n\tendpoints: readonly MulticastTierEndpoint[];\n\tinitialEndpoint?: MulticastTierEndpoint;\n\tadaptive?: boolean;\n\tnow: () => number;\n\tdwellMs?: number;\n\tjoinDeadlineMs?: number;\n}\n\nexport interface GoodputSample { bitrate: number | undefined; }\nexport interface HeadroomSample { healthy: boolean; }\n\nexport const DEFAULT_DWELL_MS = 3_000;\nexport const DEFAULT_JOIN_DEADLINE_MS = 5_000;\n\nexport function goodputFromStats(\n\tprevious: { bytesReceived: number; atMs: number } | undefined,\n\tcurrent: { bytesReceived: number; atMs: number },\n\tminSampleMs = 500,\n): number | undefined {\n\tif (!previous) return undefined;\n\tconst durationMs = current.atMs - previous.atMs;\n\tconst bytes = current.bytesReceived - previous.bytesReceived;\n\tif (durationMs < minSampleMs || bytes <= 0) return undefined;\n\treturn (bytes * 8 * 1000) / durationMs;\n}\n\nexport class MulticastOrchestrator {\n\t#state: OrchestratorState = \"stable\";\n\t#current: MulticastTierEndpoint | undefined;\n\t#target: MulticastTierEndpoint | undefined;\n\t#estimate: number | undefined;\n\t#favored: { key: string; since: number; source: \"goodput\" | \"headroom\" } | undefined;\n\t#joinStartedAt = 0;\n\t/**\n\t * Latest active-tier send time (NTP short), or undefined before the first.\n\t * \"Latest\" is serial order, not the numerically largest value: after the\n\t * field wraps, the largest value is the stale one.\n\t */\n\t#highWater: number | undefined;\n\treadonly #dwellMs: number;\n\treadonly #joinDeadlineMs: number;\n\n\tconstructor(private readonly options: OrchestratorOptions) {\n\t\tthis.#dwellMs = options.dwellMs ?? DEFAULT_DWELL_MS;\n\t\tthis.#joinDeadlineMs = options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS;\n\t\tif (!Number.isFinite(this.#dwellMs) || !Number.isFinite(this.#joinDeadlineMs) ||\n\t\t\tthis.#dwellMs <= 0 || this.#joinDeadlineMs <= 0) {\n\t\t\tthrow new Error(\"Multicast dwell and join deadline must be positive\");\n\t\t}\n\t}\n\n\tget state(): OrchestratorState { return this.#state; }\n\tget current(): MulticastTierEndpoint | undefined { return this.#current; }\n\tget target(): MulticastTierEndpoint | undefined { return this.#target; }\n\tget estimate(): number | undefined { return this.#estimate; }\n\n\tstart(): MulticastTierEndpoint | undefined {\n\t\tthis.#current ??= this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, undefined);\n\t\treturn this.#current;\n\t}\n\n\tonGoodput(sample: GoodputSample): OrchestratorCommand | undefined {\n\t\tconst bitrate = sample.bitrate;\n\t\tif (!this.#current) return undefined;\n\t\t// Before the adaptive gate: a selected join runs with adaptation off, and a\n\t\t// target that delivers nothing reaches its deadline only on this clock.\n\t\tif (this.#state === \"joining\") return this.poll();\n\t\tif (this.options.adaptive === false) return undefined;\n\t\tif (bitrate === undefined || !Number.isFinite(bitrate)) return undefined;\n\t\tthis.#estimate = bitrate;\n\t\tconst desired = selectMulticastTier(this.options.endpoints, bitrate);\n\t\tif (!desired) return undefined;\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (typeof desired.bandwidth !== \"number\" || typeof this.#current.bandwidth !== \"number\" ||\n\t\t\tdesired.bandwidth >= this.#current.bandwidth) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey || this.#favored.source !== \"goodput\") {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"goodput\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\tonHeadroom(sample: HeadroomSample): OrchestratorCommand | undefined {\n\t\tif (!this.#current || this.options.adaptive === false || this.#state === \"joining\") return undefined;\n\t\tif (!sample.healthy) {\n\t\t\tif (this.#favored?.source === \"headroom\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.#favored?.source === \"goodput\") return undefined;\n\n\t\tconst currentBandwidth = this.#current.bandwidth;\n\t\tif (typeof currentBandwidth !== \"number\") return undefined;\n\t\t// Ranked here rather than through selectMulticastTier: up-selection\n\t\t// advances exactly one adjacent rung, where the selector fits a budget to\n\t\t// an estimate. The media filter is shared so a repair-only endpoint above\n\t\t// the current rung is not joined as an upgrade, which would swap the\n\t\t// receiver onto repair symbols and no media.\n\t\tconst desired = this.options.endpoints\n\t\t\t.filter((endpoint) =>\n\t\t\t\tcarriesMedia(endpoint) && typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > currentBandwidth,\n\t\t\t)\n\t\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!)[0];\n\t\tif (!desired) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey) {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"headroom\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\t/**\n\t * Join `target` because the viewer selected it, not because goodput or\n\t * headroom ranked it. It runs whether or not the orchestrator adapts, and\n\t * takes the path an adaptive join takes: commit on the target's RAP at or\n\t * beyond the high-water mark, abandon at the join deadline. While it is\n\t * pending nothing re-ranks the ladder; goodput only advances the deadline.\n\t *\n\t * Returns undefined, changing nothing, while another join is pending (cancel\n\t * it first), when `target` is the current tier, or when it carries no media.\n\t */\n\tselect(target: MulticastTierEndpoint): OrchestratorCommand | undefined {\n\t\tif (!this.#current || this.#state === \"joining\" || !carriesMedia(target)) return undefined;\n\t\tif (endpointKey(target) === endpointKey(this.#current)) return undefined;\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = target;\n\t\tthis.#joinStartedAt = this.#now();\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target };\n\t}\n\n\t/**\n\t * Feed a packet from the active tier (to advance the high-water mark) or\n\t * from the in-flight target (to find a committable RAP).\n\t *\n\t * Serial order is only the true order while the compared instants are less\n\t * than `NTP_SHORT_HALF_RANGE` (2^15 s, ~9.1 h) apart. Feeding every\n\t * active-tier packet keeps the mark that close to the live stream. A mark\n\t * refreshed only by commits can fall further behind than that, and a\n\t * target RAP may then compare as older, so the join is abandoned.\n\t */\n\tonTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\tif (!this.#current) return undefined;\n\t\tconst sentAt = tierPacketSendTime(packet);\n\t\t// An instant exactly half the space from the mark (RFC 1982's undefined\n\t\t// case) is neither later nor earlier, so it leaves the mark alone, as a\n\t\t// reordered earlier packet does.\n\t\tif (packet.endpointKey === endpointKey(this.#current) && sentAt !== undefined &&\n\t\t\t(this.#highWater === undefined || compareNtpShort(sentAt, this.#highWater) === \"after\")) {\n\t\t\tthis.#highWater = sentAt;\n\t\t}\n\t\tif (this.#state !== \"joining\" || !this.#target) return undefined;\n\t\tconst expired = this.poll();\n\t\tif (expired) return expired;\n\t\tif (\n\t\t\tpacket.endpointKey !== endpointKey(this.#target) ||\n\t\t\t!packet.keyframe ||\n\t\t\t!packet.hasValidCodecConfig ||\n\t\t\tsentAt === undefined ||\n\t\t\t!this.#reachesHighWater(sentAt)\n\t\t) return undefined;\n\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"commit-swap\",\n\t\t\ttarget: this.#target,\n\t\t\told: this.#current,\n\t\t};\n\t\tthis.#highWater = sentAt;\n\t\tthis.#current = this.#target;\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\tpoll(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tif (this.#now() - this.#joinStartedAt < this.#joinDeadlineMs) return undefined;\n\t\treturn this.cancel();\n\t}\n\n\tcancel(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"abandon-join\",\n\t\t\tcurrent: this.#current,\n\t\t\ttarget: this.#target,\n\t\t};\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\t/**\n\t * Whether a target RAP sent at `sentAt` is at or beyond the active tier's\n\t * high-water mark. A pair exactly half the NTP-short space apart has no\n\t * serial order, so it cannot show that and does not commit.\n\t */\n\t#reachesHighWater(sentAt: number): boolean {\n\t\tif (this.#highWater === undefined) return true;\n\t\tconst order = compareNtpShort(sentAt, this.#highWater);\n\t\treturn order === \"after\" || order === \"equal\";\n\t}\n\n\t#now(): number {\n\t\tconst now = this.options.now();\n\t\tif (!Number.isFinite(now)) throw new Error(\"Multicast orchestrator clock must be finite\");\n\t\treturn now;\n\t}\n}\n\n/** A tier packet's NTP-short send time, or undefined when the host had no header to read. */\nfunction tierPacketSendTime(packet: MulticastTierPacket): number | undefined {\n\tif (!Number.isFinite(packet.timestamp)) return undefined;\n\tif (!isNtpShort(packet.timestamp)) {\n\t\tthrow new RangeError(\n\t\t\t`Multicast tier packet timestamp must be an MMTP NTP-short send time, got ${packet.timestamp}`,\n\t\t);\n\t}\n\treturn packet.timestamp;\n}\n", "import { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID, admittedMulticastEndpoints } from \"@blockcast/mmt-base\";\nimport type {\n\tWindowMulticastSubscribeConfig,\n\tWindowMulticastSubscribeConfigBytes,\n} from \"./types.js\";\n\nexport type MulticastTransportPreference = \"auto\" | \"native\" | \"tunnel\";\n\nexport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\nexport interface MulticastEndpointConfig {\n\tgroupAddress: string;\n\tsourceAddress?: string;\n\t/** ASM sender filter (`auth.senderAddress` of a protected ASM endpoint); never beside `sourceAddress`. */\n\tsenderAddress?: string;\n\tport: number;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\t/** `\"driad\"`: with no relay, the gateway discovers one per RFC 8777. */\n\tamtDiscovery?: \"driad\";\n}\n\n/**\n * Input accepted by the canonical window.multicast request converter.\n *\n * `mode` and the `amtRelay*` fields are internal endpoint-selection state;\n * they are deliberately not emitted on the page-facing request. The\n * resulting `origin` is the only acquisition selector understood by the\n * gateway.\n */\nexport interface WindowMulticastEndpointInput {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\t/** ASM sender filter; emitted as the request's `senderAddress`. Refused beside `source`. */\n\tsenderAddress?: string;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\t/** `\"driad\"`: with no relay, the gateway discovers one per RFC 8777. */\n\tamtDiscovery?: \"driad\";\n\tpacketIds?: readonly number[];\n\ttracks?: ReadonlyArray<{ name: string; packetId: number }>;\n\t/**\n\t * ATSC 3.0 tuning from the catalog's `atsc3` networkSource\n\t * (`catalogAtsc3NetworkSource`). Draft -01 3 puts the tuner first: when set,\n\t * the request is the tuner member and relay/mode state is not consulted.\n\t */\n\tatsc3?: { frequency: number; plpId?: number; serviceId?: number };\n}\n\n/**\n * Convert internal endpoint-selection state to the canonical page contract.\n *\n * Native is the default and an explicit `native` selection always wins over\n * stale relay metadata. Tunnel/AMT requests must carry the resolved relay\n * and port; there is no implicit relay-port default at this boundary.\n *\n * That rule is deliberate and is NOT the place to fix a relay that names no\n * port: this is the last hop before the page-facing request, so a default\n * applied here would be invisible to every caller and to the join it produces.\n * A catalog relay with no port gets `DEFAULT_AMT_RELAY_PORT` upstream, in the\n * readers that resolve it, and arrives here explicit like any other\n * (BLO-38176).\n */\nexport function toWindowMulticastSubscribeConfig(\n\tinput: WindowMulticastEndpointInput,\n): WindowMulticastSubscribeConfig {\n\tif (input.mode !== undefined && ![\"auto\", \"native\", \"tunnel\"].includes(input.mode)) {\n\t\tthrow new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);\n\t}\n\t// An explicit non-array (the null \"endpoint unusable\" sentinel from\n\t// selectEndpointPacketIdsForTracks) must not degrade into \"no filter\".\n\tif (input.packetIds !== undefined && !Array.isArray(input.packetIds)) {\n\t\tthrow new TypeError(\n\t\t\t`multicast packetIds must be an array, got ${input.packetIds === null ? \"null\" : typeof input.packetIds}`,\n\t\t);\n\t}\n\t// Every join request passes through here, so the track packet_id range is\n\t// enforced here rather than trusted from each caller.\n\tif (input.packetIds?.some(id => !isMulticastTrackPacketId(id)) ||\n\t\tinput.tracks?.some(track => !isMulticastTrackPacketId(track.packetId))) {\n\t\tthrow new RangeError(\n\t\t\t`multicast packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n\t\t\t\"packetId 0 is the MMTP signaling flow and is never a track's\",\n\t\t);\n\t}\n\n\tconst source = input.source?.trim() || undefined;\n\tconst senderAddress = input.senderAddress?.trim() || undefined;\n\t// Draft -01 7.2.6: S is sourceAddress on SSM and senderAddress on ASM, never both.\n\tif (source && senderAddress) {\n\t\tthrow new TypeError(\"multicast senderAddress is an ASM filter and never appears beside source\");\n\t}\n\tconst base = {\n\t\tgroup: input.group,\n\t\tport: input.port,\n\t\t...(source ? { source } : {}),\n\t\tconsume: \"bytes\" as const,\n\t\t...(input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}),\n\t};\n\tconst filter = senderAddress ? { senderAddress } : {};\n\n\tif (input.atsc3) {\n\t\t// The contract's tuner member carries no sender filter.\n\t\tif (senderAddress) throw new TypeError(\"an ATSC3 tuner request carries no senderAddress\");\n\t\tconst { frequency, plpId, serviceId } = input.atsc3;\n\t\tif (!Number.isInteger(frequency) || frequency < 1) {\n\t\t\tthrow new TypeError(`ATSC3 tuning requires a positive integer frequency in kHz, got ${String(frequency)}`);\n\t\t}\n\t\treturn {\n\t\t\t...base,\n\t\t\torigin: \"tuner\",\n\t\t\tstandard: \"atsc3\",\n\t\t\tfrequency,\n\t\t\t...(plpId !== undefined ? { plpId } : {}),\n\t\t\t...(serviceId !== undefined ? { serviceId } : {}),\n\t\t};\n\t}\n\n\tconst relay = input.amtRelay?.trim();\n\t// A relay wins over DRIAD: it is a commitment to that relay.\n\tconst driad = !relay && input.amtDiscovery === \"driad\";\n\tconst wantsAmt = input.mode === \"tunnel\" ||\n\t\t(input.mode !== \"native\" && ((relay !== undefined && relay !== \"\") || driad));\n\tif (!wantsAmt) {\n\t\tconst native: WindowMulticastSubscribeConfigBytes = {\n\t\t\t...base,\n\t\t\t...filter,\n\t\t\torigin: \"ip-multicast\",\n\t\t\t...(input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}),\n\t\t};\n\t\treturn native;\n\t}\n\n\tconst relayPort = input.amtRelayPort;\n\tif (driad) {\n\t\tif (!base.source) throw new TypeError(\"AMT DRIAD discovery requires a source address\");\n\t\tif (relayPort !== undefined && !isPositivePort(relayPort)) {\n\t\t\tthrow new TypeError(\"AMT DRIAD discovery requires a valid relay port when one is given\");\n\t\t}\n\t\treturn {\n\t\t\t...base,\n\t\t\tsource: base.source,\n\t\t\torigin: \"amt\",\n\t\t\tdiscovery: \"driad\",\n\t\t\t...(relayPort !== undefined ? { relayPort } : {}),\n\t\t};\n\t}\n\tif (!relay || relayPort === undefined || !isPositivePort(relayPort)) {\n\t\tthrow new TypeError(\"AMT requires a resolved relay and a valid relay port\");\n\t}\n\treturn {\n\t\t...base,\n\t\t...filter,\n\t\torigin: \"amt\",\n\t\trelay,\n\t\trelayPort,\n\t};\n}\n\nexport function isValidMulticastGroupAddress(address: string | undefined): boolean {\n\tconst value = address?.trim().toLowerCase();\n\tif (!value) return false;\n\tif (value.startsWith(\"ff\")) return isValidIpv6MulticastAddress(value);\n\n\tconst parts = value.split(\".\");\n\tif (parts.length !== 4) return false;\n\tconst octets = parts.map((part) => {\n\t\tif (!/^\\d+$/.test(part)) return Number.NaN;\n\t\tconst octet = Number(part);\n\t\treturn Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;\n\t});\n\treturn octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;\n}\n\nexport function normalizeMulticastEndpoint(\n\tendpoint: MulticastEndpointConfig,\n): MulticastEndpointConfig | undefined {\n\tconst groupAddress = endpoint.groupAddress.trim().toLowerCase();\n\tconst sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || undefined;\n\tconst senderAddress = endpoint.senderAddress?.trim().toLowerCase() || undefined;\n\tif (sourceAddress && senderAddress) return undefined;\n\tconst amtRelay = endpoint.amtRelay?.trim().toLowerCase() || undefined;\n\tif (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port)) return undefined;\n\tif (endpoint.mode && ![\"auto\", \"native\", \"tunnel\"].includes(endpoint.mode)) return undefined;\n\tif (endpoint.amtRelayPort !== undefined && !isPositivePort(endpoint.amtRelayPort)) return undefined;\n\n\treturn {\n\t\tgroupAddress,\n\t\t...(sourceAddress ? { sourceAddress } : {}),\n\t\t...(senderAddress ? { senderAddress } : {}),\n\t\tport: endpoint.port,\n\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t\t...(endpoint.amtDiscovery === \"driad\" ? { amtDiscovery: \"driad\" as const } : {}),\n\t};\n}\n\n/** Stable ownership key for one IWA multicast/AMT subscription. */\nexport function multicastEndpointKey(endpoint: MulticastEndpointConfig): string {\n\tconst normalized = normalizeMulticastEndpoint(endpoint);\n\tif (!normalized) throw new Error(\"Invalid multicast endpoint\");\n\treturn JSON.stringify([\n\t\tnormalized.sourceAddress ?? \"\",\n\t\tnormalized.groupAddress,\n\t\tnormalized.port,\n\t\tnormalized.mode ?? \"auto\",\n\t\tnormalized.amtRelay ?? \"\",\n\t\tnormalized.amtRelayPort ?? \"\",\n\t\t// A DRIAD join is a different gateway subscription from a native one.\n\t\t...(normalized.amtDiscovery ? [normalized.amtDiscovery] : []),\n\t\t// An ASM sender filter is a different subscription from the unfiltered join.\n\t\t...(normalized.senderAddress ? [{ senderAddress: normalized.senderAddress }] : []),\n\t]);\n}\n\n/**\n * True when `value` may be advertised as a multicast track's MMTP `packet_id`:\n * an integer in 1..MMTP_PACKET_ID_MAX. packet_id 0 (MMTP_SIGNALING_PACKET_ID)\n * is the MMTP signaling flow -- the FFmpeg `moq_mmt` muxer sends the AL-FEC\n * message (0x0203) on it into the same group as the media -- so it is never a\n * track's. Only a number qualifies: a string, boolean or null is not coerced.\n */\nexport function isMulticastTrackPacketId(value: unknown): value is number {\n\treturn Number.isInteger(value) &&\n\t\t(value as number) > MMTP_SIGNALING_PACKET_ID &&\n\t\t(value as number) <= MMTP_PACKET_ID_MAX;\n}\n\n/**\n * True when a receiver may join `endpoint`: a multicast group, a usable port\n * and, given the `catalog` it came from, admission under draft -01 4.1 / 7.2.6\n * (`admittedUnderCatalog`). Without a catalog object only the first two are checked.\n */\nexport function isUsableMulticastEndpoint(\n\tendpoint: { groupAddress?: string; port?: unknown } | undefined,\n\tcatalog?: unknown,\n): boolean {\n\tconst port = Number(endpoint?.port);\n\treturn isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port) &&\n\t\t// Not a catalog (an `Array#filter` index, say): checks the first two only.\n\t\t(catalog === null || typeof catalog !== \"object\" || admittedUnderCatalog(catalog, endpoint));\n}\n\n/**\n * Whether one endpoint survives the draft -01 receiver rules in the context\n * of `catalog`: its mode and `S` agree, it carries `auth` exactly when the\n * catalog's `multicast` object does, and that catalog holds a key set. The\n * rules are `multicastCatalogIssues` of `@blockcast/mmt-base`, the same the\n * container validator throws on. Cross-endpoint rules need the whole array;\n * `admittedEndpointsUnderCatalog` applies those.\n */\nexport function admittedUnderCatalog(catalog: unknown, endpoint: unknown): boolean {\n\treturn admittedEndpointsUnderCatalog(catalog, [endpoint]).has(0);\n}\n\n/**\n * Indices of `endpoints` that survive every draft -01 receiver rule as the\n * endpoint array of `catalog`, cross-endpoint rules included: both sides of a\n * shared `(S, G, port)` are dropped.\n */\nexport function admittedEndpointsUnderCatalog(catalog: unknown, endpoints: readonly unknown[]): ReadonlySet<number> {\n\tconst root = catalog !== null && typeof catalog === \"object\" ? (catalog as Record<string, unknown>) : {};\n\tconst multicast = root[\"multicast\"] !== null && typeof root[\"multicast\"] === \"object\"\n\t\t? (root[\"multicast\"] as Record<string, unknown>)\n\t\t: {};\n\treturn admittedMulticastEndpoints({ ...root, multicast: { ...multicast, endpoints } });\n}\n\n/**\n * Unprefixed aliases of the `mc_` URL override keys, retired in BLO-37507, each\n * paired with the key that replaces it.\n */\nconst RETIRED_MULTICAST_URL_PARAMS = [\n\t[\"amt_relay\", \"mc_amt_relay\"],\n\t[\"amt_relay_port\", \"mc_amt_relay_port\"],\n] as const;\n\n/**\n * Reject a URL multicast override that still carries a retired unprefixed alias.\n *\n * Both URL readers call this only once a valid override is established (a valid\n * multicast `mc_group` and a valid `mc_port`; `readUrlMulticastConfig` also\n * requires the whole endpoint to normalize), which is where they used to read\n * the aliases: a link that\n * relied on one fails here with its replacement named, instead of silently\n * losing its AMT relay (and, in `readUrlMulticastConfig`, its tunnel mode).\n * Presence is the error, whatever the value and whether or not the `mc_` form\n * is also present: quietly preferring one of two keys is the tolerant read being\n * retired. Outside an override the readers read no override key at all, the\n * `mc_` forms included, so the bare names are left to the page; they are live\n * page parameters elsewhere, and keeping clear of those is what the `mc_`\n * namespace is for.\n */\nexport function assertNoRetiredMulticastUrlParams(params: URLSearchParams): void {\n\tfor (const [retired, replacement] of RETIRED_MULTICAST_URL_PARAMS) {\n\t\tif (params.has(retired)) {\n\t\t\tthrow new Error(\n\t\t\t\t`multicast URL override: query parameter \"${retired}\" is retired; use \"${replacement}\"`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * Read an explicit multicast endpoint override from `mc_*` URL parameters.\n * Throws when an override carries a retired unprefixed alias\n * (`assertNoRetiredMulticastUrlParams`).\n */\nexport function readUrlMulticastConfig(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): MulticastEndpointConfig | undefined {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst groupAddress = params.get(\"mc_group\") || \"\";\n\tconst port = positivePort(params.get(\"mc_port\"));\n\tif (!groupAddress || !port) return undefined;\n\n\tconst amtRelay = params.get(\"mc_amt_relay\") || undefined;\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\"));\n\tconst requestedMode = params.get(\"mc_mode\");\n\tconst mode = amtRelay ? \"tunnel\" : multicastMode(requestedMode);\n\n\tconst endpoint = normalizeMulticastEndpoint({\n\t\tgroupAddress,\n\t\tsourceAddress: params.get(\"mc_source\") || undefined,\n\t\tport,\n\t\t...(mode ? { mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(amtRelayPort ? { amtRelayPort } : {}),\n\t});\n\t// An endpoint that does not normalize (e.g. a non-multicast mc_group) is no\n\t// override, so the page keeps its own bare names; the same scope\n\t// readPlayerMulticastConfig applies by validating the group first.\n\tif (endpoint) assertNoRetiredMulticastUrlParams(params);\n\treturn endpoint;\n}\n\nexport function isMulticastExplicitlyDisabled(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): boolean {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst multicast = params.get(\"multicast\")?.trim().toLowerCase();\n\tconst transport = params.get(\"transport\")?.trim().toLowerCase();\n\treturn transport === \"moq-only\" || [\"0\", \"false\", \"no\", \"off\"].includes(multicast || \"\");\n}\n\nfunction isValidIpv6MulticastAddress(value: string): boolean {\n\tif (!/^[0-9a-f:]+$/.test(value)) return false;\n\tconst compressed = value.includes(\"::\");\n\tif (value.indexOf(\"::\") !== value.lastIndexOf(\"::\")) return false;\n\tconst groups = value.split(\":\").filter(Boolean);\n\tif (groups.length === 0 || groups.length > 8) return false;\n\tif (!compressed && groups.length !== 8) return false;\n\treturn groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));\n}\n\nfunction multicastMode(value: string | null): MulticastTransportPreference | undefined {\n\treturn value === \"native\" || value === \"tunnel\" || value === \"auto\" ? value : undefined;\n}\n\nfunction positivePort(value: string | null): number | undefined {\n\tconst port = Number(value);\n\treturn isPositivePort(port) ? port : undefined;\n}\n\nfunction isPositivePort(port: number): boolean {\n\treturn Number.isInteger(port) && port > 0 && port <= 65535;\n}\n", "import type { WindowMulticastSubscription } from \"./types.js\";\n\n/**\n * True when `subscription` delivers packets in batches through the `packets`\n * provider extension (BLO-36599).\n *\n * The pim window-multicast-bridge (0.2.0 and later) advertises the extension\n * in `eventTypes`. It builds only the events some listener asked for, so a\n * consumer that listens to `packets` alone gets one event per reader batch\n * instead of one DOM event per packet. A provider without the field, or one\n * that does not list `packets`, is a v1 provider: listen to `bytes`.\n */\nexport function subscriptionDeliversPacketBatches(\n\tsubscription: Pick<WindowMulticastSubscription, \"eventTypes\">,\n): boolean {\n\treturn subscription.eventTypes?.includes(\"packets\") === true;\n}\n", "import {\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.js\";\nimport { subscriptionDeliversPacketBatches } from \"./packet-batch.js\";\nimport type {\n\tTransportConfig,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscription,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\n\nexport interface MulticastSubscriptionConsumer {\n\tonBytes(data: ArrayBuffer): void;\n\tonState?(state: WindowMulticastSubscriptionState): void;\n\tonError?(error: Error): void;\n}\n\ninterface PoolEntry {\n\tsubscription: WindowMulticastSubscription;\n\tconsumers: Map<symbol, MulticastSubscriptionConsumer>;\n\t/**\n\t * Exactly one packet surface is listened to: `packets` when the provider\n\t * advertises it, `bytes` otherwise (BLO-36599).\n\t */\n\tpacketListener:\n\t\t| { type: \"bytes\"; listener: (event: CustomEvent<ArrayBuffer>) => void }\n\t\t| { type: \"packets\"; listener: (event: CustomEvent<readonly ArrayBuffer[]>) => void };\n\tstateListener: (event: CustomEvent<WindowMulticastSubscriptionState>) => void;\n\terrorListener: (event: CustomEvent<unknown>) => void;\n\tclosed: boolean;\n}\n\ninterface PoolRecord { claims: number; entry: Promise<PoolEntry>; }\n\nexport interface MulticastSubscriptionLease {\n\treadonly key: string;\n\trelease(): Promise<void>;\n}\n\n/** Shares only IWA multicast/AMT subscriptions. Unicast sessions remain per manager. */\nexport class MulticastSubscriptionPool {\n\treadonly #entries = new Map<string, PoolRecord>();\n\treadonly #gatewayIds = new WeakMap<object, number>();\n\t#nextGatewayId = 1;\n\n\tasync acquire(\n\t\tgateway: WindowMulticastGateway,\n\t\tconfig: TransportConfig,\n\t\tconsumer: MulticastSubscriptionConsumer,\n\t): Promise<MulticastSubscriptionLease> {\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t\tamtDiscovery: config.amtDiscovery,\n\t\t});\n\t\tif (!endpoint) throw new Error(\"Invalid multicast endpoint\");\n\t\tif (typeof gateway.subscribe !== \"function\") throw new Error(\"window.multicast.subscribe unavailable\");\n\n\t\tconst key = multicastEndpointKey(endpoint);\n\t\tconst internalKey = `${this.#gatewayId(gateway)}|${key}`;\n\t\tlet record = this.#entries.get(internalKey);\n\t\tif (!record) {\n\t\t\trecord = { claims: 0, entry: Promise.resolve(null as unknown as PoolEntry) };\n\t\t\tconst createdRecord = record;\n\t\t\trecord.entry = this.#createEntry(gateway, endpoint, (entry) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t}).catch((error) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\tthis.#entries.set(internalKey, record);\n\t\t}\n\n\t\trecord.claims++;\n\t\tlet entry: PoolEntry;\n\t\ttry {\n\t\t\tentry = await record.entry;\n\t\t} catch (error) {\n\t\t\trecord.claims--;\n\t\t\tthrow error;\n\t\t}\n\t\tif (entry.closed) {\n\t\t\trecord.claims--;\n\t\t\treturn this.acquire(gateway, config, consumer);\n\t\t}\n\t\tconst token = Symbol(key);\n\t\tentry.consumers.set(token, consumer);\n\t\tlet released = false;\n\t\treturn {\n\t\t\tkey,\n\t\t\trelease: async () => {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tentry.consumers.delete(token);\n\t\t\t\trecord.claims--;\n\t\t\t\tif (record.claims > 0) return;\n\t\t\t\tif (this.#entries.get(internalKey) === record) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t\tif (!entry.closed) await entry.subscription.unsubscribe();\n\t\t\t},\n\t\t};\n\t}\n\n\tget size(): number { return this.#entries.size; }\n\n\tasync #createEntry(\n\t\tgateway: WindowMulticastGateway,\n\t\tendpoint: NonNullable<ReturnType<typeof normalizeMulticastEndpoint>>,\n\t\tonClosed: (entry: PoolEntry) => void,\n\t): Promise<PoolEntry> {\n\t\tconst subscription = await gateway.subscribe!(toWindowMulticastSubscribeConfig({\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\t...(endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {}),\n\t\t\tport: endpoint.port,\n\t\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t\t...(endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {}),\n\t\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t\t\t...(endpoint.amtDiscovery ? { amtDiscovery: endpoint.amtDiscovery } : {}),\n\t\t}));\n\t\tconst consumers = new Map<symbol, MulticastSubscriptionConsumer>();\n\t\tlet entry: PoolEntry;\n\t\t// A batch is delivered as the same per-packet onBytes calls the bytes\n\t\t// surface makes, in arrival order and with each call isolated, so a\n\t\t// consumer cannot tell the two apart; only the DOM event per packet is\n\t\t// gone. The batch array and its elements are the provider's own, uncopied.\n\t\tconst packetListener: PoolEntry[\"packetListener\"] = subscriptionDeliversPacketBatches(subscription)\n\t\t\t? {\n\t\t\t\t\ttype: \"packets\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tfor (const data of event.detail) {\n\t\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(data));\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\ttype: \"bytes\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));\n\t\t\t\t\t},\n\t\t\t\t};\n\t\tconst stateListener = (event: CustomEvent<WindowMulticastSubscriptionState>) => {\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onState?.(event.detail));\n\t\t\tif (event.detail === \"closed\" && !entry.closed) {\n\t\t\t\tentry.closed = true;\n\t\t\t\tonClosed(entry);\n\t\t\t}\n\t\t};\n\t\tconst errorListener = (event: CustomEvent<unknown>) => {\n\t\t\tconst error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onError?.(error));\n\t\t};\n\t\tentry = { subscription, consumers, packetListener, stateListener, errorListener, closed: false };\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tsubscription.addEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tsubscription.addEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tsubscription.addEventListener(\"state\", stateListener);\n\t\tsubscription.addEventListener(\"error\", errorListener);\n\t\treturn entry;\n\t}\n\n\t#gatewayId(gateway: WindowMulticastGateway): number {\n\t\tconst object = gateway as object;\n\t\tlet id = this.#gatewayIds.get(object);\n\t\tif (!id) {\n\t\t\tid = this.#nextGatewayId++;\n\t\t\tthis.#gatewayIds.set(object, id);\n\t\t}\n\t\treturn id;\n\t}\n\n\t#detach(entry: PoolEntry): void {\n\t\tconst { packetListener } = entry;\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tentry.subscription.removeEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tentry.subscription.removeEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tentry.subscription.removeEventListener(\"state\", entry.stateListener);\n\t\tentry.subscription.removeEventListener(\"error\", entry.errorListener);\n\t}\n\n\t#dispatch(\n\t\tconsumers: Map<symbol, MulticastSubscriptionConsumer>,\n\t\tcallback: (consumer: MulticastSubscriptionConsumer) => void,\n\t): void {\n\t\tfor (const consumer of consumers.values()) {\n\t\t\ttry { callback(consumer); } catch (error) { console.error(\"[Transport] multicast consumer callback failed\", error); }\n\t\t}\n\t}\n}\n\nexport const multicastSubscriptionPool = new MulticastSubscriptionPool();\n", "/**\n * @blockcast/transport - Transport Manager\n *\n * Unified transport abstraction for MoQ players supporting:\n * - window.multicast gateway subscriptions\n * - AMT proxy tunneling (RFC 7450) with optional DRIAD relay discovery\n * - WebTransport/QUIC (MoQ-lite protocol)\n * - WebSocket fallback\n *\n * Transport promotion hierarchy (WebTransport-first for fast startup):\n * 1. WebTransport - immediate connection for fast channel select\n * 2. Background discovery - check if window.multicast is available\n * 3. Promote to gateway multicast if available\n * 4. WebSocket fallback if WebTransport unavailable\n *\n * Features:\n * - Progressive adaptive bitrate based on network conditions\n * - FEC (Forward Error Correction) for burst loss recovery\n * - Seamless transport switching with session continuity\n * - Unified FEC/ABR pipeline across all transport modes\n */\n\nimport type {\n\tTransportConfig,\n\tTransportConnectConfig,\n\tTransportSubscription,\n\tTransportCapabilities,\n\tTransportState,\n\tQualityLevel,\n\tWindowMulticastCapabilities,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\nimport { AdaptiveBitrateController } from \"./abr-controller.js\";\nimport {\n\tMulticastOrchestrator,\n\ttype MulticastTierPacket,\n\ttype OrchestratorCommand,\n\ttype OrchestratorOptions,\n} from \"./multicast-orchestrator.js\";\nimport type { MulticastTierEndpoint } from \"./multicast-tier.js\";\nimport { multicastSubscriptionPool } from \"./multicast-subscription-pool.js\";\nimport { normalizeMulticastEndpoint } from \"./endpoint-config.js\";\n\n/**\n * Transport Manager\n *\n * Manages transport connections with automatic mode selection:\n * WebTransport -> window.multicast -> AMT proxy -> WebSocket\n */\nexport class TransportManager {\n\tprivate state: TransportState = {\n\t\tmode: \"disconnected\",\n\t\tconnected: false,\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tlastPacketTime: 0,\n\t};\n\n\tprivate capabilities: TransportCapabilities | null = null;\n\tprivate subscription: TransportSubscription | null = null;\n\t/** Built by `connect()` from the caller's ladder; there is no built-in one. */\n\tprivate abr: AdaptiveBitrateController | null = null;\n\tprivate multicastOrchestrator: MulticastOrchestrator | null = null;\n\tprivate onOrchestratorCommand?: (command: OrchestratorCommand) => void;\n\tprivate connectionGeneration = 0;\n\n\tprivate onPacket?: (data: ArrayBuffer) => void;\n\tprivate onError?: (error: Error) => void;\n\tprivate onStateChange?: (state: TransportState) => void;\n\n\tprivate getWindow(): Window | undefined {\n\t\treturn typeof window === \"undefined\" ? undefined : window;\n\t}\n\n\tprivate getWindowMulticastGateway(): WindowMulticastGateway | undefined {\n\t\treturn this.getWindow()?.multicast;\n\t}\n\n\tprivate async getGatewayCapabilities(\n\t\tgateway: WindowMulticastGateway,\n\t): Promise<WindowMulticastCapabilities | undefined> {\n\t\tif (!gateway?.capabilities) return undefined;\n\t\ttry {\n\t\t\treturn typeof gateway.capabilities === \"function\" ?\n\t\t\t\tawait gateway.capabilities() :\n\t\t\t\tgateway.capabilities;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast capabilities failed:\", error);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async gatewaySupportsBytes(gateway: WindowMulticastGateway | undefined): Promise<boolean> {\n\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\t\tif (!gateway.capabilities) return true;\n\n\t\tconst capabilities = await this.getGatewayCapabilities(gateway);\n\t\tif (!capabilities) return false;\n\t\tconst consumeModes = capabilities.consumeModes;\n\t\treturn !consumeModes || consumeModes.includes(\"bytes\");\n\t}\n\n\t/**\n\t * Detect available transport capabilities\n\t */\n\tasync detectCapabilities(): Promise<TransportCapabilities> {\n\t\tif (this.capabilities) return this.capabilities;\n\n\t\tconst gateway = this.getWindowMulticastGateway();\n\t\tconst hasGatewayBytes = await this.gatewaySupportsBytes(gateway);\n\t\tconst win = this.getWindow();\n\n\t\tthis.capabilities = {\n\t\t\tnativeSSM: hasGatewayBytes,\n\t\t\tnativeIGMP: hasGatewayBytes,\n\t\t\tdirectSockets: typeof win?.UDPSocket !== \"undefined\",\n\t\t\tamt: false,\n\t\t\tdriad: hasGatewayBytes, // the gateway runs RFC 8777 discovery\n\t\t\tfecDecode: false,\n\t\t};\n\n\t\tconsole.log(\"[Transport] Capabilities:\", this.capabilities);\n\t\treturn this.capabilities;\n\t}\n\n\t/**\n\t * Connect using best available transport\n\t *\n\t * Promotion hierarchy:\n\t * 1. window.multicast gateway bytes\n\t * 2. AMT: a configured relay via the AMT proxy, else gateway DRIAD discovery\n\t * 3. MOQ WebTransport (final fallback)\n\t *\n\t * The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)\n\t * are checked first, before anything is torn down. A config that fails is\n\t * refused with the same shape as an invalid endpoint, the error to the\n\t * error callback and `false` resolved; unlike one, it is caught before the\n\t * live subscription closes, so a refused reconnect leaves that subscription,\n\t * its state and any connect already in flight untouched.\n\t */\n\tasync connect(config: TransportConnectConfig): Promise<boolean> {\n\t\tlet abr: AdaptiveBitrateController;\n\t\ttry {\n\t\t\tabr = new AdaptiveBitrateController({\n\t\t\t\tladder: config.abrLadder,\n\t\t\t\tinitialQuality: config.initialQuality?.name,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\t// A refused config is a caller bug. Without an error callback, say so\n\t\t\t// on the console, since `false` alone also means \"no multicast path\";\n\t\t\t// with one, the callback is the report.\n\t\t\tconst refused = error instanceof Error ? error : new Error(String(error));\n\t\t\tif (this.onError) this.onError(refused);\n\t\t\telse console.error(\"[Transport] ABR config refused, not connecting:\", refused);\n\t\t\treturn false;\n\t\t}\n\t\tconst generation = ++this.connectionGeneration;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// The previous connection's ladder ends with it, as in disconnect(): a\n\t\t// reconnect that bails below (invalid endpoint, or superseded while\n\t\t// detecting capabilities) leaves no ladder, and only the success path\n\t\t// installs `abr`.\n\t\tthis.abr = null;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t});\n\t\tif (!endpoint) {\n\t\t\tconst error = new Error(\"Invalid multicast endpoint configuration\");\n\t\t\tthis.onError?.(error);\n\t\t\treturn false;\n\t\t}\n\t\tconfig = {\n\t\t\t...config,\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\tsource: endpoint.sourceAddress ?? \"\",\n\t\t\tmode: endpoint.mode,\n\t\t\tamtRelay: endpoint.amtRelay,\n\t\t\tamtRelayPort: endpoint.amtRelayPort,\n\t\t};\n\t\tconst caps = await this.detectCapabilities();\n\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t// ABR adapts over the caller's ladder (the catalog's renditions),\n\t\t// validated above before anything was torn down.\n\t\tthis.abr = abr;\n\n\t\tabr.setCallback((quality) => {\n\t\t\tthis.state.quality = quality;\n\t\t\tthis.subscription?.onqualitychange?.(quality);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t});\n\n\t\t// Try transport hierarchy\n\t\tlet connected = false;\n\n\t\t// 1. Try the canonical browser multicast gateway.\n\t\tif (caps.nativeSSM || caps.nativeIGMP) {\n\t\t\tconsole.log(\"[Transport] Attempting window.multicast...\");\n\t\t\tconnected = await this.connectWindowMulticast(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"multicast\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 2. Try AMT (configured relay, else gateway DRIAD discovery)\n\t\tif (!connected) {\n\t\t\tconsole.log(\"[Transport] Attempting AMT tunnel...\");\n\t\t\tconnected = await this.connectAmt(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"amt\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback to MOQ\n\t\tconsole.log(\"[Transport] All multicast transports failed, falling back to MOQ\");\n\t\treturn false;\n\t}\n\n\t/**\n\t * Connect via the lowercase window.multicast bytes gateway.\n\t */\n\tprivate async connectWindowMulticast(\n\t\tconfig: TransportConfig,\n\t\tgeneration: number,\n\t\tmode: \"multicast\" | \"amt\" = \"multicast\",\n\t): Promise<boolean> {\n\t\ttry {\n\t\t\tconst gateway = this.getWindowMulticastGateway();\n\t\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\n\t\t\tconst lease = await multicastSubscriptionPool.acquire(gateway, config, {\n\t\t\t\tonBytes: (data) => {\n\t\t\t\t\tif (generation === this.connectionGeneration) this.handlePacket(data);\n\t\t\t\t},\n\t\t\t\tonState: (state: WindowMulticastSubscriptionState) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tthis.state.connected = state !== \"closed\";\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t},\n\t\t\t\tonError: (error) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tconsole.error(\"[Transport] window.multicast error:\", error);\n\t\t\t\t\tthis.onError?.(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\tawait lease.release();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tthis.subscription = {\n\t\t\t\tid: lease.key,\n\t\t\t\tconfig,\n\t\t\t\tclose: () => { void lease.release(); },\n\t\t\t};\n\n\t\t\tthis.state = {\n\t\t\t\tmode,\n\t\t\t\tconnected: true,\n\t\t\t\tgroup: config.group,\n\t\t\t\tsource: config.source,\n\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\tpacketsReceived: 0,\n\t\t\t\tbytesReceived: 0,\n\t\t\t\tlastPacketTime: 0,\n\t\t\t};\n\n\t\t\tconsole.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via AMT: a configured relay goes through the AMT proxy; with no\n\t * relay, the window.multicast gateway discovers one per RFC 8777 (DRIAD).\n\t */\n\tprivate async connectAmt(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tif (config.amtRelay) return await this.connectAmtProxy(config, config.amtRelay, generation);\n\t\t\tif (config.driadEnabled === false || !config.source || config.mode === \"native\") {\n\t\t\t\tconsole.warn(\"[Transport] No AMT relay available (DRIAD requires source address for SSM)\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconsole.log(`[Transport] Asking the gateway for a DRIAD relay for source ${config.source}...`);\n\t\t\treturn await this.connectWindowMulticast({ ...config, amtDiscovery: \"driad\" }, generation, \"amt\");\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT connection failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via WebSocket-based AMT proxy (browser fallback)\n\t */\n\tprivate async connectAmtProxy(config: TransportConfig, amtRelay: string, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tconst relayHost = amtRelay.includes(\":\") && !amtRelay.startsWith(\"[\") ? `[${amtRelay}]` : amtRelay;\n\t\t\t// amtRelayPort is the relay's RFC 7450 UDP port, not the proxy's HTTPS port.\n\t\t\tconst wsUrl = new URL(`wss://${relayHost}/amt-proxy`);\n\t\t\twsUrl.searchParams.set(\"group\", config.group);\n\t\t\twsUrl.searchParams.set(\"source\", config.source || \"\");\n\t\t\twsUrl.searchParams.set(\"port\", String(config.port));\n\n\t\t\tconst ws = new WebSocket(wsUrl);\n\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tws.binaryType = \"arraybuffer\";\n\t\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\t\tws.close();\n\t\t\t\t\treject(new Error(\"AMT proxy connection timeout\"));\n\t\t\t\t}, 10000);\n\n\t\t\t\tws.onopen = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tws.close();\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);\n\n\t\t\t\t\tthis.subscription = {\n\t\t\t\t\t\tid: `amt-proxy-${Date.now()}`,\n\t\t\t\t\t\tconfig,\n\t\t\t\t\t\tclose: () => ws.close(),\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tmode: \"amt\",\n\t\t\t\t\t\tconnected: true,\n\t\t\t\t\t\tgroup: config.group,\n\t\t\t\t\t\tsource: config.source,\n\t\t\t\t\t\tamtRelay,\n\t\t\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\t\t\tpacketsReceived: 0,\n\t\t\t\t\t\tbytesReceived: 0,\n\t\t\t\t\t\tlastPacketTime: 0,\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(true);\n\t\t\t\t};\n\n\t\t\t\tws.onmessage = (event) => {\n\t\t\t\t\tif (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {\n\t\t\t\t\t\tthis.handlePacket(event.data);\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tws.onerror = (error) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.error(\"[Transport] AMT proxy error:\", error);\n\t\t\t\t\treject(error);\n\t\t\t\t};\n\n\t\t\t\tws.onclose = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.state.connected = false;\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(false);\n\t\t\t\t};\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT proxy failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Handle received packet\n\t */\n\tprivate handlePacket(data: ArrayBuffer) {\n\t\tthis.state.packetsReceived++;\n\t\tthis.state.bytesReceived += data.byteLength;\n\t\tthis.state.lastPacketTime = Date.now();\n\n\t\t// Update ABR with sample\n\t\t// Packets reach here only through a connect()ed join, which built the ABR.\n\t\tthis.abr?.recordSample(data.byteLength, 0);\n\n\t\t// Forward to callback\n\t\tthis.onPacket?.(data);\n\t}\n\n\t/**\n\t * Get current transport state\n\t */\n\tgetState(): TransportState {\n\t\treturn { ...this.state };\n\t}\n\n\t/**\n\t * The rung of the live connection's `abrLadder` ABR is on; `null` when\n\t * there is no ladder (before `connect(config)` succeeds, after\n\t * `disconnect()`, or after a reconnect that was torn down and bailed).\n\t */\n\tgetQuality(): QualityLevel | null {\n\t\treturn this.abr?.getCurrentQuality() ?? null;\n\t}\n\n\t/**\n\t * Force the rung named `name` of the live connection's `abrLadder`.\n\t * Throws `Error` when there is no ladder (see `getQuality()`), and\n\t * `RangeError` when the ladder has no rung of that name; it never falls\n\t * back to another rung.\n\t */\n\tsetQuality(name: string) {\n\t\tthis.requireAbr().setQuality(name);\n\t}\n\n\t/** The controller `connect()` built; before it there is no ladder to adapt over. */\n\tprivate requireAbr(): AdaptiveBitrateController {\n\t\tif (!this.abr) throw new Error(\"[Transport] ABR has no ladder before connect(config)\");\n\t\treturn this.abr;\n\t}\n\n\t/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */\n\tconfigureMulticastTiers(\n\t\toptions: OrchestratorOptions,\n\t\tonCommand?: (command: OrchestratorCommand) => void,\n\t): MulticastTierEndpoint | undefined {\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = new MulticastOrchestrator(options);\n\t\tthis.onOrchestratorCommand = onCommand;\n\t\treturn this.multicastOrchestrator.start();\n\t}\n\n\trecordMulticastGoodput(bitrate: number | undefined): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));\n\t}\n\n\trecordMulticastHeadroom(healthy: boolean): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));\n\t}\n\n\trecordMulticastTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));\n\t}\n\n\tpollMulticastTierSwitch(): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());\n\t}\n\n\t/** Abandon the in-flight tier join, e.g. because the host could not join the target. */\n\tcancelMulticastTierJoin(): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t}\n\n\t/**\n\t * Set packet callback\n\t */\n\tsetPacketCallback(callback: (data: ArrayBuffer) => void) {\n\t\tthis.onPacket = callback;\n\t}\n\n\t/**\n\t * Set error callback\n\t */\n\tsetErrorCallback(callback: (error: Error) => void) {\n\t\tthis.onError = callback;\n\t}\n\n\t/**\n\t * Set state change callback\n\t */\n\tsetStateCallback(callback: (state: TransportState) => void) {\n\t\tthis.onStateChange = callback;\n\t}\n\n\t/**\n\t * Disconnect\n\t */\n\tdisconnect() {\n\t\tthis.connectionGeneration++;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// No ladder after a disconnect, as before the first connect():\n\t\t// getQuality() is null and setQuality() throws until connect(config).\n\t\tthis.abr = null;\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = null;\n\t\tthis.onOrchestratorCommand = undefined;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t}\n\n\tprivate dispatchOrchestratorCommand(command: OrchestratorCommand | undefined): OrchestratorCommand | undefined {\n\t\tif (command) this.onOrchestratorCommand?.(command);\n\t\treturn command;\n\t}\n}\n\n// Export singleton instance\nexport const transportManager = new TransportManager();\n", "const MAX_MOQT_PRIORITY = 0xff;\n\ndeclare const subscriberPriorityBrand: unique symbol;\ndeclare const publisherPriorityBrand: unique symbol;\n\nexport type MoqtSubscriberPriority = number & { readonly [subscriberPriorityBrand]: true };\nexport type MoqtPublisherPriority = number & { readonly [publisherPriorityBrand]: true };\n\nfunction moqtPriority(value: number, field: string): number {\n\tif (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {\n\t\tthrow new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);\n\t}\n\treturn value;\n}\n\n/** Validate a protocol-native MOQT subscriber priority without transforming it. */\nexport function moqtSubscriberPriority(value: number): MoqtSubscriberPriority {\n\treturn moqtPriority(value, \"MOQT subscriber priority\") as MoqtSubscriberPriority;\n}\n\n/** Validate a protocol-native MOQT publisher priority without transforming it. */\nexport function moqtPublisherPriority(value: number): MoqtPublisherPriority {\n\treturn moqtPriority(value, \"MOQT publisher priority\") as MoqtPublisherPriority;\n}\n\n/** Shared media subscription policy on MOQT's native lower-number-wins scale. */\nexport const MOQT_SUBSCRIBER_PRIORITY = Object.freeze({\n\tcatalog: moqtSubscriberPriority(0),\n\t/**\n\t * @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request\n\t * from the catalog source and repair priorities (draft-ramadan-moq-fec\n\t * \u00A711.4.5), not a player constant. Use the `subscriberPriority` that\n\t * `@blockcast/mmt-container`'s `wireRepairFetch` puts on each\n\t * `RepairFetchRequest` (`repairFetchSubscriberPriority`).\n\t */\n\tdeadlineRepair: moqtSubscriberPriority(0),\n\taudio: moqtSubscriberPriority(1),\n\tvideo: moqtSubscriberPriority(2),\n\t/**\n\t * @deprecated See `deadlineRepair`: use the request's catalog-derived\n\t * `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec \u00A711.4.5).\n\t */\n\tbackgroundRepair: moqtSubscriberPriority(3),\n});\n\n/** Publisher priority bands from draft-ramadan-moq-fec section 10. */\nexport const MOQT_PUBLISHER_PRIORITY = Object.freeze({\n\tcatalog: moqtPublisherPriority(32),\n\tsource: moqtPublisherPriority(128),\n\trepair: moqtPublisherPriority(240),\n});\n", "/**\n * @blockcast/transport - AMT Gateway\n *\n * Consolidated AMT (Automatic Multicast Tunneling) implementation\n * supporting both Service Worker and Direct Sockets approaches.\n *\n * RFC 7450: Automatic Multicast Tunneling\n */\n\nimport type { GroupKey, PacketMetadata, AmtRelayInfo } from \"./types.js\";\nimport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\n/** AMT message types */\nconst AMT_MESSAGE_TYPES = {\n\tRELAY_DISCOVERY: 1,\n\tRELAY_ADVERTISEMENT: 2,\n\tREQUEST: 3,\n\tMEMBERSHIP_QUERY: 4,\n\tMEMBERSHIP_UPDATE: 5,\n\tMULTICAST_DATA: 6,\n\tTEARDOWN: 7,\n} as const;\n\n/** AMT gateway states */\nconst GATEWAY_STATES = {\n\tIDLE: \"idle\",\n\tDISCOVERING: \"discovering\",\n\tREQUESTING: \"requesting\",\n\tQUERYING: \"querying\",\n\tACTIVE: \"active\",\n\tLEAVING: \"leaving\",\n\tCLOSED: \"closed\",\n} as const;\n\ntype GatewayState = (typeof GATEWAY_STATES)[keyof typeof GATEWAY_STATES];\n\n/** AMT subscription configuration */\nexport interface AMTSubscriptionConfig {\n\tkey: GroupKey;\n\trelayAddress?: string;\n\trelayPort?: number;\n\tonPacket: (data: ArrayBuffer, metadata: PacketMetadata) => void;\n\tonError: (error: Error) => void;\n}\n\n/** AMT packet info */\nexport interface AMTPacketInfo {\n\tdata: Uint8Array;\n\tsrcAddress: string;\n\tdstAddress: string;\n\tsrcPort: number;\n\tdstPort: number;\n\tgroupKey: string;\n\ttimestamp: number;\n}\n\n/** AMT Gateway options */\nexport interface AMTGatewayOptions {\n\trelayAddress: string;\n\trelayPort?: number;\n\tonPacket?: (packet: AMTPacketInfo) => void;\n\tonStateChange?: (newState: GatewayState, oldState: GatewayState) => void;\n\tonError?: (error: Error) => void;\n\tonLog?: (message: string) => void;\n}\n\n/** Active AMT subscription */\ninterface AMTSubscription {\n\tkey: GroupKey;\n\tsubscriptionId: string;\n\tconfig: AMTSubscriptionConfig;\n\trelay?: AmtRelayInfo;\n\thandshakeComplete: boolean;\n\tkeepAliveTimer?: ReturnType<typeof setInterval>;\n\tlastKeepAlive?: number;\n}\n\n/** Check if address is IPv4 */\nfunction isIPv4(address: string): boolean {\n\treturn /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(address);\n}\n\n/**\n * AMT Gateway Manager (Service Worker based)\n *\n * Manages AMT tunnel subscriptions using Service Worker for UDP communication.\n * Handles the AMT handshake and packet decapsulation. It needs a configured\n * relay: DRIAD (RFC 8777) discovery is the window.multicast gateway's job.\n */\nexport class AMTGatewayManager {\n\tprivate subscriptions = new Map<string, AMTSubscription>();\n\tprivate messageHandler?: (event: MessageEvent) => void;\n\tprivate pendingPackets = new Map<string, Array<{ data: ArrayBuffer; remoteAddress: string; remotePort: number }>>();\n\n\tconstructor() {\n\t\t// Set up message handler for packets from service worker\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tconst data = event.data as { type: string; subscriptionId?: string; data?: unknown; error?: string; message?: string };\n\t\t\tconsole.log(`[AMT] SW message received:`, data.type);\n\t\t\tif (data.type === \"UDP_PACKET\") {\n\t\t\t\tthis.handlePacket(data);\n\t\t\t} else if (data.type === \"SOCKET_ERROR\") {\n\t\t\t\tthis.handleSocketError(data);\n\t\t\t} else if (data.type === \"DEBUG_LOG\") {\n\t\t\t\tconsole.log(`[AMT SW Debug] ${data.message}`);\n\t\t\t}\n\t\t};\n\n\t\t// Register message handler\n\t\tif (typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.addEventListener(\"message\", this.messageHandler);\n\t\t\tconsole.log(\"[AMT] Registered service worker message handler\");\n\t\t} else {\n\t\t\tconsole.warn(\"[AMT] Service worker not available\");\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe via AMT tunnel\n\t */\n\tasync subscribe(config: AMTSubscriptionConfig): Promise<string> {\n\t\tconst { key } = config;\n\t\tconst keyStr = this.getKeyString(key);\n\n\t\tif (this.subscriptions.has(keyStr)) {\n\t\t\tconsole.log(`[AMT] Reusing existing subscription to ${keyStr}`);\n\t\t\treturn keyStr;\n\t\t}\n\n\t\ttry {\n\t\t\tconst relayAddress = config.relayAddress;\n\t\t\tconst relayPort = config.relayPort || DEFAULT_AMT_RELAY_PORT;\n\t\t\tif (!relayAddress) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`AMT relay address required: this manager does no relay discovery. ` +\n\t\t\t\t\t\t`For RFC 8777 DRIAD discovery, join through window.multicast with amtDiscovery: \"driad\".`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsole.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);\n\n\t\t\tconst subscriptionId = keyStr;\n\n\t\t\t// Initialize packet queue for handshake\n\t\t\tthis.pendingPackets.set(subscriptionId, []);\n\n\t\t\t// Create subscription\n\t\t\tconst subscription: AMTSubscription = {\n\t\t\t\tkey,\n\t\t\t\tsubscriptionId,\n\t\t\t\tconfig,\n\t\t\t\thandshakeComplete: false,\n\t\t\t\trelay: {\n\t\t\t\t\taddress: relayAddress,\n\t\t\t\t\tport: relayPort,\n\t\t\t\t\tdiscoveryMethod: \"manual\",\n\t\t\t\t\tdiscoveredAt: Date.now(),\n\t\t\t\t},\n\t\t\t};\n\n\t\t\tthis.subscriptions.set(keyStr, subscription);\n\n\t\t\t// Note: Actual UDP socket creation and handshake would require\n\t\t\t// Service Worker communication - this is a simplified version\n\t\t\tconsole.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);\n\n\t\t\treturn keyStr;\n\t\t} catch (error) {\n\t\t\tconst subscription = this.subscriptions.get(keyStr);\n\t\t\tif (subscription) {\n\t\t\t\tthis.subscriptions.delete(keyStr);\n\t\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t\t}\n\n\t\t\tthrow new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Unsubscribe from AMT tunnel\n\t */\n\tasync unsubscribe(handle: string): Promise<void> {\n\t\tconst subscription = this.subscriptions.get(handle);\n\t\tif (!subscription) {\n\t\t\tthrow new Error(`Subscription ${handle} not found`);\n\t\t}\n\n\t\ttry {\n\t\t\t// Stop keep-alive timer\n\t\t\tif (subscription.keepAliveTimer) {\n\t\t\t\tclearInterval(subscription.keepAliveTimer);\n\t\t\t}\n\n\t\t\tconsole.log(`[AMT] Unsubscribed from ${handle}`);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[AMT] Error during unsubscribe:\", error);\n\t\t} finally {\n\t\t\tthis.subscriptions.delete(handle);\n\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t}\n\t}\n\n\t/**\n\t * Get list of active subscriptions\n\t */\n\tgetSubscriptions(): string[] {\n\t\treturn Array.from(this.subscriptions.keys());\n\t}\n\n\t/**\n\t * Handle packet from service worker\n\t */\n\tprivate handlePacket(message: { subscriptionId?: string; data?: unknown }): void {\n\t\tconst { subscriptionId, data } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// During handshake, queue packets\n\t\tif (!subscription.handshakeComplete) {\n\t\t\tconst queue = this.pendingPackets.get(subscriptionId);\n\t\t\tconst packetData = data as { data: ArrayBuffer; remoteAddress: string; remotePort: number };\n\t\t\tif (queue) {\n\t\t\t\tqueue.push({\n\t\t\t\t\tdata: packetData.data,\n\t\t\t\t\tremoteAddress: packetData.remoteAddress,\n\t\t\t\t\tremotePort: packetData.remotePort,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// After handshake, forward to callback\n\t\tconst packetData = data as { data: ArrayBuffer };\n\t\tconst metadata: PacketMetadata = {\n\t\t\tsourceAddress: subscription.relay?.address || \"unknown\",\n\t\t\tsourcePort: subscription.relay?.port || 0,\n\t\t\ttransport: \"tunnel\",\n\t\t\ttimestamp: performance.now(),\n\t\t};\n\n\t\tsubscription.config.onPacket(packetData.data, metadata);\n\t}\n\n\t/**\n\t * Handle socket error from service worker\n\t */\n\tprivate handleSocketError(message: { subscriptionId?: string; error?: string }): void {\n\t\tconst { subscriptionId, error } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\tsubscription.config.onError(new Error(error || \"Unknown error\"));\n\t}\n\n\t/**\n\t * Cleanup on destroy\n\t */\n\tdestroy(): void {\n\t\tif (this.messageHandler && typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.removeEventListener(\"message\", this.messageHandler);\n\t\t}\n\n\t\tconst handles = Array.from(this.subscriptions.keys());\n\t\tfor (const handle of handles) {\n\t\t\tthis.unsubscribe(handle).catch((err) => {\n\t\t\t\tconsole.error(`[AMT] Error unsubscribing ${handle}:`, err);\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Get key string for HashMap\n\t */\n\tprivate getKeyString(key: GroupKey): string {\n\t\tconst source = key.source ? `@${key.source}` : \"\";\n\t\treturn `${key.group}:${key.port}${source}`;\n\t}\n}\n\n/**\n * AMT Gateway Client (Direct Sockets based)\n *\n * Connects to an AMT relay to receive multicast data via unicast tunnel.\n * Implements the gateway side of RFC 7450.\n * Supports multiple (S,G) groups over a single AMT tunnel.\n */\nexport class AMTGateway {\n\tprivate relayAddress: string;\n\tprivate relayPort: number;\n\tprivate groups = new Map<\n\t\tstring,\n\t\t{\n\t\t\tgroupPort: number;\n\t\t\tonPacket: ((packet: AMTPacketInfo) => void) | null;\n\t\t\tjoinedAt: number;\n\t\t}\n\t>();\n\n\t// Callbacks\n\tprivate onPacket: (packet: AMTPacketInfo) => void;\n\tprivate onStateChange: (newState: GatewayState, oldState: GatewayState) => void;\n\tprivate onError: (error: Error) => void;\n\tprivate onLog: (message: string) => void;\n\n\t// State\n\tprivate state: GatewayState = GATEWAY_STATES.IDLE;\n\tprivate nonce: Uint8Array | null = null;\n\tprivate responseMAC: Uint8Array | null = null;\n\tprivate relayConfirmedAddress: string | null = null;\n\tprivate lastDataTime = 0;\n\n\t// Stats\n\tprivate stats = {\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tqueriesReceived: 0,\n\t\tupdatesSet: 0,\n\t};\n\n\tconstructor(options: AMTGatewayOptions) {\n\t\tthis.relayAddress = options.relayAddress;\n\t\tthis.relayPort = options.relayPort || DEFAULT_AMT_RELAY_PORT;\n\n\t\tthis.onPacket = options.onPacket || (() => {});\n\t\tthis.onStateChange = options.onStateChange || (() => {});\n\t\tthis.onError = options.onError || ((err) => console.error(\"AMT Error:\", err));\n\t\tthis.onLog = options.onLog || ((msg) => console.log(\"AMT:\", msg));\n\t}\n\n\t/**\n\t * Generate random nonce\n\t */\n\tprivate generateNonce(): Uint8Array {\n\t\tconst nonce = new Uint8Array(4);\n\t\tcrypto.getRandomValues(nonce);\n\t\treturn nonce;\n\t}\n\n\t/**\n\t * Set gateway state\n\t */\n\tprivate setState(newState: GatewayState): void {\n\t\tconst oldState = this.state;\n\t\tthis.state = newState;\n\t\tthis.onLog(`State: ${oldState} -> ${newState}`);\n\t\tthis.onStateChange(newState, oldState);\n\t}\n\n\t/**\n\t * Join a multicast group\n\t */\n\tasync joinGroup(\n\t\tgroupAddress: string,\n\t\tgroupPort: number,\n\t\tsourceAddress: string | null = null,\n\t\tonPacket: ((packet: AMTPacketInfo) => void) | null = null,\n\t): Promise<string> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group already joined: ${groupKey}`);\n\t\t\treturn groupKey;\n\t\t}\n\n\t\tthis.groups.set(groupKey, {\n\t\t\tgroupPort,\n\t\t\tonPacket,\n\t\t\tjoinedAt: Date.now(),\n\t\t});\n\n\t\tthis.onLog(`Joined group: ${groupKey} (port ${groupPort})`);\n\t\treturn groupKey;\n\t}\n\n\t/**\n\t * Leave a multicast group\n\t */\n\tasync leaveGroup(groupAddress: string, sourceAddress: string | null = null): Promise<boolean> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (!this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group not found: ${groupKey}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.groups.delete(groupKey);\n\t\tthis.onLog(`Left group: ${groupKey} (${this.groups.size} remaining)`);\n\n\t\tif (this.groups.size === 0) {\n\t\t\tthis.onLog(\"No groups remaining - closing AMT tunnel\");\n\t\t\tawait this.close();\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Get list of active groups\n\t */\n\tgetGroups(): Array<{\n\t\tgroupKey: string;\n\t\tgroupAddress: string;\n\t\tsourceAddress: string | null;\n\t\tgroupPort: number;\n\t\tjoinedAt: number;\n\t}> {\n\t\treturn Array.from(this.groups.entries()).map(([key, info]) => {\n\t\t\tconst [source, group] = key.split(\",\");\n\t\t\treturn {\n\t\t\t\tgroupKey: key,\n\t\t\t\tgroupAddress: group,\n\t\t\t\tsourceAddress: source !== \"*\" ? source : null,\n\t\t\t\tgroupPort: info.groupPort,\n\t\t\t\tjoinedAt: info.joinedAt,\n\t\t\t};\n\t\t});\n\t}\n\n\t/**\n\t * Open connection to AMT relay\n\t */\n\tasync open(): Promise<void> {\n\t\tif (this.state !== GATEWAY_STATES.IDLE) {\n\t\t\tthrow new Error(`Cannot open in state: ${this.state}`);\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.DISCOVERING);\n\t\tthis.onLog(`Connecting to AMT relay ${this.relayAddress}:${this.relayPort}`);\n\t\tthis.onLog(`Active groups: ${this.groups.size}`);\n\n\t\t// Generate session nonce\n\t\tthis.nonce = this.generateNonce();\n\t\tthis.onLog(`Generated nonce: ${Array.from(this.nonce).map((b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`);\n\n\t\t// Note: Actual UDPSocket connection would go here\n\t\t// This is a simplified version showing the state machine\n\t}\n\n\t/**\n\t * Close connection\n\t */\n\tasync close(): Promise<void> {\n\t\tif (this.state === GATEWAY_STATES.CLOSED) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.LEAVING);\n\n\t\t// Clear all groups\n\t\tthis.groups.clear();\n\n\t\tthis.setState(GATEWAY_STATES.CLOSED);\n\t\tthis.onLog(\"AMT Gateway closed\");\n\t}\n\n\t/**\n\t * Get current statistics\n\t */\n\tgetStats(): typeof this.stats {\n\t\treturn { ...this.stats };\n\t}\n\n\t/**\n\t * Get current state\n\t */\n\tgetState(): GatewayState {\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Check if gateway is active and receiving data\n\t */\n\tisActive(): boolean {\n\t\treturn this.state === GATEWAY_STATES.ACTIVE;\n\t}\n}\n\n// Export singleton manager instance\nexport const amtGatewayManager = new AMTGatewayManager();\n", "/**\n * Strict v2 multicast delivery catalog: the DASH/HLS/MMTP routes a receiver\n * joins, their SSM endpoints, FEC geometry and MMTP packet-ID bindings.\n *\n * Moved from moqtail-private `libs/moqtail-ts/src/model/catalog/delivery.ts`\n * (BLO-47003) so every player reads one contract. Two rules are stricter than\n * the copy it replaces, both libmmt invariants rather than local choices:\n * `fec.blockProfile` is required and must be `iso-ssbg1-v1` (BLO-35597), and a\n * DASH route's `mmtp.packetId` is refused when it is the signaling flow 0\n * (`isMulticastTrackPacketId`).\n */\nimport { parseFecBlockProfile, type FecBlockProfile } from '@blockcast/mmt-base'\nimport { isMulticastTrackPacketId } from '@blockcast/mmt-transport'\n\nexport type DeliveryPackaging = 'mmtp' | 'dash' | 'hls'\nexport type DeliveryContentType = 'video' | 'audio' | 'text'\n\nexport interface DeliveryEndpoint {\n source: string\n group: string\n port: number\n}\n\nexport interface DeliveryPolicy {\n bufferWindowSegments: number\n requestTimeoutMs: number\n missPolicy: 'fail'\n}\n\nexport interface DeliveryObjectMapping {\n namespace: string\n initObject: string\n mediaObjectTemplate: string\n}\n\nexport interface DeliveryFec {\n K: number\n /** Repair symbols per source block. Zero means source-only delivery. */\n P: number\n T: number\n /** Full FEC block span in milliseconds (draft-ramadan-moq-fec \u00A75.1). */\n interleaveDepthMs: number\n /**\n * The catalog `fec.blockProfile` (BLO-35597). Required and never\n * defaulted; `parseFecBlockProfile` refuses anything but `iso-ssbg1-v1`,\n * including the retired `k-padded-v1`.\n */\n blockProfile: FecBlockProfile\n}\n\nexport interface DeliveryRouteBase {\n id: string\n contentType: DeliveryContentType\n endpointRef: string\n initUri: string\n mediaUriTemplate: string\n objects: DeliveryObjectMapping\n fec: DeliveryFec\n}\n\nexport interface DashDeliveryRoute extends DeliveryRouteBase {\n representationId: string\n /**\n * Authoritative MMTP binding used to assemble the DASH objects carried by\n * this route. The current source accepts MFU-mode MMTP only; the init MPU is\n * the representation init segment. MMTP-backed DASH URI and object templates\n * contain exactly one $Number$ and may also contain $RepresentationID$;\n * $Time$ is rejected because the catalog has no publisher-authored first-time\n * anchor. Publishers explicitly anchor MMTP MPU sequence numbers to DASH\n * $Number$ values so receivers never infer object boundaries from cadence.\n */\n mmtp: {\n trackName: string\n packetId: number\n mode: 'mfu'\n codec: string\n /** fMP4 media timescale used by Shaka's MMTP transmuxer. */\n timescale: number\n /** Exact duration of one MFU in that timescale, from publisher timing. */\n mediaUnitDurationTicks: number\n /** Exact MFU count carried by each numbered DASH media object. */\n mediaUnitsPerObject: number\n /** First publisher MMTP MPU sequence represented by this route. */\n firstMpuSequenceNumber: number\n /** DASH $Number$ containing firstMpuSequenceNumber. */\n firstObjectNumber: number\n /** Consecutive MMTP MPU sequence numbers carried by each DASH object. */\n mpuSequencesPerObject: number\n /** Optional reliable repair-track name under objects.namespace. */\n repairTrack?: string\n }\n}\n\nexport interface HlsDeliveryRoute extends DeliveryRouteBase {\n variantId: string\n trackId: string\n}\n\nexport interface MmtpDeliveryRoute extends DeliveryRouteBase {\n trackName: string\n}\n\nexport interface DashDeliveryConfig {\n manifestUrl: string\n routes: DashDeliveryRoute[]\n}\n\nexport interface HlsDeliveryConfig {\n manifestUrl: string\n routes: HlsDeliveryRoute[]\n}\n\nexport interface MmtpDeliveryConfig {\n routes: MmtpDeliveryRoute[]\n}\n\nexport interface DeliveryMulticastBase {\n endpoints: Record<string, DeliveryEndpoint>\n deliveryPolicy: DeliveryPolicy\n}\n\nexport type DeliveryMulticastV2 = DeliveryMulticastBase &\n (\n | { packaging: 'dash'; dash: DashDeliveryConfig }\n | { packaging: 'hls'; hls: HlsDeliveryConfig }\n | { packaging: 'mmtp'; mmtp: MmtpDeliveryConfig }\n )\n\nexport interface DeliveryCatalogV2 {\n schemaVersion: 2\n channelId: string\n multicast: DeliveryMulticastV2\n}\n\nconst TEMPLATE_VARIABLE = /\\$([A-Za-z][A-Za-z0-9]*)\\$/g\nconst ALLOWED_TEMPLATE_VARIABLES = new Set(['Number', 'Time', 'RepresentationID', 'VariantID', 'TrackID'])\n\nfunction object(value: unknown, path: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`${path} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction string(value: unknown, path: string): string {\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new Error(`${path} must be a non-empty string`)\n }\n return value\n}\n\nfunction positiveInteger(value: unknown, path: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new Error(`${path} must be a positive integer`)\n }\n return value\n}\n\nfunction nonNegativeInteger(value: unknown, path: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n throw new Error(`${path} must be a non-negative integer`)\n }\n return value\n}\n\nfunction nonNegativeSafeInteger(value: unknown, path: string): number {\n const parsed = nonNegativeInteger(value, path)\n if (!Number.isSafeInteger(parsed)) throw new Error(`${path} must be a safe integer`)\n return parsed\n}\n\nfunction mpuSequenceNumber(value: unknown, path: string): number {\n const parsed = nonNegativeSafeInteger(value, path)\n if (parsed > 0xffffffff) throw new Error(`${path} must be between 0 and 4294967295`)\n return parsed\n}\n\nfunction port(value: unknown, path: string): number {\n const parsed = positiveInteger(value, path)\n if (parsed > 65535) throw new Error(`${path} must be between 1 and 65535`)\n return parsed\n}\n\nfunction httpUrl(value: unknown, path: string): string {\n const raw = string(value, path)\n let parsed: URL\n try {\n parsed = new URL(raw)\n } catch {\n throw new Error(`${path} must be an absolute HTTP(S) URL`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${path} must be an absolute HTTP(S) URL`)\n }\n return raw\n}\n\nfunction templateVariables(template: string, path: string): Set<string> {\n const variables = new Set<string>()\n for (const match of template.matchAll(TEMPLATE_VARIABLE)) {\n const variable = match[1]\n if (variable === undefined || !ALLOWED_TEMPLATE_VARIABLES.has(variable)) {\n throw new Error(`${path} contains unsupported template variable $${variable ?? ''}$`)\n }\n variables.add(variable)\n }\n if (template.includes('$') && template.replace(TEMPLATE_VARIABLE, '').includes('$')) {\n throw new Error(`${path} contains a malformed template variable`)\n }\n return variables\n}\n\nfunction validateTemplatePair(mediaUriTemplate: string, mediaObjectTemplate: string, path: string): void {\n const uriVariables = templateVariables(mediaUriTemplate, `${path}.mediaUriTemplate`)\n const objectVariables = templateVariables(mediaObjectTemplate, `${path}.objects.mediaObjectTemplate`)\n if (uriVariables.size === 0) {\n throw new Error(`${path}.mediaUriTemplate must contain a segment template variable`)\n }\n if (\n uriVariables.size !== objectVariables.size ||\n [...uriVariables].some((variable) => !objectVariables.has(variable))\n ) {\n throw new Error(`${path} URI and object templates must use the same variables`)\n }\n}\n\nfunction validateMmtpDashTemplate(template: string, path: string): void {\n const variables = [...template.matchAll(TEMPLATE_VARIABLE)].map((match) => match[1])\n const unsupported = variables.find((variable) => variable !== 'Number' && variable !== 'RepresentationID')\n if (unsupported !== undefined) {\n throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported}$`)\n }\n if (variables.filter((variable) => variable === 'Number').length !== 1) {\n throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`)\n }\n}\n\nfunction validateMmtpDashTemplates(route: DeliveryRouteBase, path: string): void {\n validateMmtpDashTemplate(route.mediaUriTemplate, `${path}.mediaUriTemplate`)\n validateMmtpDashTemplate(route.objects.mediaObjectTemplate, `${path}.objects.mediaObjectTemplate`)\n}\n\nfunction validateEndpoint(value: unknown, path: string): DeliveryEndpoint {\n const endpoint = object(value, path)\n return {\n source: string(endpoint['source'], `${path}.source`),\n group: string(endpoint['group'], `${path}.group`),\n port: port(endpoint['port'], `${path}.port`),\n }\n}\n\nfunction validateEndpoints(value: unknown): Record<string, DeliveryEndpoint> {\n const endpoints = object(value, 'multicast.endpoints')\n const entries = Object.entries(endpoints)\n if (entries.length === 0) throw new Error('multicast.endpoints must not be empty')\n return Object.fromEntries(\n entries.map(([id, endpoint]) => [\n string(id, 'multicast endpoint id'),\n validateEndpoint(endpoint, `multicast.endpoints.${id}`),\n ]),\n )\n}\n\nfunction validateDeliveryPolicy(value: unknown): DeliveryPolicy {\n const policy = object(value, 'multicast.deliveryPolicy')\n const missPolicy = policy['missPolicy']\n if (missPolicy !== 'fail') {\n throw new Error(\"multicast.deliveryPolicy.missPolicy must be 'fail'\")\n }\n return {\n bufferWindowSegments: positiveInteger(\n policy['bufferWindowSegments'],\n 'multicast.deliveryPolicy.bufferWindowSegments',\n ),\n requestTimeoutMs: positiveInteger(policy['requestTimeoutMs'], 'multicast.deliveryPolicy.requestTimeoutMs'),\n missPolicy,\n }\n}\n\nfunction validateRouteBase(\n value: unknown,\n path: string,\n endpoints: Record<string, DeliveryEndpoint>,\n): DeliveryRouteBase {\n const route = object(value, path)\n const endpointRef = string(route['endpointRef'], `${path}.endpointRef`)\n if (!(endpointRef in endpoints)) throw new Error(`${path}.endpointRef does not resolve: ${endpointRef}`)\n\n const objects = object(route['objects'], `${path}.objects`)\n const fec = object(route['fec'], `${path}.fec`)\n if ('interleaveDepth' in fec || 'interleaveMs' in fec) {\n throw new Error(`${path}.fec.interleaveDepthMs is the only supported interleave field`)\n }\n const mediaUriTemplate = httpUrl(route['mediaUriTemplate'], `${path}.mediaUriTemplate`)\n const mediaObjectTemplate = string(objects['mediaObjectTemplate'], `${path}.objects.mediaObjectTemplate`)\n validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path)\n\n return {\n id: string(route['id'], `${path}.id`),\n contentType: validateContentType(route['contentType'], `${path}.contentType`),\n endpointRef,\n initUri: httpUrl(route['initUri'], `${path}.initUri`),\n mediaUriTemplate,\n objects: {\n namespace: string(objects['namespace'], `${path}.objects.namespace`),\n initObject: string(objects['initObject'], `${path}.objects.initObject`),\n mediaObjectTemplate,\n },\n fec: {\n K: positiveInteger(fec['K'], `${path}.fec.K`),\n P: nonNegativeInteger(fec['P'], `${path}.fec.P`),\n T: positiveInteger(fec['T'], `${path}.fec.T`),\n interleaveDepthMs: positiveInteger(fec['interleaveDepthMs'], `${path}.fec.interleaveDepthMs`),\n blockProfile: parseFecBlockProfile(fec['blockProfile'], `${path}.fec.blockProfile`),\n },\n }\n}\n\nfunction validateContentType(value: unknown, path: string): DeliveryContentType {\n if (value !== 'video' && value !== 'audio' && value !== 'text') {\n throw new Error(`${path} must be 'video', 'audio', or 'text'`)\n }\n return value\n}\n\nfunction templateShape(template: string): string {\n return template.replace(TEMPLATE_VARIABLE, '$variable$')\n}\n\nfunction validateRouteUniqueness(\n routes: DeliveryRouteBase[],\n selectorKeys: string[],\n rawRoutes: Record<string, unknown>[],\n): void {\n const ids = new Set<string>()\n const selectors = new Set<string>()\n const initUris = new Set<string>()\n const mediaShapes = new Set<string>()\n routes.forEach((route, index) => {\n if (ids.has(route.id)) throw new Error(`duplicate route id: ${route.id}`)\n ids.add(route.id)\n\n const selector = selectorKeys.map((key) => String(rawRoutes[index]?.[key])).join('\\u0000')\n if (selectors.has(selector)) throw new Error(`duplicate route selector: ${selectorKeys.join(', ')}`)\n selectors.add(selector)\n\n if (initUris.has(route.initUri)) throw new Error(`ambiguous init URI mapping: ${route.initUri}`)\n initUris.add(route.initUri)\n\n const mediaShape = templateShape(route.mediaUriTemplate)\n if (mediaShapes.has(mediaShape)) {\n throw new Error(`ambiguous media URI mapping: ${route.mediaUriTemplate}`)\n }\n mediaShapes.add(mediaShape)\n })\n}\n\nfunction validateRoutes(value: unknown, kind: 'dash', endpoints: Record<string, DeliveryEndpoint>): DashDeliveryRoute[]\nfunction validateRoutes(value: unknown, kind: 'hls', endpoints: Record<string, DeliveryEndpoint>): HlsDeliveryRoute[]\nfunction validateRoutes(value: unknown, kind: 'mmtp', endpoints: Record<string, DeliveryEndpoint>): MmtpDeliveryRoute[]\nfunction validateRoutes(\n value: unknown,\n kind: DeliveryPackaging,\n endpoints: Record<string, DeliveryEndpoint>,\n): (DashDeliveryRoute | HlsDeliveryRoute | MmtpDeliveryRoute)[] {\n if (!Array.isArray(value) || value.length === 0) throw new Error(`multicast.${kind}.routes must not be empty`)\n const rawRoutes = value.map((route, index) => object(route, `multicast.${kind}.routes[${index}]`))\n const routes = rawRoutes.map((route, index) => {\n const path = `multicast.${kind}.routes[${index}]`\n const base = validateRouteBase(route, path, endpoints)\n if (kind === 'dash') {\n validateMmtpDashTemplates(base, path)\n const mmtp = object(route['mmtp'], `${path}.mmtp`)\n if (mmtp['mode'] !== 'mfu') throw new Error(`${path}.mmtp.mode must be 'mfu'`)\n const mode = 'mfu' as const\n const packetId = mmtp['packetId']\n if (!isMulticastTrackPacketId(packetId)) {\n throw new Error(`${path}.mmtp.packetId must be an integer from 1 to 65535 (0 is the MMTP signaling flow)`)\n }\n const repairTrack = mmtp['repairTrack']\n if (repairTrack !== undefined && (typeof repairTrack !== 'string' || repairTrack.trim().length === 0)) {\n throw new Error(`${path}.mmtp.repairTrack must be a non-empty string when provided`)\n }\n const mediaUnitsPerObject = positiveInteger(mmtp['mediaUnitsPerObject'], `${path}.mmtp.mediaUnitsPerObject`)\n const firstMpuSequenceNumber = mpuSequenceNumber(\n mmtp['firstMpuSequenceNumber'],\n `${path}.mmtp.firstMpuSequenceNumber`,\n )\n const firstObjectNumber = nonNegativeSafeInteger(mmtp['firstObjectNumber'], `${path}.mmtp.firstObjectNumber`)\n const mpuSequencesPerObject = positiveInteger(mmtp['mpuSequencesPerObject'], `${path}.mmtp.mpuSequencesPerObject`)\n if (mpuSequencesPerObject > mediaUnitsPerObject) {\n throw new Error(`${path}.mmtp.mpuSequencesPerObject must not exceed mediaUnitsPerObject`)\n }\n if (firstMpuSequenceNumber + mpuSequencesPerObject - 1 > 0xffffffff) {\n throw new Error(`${path}.mmtp first object exceeds the MMTP MPU sequence-number range`)\n }\n return {\n ...base,\n representationId: string(route['representationId'], `${path}.representationId`),\n mmtp: {\n trackName: string(mmtp['trackName'], `${path}.mmtp.trackName`),\n packetId,\n mode,\n codec: string(mmtp['codec'], `${path}.mmtp.codec`),\n timescale: positiveInteger(mmtp['timescale'], `${path}.mmtp.timescale`),\n mediaUnitDurationTicks: positiveInteger(\n mmtp['mediaUnitDurationTicks'],\n `${path}.mmtp.mediaUnitDurationTicks`,\n ),\n mediaUnitsPerObject,\n firstMpuSequenceNumber,\n firstObjectNumber,\n mpuSequencesPerObject,\n ...(repairTrack === undefined ? {} : { repairTrack }),\n },\n }\n }\n if (kind === 'hls') {\n return {\n ...base,\n variantId: string(route['variantId'], `${path}.variantId`),\n trackId: string(route['trackId'], `${path}.trackId`),\n }\n }\n return { ...base, trackName: string(route['trackName'], `${path}.trackName`) }\n })\n validateRouteUniqueness(\n routes,\n kind === 'dash' ? ['representationId', 'contentType'] : kind === 'hls' ? ['variantId', 'trackId'] : ['trackName'],\n rawRoutes,\n )\n return routes\n}\n\nfunction validateV2Catalog(value: unknown): DeliveryCatalogV2 {\n const catalog = object(value, 'catalog')\n if (catalog['schemaVersion'] !== 2) throw new Error('catalog.schemaVersion must be 2')\n const multicast = object(catalog['multicast'], 'multicast')\n const packaging = multicast['packaging']\n if (packaging !== 'dash' && packaging !== 'hls' && packaging !== 'mmtp') {\n throw new Error(\"multicast.packaging must be 'dash', 'hls', or 'mmtp'\")\n }\n const endpoints = validateEndpoints(multicast['endpoints'])\n const deliveryPolicy = validateDeliveryPolicy(multicast['deliveryPolicy'])\n const common = {\n schemaVersion: 2 as const,\n channelId: string(catalog['channelId'], 'catalog.channelId'),\n }\n\n if (packaging === 'dash') {\n const dash = object(multicast['dash'], 'multicast.dash')\n return {\n ...common,\n multicast: {\n packaging,\n endpoints,\n deliveryPolicy,\n dash: {\n manifestUrl: httpUrl(dash['manifestUrl'], 'multicast.dash.manifestUrl'),\n routes: validateRoutes(dash['routes'], packaging, endpoints),\n },\n },\n }\n }\n if (packaging === 'hls') {\n const hls = object(multicast['hls'], 'multicast.hls')\n return {\n ...common,\n multicast: {\n packaging,\n endpoints,\n deliveryPolicy,\n hls: {\n manifestUrl: httpUrl(hls['manifestUrl'], 'multicast.hls.manifestUrl'),\n routes: validateRoutes(hls['routes'], packaging, endpoints),\n },\n },\n }\n }\n const mmtp = object(multicast['mmtp'], 'multicast.mmtp')\n return {\n ...common,\n multicast: {\n packaging,\n endpoints,\n deliveryPolicy,\n mmtp: { routes: validateRoutes(mmtp['routes'], packaging, endpoints) },\n },\n }\n}\n\n/** Parse and normalize the strict v2 delivery-catalog contract. */\nexport function normalizeDeliveryCatalog(value: unknown): DeliveryCatalogV2 {\n const catalog = object(value, 'catalog')\n if (catalog['schemaVersion'] === 2) return validateV2Catalog(catalog)\n if (catalog['schemaVersion'] === undefined) throw new Error('catalog.schemaVersion is required')\n throw new Error(`unsupported catalog.schemaVersion: ${String(catalog['schemaVersion'])}`)\n}\n"],
5
- "mappings": ";AAsEO,IAAM,kBAAkB;AAI/B,IAAM,wBAAwB;AAC9B,IAAM,4BAA4B,kBAAkB;;;AC9D7C,IAAM,iBAAiB;AAGvB,IAAM,oBAAoB,KAAK;AAQ/B,IAAM,uBAAuB,MAAM,iBAAiB;;;ACSpD,IAAM,qBAAiD,OAAO,OAAO;EAC1E;CACD;AAGK,SAAU,kBAAkB,OAAc;AAC9C,SAAO,UAAU;AACnB;AASM,SAAU,qBAAqB,OAAgB,OAAa;AAChE,MAAI,kBAAkB,KAAK;AAAG,WAAO;AACrC,MAAI,UAAU,QAAW;AACvB,UAAM,IAAI,MAAM,GAAG,KAAK,2DAA2D;EACrF;AACA,QAAM,IAAI,MAAM,GAAG,KAAK,gCAAgC,KAAK,UAAU,KAAK,CAAC,GAAG;AAClF;;;AC7BO,IAAM,uBAA2C,OAAO,OAAO;EACpE,SAAS,OAAO,OAAO,CAAC,WAAW,cAAc,CAAC;EAClD,eAAe,OAAO,OAAO,CAAC,cAAc,CAAC;EAC7C,UAAU;CACX;;;ACZK,SAAU,YAAY,UAA+B;AAC1D,SAAO,GAAG,SAAS,iBAAiB,EAAE,IAAI,SAAS,YAAY,IAAI,SAAS,IAAI;AACjF;AAWM,SAAU,aAAa,UAA+B;AAC3D,SAAO,SAAS,iBAAiB;AAClC;AAEM,SAAU,oBACf,WACA,UAA4B;AAM5B,QAAM,QAAQ,UAAU,OAAO,YAAY;AAC3C,MAAI,MAAM,UAAU;AAAG,WAAO,MAAM,CAAC;AAErC,QAAM,SAAS,MACb,OAAO,CAAC,aAAa,OAAO,SAAS,cAAc,YAAY,SAAS,YAAY,CAAC,EACrF,KAAK,CAAC,GAAG,MAAM,EAAE,YAAa,EAAE,SAAU;AAC5C,MAAI,OAAO,SAAS;AAAG,WAAO,MAAM,CAAC;AACrC,MAAI,aAAa;AAAW,WAAO,OAAO,CAAC;AAE3C,QAAM,SAAS,WAAW;AAC1B,MAAI;AACJ,aAAW,YAAY,QAAQ;AAC9B,QAAI,SAAS,aAAc;AAAQ,iBAAW;EAC/C;AACA,SAAO,YAAY,OAAO,CAAC;AAC5B;;;ACpBO,IAAM,iBAA0C,OAAO,OAAO;EACpE,EAAE,MAAM,QAAQ,SAAS,KAAS,OAAO,KAAK,QAAQ,KAAK,KAAK,GAAE;EAClE,EAAE,MAAM,QAAQ,SAAS,KAAS,OAAO,KAAK,QAAQ,KAAK,KAAK,GAAE;EAClE,EAAE,MAAM,QAAQ,SAAS,KAAW,OAAO,KAAK,QAAQ,KAAK,KAAK,GAAE;EACpE,EAAE,MAAM,QAAQ,SAAS,MAAW,OAAO,MAAM,QAAQ,KAAK,KAAK,GAAE;EACrE,EAAE,MAAM,SAAS,SAAS,KAAW,OAAO,MAAM,QAAQ,MAAM,KAAK,GAAE;EACvE,EAAE,MAAM,SAAS,SAAS,KAAW,OAAO,MAAM,QAAQ,MAAM,KAAK,GAAE;EACvE,EAAE,MAAM,MAAM,SAAS,MAAY,OAAO,MAAM,QAAQ,MAAM,KAAK,GAAE;EACpE,IAAI,CAAC,UAAU,OAAO,OAAO,KAAK,CAAC,CAAC;AAetC,SAAS,eAAe,OAAc;AACrC,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ;AACvE;AAUM,SAAU,sBAAsB,QAA+B;AACpE,MAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,GAAG;AAClD,UAAM,IAAI,UAAU,iFAAiF;EACtG;AACA,QAAM,QAAQ,oBAAI,IAAG;AACrB,aAAW,SAAS,QAAQ;AAC3B,QAAI,OAAO,OAAO,SAAS,YAAY,MAAM,KAAK,WAAW,GAAG;AAC/D,YAAM,IAAI,UAAU,2CAA2C;IAChE;AACA,QAAI,CAAC,eAAe,MAAM,OAAO,GAAG;AACnC,YAAM,IAAI,WAAW,mBAAmB,MAAM,IAAI,mDAAmD,OAAO,MAAM,OAAO,CAAC,EAAE;IAC7H;AACA,eAAW,SAAS,CAAC,SAAS,UAAU,KAAK,GAAY;AACxD,UAAI,MAAM,KAAK,MAAM,UAAa,CAAC,eAAe,MAAM,KAAK,CAAC,GAAG;AAChE,cAAM,IAAI,WAAW,mBAAmB,MAAM,IAAI,KAAK,KAAK,sDAAsD,OAAO,MAAM,KAAK,CAAC,CAAC,EAAE;MACzI;IACD;AACA,QAAI,MAAM,IAAI,MAAM,IAAI;AAAG,YAAM,IAAI,WAAW,mBAAmB,MAAM,IAAI,8BAA8B;AAC3G,UAAM,IAAI,MAAM,IAAI;EACrB;AACA,SAAO,OAAO,OACb,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,UAAU,OAAO,OAAO,EAAE,GAAG,MAAK,CAAE,CAAC,CAAC;AAE/F;AAYM,IAAO,4BAAP,MAAgC;;EAE5B;EACD;EACA,UAA+E,CAAA;EACtE,gBAAgB;;EAChB,mBAAmB;;EACnB,qBAAqB;;EACrB,kBAAkB;;EAE3B,oBAAoB;EACpB;;EAGA,WAA2B;IAClC,aAAa;IACb,mBAAmB;IACnB,cAAc;IACd,YAAY;;EAGb,YAAY,SAAyC;AACpD,QAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC5C,YAAM,IAAI,UAAU,kEAAkE;IACvF;AACA,SAAK,SAAS,sBAAsB,QAAQ,MAAM;AAClD,SAAK,sBACJ,QAAQ,mBAAmB,SAAY,IAAI,KAAK,OAAO,UAAU,CAAC,MAAM,EAAE,SAAS,QAAQ,cAAc;AAC1G,QAAI,KAAK,sBAAsB,GAAG;AACjC,YAAM,IAAI,WAAW,2BAA2B,OAAO,QAAQ,cAAc,CAAC,uBAAuB;IACtG;EACD;EAEA,YAAY,UAAyC;AACpD,SAAK,kBAAkB;EACxB;EAEA,oBAAiB;AAChB,WAAO,KAAK,OAAO,KAAK,mBAAmB;EAC5C;;EAGA,oBACC,WACA,WAA+B,KAAK,WAAU,EAAG,mBAAmB,QAAS;AAE7E,WAAO,oBAAoB,WAAW,QAAQ;EAC/C;;;;;;;EAQA,aAAa,eAAuB,aAAmB;AACtD,UAAM,MAAM,KAAK,IAAG;AAEpB,SAAK,QAAQ,KAAK,EAAE,WAAW,KAAK,eAAe,YAAW,CAAE;AAGhE,SAAK,UAAU,KAAK,QAAQ,OAAO,CAAC,MAAM,MAAM,EAAE,YAAY,KAAK,aAAa;AAGhF,UAAM,EAAE,iBAAiB,UAAU,YAAW,IAAK,KAAK,iBAAgB;AACxE,UAAM,iBAAiB,KAAK,kBAAiB;AAI7C,UAAM,oBAAoB,KAAK,SAAS,aACrC,KAAK,SAAS,oBACd;AAGH,QAAI,MAAM,KAAK,oBAAoB,KAAK,iBAAiB;AACxD;IACD;AAIA,QAAI,oBAAoB,QAAQ,kBAAkB,eAAe,UAAU,KAAK,oBAAoB;AACnG,WAAK,UAAS;AACd;IACD;AAGA,QACC,kBAAkB,eAAe,UAAU,KAAK,oBAChD,oBAAoB,QACpB,KAAK,sBAAsB,KAAK,OAAO,SAAS,GAC/C;AACD,WAAK,QAAO;IACb;EACD;;;;;;;;;EAUA,YAAY,OAAqB;AAChC,SAAK,WAAW;EACjB;;;;EAKA,cAAW;AACV,WAAO,EAAE,GAAG,KAAK,SAAQ;EAC1B;EAEQ,mBAAgB;AACvB,QAAI,KAAK,QAAQ,SAAS,GAAG;AAC5B,aAAO,EAAE,iBAAiB,GAAG,UAAU,EAAC;IACzC;AAEA,UAAM,aAAa,KAAK,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,eAAe,CAAC;AAC3E,UAAM,YAAY,KAAK,QAAQ,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,aAAa,CAAC;AACxE,UAAM,YAAY,KAAK,QAAQ,KAAK,QAAQ,SAAS,CAAC,EAAE,YAAY,KAAK,QAAQ,CAAC,EAAE,aAAa;AAEjG,UAAM,kBAAkB,WAAW,IAAK,aAAa,IAAK,WAAW;AACrE,UAAM,WAAW,aAAa,KAAK,QAAQ,SAAS;AAEpD,WAAO,EAAE,iBAAiB,SAAQ;EACnC;EAEQ,UAAO;AACd,QAAI,KAAK,uBAAuB,KAAK,OAAO,SAAS;AAAG;AAExD,SAAK;AACL,SAAK,oBAAoB,KAAK,IAAG;AAEjC,UAAM,aAAa,KAAK,kBAAiB;AACzC,YAAQ,IAAI,sBAAsB,WAAW,IAAI,KAAK,WAAW,UAAU,GAAS,OAAO;AAE3F,SAAK,kBAAkB,UAAU;EAClC;EAEQ,YAAS;AAChB,QAAI,KAAK,uBAAuB;AAAG;AAEnC,SAAK;AACL,SAAK,oBAAoB,KAAK,IAAG;AAEjC,UAAM,aAAa,KAAK,kBAAiB;AACzC,YAAQ,IAAI,wBAAwB,WAAW,IAAI,KAAK,WAAW,UAAU,GAAS,OAAO;AAE7F,SAAK,kBAAkB,UAAU;EAClC;;;;;EAMA,WAAW,MAAY;AACtB,UAAM,QAAQ,KAAK,OAAO,UAAU,CAAC,MAAM,EAAE,SAAS,IAAI;AAC1D,QAAI,QAAQ;AAAG,YAAM,IAAI,WAAW,mBAAmB,IAAI,uBAAuB;AAClF,SAAK,sBAAsB;AAC3B,SAAK,oBAAoB,KAAK,IAAG;AACjC,SAAK,kBAAkB,KAAK,kBAAiB,CAAE;EAChD;;;;EAKA,aAAU;AAOT,UAAM,EAAE,iBAAiB,UAAU,YAAW,IAAK,KAAK,iBAAgB;AACxE,WAAO;MACN;MACA;MACA,mBAAmB,KAAK,SAAS,aAAa,KAAK,SAAS,oBAAoB;MAChF,YAAY,KAAK,SAAS;MAC1B,aAAa,KAAK,QAAQ;;EAE5B;;;;EAKA,QAAK;AACJ,SAAK,UAAU,CAAA;AACf,SAAK,oBAAoB;EAC1B;;;;ACrOM,IAAMA,mBAAkB;AAI/B,IAAMC,yBAAwB;AAC9B,IAAMC,6BAA4BC,mBAAkBF;AAkB7C,IAAMG,4BAA2B;AAGjC,IAAM,qBAAqB;;;AChF3B,IAAM,yBAAyB;;;ACH/B,IAAMC,kBAAiB;AAGvB,IAAMC,qBAAoB,KAAKD;AAQ/B,IAAME,wBAAuB,MAAMF,kBAAiB;AAarD,SAAU,WAAW,OAAa;AACtC,SAAO,OAAO,UAAU,KAAK,KAAK,SAAS,KAAK,QAAQC;AAC1D;AAcM,SAAU,gBAAgB,GAAW,GAAS;AAClD,MAAI,CAAC,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG;AACpC,UAAM,IAAI,WACR,8CAA8CD,eAAc,UAAU,OAAO,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,EAAE;EAEtG;AACA,MAAI,MAAM;AAAG,WAAO;AAGpB,QAAM,WAAW,IAAI,IAAIC,sBAAqBA;AAC9C,MAAI,UAAUC;AAAsB,WAAO;AAC3C,MAAI,UAAUA;AAAsB,WAAO;AAC3C,SAAO;AACT;;;ACjCO,IAAMC,sBAAiD,OAAO,OAAO;EAC1E;CACD;;;ACTM,IAAMC,wBAA2C,OAAO,OAAO;EACpE,SAAS,OAAO,OAAO,CAAC,WAAW,cAAc,CAAC;EAClD,eAAe,OAAO,OAAO,CAAC,cAAc,CAAC;EAC7C,UAAU;CACX;;;ACQM,IAAM,mBAAmB;AACzB,IAAM,2BAA2B;AAclC,IAAO,wBAAP,MAA4B;EAgBJ;EAf7B,SAA4B;EAC5B;EACA;EACA;EACA;EACA,iBAAiB;;;;;;EAMjB;EACS;EACA;EAET,YAA6B,SAA4B;AAA5B,SAAA,UAAA;AAC5B,SAAK,WAAW,QAAQ,WAAW;AACnC,SAAK,kBAAkB,QAAQ,kBAAkB;AACjD,QAAI,CAAC,OAAO,SAAS,KAAK,QAAQ,KAAK,CAAC,OAAO,SAAS,KAAK,eAAe,KAC3E,KAAK,YAAY,KAAK,KAAK,mBAAmB,GAAG;AACjD,YAAM,IAAI,MAAM,oDAAoD;IACrE;EACD;EAEA,IAAI,QAAK;AAAwB,WAAO,KAAK;EAAQ;EACrD,IAAI,UAAO;AAAwC,WAAO,KAAK;EAAU;EACzE,IAAI,SAAM;AAAwC,WAAO,KAAK;EAAS;EACvE,IAAI,WAAQ;AAAyB,WAAO,KAAK;EAAW;EAE5D,QAAK;AACJ,SAAK,aAAa,KAAK,QAAQ,mBAAmB,oBAAoB,KAAK,QAAQ,WAAW,MAAS;AACvG,WAAO,KAAK;EACb;EAEA,UAAU,QAAqB;AAC9B,UAAM,UAAU,OAAO;AACvB,QAAI,CAAC,KAAK;AAAU,aAAO;AAG3B,QAAI,KAAK,WAAW;AAAW,aAAO,KAAK,KAAI;AAC/C,QAAI,KAAK,QAAQ,aAAa;AAAO,aAAO;AAC5C,QAAI,YAAY,UAAa,CAAC,OAAO,SAAS,OAAO;AAAG,aAAO;AAC/D,SAAK,YAAY;AACjB,UAAM,UAAU,oBAAoB,KAAK,QAAQ,WAAW,OAAO;AACnE,QAAI,CAAC;AAAS,aAAO;AACrB,UAAM,aAAa,YAAY,OAAO;AACtC,QAAI,eAAe,YAAY,KAAK,QAAQ,GAAG;AAC9C,UAAI,KAAK,UAAU,WAAW;AAAW,aAAK,WAAW;AACzD,aAAO;IACR;AACA,QAAI,OAAO,QAAQ,cAAc,YAAY,OAAO,KAAK,SAAS,cAAc,YAC/E,QAAQ,aAAa,KAAK,SAAS,WAAW;AAC9C,UAAI,KAAK,UAAU,WAAW;AAAW,aAAK,WAAW;AACzD,aAAO;IACR;AAEA,UAAM,MAAM,KAAK,KAAI;AACrB,QAAI,CAAC,KAAK,YAAY,KAAK,SAAS,QAAQ,cAAc,KAAK,SAAS,WAAW,WAAW;AAC7F,WAAK,WAAW,EAAE,KAAK,YAAY,OAAO,KAAK,QAAQ,UAAS;AAChE,aAAO;IACR;AACA,QAAI,MAAM,KAAK,SAAS,QAAQ,KAAK;AAAU,aAAO;AAEtD,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,iBAAiB;AACtB,SAAK,WAAW;AAChB,WAAO,EAAE,MAAM,eAAe,SAAS,KAAK,UAAU,QAAQ,QAAO;EACtE;EAEA,WAAW,QAAsB;AAChC,QAAI,CAAC,KAAK,YAAY,KAAK,QAAQ,aAAa,SAAS,KAAK,WAAW;AAAW,aAAO;AAC3F,QAAI,CAAC,OAAO,SAAS;AACpB,UAAI,KAAK,UAAU,WAAW;AAAY,aAAK,WAAW;AAC1D,aAAO;IACR;AACA,QAAI,KAAK,UAAU,WAAW;AAAW,aAAO;AAEhD,UAAM,mBAAmB,KAAK,SAAS;AACvC,QAAI,OAAO,qBAAqB;AAAU,aAAO;AAMjD,UAAM,UAAU,KAAK,QAAQ,UAC3B,OAAO,CAAC,aACR,aAAa,QAAQ,KAAK,OAAO,SAAS,cAAc,YAAY,SAAS,YAAY,gBAAgB,EAEzG,KAAK,CAAC,GAAG,MAAM,EAAE,YAAa,EAAE,SAAU,EAAE,CAAC;AAC/C,QAAI,CAAC,SAAS;AACb,WAAK,WAAW;AAChB,aAAO;IACR;AAEA,UAAM,aAAa,YAAY,OAAO;AACtC,QAAI,eAAe,YAAY,KAAK,QAAQ,GAAG;AAC9C,WAAK,WAAW;AAChB,aAAO;IACR;AACA,UAAM,MAAM,KAAK,KAAI;AACrB,QAAI,CAAC,KAAK,YAAY,KAAK,SAAS,QAAQ,YAAY;AACvD,WAAK,WAAW,EAAE,KAAK,YAAY,OAAO,KAAK,QAAQ,WAAU;AACjE,aAAO;IACR;AACA,QAAI,MAAM,KAAK,SAAS,QAAQ,KAAK;AAAU,aAAO;AAEtD,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,iBAAiB;AACtB,SAAK,WAAW;AAChB,WAAO,EAAE,MAAM,eAAe,SAAS,KAAK,UAAU,QAAQ,QAAO;EACtE;;;;;;;;;;;EAYA,OAAO,QAA6B;AACnC,QAAI,CAAC,KAAK,YAAY,KAAK,WAAW,aAAa,CAAC,aAAa,MAAM;AAAG,aAAO;AACjF,QAAI,YAAY,MAAM,MAAM,YAAY,KAAK,QAAQ;AAAG,aAAO;AAC/D,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,iBAAiB,KAAK,KAAI;AAC/B,SAAK,WAAW;AAChB,WAAO,EAAE,MAAM,eAAe,SAAS,KAAK,UAAU,OAAM;EAC7D;;;;;;;;;;;EAYA,aAAa,QAA2B;AACvC,QAAI,CAAC,KAAK;AAAU,aAAO;AAC3B,UAAM,SAAS,mBAAmB,MAAM;AAIxC,QAAI,OAAO,gBAAgB,YAAY,KAAK,QAAQ,KAAK,WAAW,WAClE,KAAK,eAAe,UAAa,gBAAgB,QAAQ,KAAK,UAAU,MAAM,UAAU;AACzF,WAAK,aAAa;IACnB;AACA,QAAI,KAAK,WAAW,aAAa,CAAC,KAAK;AAAS,aAAO;AACvD,UAAM,UAAU,KAAK,KAAI;AACzB,QAAI;AAAS,aAAO;AACpB,QACC,OAAO,gBAAgB,YAAY,KAAK,OAAO,KAC/C,CAAC,OAAO,YACR,CAAC,OAAO,uBACR,WAAW,UACX,CAAC,KAAK,kBAAkB,MAAM;AAC7B,aAAO;AAET,UAAM,UAA+B;MACpC,MAAM;MACN,QAAQ,KAAK;MACb,KAAK,KAAK;;AAEX,SAAK,aAAa;AAClB,SAAK,WAAW,KAAK;AACrB,SAAK,UAAU;AACf,SAAK,SAAS;AACd,WAAO;EACR;EAEA,OAAI;AACH,QAAI,KAAK,WAAW,aAAa,CAAC,KAAK,WAAW,CAAC,KAAK;AAAU,aAAO;AACzE,QAAI,KAAK,KAAI,IAAK,KAAK,iBAAiB,KAAK;AAAiB,aAAO;AACrE,WAAO,KAAK,OAAM;EACnB;EAEA,SAAM;AACL,QAAI,KAAK,WAAW,aAAa,CAAC,KAAK,WAAW,CAAC,KAAK;AAAU,aAAO;AACzE,UAAM,UAA+B;MACpC,MAAM;MACN,SAAS,KAAK;MACd,QAAQ,KAAK;;AAEd,SAAK,UAAU;AACf,SAAK,SAAS;AACd,WAAO;EACR;;;;;;EAOA,kBAAkB,QAAc;AAC/B,QAAI,KAAK,eAAe;AAAW,aAAO;AAC1C,UAAM,QAAQ,gBAAgB,QAAQ,KAAK,UAAU;AACrD,WAAO,UAAU,WAAW,UAAU;EACvC;EAEA,OAAI;AACH,UAAM,MAAM,KAAK,QAAQ,IAAG;AAC5B,QAAI,CAAC,OAAO,SAAS,GAAG;AAAG,YAAM,IAAI,MAAM,6CAA6C;AACxF,WAAO;EACR;;AAID,SAAS,mBAAmB,QAA2B;AACtD,MAAI,CAAC,OAAO,SAAS,OAAO,SAAS;AAAG,WAAO;AAC/C,MAAI,CAAC,WAAW,OAAO,SAAS,GAAG;AAClC,UAAM,IAAI,WACT,4EAA4E,OAAO,SAAS,EAAE;EAEhG;AACA,SAAO,OAAO;AACf;;;AClNM,SAAU,iCACf,OAAmC;AAEnC,MAAI,MAAM,SAAS,UAAa,CAAC,CAAC,QAAQ,UAAU,QAAQ,EAAE,SAAS,MAAM,IAAI,GAAG;AACnF,UAAM,IAAI,UAAU,+BAA+B,OAAO,MAAM,IAAI,CAAC,EAAE;EACxE;AAGA,MAAI,MAAM,cAAc,UAAa,CAAC,MAAM,QAAQ,MAAM,SAAS,GAAG;AACrE,UAAM,IAAI,UACT,6CAA6C,MAAM,cAAc,OAAO,SAAS,OAAO,MAAM,SAAS,EAAE;EAE3G;AAGA,MAAI,MAAM,WAAW,KAAK,QAAM,CAAC,yBAAyB,EAAE,CAAC,KAC5D,MAAM,QAAQ,KAAK,WAAS,CAAC,yBAAyB,MAAM,QAAQ,CAAC,GAAG;AACxE,UAAM,IAAI,WACT,mDAAmD,kBAAkB,gEACP;EAEhE;AAEA,QAAM,SAAS,MAAM,QAAQ,KAAI,KAAM;AACvC,QAAM,gBAAgB,MAAM,eAAe,KAAI,KAAM;AAErD,MAAI,UAAU,eAAe;AAC5B,UAAM,IAAI,UAAU,0EAA0E;EAC/F;AACA,QAAM,OAAO;IACZ,OAAO,MAAM;IACb,MAAM,MAAM;IACZ,GAAI,SAAS,EAAE,OAAM,IAAK,CAAA;IAC1B,SAAS;IACT,GAAI,MAAM,WAAW,SAAS,EAAE,WAAW,CAAC,GAAG,MAAM,SAAS,EAAC,IAAK,CAAA;;AAErE,QAAM,SAAS,gBAAgB,EAAE,cAAa,IAAK,CAAA;AAEnD,MAAI,MAAM,OAAO;AAEhB,QAAI;AAAe,YAAM,IAAI,UAAU,iDAAiD;AACxF,UAAM,EAAE,WAAW,OAAO,UAAS,IAAK,MAAM;AAC9C,QAAI,CAAC,OAAO,UAAU,SAAS,KAAK,YAAY,GAAG;AAClD,YAAM,IAAI,UAAU,kEAAkE,OAAO,SAAS,CAAC,EAAE;IAC1G;AACA,WAAO;MACN,GAAG;MACH,QAAQ;MACR,UAAU;MACV;MACA,GAAI,UAAU,SAAY,EAAE,MAAK,IAAK,CAAA;MACtC,GAAI,cAAc,SAAY,EAAE,UAAS,IAAK,CAAA;;EAEhD;AAEA,QAAM,QAAQ,MAAM,UAAU,KAAI;AAElC,QAAM,QAAQ,CAAC,SAAS,MAAM,iBAAiB;AAC/C,QAAM,WAAW,MAAM,SAAS,YAC9B,MAAM,SAAS,aAAc,UAAU,UAAa,UAAU,MAAO;AACvE,MAAI,CAAC,UAAU;AACd,UAAM,SAA8C;MACnD,GAAG;MACH,GAAG;MACH,QAAQ;MACR,GAAI,MAAM,QAAQ,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC,EAAE,MAAM,SAAQ,OAAQ,EAAE,MAAM,SAAQ,EAAG,EAAC,IAAK,CAAA;;AAEzG,WAAO;EACR;AAEA,QAAM,YAAY,MAAM;AACxB,MAAI,OAAO;AACV,QAAI,CAAC,KAAK;AAAQ,YAAM,IAAI,UAAU,+CAA+C;AACrF,QAAI,cAAc,UAAa,CAAC,eAAe,SAAS,GAAG;AAC1D,YAAM,IAAI,UAAU,mEAAmE;IACxF;AACA,WAAO;MACN,GAAG;MACH,QAAQ,KAAK;MACb,QAAQ;MACR,WAAW;MACX,GAAI,cAAc,SAAY,EAAE,UAAS,IAAK,CAAA;;EAEhD;AACA,MAAI,CAAC,SAAS,cAAc,UAAa,CAAC,eAAe,SAAS,GAAG;AACpE,UAAM,IAAI,UAAU,sDAAsD;EAC3E;AACA,SAAO;IACN,GAAG;IACH,GAAG;IACH,QAAQ;IACR;IACA;;AAEF;AAEM,SAAU,6BAA6B,SAA2B;AACvE,QAAM,QAAQ,SAAS,KAAI,EAAG,YAAW;AACzC,MAAI,CAAC;AAAO,WAAO;AACnB,MAAI,MAAM,WAAW,IAAI;AAAG,WAAO,4BAA4B,KAAK;AAEpE,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW;AAAG,WAAO;AAC/B,QAAM,SAAS,MAAM,IAAI,CAAC,SAAQ;AACjC,QAAI,CAAC,QAAQ,KAAK,IAAI;AAAG,aAAO,OAAO;AACvC,UAAM,QAAQ,OAAO,IAAI;AACzB,WAAO,OAAO,UAAU,KAAK,KAAK,SAAS,KAAK,SAAS,MAAM,QAAQ,OAAO;EAC/E,CAAC;AACD,SAAO,OAAO,MAAM,OAAO,SAAS,KAAK,OAAO,CAAC,KAAK,OAAO,OAAO,CAAC,KAAK;AAC3E;AAEM,SAAU,2BACf,UAAiC;AAEjC,QAAM,eAAe,SAAS,aAAa,KAAI,EAAG,YAAW;AAC7D,QAAM,gBAAgB,SAAS,eAAe,KAAI,EAAG,YAAW,KAAM;AACtE,QAAM,gBAAgB,SAAS,eAAe,KAAI,EAAG,YAAW,KAAM;AACtE,MAAI,iBAAiB;AAAe,WAAO;AAC3C,QAAM,WAAW,SAAS,UAAU,KAAI,EAAG,YAAW,KAAM;AAC5D,MAAI,CAAC,6BAA6B,YAAY,KAAK,CAAC,eAAe,SAAS,IAAI;AAAG,WAAO;AAC1F,MAAI,SAAS,QAAQ,CAAC,CAAC,QAAQ,UAAU,QAAQ,EAAE,SAAS,SAAS,IAAI;AAAG,WAAO;AACnF,MAAI,SAAS,iBAAiB,UAAa,CAAC,eAAe,SAAS,YAAY;AAAG,WAAO;AAE1F,SAAO;IACN;IACA,GAAI,gBAAgB,EAAE,cAAa,IAAK,CAAA;IACxC,GAAI,gBAAgB,EAAE,cAAa,IAAK,CAAA;IACxC,MAAM,SAAS;IACf,GAAI,SAAS,OAAO,EAAE,MAAM,SAAS,KAAI,IAAK,CAAA;IAC9C,GAAI,WAAW,EAAE,SAAQ,IAAK,CAAA;IAC9B,GAAI,SAAS,eAAe,EAAE,cAAc,SAAS,aAAY,IAAK,CAAA;IACtE,GAAI,SAAS,iBAAiB,UAAU,EAAE,cAAc,QAAgB,IAAK,CAAA;;AAE/E;AAGM,SAAU,qBAAqB,UAAiC;AACrE,QAAM,aAAa,2BAA2B,QAAQ;AACtD,MAAI,CAAC;AAAY,UAAM,IAAI,MAAM,4BAA4B;AAC7D,SAAO,KAAK,UAAU;IACrB,WAAW,iBAAiB;IAC5B,WAAW;IACX,WAAW;IACX,WAAW,QAAQ;IACnB,WAAW,YAAY;IACvB,WAAW,gBAAgB;;IAE3B,GAAI,WAAW,eAAe,CAAC,WAAW,YAAY,IAAI,CAAA;;IAE1D,GAAI,WAAW,gBAAgB,CAAC,EAAE,eAAe,WAAW,cAAa,CAAE,IAAI,CAAA;GAC/E;AACF;AASM,SAAU,yBAAyB,OAAc;AACtD,SAAO,OAAO,UAAU,KAAK,KAC3B,QAAmBC,6BACnB,SAAoB;AACvB;AAuHA,SAAS,4BAA4B,OAAa;AACjD,MAAI,CAAC,eAAe,KAAK,KAAK;AAAG,WAAO;AACxC,QAAM,aAAa,MAAM,SAAS,IAAI;AACtC,MAAI,MAAM,QAAQ,IAAI,MAAM,MAAM,YAAY,IAAI;AAAG,WAAO;AAC5D,QAAM,SAAS,MAAM,MAAM,GAAG,EAAE,OAAO,OAAO;AAC9C,MAAI,OAAO,WAAW,KAAK,OAAO,SAAS;AAAG,WAAO;AACrD,MAAI,CAAC,cAAc,OAAO,WAAW;AAAG,WAAO;AAC/C,SAAO,OAAO,MAAM,CAAC,UAAU,kBAAkB,KAAK,KAAK,CAAC;AAC7D;AAWA,SAAS,eAAeC,OAAY;AACnC,SAAO,OAAO,UAAUA,KAAI,KAAKA,QAAO,KAAKA,SAAQ;AACtD;;;ACtWM,SAAU,kCACf,cAA6D;AAE7D,SAAO,aAAa,YAAY,SAAS,SAAS,MAAM;AACzD;;;AC0BM,IAAO,4BAAP,MAAgC;EAC5B,WAAW,oBAAI,IAAG;EAClB,cAAc,oBAAI,QAAO;EAClC,iBAAiB;EAEjB,MAAM,QACL,SACA,QACA,UAAuC;AAEvC,UAAM,WAAW,2BAA2B;MAC3C,cAAc,OAAO;MACrB,eAAe,OAAO;MACtB,MAAM,OAAO;MACb,MAAM,OAAO;MACb,UAAU,OAAO;MACjB,cAAc,OAAO;MACrB,cAAc,OAAO;KACrB;AACD,QAAI,CAAC;AAAU,YAAM,IAAI,MAAM,4BAA4B;AAC3D,QAAI,OAAO,QAAQ,cAAc;AAAY,YAAM,IAAI,MAAM,wCAAwC;AAErG,UAAM,MAAM,qBAAqB,QAAQ;AACzC,UAAM,cAAc,GAAG,KAAK,WAAW,OAAO,CAAC,IAAI,GAAG;AACtD,QAAI,SAAS,KAAK,SAAS,IAAI,WAAW;AAC1C,QAAI,CAAC,QAAQ;AACZ,eAAS,EAAE,QAAQ,GAAG,OAAO,QAAQ,QAAQ,IAA4B,EAAC;AAC1E,YAAM,gBAAgB;AACtB,aAAO,QAAQ,KAAK,aAAa,SAAS,UAAU,CAACC,WAAS;AAC7D,YAAI,KAAK,SAAS,IAAI,WAAW,MAAM;AAAe,eAAK,SAAS,OAAO,WAAW;AACtF,aAAK,QAAQA,MAAK;MACnB,CAAC,EAAE,MAAM,CAAC,UAAS;AAClB,YAAI,KAAK,SAAS,IAAI,WAAW,MAAM;AAAe,eAAK,SAAS,OAAO,WAAW;AACtF,cAAM;MACP,CAAC;AACD,WAAK,SAAS,IAAI,aAAa,MAAM;IACtC;AAEA,WAAO;AACP,QAAI;AACJ,QAAI;AACH,cAAQ,MAAM,OAAO;IACtB,SAAS,OAAO;AACf,aAAO;AACP,YAAM;IACP;AACA,QAAI,MAAM,QAAQ;AACjB,aAAO;AACP,aAAO,KAAK,QAAQ,SAAS,QAAQ,QAAQ;IAC9C;AACA,UAAM,QAAQ,OAAO,GAAG;AACxB,UAAM,UAAU,IAAI,OAAO,QAAQ;AACnC,QAAI,WAAW;AACf,WAAO;MACN;MACA,SAAS,YAAW;AACnB,YAAI;AAAU;AACd,mBAAW;AACX,cAAM,UAAU,OAAO,KAAK;AAC5B,eAAO;AACP,YAAI,OAAO,SAAS;AAAG;AACvB,YAAI,KAAK,SAAS,IAAI,WAAW,MAAM;AAAQ,eAAK,SAAS,OAAO,WAAW;AAC/E,aAAK,QAAQ,KAAK;AAClB,YAAI,CAAC,MAAM;AAAQ,gBAAM,MAAM,aAAa,YAAW;MACxD;;EAEF;EAEA,IAAI,OAAI;AAAa,WAAO,KAAK,SAAS;EAAM;EAEhD,MAAM,aACL,SACA,UACA,UAAoC;AAEpC,UAAM,eAAe,MAAM,QAAQ,UAAW,iCAAiC;MAC9E,OAAO,SAAS;MAChB,GAAI,SAAS,gBAAgB,EAAE,QAAQ,SAAS,cAAa,IAAK,CAAA;MAClE,MAAM,SAAS;MACf,GAAI,SAAS,OAAO,EAAE,MAAM,SAAS,KAAI,IAAK,CAAA;MAC9C,GAAI,SAAS,WAAW,EAAE,UAAU,SAAS,SAAQ,IAAK,CAAA;MAC1D,GAAI,SAAS,eAAe,EAAE,cAAc,SAAS,aAAY,IAAK,CAAA;MACtE,GAAI,SAAS,eAAe,EAAE,cAAc,SAAS,aAAY,IAAK,CAAA;KACtE,CAAC;AACF,UAAM,YAAY,oBAAI,IAAG;AACzB,QAAI;AAKJ,UAAM,iBAA8C,kCAAkC,YAAY,IAC/F;MACA,MAAM;MACN,UAAU,CAAC,UAAS;AACnB,mBAAW,QAAQ,MAAM,QAAQ;AAChC,eAAK,UAAU,WAAW,CAAC,aAAa,SAAS,QAAQ,IAAI,CAAC;QAC/D;MACD;QAEA;MACA,MAAM;MACN,UAAU,CAAC,UAAS;AACnB,aAAK,UAAU,WAAW,CAAC,aAAa,SAAS,QAAQ,MAAM,MAAM,CAAC;MACvE;;AAEH,UAAM,gBAAgB,CAAC,UAAwD;AAC9E,WAAK,UAAU,WAAW,CAAC,aAAa,SAAS,UAAU,MAAM,MAAM,CAAC;AACxE,UAAI,MAAM,WAAW,YAAY,CAAC,MAAM,QAAQ;AAC/C,cAAM,SAAS;AACf,iBAAS,KAAK;MACf;IACD;AACA,UAAM,gBAAgB,CAAC,UAA+B;AACrD,YAAM,QAAQ,MAAM,kBAAkB,QAAQ,MAAM,SAAS,IAAI,MAAM,OAAO,MAAM,MAAM,CAAC;AAC3F,WAAK,UAAU,WAAW,CAAC,aAAa,SAAS,UAAU,KAAK,CAAC;IAClE;AACA,YAAQ,EAAE,cAAc,WAAW,gBAAgB,eAAe,eAAe,QAAQ,MAAK;AAC9F,QAAI,eAAe,SAAS,WAAW;AACtC,mBAAa,iBAAiB,WAAW,eAAe,QAAQ;IACjE,OAAO;AACN,mBAAa,iBAAiB,SAAS,eAAe,QAAQ;IAC/D;AACA,iBAAa,iBAAiB,SAAS,aAAa;AACpD,iBAAa,iBAAiB,SAAS,aAAa;AACpD,WAAO;EACR;EAEA,WAAW,SAA+B;AACzC,UAAMC,UAAS;AACf,QAAI,KAAK,KAAK,YAAY,IAAIA,OAAM;AACpC,QAAI,CAAC,IAAI;AACR,WAAK,KAAK;AACV,WAAK,YAAY,IAAIA,SAAQ,EAAE;IAChC;AACA,WAAO;EACR;EAEA,QAAQ,OAAgB;AACvB,UAAM,EAAE,eAAc,IAAK;AAC3B,QAAI,eAAe,SAAS,WAAW;AACtC,YAAM,aAAa,oBAAoB,WAAW,eAAe,QAAQ;IAC1E,OAAO;AACN,YAAM,aAAa,oBAAoB,SAAS,eAAe,QAAQ;IACxE;AACA,UAAM,aAAa,oBAAoB,SAAS,MAAM,aAAa;AACnE,UAAM,aAAa,oBAAoB,SAAS,MAAM,aAAa;EACpE;EAEA,UACC,WACA,UAA2D;AAE3D,eAAW,YAAY,UAAU,OAAM,GAAI;AAC1C,UAAI;AAAE,iBAAS,QAAQ;MAAG,SAAS,OAAO;AAAE,gBAAQ,MAAM,kDAAkD,KAAK;MAAG;IACrH;EACD;;AAGM,IAAM,4BAA4B,IAAI,0BAAyB;;;ACtJhE,IAAO,mBAAP,MAAuB;EACpB,QAAwB;IAC/B,MAAM;IACN,WAAW;IACX,iBAAiB;IACjB,eAAe;IACf,gBAAgB;;EAGT,eAA6C;EAC7C,eAA6C;;EAE7C,MAAwC;EACxC,wBAAsD;EACtD;EACA,uBAAuB;EAEvB;EACA;EACA;EAEA,YAAS;AAChB,WAAO,OAAO,WAAW,cAAc,SAAY;EACpD;EAEQ,4BAAyB;AAChC,WAAO,KAAK,UAAS,GAAI;EAC1B;EAEQ,MAAM,uBACb,SAA+B;AAE/B,QAAI,CAAC,SAAS;AAAc,aAAO;AACnC,QAAI;AACH,aAAO,OAAO,QAAQ,iBAAiB,aACtC,MAAM,QAAQ,aAAY,IAC1B,QAAQ;IACV,SAAS,OAAO;AACf,cAAQ,KAAK,qDAAqD,KAAK;AACvE,aAAO;IACR;EACD;EAEQ,MAAM,qBAAqB,SAA2C;AAC7E,QAAI,OAAO,SAAS,cAAc;AAAY,aAAO;AACrD,QAAI,CAAC,QAAQ;AAAc,aAAO;AAElC,UAAM,eAAe,MAAM,KAAK,uBAAuB,OAAO;AAC9D,QAAI,CAAC;AAAc,aAAO;AAC1B,UAAM,eAAe,aAAa;AAClC,WAAO,CAAC,gBAAgB,aAAa,SAAS,OAAO;EACtD;;;;EAKA,MAAM,qBAAkB;AACvB,QAAI,KAAK;AAAc,aAAO,KAAK;AAEnC,UAAM,UAAU,KAAK,0BAAyB;AAC9C,UAAM,kBAAkB,MAAM,KAAK,qBAAqB,OAAO;AAC/D,UAAM,MAAM,KAAK,UAAS;AAE1B,SAAK,eAAe;MACnB,WAAW;MACX,YAAY;MACZ,eAAe,OAAO,KAAK,cAAc;MACzC,KAAK;MACL,OAAO;;MACP,WAAW;;AAGZ,YAAQ,IAAI,6BAA6B,KAAK,YAAY;AAC1D,WAAO,KAAK;EACb;;;;;;;;;;;;;;;;EAiBA,MAAM,QAAQ,QAA8B;AAC3C,QAAI;AACJ,QAAI;AACH,YAAM,IAAI,0BAA0B;QACnC,QAAQ,OAAO;QACf,gBAAgB,OAAO,gBAAgB;OACvC;IACF,SAAS,OAAO;AAIf,YAAM,UAAU,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACxE,UAAI,KAAK;AAAS,aAAK,QAAQ,OAAO;;AACjC,gBAAQ,MAAM,mDAAmD,OAAO;AAC7E,aAAO;IACR;AACA,UAAM,aAAa,EAAE,KAAK;AAC1B,SAAK,cAAc,MAAK;AACxB,SAAK,eAAe;AAKpB,SAAK,MAAM;AACX,SAAK,QAAQ;MACZ,MAAM;MACN,WAAW;MACX,iBAAiB;MACjB,eAAe;MACf,gBAAgB;;AAEjB,SAAK,gBAAgB,KAAK,KAAK;AAC/B,UAAM,WAAW,2BAA2B;MAC3C,cAAc,OAAO;MACrB,eAAe,OAAO;MACtB,MAAM,OAAO;MACb,MAAM,OAAO;MACb,UAAU,OAAO;MACjB,cAAc,OAAO;KACrB;AACD,QAAI,CAAC,UAAU;AACd,YAAM,QAAQ,IAAI,MAAM,0CAA0C;AAClE,WAAK,UAAU,KAAK;AACpB,aAAO;IACR;AACA,aAAS;MACR,GAAG;MACH,OAAO,SAAS;MAChB,QAAQ,SAAS,iBAAiB;MAClC,MAAM,SAAS;MACf,UAAU,SAAS;MACnB,cAAc,SAAS;;AAExB,UAAM,OAAO,MAAM,KAAK,mBAAkB;AAC1C,QAAI,eAAe,KAAK;AAAsB,aAAO;AAIrD,SAAK,MAAM;AAEX,QAAI,YAAY,CAAC,YAAW;AAC3B,WAAK,MAAM,UAAU;AACrB,WAAK,cAAc,kBAAkB,OAAO;AAC5C,WAAK,gBAAgB,KAAK,KAAK;IAChC,CAAC;AAGD,QAAI,YAAY;AAGhB,QAAI,KAAK,aAAa,KAAK,YAAY;AACtC,cAAQ,IAAI,4CAA4C;AACxD,kBAAY,MAAM,KAAK,uBAAuB,QAAQ,UAAU;AAChE,UAAI,WAAW;AACd,YAAI,eAAe,KAAK;AAAsB,iBAAO;AACrD,aAAK,MAAM,OAAO;AAClB,eAAO;MACR;IACD;AAGA,QAAI,CAAC,WAAW;AACf,cAAQ,IAAI,sCAAsC;AAClD,kBAAY,MAAM,KAAK,WAAW,QAAQ,UAAU;AACpD,UAAI,WAAW;AACd,YAAI,eAAe,KAAK;AAAsB,iBAAO;AACrD,aAAK,MAAM,OAAO;AAClB,eAAO;MACR;IACD;AAGA,YAAQ,IAAI,kEAAkE;AAC9E,WAAO;EACR;;;;EAKQ,MAAM,uBACb,QACA,YACA,OAA4B,aAAW;AAEvC,QAAI;AACH,YAAM,UAAU,KAAK,0BAAyB;AAC9C,UAAI,OAAO,SAAS,cAAc;AAAY,eAAO;AAErD,YAAM,QAAQ,MAAM,0BAA0B,QAAQ,SAAS,QAAQ;QACtE,SAAS,CAAC,SAAQ;AACjB,cAAI,eAAe,KAAK;AAAsB,iBAAK,aAAa,IAAI;QACrE;QACA,SAAS,CAAC,UAA2C;AACpD,cAAI,eAAe,KAAK;AAAsB;AAC9C,eAAK,MAAM,YAAY,UAAU;AACjC,eAAK,gBAAgB,KAAK,KAAK;QAChC;QACA,SAAS,CAAC,UAAS;AAClB,cAAI,eAAe,KAAK;AAAsB;AAC9C,kBAAQ,MAAM,uCAAuC,KAAK;AAC1D,eAAK,UAAU,KAAK;QACrB;OACA;AACD,UAAI,eAAe,KAAK,sBAAsB;AAC7C,cAAM,MAAM,QAAO;AACnB,eAAO;MACR;AAEA,WAAK,eAAe;QACnB,IAAI,MAAM;QACV;QACA,OAAO,MAAK;AAAG,eAAK,MAAM,QAAO;QAAI;;AAGtC,WAAK,QAAQ;QACZ;QACA,WAAW;QACX,OAAO,OAAO;QACd,QAAQ,OAAO;QACf,SAAS,KAAK,WAAU,EAAG,kBAAiB;QAC5C,iBAAiB;QACjB,eAAe;QACf,gBAAgB;;AAGjB,cAAQ,IAAI,gDAAgD,OAAO,MAAM,KAAK,OAAO,KAAK,GAAG;AAC7F,WAAK,gBAAgB,KAAK,KAAK;AAC/B,aAAO;IACR,SAAS,OAAO;AACf,cAAQ,KAAK,wCAAwC,KAAK;AAC1D,aAAO;IACR;EACD;;;;;EAMQ,MAAM,WAAW,QAAyB,YAAkB;AACnE,QAAI;AACH,UAAI,OAAO;AAAU,eAAO,MAAM,KAAK,gBAAgB,QAAQ,OAAO,UAAU,UAAU;AAC1F,UAAI,OAAO,iBAAiB,SAAS,CAAC,OAAO,UAAU,OAAO,SAAS,UAAU;AAChF,gBAAQ,KAAK,4EAA4E;AACzF,eAAO;MACR;AACA,cAAQ,IAAI,+DAA+D,OAAO,MAAM,KAAK;AAC7F,aAAO,MAAM,KAAK,uBAAuB,EAAE,GAAG,QAAQ,cAAc,QAAO,GAAI,YAAY,KAAK;IACjG,SAAS,OAAO;AACf,cAAQ,KAAK,sCAAsC,KAAK;AACxD,aAAO;IACR;EACD;;;;EAKQ,MAAM,gBAAgB,QAAyB,UAAkB,YAAkB;AAC1F,QAAI;AACH,YAAM,YAAY,SAAS,SAAS,GAAG,KAAK,CAAC,SAAS,WAAW,GAAG,IAAI,IAAI,QAAQ,MAAM;AAE1F,YAAM,QAAQ,IAAI,IAAI,SAAS,SAAS,YAAY;AACpD,YAAM,aAAa,IAAI,SAAS,OAAO,KAAK;AAC5C,YAAM,aAAa,IAAI,UAAU,OAAO,UAAU,EAAE;AACpD,YAAM,aAAa,IAAI,QAAQ,OAAO,OAAO,IAAI,CAAC;AAElD,YAAM,KAAK,IAAI,UAAU,KAAK;AAE9B,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAU;AACtC,WAAG,aAAa;AAChB,cAAM,UAAU,WAAW,MAAK;AAC/B,aAAG,MAAK;AACR,iBAAO,IAAI,MAAM,8BAA8B,CAAC;QACjD,GAAG,GAAK;AAER,WAAG,SAAS,MAAK;AAChB,uBAAa,OAAO;AACpB,cAAI,eAAe,KAAK,sBAAsB;AAC7C,eAAG,MAAK;AACR,oBAAQ,KAAK;AACb;UACD;AACA,kBAAQ,IAAI,uCAAuC,QAAQ,EAAE;AAE7D,eAAK,eAAe;YACnB,IAAI,aAAa,KAAK,IAAG,CAAE;YAC3B;YACA,OAAO,MAAM,GAAG,MAAK;;AAGtB,eAAK,QAAQ;YACZ,MAAM;YACN,WAAW;YACX,OAAO,OAAO;YACd,QAAQ,OAAO;YACf;YACA,SAAS,KAAK,WAAU,EAAG,kBAAiB;YAC5C,iBAAiB;YACjB,eAAe;YACf,gBAAgB;;AAGjB,eAAK,gBAAgB,KAAK,KAAK;AAC/B,kBAAQ,IAAI;QACb;AAEA,WAAG,YAAY,CAAC,UAAS;AACxB,cAAI,eAAe,KAAK,wBAAwB,MAAM,gBAAgB,aAAa;AAClF,iBAAK,aAAa,MAAM,IAAI;UAC7B;QACD;AAEA,WAAG,UAAU,CAAC,UAAS;AACtB,uBAAa,OAAO;AACpB,cAAI,eAAe,KAAK,sBAAsB;AAC7C,oBAAQ,KAAK;AACb;UACD;AACA,kBAAQ,MAAM,gCAAgC,KAAK;AACnD,iBAAO,KAAK;QACb;AAEA,WAAG,UAAU,MAAK;AACjB,uBAAa,OAAO;AACpB,cAAI,eAAe,KAAK,sBAAsB;AAC7C,oBAAQ,KAAK;AACb;UACD;AACA,eAAK,MAAM,YAAY;AACvB,eAAK,gBAAgB,KAAK,KAAK;AAC/B,kBAAQ,KAAK;QACd;MACD,CAAC;IACF,SAAS,OAAO;AACf,cAAQ,KAAK,iCAAiC,KAAK;AACnD,aAAO;IACR;EACD;;;;EAKQ,aAAa,MAAiB;AACrC,SAAK,MAAM;AACX,SAAK,MAAM,iBAAiB,KAAK;AACjC,SAAK,MAAM,iBAAiB,KAAK,IAAG;AAIpC,SAAK,KAAK,aAAa,KAAK,YAAY,CAAC;AAGzC,SAAK,WAAW,IAAI;EACrB;;;;EAKA,WAAQ;AACP,WAAO,EAAE,GAAG,KAAK,MAAK;EACvB;;;;;;EAOA,aAAU;AACT,WAAO,KAAK,KAAK,kBAAiB,KAAM;EACzC;;;;;;;EAQA,WAAW,MAAY;AACtB,SAAK,WAAU,EAAG,WAAW,IAAI;EAClC;;EAGQ,aAAU;AACjB,QAAI,CAAC,KAAK;AAAK,YAAM,IAAI,MAAM,sDAAsD;AACrF,WAAO,KAAK;EACb;;EAGA,wBACC,SACA,WAAkD;AAElD,SAAK,4BAA4B,KAAK,uBAAuB,OAAM,CAAE;AACrE,SAAK,wBAAwB,IAAI,sBAAsB,OAAO;AAC9D,SAAK,wBAAwB;AAC7B,WAAO,KAAK,sBAAsB,MAAK;EACxC;EAEA,uBAAuB,SAA2B;AACjD,WAAO,KAAK,4BAA4B,KAAK,uBAAuB,UAAU,EAAE,QAAO,CAAE,CAAC;EAC3F;EAEA,wBAAwB,SAAgB;AACvC,WAAO,KAAK,4BAA4B,KAAK,uBAAuB,WAAW,EAAE,QAAO,CAAE,CAAC;EAC5F;EAEA,0BAA0B,QAA2B;AACpD,WAAO,KAAK,4BAA4B,KAAK,uBAAuB,aAAa,MAAM,CAAC;EACzF;EAEA,0BAAuB;AACtB,WAAO,KAAK,4BAA4B,KAAK,uBAAuB,KAAI,CAAE;EAC3E;;EAGA,0BAAuB;AACtB,WAAO,KAAK,4BAA4B,KAAK,uBAAuB,OAAM,CAAE;EAC7E;;;;EAKA,kBAAkB,UAAqC;AACtD,SAAK,WAAW;EACjB;;;;EAKA,iBAAiB,UAAgC;AAChD,SAAK,UAAU;EAChB;;;;EAKA,iBAAiB,UAAyC;AACzD,SAAK,gBAAgB;EACtB;;;;EAKA,aAAU;AACT,SAAK;AACL,SAAK,cAAc,MAAK;AACxB,SAAK,eAAe;AAGpB,SAAK,MAAM;AACX,SAAK,4BAA4B,KAAK,uBAAuB,OAAM,CAAE;AACrE,SAAK,wBAAwB;AAC7B,SAAK,wBAAwB;AAC7B,SAAK,QAAQ;MACZ,MAAM;MACN,WAAW;MACX,iBAAiB;MACjB,eAAe;MACf,gBAAgB;;AAEjB,SAAK,gBAAgB,KAAK,KAAK;EAChC;EAEQ,4BAA4B,SAAwC;AAC3E,QAAI;AAAS,WAAK,wBAAwB,OAAO;AACjD,WAAO;EACR;;AAIM,IAAM,mBAAmB,IAAI,iBAAgB;;;AClhBpD,IAAM,oBAAoB;AAQ1B,SAAS,aAAa,OAAe,OAAa;AACjD,MAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,mBAAmB;AACvE,UAAM,IAAI,WAAW,GAAG,KAAK,8BAA8B,iBAAiB,UAAU,KAAK,EAAE;EAC9F;AACA,SAAO;AACR;AAGM,SAAU,uBAAuB,OAAa;AACnD,SAAO,aAAa,OAAO,0BAA0B;AACtD;AAGM,SAAU,sBAAsB,OAAa;AAClD,SAAO,aAAa,OAAO,yBAAyB;AACrD;AAGO,IAAM,2BAA2B,OAAO,OAAO;EACrD,SAAS,uBAAuB,CAAC;;;;;;;;EAQjC,gBAAgB,uBAAuB,CAAC;EACxC,OAAO,uBAAuB,CAAC;EAC/B,OAAO,uBAAuB,CAAC;;;;;EAK/B,kBAAkB,uBAAuB,CAAC;CAC1C;AAGM,IAAM,0BAA0B,OAAO,OAAO;EACpD,SAAS,sBAAsB,EAAE;EACjC,QAAQ,sBAAsB,GAAG;EACjC,QAAQ,sBAAsB,GAAG;CACjC;;;ACuCK,IAAO,oBAAP,MAAwB;EACrB,gBAAgB,oBAAI,IAAG;EACvB;EACA,iBAAiB,oBAAI,IAAG;EAEhC,cAAA;AAEC,SAAK,iBAAiB,CAAC,UAAuB;AAC7C,YAAM,OAAO,MAAM;AACnB,cAAQ,IAAI,8BAA8B,KAAK,IAAI;AACnD,UAAI,KAAK,SAAS,cAAc;AAC/B,aAAK,aAAa,IAAI;MACvB,WAAW,KAAK,SAAS,gBAAgB;AACxC,aAAK,kBAAkB,IAAI;MAC5B,WAAW,KAAK,SAAS,aAAa;AACrC,gBAAQ,IAAI,kBAAkB,KAAK,OAAO,EAAE;MAC7C;IACD;AAGA,QAAI,OAAO,cAAc,eAAe,UAAU,eAAe;AAChE,gBAAU,cAAc,iBAAiB,WAAW,KAAK,cAAc;AACvE,cAAQ,IAAI,iDAAiD;IAC9D,OAAO;AACN,cAAQ,KAAK,oCAAoC;IAClD;EACD;;;;EAKA,MAAM,UAAU,QAA6B;AAC5C,UAAM,EAAE,IAAG,IAAK;AAChB,UAAM,SAAS,KAAK,aAAa,GAAG;AAEpC,QAAI,KAAK,cAAc,IAAI,MAAM,GAAG;AACnC,cAAQ,IAAI,0CAA0C,MAAM,EAAE;AAC9D,aAAO;IACR;AAEA,QAAI;AACH,YAAM,eAAe,OAAO;AAC5B,YAAM,YAAY,OAAO,aAAa;AACtC,UAAI,CAAC,cAAc;AAClB,cAAM,IAAI,MACT,2JAC0F;MAE5F;AACA,cAAQ,IAAI,gCAAgC,YAAY,IAAI,SAAS,EAAE;AAEvE,YAAM,iBAAiB;AAGvB,WAAK,eAAe,IAAI,gBAAgB,CAAA,CAAE;AAG1C,YAAM,eAAgC;QACrC;QACA;QACA;QACA,mBAAmB;QACnB,OAAO;UACN,SAAS;UACT,MAAM;UACN,iBAAiB;UACjB,cAAc,KAAK,IAAG;;;AAIxB,WAAK,cAAc,IAAI,QAAQ,YAAY;AAI3C,cAAQ,IAAI,kCAAkC,MAAM,QAAQ,YAAY,IAAI,SAAS,EAAE;AAEvF,aAAO;IACR,SAAS,OAAO;AACf,YAAM,eAAe,KAAK,cAAc,IAAI,MAAM;AAClD,UAAI,cAAc;AACjB,aAAK,cAAc,OAAO,MAAM;AAChC,aAAK,eAAe,OAAO,aAAa,cAAc;MACvD;AAEA,YAAM,IAAI,MAAM,gCAAgC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,EAAE;IACzG;EACD;;;;EAKA,MAAM,YAAY,QAAc;AAC/B,UAAM,eAAe,KAAK,cAAc,IAAI,MAAM;AAClD,QAAI,CAAC,cAAc;AAClB,YAAM,IAAI,MAAM,gBAAgB,MAAM,YAAY;IACnD;AAEA,QAAI;AAEH,UAAI,aAAa,gBAAgB;AAChC,sBAAc,aAAa,cAAc;MAC1C;AAEA,cAAQ,IAAI,2BAA2B,MAAM,EAAE;IAChD,SAAS,OAAO;AACf,cAAQ,MAAM,mCAAmC,KAAK;IACvD;AACC,WAAK,cAAc,OAAO,MAAM;AAChC,WAAK,eAAe,OAAO,aAAa,cAAc;IACvD;EACD;;;;EAKA,mBAAgB;AACf,WAAO,MAAM,KAAK,KAAK,cAAc,KAAI,CAAE;EAC5C;;;;EAKQ,aAAa,SAAoD;AACxE,UAAM,EAAE,gBAAgB,KAAI,IAAK;AACjC,QAAI,CAAC;AAAgB;AAErB,UAAM,eAAe,KAAK,cAAc,IAAI,cAAc;AAC1D,QAAI,CAAC,cAAc;AAClB,cAAQ,KAAK,mDAAmD,cAAc,EAAE;AAChF;IACD;AAGA,QAAI,CAAC,aAAa,mBAAmB;AACpC,YAAM,QAAQ,KAAK,eAAe,IAAI,cAAc;AACpD,YAAMC,cAAa;AACnB,UAAI,OAAO;AACV,cAAM,KAAK;UACV,MAAMA,YAAW;UACjB,eAAeA,YAAW;UAC1B,YAAYA,YAAW;SACvB;MACF;AACA;IACD;AAGA,UAAM,aAAa;AACnB,UAAM,WAA2B;MAChC,eAAe,aAAa,OAAO,WAAW;MAC9C,YAAY,aAAa,OAAO,QAAQ;MACxC,WAAW;MACX,WAAW,YAAY,IAAG;;AAG3B,iBAAa,OAAO,SAAS,WAAW,MAAM,QAAQ;EACvD;;;;EAKQ,kBAAkB,SAAoD;AAC7E,UAAM,EAAE,gBAAgB,MAAK,IAAK;AAClC,QAAI,CAAC;AAAgB;AAErB,UAAM,eAAe,KAAK,cAAc,IAAI,cAAc;AAC1D,QAAI,CAAC,cAAc;AAClB,cAAQ,KAAK,kDAAkD,cAAc,EAAE;AAC/E;IACD;AAEA,iBAAa,OAAO,QAAQ,IAAI,MAAM,SAAS,eAAe,CAAC;EAChE;;;;EAKA,UAAO;AACN,QAAI,KAAK,kBAAkB,OAAO,cAAc,eAAe,UAAU,eAAe;AACvF,gBAAU,cAAc,oBAAoB,WAAW,KAAK,cAAc;IAC3E;AAEA,UAAM,UAAU,MAAM,KAAK,KAAK,cAAc,KAAI,CAAE;AACpD,eAAW,UAAU,SAAS;AAC7B,WAAK,YAAY,MAAM,EAAE,MAAM,CAAC,QAAO;AACtC,gBAAQ,MAAM,6BAA6B,MAAM,KAAK,GAAG;MAC1D,CAAC;IACF;EACD;;;;EAKQ,aAAa,KAAa;AACjC,UAAM,SAAS,IAAI,SAAS,IAAI,IAAI,MAAM,KAAK;AAC/C,WAAO,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,GAAG,MAAM;EACzC;;AA0MM,IAAM,oBAAoB,IAAI,kBAAiB;;;ACjWtD,IAAM,oBAAoB;AAC1B,IAAM,6BAA6B,oBAAI,IAAI,CAAC,UAAU,QAAQ,oBAAoB,aAAa,SAAS,CAAC;AAEzG,SAAS,OAAO,OAAgB,MAAuC;AACrE,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,UAAM,IAAI,MAAM,GAAG,IAAI,oBAAoB;AAAA,EAC7C;AACA,SAAO;AACT;AAEA,SAAS,OAAO,OAAgB,MAAsB;AACpD,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,WAAW,GAAG;AAC1D,UAAM,IAAI,MAAM,GAAG,IAAI,6BAA6B;AAAA,EACtD;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,OAAgB,MAAsB;AAC7D,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,UAAU,KAAK,KAAK,SAAS,GAAG;AACvE,UAAM,IAAI,MAAM,GAAG,IAAI,6BAA6B;AAAA,EACtD;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,OAAgB,MAAsB;AAChE,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GAAG;AACtE,UAAM,IAAI,MAAM,GAAG,IAAI,iCAAiC;AAAA,EAC1D;AACA,SAAO;AACT;AAEA,SAAS,uBAAuB,OAAgB,MAAsB;AACpE,QAAM,SAAS,mBAAmB,OAAO,IAAI;AAC7C,MAAI,CAAC,OAAO,cAAc,MAAM,EAAG,OAAM,IAAI,MAAM,GAAG,IAAI,yBAAyB;AACnF,SAAO;AACT;AAEA,SAAS,kBAAkB,OAAgB,MAAsB;AAC/D,QAAM,SAAS,uBAAuB,OAAO,IAAI;AACjD,MAAI,SAAS,WAAY,OAAM,IAAI,MAAM,GAAG,IAAI,mCAAmC;AACnF,SAAO;AACT;AAEA,SAAS,KAAK,OAAgB,MAAsB;AAClD,QAAM,SAAS,gBAAgB,OAAO,IAAI;AAC1C,MAAI,SAAS,MAAO,OAAM,IAAI,MAAM,GAAG,IAAI,8BAA8B;AACzE,SAAO;AACT;AAEA,SAAS,QAAQ,OAAgB,MAAsB;AACrD,QAAM,MAAM,OAAO,OAAO,IAAI;AAC9B,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,UAAM,IAAI,MAAM,GAAG,IAAI,kCAAkC;AAAA,EAC3D;AACA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,UAAM,IAAI,MAAM,GAAG,IAAI,kCAAkC;AAAA,EAC3D;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,UAAkB,MAA2B;AACtE,QAAM,YAAY,oBAAI,IAAY;AAClC,aAAW,SAAS,SAAS,SAAS,iBAAiB,GAAG;AACxD,UAAM,WAAW,MAAM,CAAC;AACxB,QAAI,aAAa,UAAa,CAAC,2BAA2B,IAAI,QAAQ,GAAG;AACvE,YAAM,IAAI,MAAM,GAAG,IAAI,4CAA4C,YAAY,EAAE,GAAG;AAAA,IACtF;AACA,cAAU,IAAI,QAAQ;AAAA,EACxB;AACA,MAAI,SAAS,SAAS,GAAG,KAAK,SAAS,QAAQ,mBAAmB,EAAE,EAAE,SAAS,GAAG,GAAG;AACnF,UAAM,IAAI,MAAM,GAAG,IAAI,yCAAyC;AAAA,EAClE;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,kBAA0B,qBAA6B,MAAoB;AACvG,QAAM,eAAe,kBAAkB,kBAAkB,GAAG,IAAI,mBAAmB;AACnF,QAAM,kBAAkB,kBAAkB,qBAAqB,GAAG,IAAI,8BAA8B;AACpG,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,IAAI,MAAM,GAAG,IAAI,4DAA4D;AAAA,EACrF;AACA,MACE,aAAa,SAAS,gBAAgB,QACtC,CAAC,GAAG,YAAY,EAAE,KAAK,CAAC,aAAa,CAAC,gBAAgB,IAAI,QAAQ,CAAC,GACnE;AACA,UAAM,IAAI,MAAM,GAAG,IAAI,uDAAuD;AAAA,EAChF;AACF;AAEA,SAAS,yBAAyB,UAAkB,MAAoB;AACtE,QAAM,YAAY,CAAC,GAAG,SAAS,SAAS,iBAAiB,CAAC,EAAE,IAAI,CAAC,UAAU,MAAM,CAAC,CAAC;AACnF,QAAM,cAAc,UAAU,KAAK,CAAC,aAAa,aAAa,YAAY,aAAa,kBAAkB;AACzG,MAAI,gBAAgB,QAAW;AAC7B,UAAM,IAAI,MAAM,GAAG,IAAI,6DAA6D,WAAW,GAAG;AAAA,EACpG;AACA,MAAI,UAAU,OAAO,CAAC,aAAa,aAAa,QAAQ,EAAE,WAAW,GAAG;AACtE,UAAM,IAAI,MAAM,GAAG,IAAI,yDAAyD;AAAA,EAClF;AACF;AAEA,SAAS,0BAA0B,OAA0B,MAAoB;AAC/E,2BAAyB,MAAM,kBAAkB,GAAG,IAAI,mBAAmB;AAC3E,2BAAyB,MAAM,QAAQ,qBAAqB,GAAG,IAAI,8BAA8B;AACnG;AAEA,SAAS,iBAAiB,OAAgB,MAAgC;AACxE,QAAM,WAAW,OAAO,OAAO,IAAI;AACnC,SAAO;AAAA,IACL,QAAQ,OAAO,SAAS,QAAQ,GAAG,GAAG,IAAI,SAAS;AAAA,IACnD,OAAO,OAAO,SAAS,OAAO,GAAG,GAAG,IAAI,QAAQ;AAAA,IAChD,MAAM,KAAK,SAAS,MAAM,GAAG,GAAG,IAAI,OAAO;AAAA,EAC7C;AACF;AAEA,SAAS,kBAAkB,OAAkD;AAC3E,QAAM,YAAY,OAAO,OAAO,qBAAqB;AACrD,QAAM,UAAU,OAAO,QAAQ,SAAS;AACxC,MAAI,QAAQ,WAAW,EAAG,OAAM,IAAI,MAAM,uCAAuC;AACjF,SAAO,OAAO;AAAA,IACZ,QAAQ,IAAI,CAAC,CAAC,IAAI,QAAQ,MAAM;AAAA,MAC9B,OAAO,IAAI,uBAAuB;AAAA,MAClC,iBAAiB,UAAU,uBAAuB,EAAE,EAAE;AAAA,IACxD,CAAC;AAAA,EACH;AACF;AAEA,SAAS,uBAAuB,OAAgC;AAC9D,QAAM,SAAS,OAAO,OAAO,0BAA0B;AACvD,QAAM,aAAa,OAAO,YAAY;AACtC,MAAI,eAAe,QAAQ;AACzB,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AACA,SAAO;AAAA,IACL,sBAAsB;AAAA,MACpB,OAAO,sBAAsB;AAAA,MAC7B;AAAA,IACF;AAAA,IACA,kBAAkB,gBAAgB,OAAO,kBAAkB,GAAG,2CAA2C;AAAA,IACzG;AAAA,EACF;AACF;AAEA,SAAS,kBACP,OACA,MACA,WACmB;AACnB,QAAM,QAAQ,OAAO,OAAO,IAAI;AAChC,QAAM,cAAc,OAAO,MAAM,aAAa,GAAG,GAAG,IAAI,cAAc;AACtE,MAAI,EAAE,eAAe,WAAY,OAAM,IAAI,MAAM,GAAG,IAAI,kCAAkC,WAAW,EAAE;AAEvG,QAAM,UAAU,OAAO,MAAM,SAAS,GAAG,GAAG,IAAI,UAAU;AAC1D,QAAM,MAAM,OAAO,MAAM,KAAK,GAAG,GAAG,IAAI,MAAM;AAC9C,MAAI,qBAAqB,OAAO,kBAAkB,KAAK;AACrD,UAAM,IAAI,MAAM,GAAG,IAAI,+DAA+D;AAAA,EACxF;AACA,QAAM,mBAAmB,QAAQ,MAAM,kBAAkB,GAAG,GAAG,IAAI,mBAAmB;AACtF,QAAM,sBAAsB,OAAO,QAAQ,qBAAqB,GAAG,GAAG,IAAI,8BAA8B;AACxG,uBAAqB,kBAAkB,qBAAqB,IAAI;AAEhE,SAAO;AAAA,IACL,IAAI,OAAO,MAAM,IAAI,GAAG,GAAG,IAAI,KAAK;AAAA,IACpC,aAAa,oBAAoB,MAAM,aAAa,GAAG,GAAG,IAAI,cAAc;AAAA,IAC5E;AAAA,IACA,SAAS,QAAQ,MAAM,SAAS,GAAG,GAAG,IAAI,UAAU;AAAA,IACpD;AAAA,IACA,SAAS;AAAA,MACP,WAAW,OAAO,QAAQ,WAAW,GAAG,GAAG,IAAI,oBAAoB;AAAA,MACnE,YAAY,OAAO,QAAQ,YAAY,GAAG,GAAG,IAAI,qBAAqB;AAAA,MACtE;AAAA,IACF;AAAA,IACA,KAAK;AAAA,MACH,GAAG,gBAAgB,IAAI,GAAG,GAAG,GAAG,IAAI,QAAQ;AAAA,MAC5C,GAAG,mBAAmB,IAAI,GAAG,GAAG,GAAG,IAAI,QAAQ;AAAA,MAC/C,GAAG,gBAAgB,IAAI,GAAG,GAAG,GAAG,IAAI,QAAQ;AAAA,MAC5C,mBAAmB,gBAAgB,IAAI,mBAAmB,GAAG,GAAG,IAAI,wBAAwB;AAAA,MAC5F,cAAc,qBAAqB,IAAI,cAAc,GAAG,GAAG,IAAI,mBAAmB;AAAA,IACpF;AAAA,EACF;AACF;AAEA,SAAS,oBAAoB,OAAgB,MAAmC;AAC9E,MAAI,UAAU,WAAW,UAAU,WAAW,UAAU,QAAQ;AAC9D,UAAM,IAAI,MAAM,GAAG,IAAI,sCAAsC;AAAA,EAC/D;AACA,SAAO;AACT;AAEA,SAAS,cAAc,UAA0B;AAC/C,SAAO,SAAS,QAAQ,mBAAmB,YAAY;AACzD;AAEA,SAAS,wBACP,QACA,cACA,WACM;AACN,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,cAAc,oBAAI,IAAY;AACpC,SAAO,QAAQ,CAAC,OAAO,UAAU;AAC/B,QAAI,IAAI,IAAI,MAAM,EAAE,EAAG,OAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE,EAAE;AACxE,QAAI,IAAI,MAAM,EAAE;AAEhB,UAAM,WAAW,aAAa,IAAI,CAAC,QAAQ,OAAO,UAAU,KAAK,IAAI,GAAG,CAAC,CAAC,EAAE,KAAK,IAAQ;AACzF,QAAI,UAAU,IAAI,QAAQ,EAAG,OAAM,IAAI,MAAM,6BAA6B,aAAa,KAAK,IAAI,CAAC,EAAE;AACnG,cAAU,IAAI,QAAQ;AAEtB,QAAI,SAAS,IAAI,MAAM,OAAO,EAAG,OAAM,IAAI,MAAM,+BAA+B,MAAM,OAAO,EAAE;AAC/F,aAAS,IAAI,MAAM,OAAO;AAE1B,UAAM,aAAa,cAAc,MAAM,gBAAgB;AACvD,QAAI,YAAY,IAAI,UAAU,GAAG;AAC/B,YAAM,IAAI,MAAM,gCAAgC,MAAM,gBAAgB,EAAE;AAAA,IAC1E;AACA,gBAAY,IAAI,UAAU;AAAA,EAC5B,CAAC;AACH;AAKA,SAAS,eACP,OACA,MACA,WAC8D;AAC9D,MAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAG,OAAM,IAAI,MAAM,aAAa,IAAI,2BAA2B;AAC7G,QAAM,YAAY,MAAM,IAAI,CAAC,OAAO,UAAU,OAAO,OAAO,aAAa,IAAI,WAAW,KAAK,GAAG,CAAC;AACjG,QAAM,SAAS,UAAU,IAAI,CAAC,OAAO,UAAU;AAC7C,UAAM,OAAO,aAAa,IAAI,WAAW,KAAK;AAC9C,UAAM,OAAO,kBAAkB,OAAO,MAAM,SAAS;AACrD,QAAI,SAAS,QAAQ;AACnB,gCAA0B,MAAM,IAAI;AACpC,YAAM,OAAO,OAAO,MAAM,MAAM,GAAG,GAAG,IAAI,OAAO;AACjD,UAAI,KAAK,MAAM,MAAM,MAAO,OAAM,IAAI,MAAM,GAAG,IAAI,0BAA0B;AAC7E,YAAM,OAAO;AACb,YAAM,WAAW,KAAK,UAAU;AAChC,UAAI,CAAC,yBAAyB,QAAQ,GAAG;AACvC,cAAM,IAAI,MAAM,GAAG,IAAI,kFAAkF;AAAA,MAC3G;AACA,YAAM,cAAc,KAAK,aAAa;AACtC,UAAI,gBAAgB,WAAc,OAAO,gBAAgB,YAAY,YAAY,KAAK,EAAE,WAAW,IAAI;AACrG,cAAM,IAAI,MAAM,GAAG,IAAI,4DAA4D;AAAA,MACrF;AACA,YAAM,sBAAsB,gBAAgB,KAAK,qBAAqB,GAAG,GAAG,IAAI,2BAA2B;AAC3G,YAAM,yBAAyB;AAAA,QAC7B,KAAK,wBAAwB;AAAA,QAC7B,GAAG,IAAI;AAAA,MACT;AACA,YAAM,oBAAoB,uBAAuB,KAAK,mBAAmB,GAAG,GAAG,IAAI,yBAAyB;AAC5G,YAAM,wBAAwB,gBAAgB,KAAK,uBAAuB,GAAG,GAAG,IAAI,6BAA6B;AACjH,UAAI,wBAAwB,qBAAqB;AAC/C,cAAM,IAAI,MAAM,GAAG,IAAI,iEAAiE;AAAA,MAC1F;AACA,UAAI,yBAAyB,wBAAwB,IAAI,YAAY;AACnE,cAAM,IAAI,MAAM,GAAG,IAAI,+DAA+D;AAAA,MACxF;AACA,aAAO;AAAA,QACL,GAAG;AAAA,QACH,kBAAkB,OAAO,MAAM,kBAAkB,GAAG,GAAG,IAAI,mBAAmB;AAAA,QAC9E,MAAM;AAAA,UACJ,WAAW,OAAO,KAAK,WAAW,GAAG,GAAG,IAAI,iBAAiB;AAAA,UAC7D;AAAA,UACA;AAAA,UACA,OAAO,OAAO,KAAK,OAAO,GAAG,GAAG,IAAI,aAAa;AAAA,UACjD,WAAW,gBAAgB,KAAK,WAAW,GAAG,GAAG,IAAI,iBAAiB;AAAA,UACtE,wBAAwB;AAAA,YACtB,KAAK,wBAAwB;AAAA,YAC7B,GAAG,IAAI;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,GAAI,gBAAgB,SAAY,CAAC,IAAI,EAAE,YAAY;AAAA,QACrD;AAAA,MACF;AAAA,IACF;AACA,QAAI,SAAS,OAAO;AAClB,aAAO;AAAA,QACL,GAAG;AAAA,QACH,WAAW,OAAO,MAAM,WAAW,GAAG,GAAG,IAAI,YAAY;AAAA,QACzD,SAAS,OAAO,MAAM,SAAS,GAAG,GAAG,IAAI,UAAU;AAAA,MACrD;AAAA,IACF;AACA,WAAO,EAAE,GAAG,MAAM,WAAW,OAAO,MAAM,WAAW,GAAG,GAAG,IAAI,YAAY,EAAE;AAAA,EAC/E,CAAC;AACD;AAAA,IACE;AAAA,IACA,SAAS,SAAS,CAAC,oBAAoB,aAAa,IAAI,SAAS,QAAQ,CAAC,aAAa,SAAS,IAAI,CAAC,WAAW;AAAA,IAChH;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,OAAmC;AAC5D,QAAM,UAAU,OAAO,OAAO,SAAS;AACvC,MAAI,QAAQ,eAAe,MAAM,EAAG,OAAM,IAAI,MAAM,iCAAiC;AACrF,QAAM,YAAY,OAAO,QAAQ,WAAW,GAAG,WAAW;AAC1D,QAAM,YAAY,UAAU,WAAW;AACvC,MAAI,cAAc,UAAU,cAAc,SAAS,cAAc,QAAQ;AACvE,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AACA,QAAM,YAAY,kBAAkB,UAAU,WAAW,CAAC;AAC1D,QAAM,iBAAiB,uBAAuB,UAAU,gBAAgB,CAAC;AACzE,QAAM,SAAS;AAAA,IACb,eAAe;AAAA,IACf,WAAW,OAAO,QAAQ,WAAW,GAAG,mBAAmB;AAAA,EAC7D;AAEA,MAAI,cAAc,QAAQ;AACxB,UAAM,OAAO,OAAO,UAAU,MAAM,GAAG,gBAAgB;AACvD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM;AAAA,UACJ,aAAa,QAAQ,KAAK,aAAa,GAAG,4BAA4B;AAAA,UACtE,QAAQ,eAAe,KAAK,QAAQ,GAAG,WAAW,SAAS;AAAA,QAC7D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,MAAI,cAAc,OAAO;AACvB,UAAM,MAAM,OAAO,UAAU,KAAK,GAAG,eAAe;AACpD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA,KAAK;AAAA,UACH,aAAa,QAAQ,IAAI,aAAa,GAAG,2BAA2B;AAAA,UACpE,QAAQ,eAAe,IAAI,QAAQ,GAAG,WAAW,SAAS;AAAA,QAC5D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,OAAO,UAAU,MAAM,GAAG,gBAAgB;AACvD,SAAO;AAAA,IACL,GAAG;AAAA,IACH,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,EAAE,QAAQ,eAAe,KAAK,QAAQ,GAAG,WAAW,SAAS,EAAE;AAAA,IACvE;AAAA,EACF;AACF;AAGO,SAAS,yBAAyB,OAAmC;AAC1E,QAAM,UAAU,OAAO,OAAO,SAAS;AACvC,MAAI,QAAQ,eAAe,MAAM,EAAG,QAAO,kBAAkB,OAAO;AACpE,MAAI,QAAQ,eAAe,MAAM,OAAW,OAAM,IAAI,MAAM,mCAAmC;AAC/F,QAAM,IAAI,MAAM,sCAAsC,OAAO,QAAQ,eAAe,CAAC,CAAC,EAAE;AAC1F;",
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", "export interface MulticastTierEndpoint {\n\tsourceAddress?: string;\n\tgroupAddress: string;\n\tport: number;\n\ttracks: { name: string; packetId?: number }[];\n\tbandwidth?: number;\n\tprotocol?: string;\n\t/** False when every track on this endpoint is a `fec-repair` track, so the\n\t * endpoint carries repair symbols only and no media. A repair-layer\n\t * endpoint is joined by the loss-driven layer policy, never by the\n\t * bandwidth ladder: selecting one as a tier would leave the receiver with\n\t * no media at all. Set by the catalog policy, which resolves repair-track\n\t * membership from `depends`/`fec.repairTrack` rather than from names.\n\t * Undefined means \"not classified\" and is treated as carrying media, so a\n\t * caller that builds endpoints without the policy is unaffected. */\n\tcarriesMedia?: boolean;\n}\n\nexport function endpointKey(endpoint: MulticastTierEndpoint): string {\n\treturn `${endpoint.sourceAddress ?? \"\"}|${endpoint.groupAddress}|${endpoint.port}`;\n}\n\n/**\n * Whether this endpoint may be selected as a media tier.\n *\n * Every path that picks a tier must apply this, not only `selectMulticastTier`:\n * the orchestrator's headroom up-selection ranks endpoints itself, because it\n * advances one adjacent rung rather than fitting a bandwidth budget. Exported\n * so both rankings share one definition of \"carries media\" instead of\n * repeating the comparison and drifting apart.\n */\nexport function carriesMedia(endpoint: MulticastTierEndpoint): boolean {\n\treturn endpoint.carriesMedia !== false;\n}\n\nexport function selectMulticastTier(\n\tendpoints: readonly MulticastTierEndpoint[],\n\testimate: number | undefined,\n): MulticastTierEndpoint | undefined {\n\t// Repair-only endpoints are never a media tier. Filtering once here covers\n\t// every return path below, including the two early returns and the fallback:\n\t// a ladder that has only repair-layer endpoints returns undefined rather\n\t// than joining one and receiving no media.\n\tconst media = endpoints.filter(carriesMedia);\n\tif (media.length <= 1) return media[0];\n\n\tconst ranked = media\n\t\t.filter((endpoint) => typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > 0)\n\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!);\n\tif (ranked.length < 2) return media[0];\n\tif (estimate === undefined) return ranked[0];\n\n\tconst budget = estimate * 0.8;\n\tlet selected: MulticastTierEndpoint | undefined;\n\tfor (const endpoint of ranked) {\n\t\tif (endpoint.bandwidth! <= budget) selected = endpoint;\n\t}\n\treturn selected ?? ranked[0];\n}\n", "/**\n * @blockcast/transport - Adaptive Bitrate Controller\n *\n * Progressive adaptive bitrate increase based on network conditions.\n * Monitors received bandwidth and packet loss to adjust quality levels.\n *\n * When FEC is enabled, ABR decisions use the **effective loss rate** after FEC\n * recovery, not the raw transport-level loss rate, so FEC can mask burst losses\n * without unnecessarily downgrading quality. This is libmmt policy; no draft\n * specifies it.\n */\n\nimport type { QualityLevel } from \"./types.js\";\nimport { selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\n/**\n * FEC statistics for ABR decisions\n */\nexport interface FecStatsForAbr {\n\t/** Raw loss rate before FEC recovery (0.0-1.0) */\n\trawLossRate: number;\n\t/** Effective loss rate after FEC recovery (0.0-1.0) */\n\teffectiveLossRate: number;\n\t/** FEC recovery rate (percentage of lost packets recovered) */\n\trecoveryRate: number;\n\t/** Whether FEC is enabled */\n\tfecEnabled: boolean;\n}\n\n/**\n * A generic 240p..4K ladder, kept as public API only.\n *\n * @deprecated Not a stream's ladder and never a default: the controller\n * adapts over the ladder its caller supplies (`AdaptiveBitrateControllerOptions.ladder`,\n * e.g. the catalog's video renditions). Walking this list is how a player on a\n * 360p/480p/720p catalog logged upgrades to 1080p and 1440p it could never\n * switch to (BLO-35597).\n */\nexport const QUALITY_LEVELS: readonly QualityLevel[] = Object.freeze([\n\t{ name: \"240p\", bitrate: 300_000, width: 426, height: 240, fps: 15 },\n\t{ name: \"360p\", bitrate: 500_000, width: 640, height: 360, fps: 24 },\n\t{ name: \"480p\", bitrate: 1_000_000, width: 854, height: 480, fps: 30 },\n\t{ name: \"720p\", bitrate: 2_500_000, width: 1280, height: 720, fps: 30 },\n\t{ name: \"1080p\", bitrate: 5_000_000, width: 1920, height: 1080, fps: 30 },\n\t{ name: \"1440p\", bitrate: 8_000_000, width: 2560, height: 1440, fps: 30 },\n\t{ name: \"4K\", bitrate: 15_000_000, width: 3840, height: 2160, fps: 30 },\n].map((level) => Object.freeze(level)));\n\n/** What the controller adapts over and where it starts; both from the caller. */\nexport interface AdaptiveBitrateControllerOptions {\n\t/**\n\t * The renditions this stream carries (e.g. the catalog's video tracks), in\n\t * any order: a unique name and a bitrate (bps) each, plus dimensions and\n\t * fps when the catalog states them. Renditions may share a bitrate.\n\t */\n\tladder: readonly QualityLevel[];\n\t/** Name of the ladder rendition presented at construction; the lowest\n\t * rung when omitted. A name that matches no rung is refused. */\n\tinitialQuality?: string;\n}\n\nfunction positiveFinite(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n\n/**\n * Validate a caller ladder and return it frozen, ascending by bitrate. Throws\n * on an empty ladder, a rung without a name or a positive finite bitrate, a\n * width, height or fps that is present but not positive finite, or a name that\n * appears twice: a name is how `setQuality` and the initial rendition find a\n * rung. A shared bitrate is legal (e.g. 720p30 and 720p60, or two codecs);\n * `Array.prototype.sort` is stable, so equal bitrates keep the caller's order.\n */\nexport function validateQualityLadder(ladder: readonly QualityLevel[]): readonly QualityLevel[] {\n\tif (!Array.isArray(ladder) || ladder.length === 0) {\n\t\tthrow new TypeError(\"[ABR] ladder must list at least one rendition (e.g. the catalog's video tracks)\");\n\t}\n\tconst names = new Set<string>();\n\tfor (const level of ladder) {\n\t\tif (typeof level?.name !== \"string\" || level.name.length === 0) {\n\t\t\tthrow new TypeError(\"[ABR] every ladder rendition needs a name\");\n\t\t}\n\t\tif (!positiveFinite(level.bitrate)) {\n\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: bitrate must be a positive finite number, got ${String(level.bitrate)}`);\n\t\t}\n\t\tfor (const field of [\"width\", \"height\", \"fps\"] as const) {\n\t\t\tif (level[field] !== undefined && !positiveFinite(level[field])) {\n\t\t\t\tthrow new RangeError(`[ABR] rendition ${level.name}: ${field} must be a positive finite number when stated, got ${String(level[field])}`);\n\t\t\t}\n\t\t}\n\t\tif (names.has(level.name)) throw new RangeError(`[ABR] rendition ${level.name} appears twice in the ladder`);\n\t\tnames.add(level.name);\n\t}\n\treturn Object.freeze(\n\t\t[...ladder].sort((a, b) => a.bitrate - b.bitrate).map((level) => Object.freeze({ ...level })),\n\t);\n}\n\n/**\n * Progressive Adaptive Bitrate Controller\n *\n * Features:\n * - Monitors bandwidth via received bytes over time window\n * - Tracks packet loss rate (raw and effective after FEC)\n * - Uses effective loss rate (after FEC) for ABR decisions\n * - Progressive quality upgrades (waits for stability)\n * - Fast quality downgrades on congestion\n */\nexport class AdaptiveBitrateController {\n\t/** The caller's renditions, ascending by bitrate (`validateQualityLadder`). */\n\treadonly ladder: readonly QualityLevel[];\n\tprivate currentQualityIndex: number;\n\tprivate history: { timestamp: number; bytesReceived: number; packetsLost: number }[] = [];\n\tprivate readonly historyWindow = 10_000; // 10 second window\n\tprivate readonly upgradeThreshold = 0.8; // 80% of target bitrate sustained\n\tprivate readonly downgradeThreshold = 0.5; // 50% of target bitrate triggers downgrade\n\tprivate readonly stabilityPeriod = 5_000; // Wait 5s before upgrading\n\n\tprivate lastQualityChange = 0;\n\tprivate onQualityChange?: (quality: QualityLevel) => void;\n\n\t// FEC stats for effective loss rate calculation\n\tprivate fecStats: FecStatsForAbr = {\n\t\trawLossRate: 0,\n\t\teffectiveLossRate: 0,\n\t\trecoveryRate: 1,\n\t\tfecEnabled: false,\n\t};\n\n\tconstructor(options: AdaptiveBitrateControllerOptions) {\n\t\tif (!options || typeof options !== \"object\") {\n\t\t\tthrow new TypeError(\"[ABR] AdaptiveBitrateController needs { ladder, initialQuality }\");\n\t\t}\n\t\tthis.ladder = validateQualityLadder(options.ladder);\n\t\tthis.currentQualityIndex =\n\t\t\toptions.initialQuality === undefined ? 0 : this.ladder.findIndex((q) => q.name === options.initialQuality);\n\t\tif (this.currentQualityIndex < 0) {\n\t\t\tthrow new RangeError(`[ABR] initial rendition ${String(options.initialQuality)} is not in the ladder`);\n\t\t}\n\t}\n\n\tsetCallback(callback: (quality: QualityLevel) => void) {\n\t\tthis.onQualityChange = callback;\n\t}\n\n\tgetCurrentQuality(): QualityLevel {\n\t\treturn this.ladder[this.currentQualityIndex];\n\t}\n\n\t/** Selects the multicast (S,G) tier without replacing a player's unicast ABR manager. */\n\tselectMulticastTier(\n\t\tendpoints: readonly MulticastTierEndpoint[],\n\t\testimate: number | undefined = this.getMetrics().measuredBitrate || undefined,\n\t): MulticastTierEndpoint | undefined {\n\t\treturn selectMulticastTier(endpoints, estimate);\n\t}\n\n\t/**\n\t * Record network sample for bitrate adaptation\n\t *\n\t * @param bytesReceived - Bytes received in this sample\n\t * @param packetsLost - Packets lost (raw, before FEC recovery)\n\t */\n\trecordSample(bytesReceived: number, packetsLost: number) {\n\t\tconst now = Date.now();\n\n\t\tthis.history.push({ timestamp: now, bytesReceived, packetsLost });\n\n\t\t// Prune old samples\n\t\tthis.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);\n\n\t\t// Calculate metrics\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\tconst currentQuality = this.getCurrentQuality();\n\n\t\t// Use effective loss rate after FEC for ABR decisions\n\t\t// This allows FEC to mask burst losses without triggering unnecessary downgrades\n\t\tconst effectiveLossRate = this.fecStats.fecEnabled\n\t\t\t? this.fecStats.effectiveLossRate\n\t\t\t: rawLossRate;\n\n\t\t// Decision logic\n\t\tif (now - this.lastQualityChange < this.stabilityPeriod) {\n\t\t\treturn; // Wait for stability\n\t\t}\n\n\t\t// Check for downgrade - use effective loss rate (post-FEC)\n\t\t// Only downgrade if FEC cannot recover the losses\n\t\tif (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {\n\t\t\tthis.downgrade();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check for upgrade (progressive increase) - use effective loss rate\n\t\tif (\n\t\t\tmeasuredBitrate > currentQuality.bitrate * this.upgradeThreshold &&\n\t\t\teffectiveLossRate < 0.01 &&\n\t\t\tthis.currentQualityIndex < this.ladder.length - 1\n\t\t) {\n\t\t\tthis.upgrade();\n\t\t}\n\t}\n\n\t/**\n\t * Update FEC statistics for ABR decisions\n\t *\n\t * ABR uses the effective loss rate (after FEC recovery) rather than the\n\t * raw transport loss rate.\n\t *\n\t * @param stats - FEC statistics from the FEC decoder\n\t */\n\tsetFecStats(stats: FecStatsForAbr) {\n\t\tthis.fecStats = stats;\n\t}\n\n\t/**\n\t * Get current FEC statistics\n\t */\n\tgetFecStats(): FecStatsForAbr {\n\t\treturn { ...this.fecStats };\n\t}\n\n\tprivate calculateMetrics() {\n\t\tif (this.history.length < 2) {\n\t\t\treturn { measuredBitrate: 0, lossRate: 0 };\n\t\t}\n\n\t\tconst totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);\n\t\tconst totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);\n\t\tconst duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1000;\n\n\t\tconst measuredBitrate = duration > 0 ? (totalBytes * 8) / duration : 0;\n\t\tconst lossRate = totalLost / (this.history.length + totalLost);\n\n\t\treturn { measuredBitrate, lossRate };\n\t}\n\n\tprivate upgrade() {\n\t\tif (this.currentQualityIndex >= this.ladder.length - 1) return;\n\n\t\tthis.currentQualityIndex++;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\tprivate downgrade() {\n\t\tif (this.currentQualityIndex <= 0) return;\n\n\t\tthis.currentQualityIndex--;\n\t\tthis.lastQualityChange = Date.now();\n\n\t\tconst newQuality = this.getCurrentQuality();\n\t\tconsole.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);\n\n\t\tthis.onQualityChange?.(newQuality);\n\t}\n\n\t/**\n\t * Select a ladder rendition: the caller switched to it (a manual override,\n\t * or a switch the caller made). Throws for a name the ladder does not carry.\n\t */\n\tsetQuality(name: string) {\n\t\tconst index = this.ladder.findIndex((q) => q.name === name);\n\t\tif (index < 0) throw new RangeError(`[ABR] rendition ${name} is not in the ladder`);\n\t\tthis.currentQualityIndex = index;\n\t\tthis.lastQualityChange = Date.now();\n\t\tthis.onQualityChange?.(this.getCurrentQuality());\n\t}\n\n\t/**\n\t * Get current metrics including both raw and effective loss rates\n\t */\n\tgetMetrics(): {\n\t\tmeasuredBitrate: number;\n\t\trawLossRate: number;\n\t\teffectiveLossRate: number;\n\t\tfecEnabled: boolean;\n\t\tsampleCount: number;\n\t} {\n\t\tconst { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();\n\t\treturn {\n\t\t\tmeasuredBitrate,\n\t\t\trawLossRate,\n\t\t\teffectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,\n\t\t\tfecEnabled: this.fecStats.fecEnabled,\n\t\t\tsampleCount: this.history.length,\n\t\t};\n\t}\n\n\t/**\n\t * Reset history and start fresh\n\t */\n\treset() {\n\t\tthis.history = [];\n\t\tthis.lastQualityChange = 0;\n\t}\n}\n", "/**\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 * AMT well-known UDP port (RFC 7450 \u00A74).\n *\n * The single definition for the whole repo. It lives here, in the one package\n * both `@blockcast/mmt-transport` and `@blockcast/mmt-container` already\n * depend on, because the two apply it on opposite sides of a boundary that\n * does not otherwise share code:\n *\n * - container resolves it where `networkSource` semantics live, so a catalog\n * relay that names no port is reachable instead of handing a join a\n * portless relay that fails.\n * - transport applies it when reading a catalog, dialling a gateway, and\n * answering DRIAD \u2014 but deliberately not at every boundary. Which call\n * sites do and do not take the default is transport's business; the\n * reasoning lives beside those call sites, not here.\n */\nexport const DEFAULT_AMT_RELAY_PORT = 2268;\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", "import { compareNtpShort, isNtpShort } from \"@blockcast/mmt-base\";\nimport { carriesMedia, endpointKey, selectMulticastTier, type MulticastTierEndpoint } from \"./multicast-tier.js\";\n\nexport type OrchestratorState = \"stable\" | \"joining\";\nexport type OrchestratorCommand =\n\t| { kind: \"join-target\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint }\n\t| { kind: \"commit-swap\"; target: MulticastTierEndpoint; old: MulticastTierEndpoint }\n\t| { kind: \"abandon-join\"; current: MulticastTierEndpoint; target: MulticastTierEndpoint };\n\nexport interface MulticastTierPacket {\n\tendpointKey: string;\n\tkeyframe: boolean;\n\t/**\n\t * The packet's MMTP header `timestamp`: its UTC sending instant in NTP short\n\t * format (ISO/IEC 23008-1 \u00A79.2.2), which wraps every 2^16 s (~18.2 h). The\n\t * orchestrator orders these only with RFC 1982 serial arithmetic\n\t * (`compareNtpShort`), never with `<` or `Math.max`.\n\t *\n\t * A non-finite value means the host could not read a header; that packet\n\t * neither raises the high-water mark nor commits. Any other value that is\n\t * not NTP short is a host bug and throws a RangeError.\n\t */\n\ttimestamp: number;\n\thasValidCodecConfig: boolean;\n}\n\nexport interface OrchestratorOptions {\n\tendpoints: readonly MulticastTierEndpoint[];\n\tinitialEndpoint?: MulticastTierEndpoint;\n\tadaptive?: boolean;\n\tnow: () => number;\n\tdwellMs?: number;\n\tjoinDeadlineMs?: number;\n}\n\nexport interface GoodputSample { bitrate: number | undefined; }\nexport interface HeadroomSample { healthy: boolean; }\n\nexport const DEFAULT_DWELL_MS = 3_000;\nexport const DEFAULT_JOIN_DEADLINE_MS = 5_000;\n\nexport function goodputFromStats(\n\tprevious: { bytesReceived: number; atMs: number } | undefined,\n\tcurrent: { bytesReceived: number; atMs: number },\n\tminSampleMs = 500,\n): number | undefined {\n\tif (!previous) return undefined;\n\tconst durationMs = current.atMs - previous.atMs;\n\tconst bytes = current.bytesReceived - previous.bytesReceived;\n\tif (durationMs < minSampleMs || bytes <= 0) return undefined;\n\treturn (bytes * 8 * 1000) / durationMs;\n}\n\nexport class MulticastOrchestrator {\n\t#state: OrchestratorState = \"stable\";\n\t#current: MulticastTierEndpoint | undefined;\n\t#target: MulticastTierEndpoint | undefined;\n\t#estimate: number | undefined;\n\t#favored: { key: string; since: number; source: \"goodput\" | \"headroom\" } | undefined;\n\t#joinStartedAt = 0;\n\t/**\n\t * Latest active-tier send time (NTP short), or undefined before the first.\n\t * \"Latest\" is serial order, not the numerically largest value: after the\n\t * field wraps, the largest value is the stale one.\n\t */\n\t#highWater: number | undefined;\n\treadonly #dwellMs: number;\n\treadonly #joinDeadlineMs: number;\n\n\tconstructor(private readonly options: OrchestratorOptions) {\n\t\tthis.#dwellMs = options.dwellMs ?? DEFAULT_DWELL_MS;\n\t\tthis.#joinDeadlineMs = options.joinDeadlineMs ?? DEFAULT_JOIN_DEADLINE_MS;\n\t\tif (!Number.isFinite(this.#dwellMs) || !Number.isFinite(this.#joinDeadlineMs) ||\n\t\t\tthis.#dwellMs <= 0 || this.#joinDeadlineMs <= 0) {\n\t\t\tthrow new Error(\"Multicast dwell and join deadline must be positive\");\n\t\t}\n\t}\n\n\tget state(): OrchestratorState { return this.#state; }\n\tget current(): MulticastTierEndpoint | undefined { return this.#current; }\n\tget target(): MulticastTierEndpoint | undefined { return this.#target; }\n\tget estimate(): number | undefined { return this.#estimate; }\n\n\tstart(): MulticastTierEndpoint | undefined {\n\t\tthis.#current ??= this.options.initialEndpoint ?? selectMulticastTier(this.options.endpoints, undefined);\n\t\treturn this.#current;\n\t}\n\n\tonGoodput(sample: GoodputSample): OrchestratorCommand | undefined {\n\t\tconst bitrate = sample.bitrate;\n\t\tif (!this.#current) return undefined;\n\t\t// Before the adaptive gate: a selected join runs with adaptation off, and a\n\t\t// target that delivers nothing reaches its deadline only on this clock.\n\t\tif (this.#state === \"joining\") return this.poll();\n\t\tif (this.options.adaptive === false) return undefined;\n\t\tif (bitrate === undefined || !Number.isFinite(bitrate)) return undefined;\n\t\tthis.#estimate = bitrate;\n\t\tconst desired = selectMulticastTier(this.options.endpoints, bitrate);\n\t\tif (!desired) return undefined;\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (typeof desired.bandwidth !== \"number\" || typeof this.#current.bandwidth !== \"number\" ||\n\t\t\tdesired.bandwidth >= this.#current.bandwidth) {\n\t\t\tif (this.#favored?.source === \"goodput\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey || this.#favored.source !== \"goodput\") {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"goodput\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\tonHeadroom(sample: HeadroomSample): OrchestratorCommand | undefined {\n\t\tif (!this.#current || this.options.adaptive === false || this.#state === \"joining\") return undefined;\n\t\tif (!sample.healthy) {\n\t\t\tif (this.#favored?.source === \"headroom\") this.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.#favored?.source === \"goodput\") return undefined;\n\n\t\tconst currentBandwidth = this.#current.bandwidth;\n\t\tif (typeof currentBandwidth !== \"number\") return undefined;\n\t\t// Ranked here rather than through selectMulticastTier: up-selection\n\t\t// advances exactly one adjacent rung, where the selector fits a budget to\n\t\t// an estimate. The media filter is shared so a repair-only endpoint above\n\t\t// the current rung is not joined as an upgrade, which would swap the\n\t\t// receiver onto repair symbols and no media.\n\t\tconst desired = this.options.endpoints\n\t\t\t.filter((endpoint) =>\n\t\t\t\tcarriesMedia(endpoint) && typeof endpoint.bandwidth === \"number\" && endpoint.bandwidth > currentBandwidth,\n\t\t\t)\n\t\t\t.sort((a, b) => a.bandwidth! - b.bandwidth!)[0];\n\t\tif (!desired) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst desiredKey = endpointKey(desired);\n\t\tif (desiredKey === endpointKey(this.#current)) {\n\t\t\tthis.#favored = undefined;\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = this.#now();\n\t\tif (!this.#favored || this.#favored.key !== desiredKey) {\n\t\t\tthis.#favored = { key: desiredKey, since: now, source: \"headroom\" };\n\t\t\treturn undefined;\n\t\t}\n\t\tif (now - this.#favored.since < this.#dwellMs) return undefined;\n\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = desired;\n\t\tthis.#joinStartedAt = now;\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target: desired };\n\t}\n\n\t/**\n\t * Join `target` because the viewer selected it, not because goodput or\n\t * headroom ranked it. It runs whether or not the orchestrator adapts, and\n\t * takes the path an adaptive join takes: commit on the target's RAP at or\n\t * beyond the high-water mark, abandon at the join deadline. While it is\n\t * pending nothing re-ranks the ladder; goodput only advances the deadline.\n\t *\n\t * Returns undefined, changing nothing, while another join is pending (cancel\n\t * it first), when `target` is the current tier, or when it carries no media.\n\t */\n\tselect(target: MulticastTierEndpoint): OrchestratorCommand | undefined {\n\t\tif (!this.#current || this.#state === \"joining\" || !carriesMedia(target)) return undefined;\n\t\tif (endpointKey(target) === endpointKey(this.#current)) return undefined;\n\t\tthis.#state = \"joining\";\n\t\tthis.#target = target;\n\t\tthis.#joinStartedAt = this.#now();\n\t\tthis.#favored = undefined;\n\t\treturn { kind: \"join-target\", current: this.#current, target };\n\t}\n\n\t/**\n\t * Feed a packet from the active tier (to advance the high-water mark) or\n\t * from the in-flight target (to find a committable RAP).\n\t *\n\t * Serial order is only the true order while the compared instants are less\n\t * than `NTP_SHORT_HALF_RANGE` (2^15 s, ~9.1 h) apart. Feeding every\n\t * active-tier packet keeps the mark that close to the live stream. A mark\n\t * refreshed only by commits can fall further behind than that, and a\n\t * target RAP may then compare as older, so the join is abandoned.\n\t */\n\tonTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\tif (!this.#current) return undefined;\n\t\tconst sentAt = tierPacketSendTime(packet);\n\t\t// An instant exactly half the space from the mark (RFC 1982's undefined\n\t\t// case) is neither later nor earlier, so it leaves the mark alone, as a\n\t\t// reordered earlier packet does.\n\t\tif (packet.endpointKey === endpointKey(this.#current) && sentAt !== undefined &&\n\t\t\t(this.#highWater === undefined || compareNtpShort(sentAt, this.#highWater) === \"after\")) {\n\t\t\tthis.#highWater = sentAt;\n\t\t}\n\t\tif (this.#state !== \"joining\" || !this.#target) return undefined;\n\t\tconst expired = this.poll();\n\t\tif (expired) return expired;\n\t\tif (\n\t\t\tpacket.endpointKey !== endpointKey(this.#target) ||\n\t\t\t!packet.keyframe ||\n\t\t\t!packet.hasValidCodecConfig ||\n\t\t\tsentAt === undefined ||\n\t\t\t!this.#reachesHighWater(sentAt)\n\t\t) return undefined;\n\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"commit-swap\",\n\t\t\ttarget: this.#target,\n\t\t\told: this.#current,\n\t\t};\n\t\tthis.#highWater = sentAt;\n\t\tthis.#current = this.#target;\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\tpoll(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tif (this.#now() - this.#joinStartedAt < this.#joinDeadlineMs) return undefined;\n\t\treturn this.cancel();\n\t}\n\n\tcancel(): OrchestratorCommand | undefined {\n\t\tif (this.#state !== \"joining\" || !this.#target || !this.#current) return undefined;\n\t\tconst command: OrchestratorCommand = {\n\t\t\tkind: \"abandon-join\",\n\t\t\tcurrent: this.#current,\n\t\t\ttarget: this.#target,\n\t\t};\n\t\tthis.#target = undefined;\n\t\tthis.#state = \"stable\";\n\t\treturn command;\n\t}\n\n\t/**\n\t * Whether a target RAP sent at `sentAt` is at or beyond the active tier's\n\t * high-water mark. A pair exactly half the NTP-short space apart has no\n\t * serial order, so it cannot show that and does not commit.\n\t */\n\t#reachesHighWater(sentAt: number): boolean {\n\t\tif (this.#highWater === undefined) return true;\n\t\tconst order = compareNtpShort(sentAt, this.#highWater);\n\t\treturn order === \"after\" || order === \"equal\";\n\t}\n\n\t#now(): number {\n\t\tconst now = this.options.now();\n\t\tif (!Number.isFinite(now)) throw new Error(\"Multicast orchestrator clock must be finite\");\n\t\treturn now;\n\t}\n}\n\n/** A tier packet's NTP-short send time, or undefined when the host had no header to read. */\nfunction tierPacketSendTime(packet: MulticastTierPacket): number | undefined {\n\tif (!Number.isFinite(packet.timestamp)) return undefined;\n\tif (!isNtpShort(packet.timestamp)) {\n\t\tthrow new RangeError(\n\t\t\t`Multicast tier packet timestamp must be an MMTP NTP-short send time, got ${packet.timestamp}`,\n\t\t);\n\t}\n\treturn packet.timestamp;\n}\n", "import { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID, admittedMulticastEndpoints } from \"@blockcast/mmt-base\";\nimport type {\n\tWindowMulticastSubscribeConfig,\n\tWindowMulticastSubscribeConfigBytes,\n} from \"./types.js\";\n\nexport type MulticastTransportPreference = \"auto\" | \"native\" | \"tunnel\";\n\nexport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\nexport interface MulticastEndpointConfig {\n\tgroupAddress: string;\n\tsourceAddress?: string;\n\t/** ASM sender filter (`auth.senderAddress` of a protected ASM endpoint); never beside `sourceAddress`. */\n\tsenderAddress?: string;\n\tport: number;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\t/** `\"driad\"`: with no relay, the gateway discovers one per RFC 8777. */\n\tamtDiscovery?: \"driad\";\n}\n\n/**\n * Input accepted by the canonical window.multicast request converter.\n *\n * `mode` and the `amtRelay*` fields are internal endpoint-selection state;\n * they are deliberately not emitted on the page-facing request. The\n * resulting `origin` is the only acquisition selector understood by the\n * gateway.\n */\nexport interface WindowMulticastEndpointInput {\n\tgroup: string;\n\tport: number;\n\tsource?: string;\n\t/** ASM sender filter; emitted as the request's `senderAddress`. Refused beside `source`. */\n\tsenderAddress?: string;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\t/** `\"driad\"`: with no relay, the gateway discovers one per RFC 8777. */\n\tamtDiscovery?: \"driad\";\n\tpacketIds?: readonly number[];\n\ttracks?: ReadonlyArray<{ name: string; packetId: number }>;\n\t/**\n\t * ATSC 3.0 tuning from the catalog's `atsc3` networkSource\n\t * (`catalogAtsc3NetworkSource`). Draft -01 3 puts the tuner first: when set,\n\t * the request is the tuner member and relay/mode state is not consulted.\n\t */\n\tatsc3?: { frequency: number; plpId?: number; serviceId?: number };\n}\n\n/**\n * Convert internal endpoint-selection state to the canonical page contract.\n *\n * Native is the default and an explicit `native` selection always wins over\n * stale relay metadata. Tunnel/AMT requests must carry the resolved relay\n * and port; there is no implicit relay-port default at this boundary.\n *\n * That rule is deliberate and is NOT the place to fix a relay that names no\n * port: this is the last hop before the page-facing request, so a default\n * applied here would be invisible to every caller and to the join it produces.\n * A catalog relay with no port gets `DEFAULT_AMT_RELAY_PORT` upstream, in the\n * readers that resolve it, and arrives here explicit like any other\n * (BLO-38176).\n */\nexport function toWindowMulticastSubscribeConfig(\n\tinput: WindowMulticastEndpointInput,\n): WindowMulticastSubscribeConfig {\n\tif (input.mode !== undefined && ![\"auto\", \"native\", \"tunnel\"].includes(input.mode)) {\n\t\tthrow new TypeError(`Unsupported multicast mode: ${String(input.mode)}`);\n\t}\n\t// An explicit non-array (the null \"endpoint unusable\" sentinel from\n\t// selectEndpointPacketIdsForTracks) must not degrade into \"no filter\".\n\tif (input.packetIds !== undefined && !Array.isArray(input.packetIds)) {\n\t\tthrow new TypeError(\n\t\t\t`multicast packetIds must be an array, got ${input.packetIds === null ? \"null\" : typeof input.packetIds}`,\n\t\t);\n\t}\n\t// Every join request passes through here, so the track packet_id range is\n\t// enforced here rather than trusted from each caller.\n\tif (input.packetIds?.some(id => !isMulticastTrackPacketId(id)) ||\n\t\tinput.tracks?.some(track => !isMulticastTrackPacketId(track.packetId))) {\n\t\tthrow new RangeError(\n\t\t\t`multicast packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n\t\t\t\"packetId 0 is the MMTP signaling flow and is never a track's\",\n\t\t);\n\t}\n\n\tconst source = input.source?.trim() || undefined;\n\tconst senderAddress = input.senderAddress?.trim() || undefined;\n\t// Draft -01 7.2.6: S is sourceAddress on SSM and senderAddress on ASM, never both.\n\tif (source && senderAddress) {\n\t\tthrow new TypeError(\"multicast senderAddress is an ASM filter and never appears beside source\");\n\t}\n\tconst base = {\n\t\tgroup: input.group,\n\t\tport: input.port,\n\t\t...(source ? { source } : {}),\n\t\tconsume: \"bytes\" as const,\n\t\t...(input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}),\n\t};\n\tconst filter = senderAddress ? { senderAddress } : {};\n\n\tif (input.atsc3) {\n\t\t// The contract's tuner member carries no sender filter.\n\t\tif (senderAddress) throw new TypeError(\"an ATSC3 tuner request carries no senderAddress\");\n\t\tconst { frequency, plpId, serviceId } = input.atsc3;\n\t\tif (!Number.isInteger(frequency) || frequency < 1) {\n\t\t\tthrow new TypeError(`ATSC3 tuning requires a positive integer frequency in kHz, got ${String(frequency)}`);\n\t\t}\n\t\treturn {\n\t\t\t...base,\n\t\t\torigin: \"tuner\",\n\t\t\tstandard: \"atsc3\",\n\t\t\tfrequency,\n\t\t\t...(plpId !== undefined ? { plpId } : {}),\n\t\t\t...(serviceId !== undefined ? { serviceId } : {}),\n\t\t};\n\t}\n\n\tconst relay = input.amtRelay?.trim();\n\t// A relay wins over DRIAD: it is a commitment to that relay.\n\tconst driad = !relay && input.amtDiscovery === \"driad\";\n\tconst wantsAmt = input.mode === \"tunnel\" ||\n\t\t(input.mode !== \"native\" && ((relay !== undefined && relay !== \"\") || driad));\n\tif (!wantsAmt) {\n\t\tconst native: WindowMulticastSubscribeConfigBytes = {\n\t\t\t...base,\n\t\t\t...filter,\n\t\t\torigin: \"ip-multicast\",\n\t\t\t...(input.tracks?.length ? { tracks: input.tracks.map(({ name, packetId }) => ({ name, packetId })) } : {}),\n\t\t};\n\t\treturn native;\n\t}\n\n\tconst relayPort = input.amtRelayPort;\n\tif (driad) {\n\t\tif (!base.source) throw new TypeError(\"AMT DRIAD discovery requires a source address\");\n\t\tif (relayPort !== undefined && !isPositivePort(relayPort)) {\n\t\t\tthrow new TypeError(\"AMT DRIAD discovery requires a valid relay port when one is given\");\n\t\t}\n\t\treturn {\n\t\t\t...base,\n\t\t\tsource: base.source,\n\t\t\torigin: \"amt\",\n\t\t\tdiscovery: \"driad\",\n\t\t\t...(relayPort !== undefined ? { relayPort } : {}),\n\t\t};\n\t}\n\tif (!relay || relayPort === undefined || !isPositivePort(relayPort)) {\n\t\tthrow new TypeError(\"AMT requires a resolved relay and a valid relay port\");\n\t}\n\treturn {\n\t\t...base,\n\t\t...filter,\n\t\torigin: \"amt\",\n\t\trelay,\n\t\trelayPort,\n\t};\n}\n\nexport function isValidMulticastGroupAddress(address: string | undefined): boolean {\n\tconst value = address?.trim().toLowerCase();\n\tif (!value) return false;\n\tif (value.startsWith(\"ff\")) return isValidIpv6MulticastAddress(value);\n\n\tconst parts = value.split(\".\");\n\tif (parts.length !== 4) return false;\n\tconst octets = parts.map((part) => {\n\t\tif (!/^\\d+$/.test(part)) return Number.NaN;\n\t\tconst octet = Number(part);\n\t\treturn Number.isInteger(octet) && octet >= 0 && octet <= 255 ? octet : Number.NaN;\n\t});\n\treturn octets.every(Number.isInteger) && octets[0] >= 224 && octets[0] <= 239;\n}\n\nexport function normalizeMulticastEndpoint(\n\tendpoint: MulticastEndpointConfig,\n): MulticastEndpointConfig | undefined {\n\tconst groupAddress = endpoint.groupAddress.trim().toLowerCase();\n\tconst sourceAddress = endpoint.sourceAddress?.trim().toLowerCase() || undefined;\n\tconst senderAddress = endpoint.senderAddress?.trim().toLowerCase() || undefined;\n\tif (sourceAddress && senderAddress) return undefined;\n\tconst amtRelay = endpoint.amtRelay?.trim().toLowerCase() || undefined;\n\tif (!isValidMulticastGroupAddress(groupAddress) || !isPositivePort(endpoint.port)) return undefined;\n\tif (endpoint.mode && ![\"auto\", \"native\", \"tunnel\"].includes(endpoint.mode)) return undefined;\n\tif (endpoint.amtRelayPort !== undefined && !isPositivePort(endpoint.amtRelayPort)) return undefined;\n\n\treturn {\n\t\tgroupAddress,\n\t\t...(sourceAddress ? { sourceAddress } : {}),\n\t\t...(senderAddress ? { senderAddress } : {}),\n\t\tport: endpoint.port,\n\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t\t...(endpoint.amtDiscovery === \"driad\" ? { amtDiscovery: \"driad\" as const } : {}),\n\t};\n}\n\n/** Stable ownership key for one IWA multicast/AMT subscription. */\nexport function multicastEndpointKey(endpoint: MulticastEndpointConfig): string {\n\tconst normalized = normalizeMulticastEndpoint(endpoint);\n\tif (!normalized) throw new Error(\"Invalid multicast endpoint\");\n\treturn JSON.stringify([\n\t\tnormalized.sourceAddress ?? \"\",\n\t\tnormalized.groupAddress,\n\t\tnormalized.port,\n\t\tnormalized.mode ?? \"auto\",\n\t\tnormalized.amtRelay ?? \"\",\n\t\tnormalized.amtRelayPort ?? \"\",\n\t\t// A DRIAD join is a different gateway subscription from a native one.\n\t\t...(normalized.amtDiscovery ? [normalized.amtDiscovery] : []),\n\t\t// An ASM sender filter is a different subscription from the unfiltered join.\n\t\t...(normalized.senderAddress ? [{ senderAddress: normalized.senderAddress }] : []),\n\t]);\n}\n\n/**\n * True when `value` may be advertised as a multicast track's MMTP `packet_id`:\n * an integer in 1..MMTP_PACKET_ID_MAX. packet_id 0 (MMTP_SIGNALING_PACKET_ID)\n * is the MMTP signaling flow -- the FFmpeg `moq_mmt` muxer sends the AL-FEC\n * message (0x0203) on it into the same group as the media -- so it is never a\n * track's. Only a number qualifies: a string, boolean or null is not coerced.\n */\nexport function isMulticastTrackPacketId(value: unknown): value is number {\n\treturn Number.isInteger(value) &&\n\t\t(value as number) > MMTP_SIGNALING_PACKET_ID &&\n\t\t(value as number) <= MMTP_PACKET_ID_MAX;\n}\n\n/**\n * True when a receiver may join `endpoint`: a multicast group, a usable port\n * and, given the `catalog` it came from, admission under draft -01 4.1 / 7.2.6\n * (`admittedUnderCatalog`). Without a catalog object only the first two are checked.\n */\nexport function isUsableMulticastEndpoint(\n\tendpoint: { groupAddress?: string; port?: unknown } | undefined,\n\tcatalog?: unknown,\n): boolean {\n\tconst port = Number(endpoint?.port);\n\treturn isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port) &&\n\t\t// Not a catalog (an `Array#filter` index, say): checks the first two only.\n\t\t(catalog === null || typeof catalog !== \"object\" || admittedUnderCatalog(catalog, endpoint));\n}\n\n/**\n * Whether one endpoint survives the draft -01 receiver rules in the context\n * of `catalog`: its mode and `S` agree, it carries `auth` exactly when the\n * catalog's `multicast` object does, and that catalog holds a key set. The\n * rules are `multicastCatalogIssues` of `@blockcast/mmt-base`, the same the\n * container validator throws on. Cross-endpoint rules need the whole array;\n * `admittedEndpointsUnderCatalog` applies those.\n */\nexport function admittedUnderCatalog(catalog: unknown, endpoint: unknown): boolean {\n\treturn admittedEndpointsUnderCatalog(catalog, [endpoint]).has(0);\n}\n\n/**\n * Indices of `endpoints` that survive every draft -01 receiver rule as the\n * endpoint array of `catalog`, cross-endpoint rules included: both sides of a\n * shared `(S, G, port)` are dropped.\n */\nexport function admittedEndpointsUnderCatalog(catalog: unknown, endpoints: readonly unknown[]): ReadonlySet<number> {\n\tconst root = catalog !== null && typeof catalog === \"object\" ? (catalog as Record<string, unknown>) : {};\n\tconst multicast = root[\"multicast\"] !== null && typeof root[\"multicast\"] === \"object\"\n\t\t? (root[\"multicast\"] as Record<string, unknown>)\n\t\t: {};\n\treturn admittedMulticastEndpoints({ ...root, multicast: { ...multicast, endpoints } });\n}\n\n/**\n * Unprefixed aliases of the `mc_` URL override keys, retired in BLO-37507, each\n * paired with the key that replaces it.\n */\nconst RETIRED_MULTICAST_URL_PARAMS = [\n\t[\"amt_relay\", \"mc_amt_relay\"],\n\t[\"amt_relay_port\", \"mc_amt_relay_port\"],\n] as const;\n\n/**\n * Reject a URL multicast override that still carries a retired unprefixed alias.\n *\n * Both URL readers call this only once a valid override is established (a valid\n * multicast `mc_group` and a valid `mc_port`; `readUrlMulticastConfig` also\n * requires the whole endpoint to normalize), which is where they used to read\n * the aliases: a link that\n * relied on one fails here with its replacement named, instead of silently\n * losing its AMT relay (and, in `readUrlMulticastConfig`, its tunnel mode).\n * Presence is the error, whatever the value and whether or not the `mc_` form\n * is also present: quietly preferring one of two keys is the tolerant read being\n * retired. Outside an override the readers read no override key at all, the\n * `mc_` forms included, so the bare names are left to the page; they are live\n * page parameters elsewhere, and keeping clear of those is what the `mc_`\n * namespace is for.\n */\nexport function assertNoRetiredMulticastUrlParams(params: URLSearchParams): void {\n\tfor (const [retired, replacement] of RETIRED_MULTICAST_URL_PARAMS) {\n\t\tif (params.has(retired)) {\n\t\t\tthrow new Error(\n\t\t\t\t`multicast URL override: query parameter \"${retired}\" is retired; use \"${replacement}\"`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * Read an explicit multicast endpoint override from `mc_*` URL parameters.\n * Throws when an override carries a retired unprefixed alias\n * (`assertNoRetiredMulticastUrlParams`).\n */\nexport function readUrlMulticastConfig(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): MulticastEndpointConfig | undefined {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst groupAddress = params.get(\"mc_group\") || \"\";\n\tconst port = positivePort(params.get(\"mc_port\"));\n\tif (!groupAddress || !port) return undefined;\n\n\tconst amtRelay = params.get(\"mc_amt_relay\") || undefined;\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\"));\n\tconst requestedMode = params.get(\"mc_mode\");\n\tconst mode = amtRelay ? \"tunnel\" : multicastMode(requestedMode);\n\n\tconst endpoint = normalizeMulticastEndpoint({\n\t\tgroupAddress,\n\t\tsourceAddress: params.get(\"mc_source\") || undefined,\n\t\tport,\n\t\t...(mode ? { mode } : {}),\n\t\t...(amtRelay ? { amtRelay } : {}),\n\t\t...(amtRelayPort ? { amtRelayPort } : {}),\n\t});\n\t// An endpoint that does not normalize (e.g. a non-multicast mc_group) is no\n\t// override, so the page keeps its own bare names; the same scope\n\t// readPlayerMulticastConfig applies by validating the group first.\n\tif (endpoint) assertNoRetiredMulticastUrlParams(params);\n\treturn endpoint;\n}\n\nexport function isMulticastExplicitlyDisabled(\n\tsearch: string | URLSearchParams = typeof location === \"undefined\" ? \"\" : location.search,\n): boolean {\n\tconst params = typeof search === \"string\" ? new URLSearchParams(search) : search;\n\tconst multicast = params.get(\"multicast\")?.trim().toLowerCase();\n\tconst transport = params.get(\"transport\")?.trim().toLowerCase();\n\treturn transport === \"moq-only\" || [\"0\", \"false\", \"no\", \"off\"].includes(multicast || \"\");\n}\n\nfunction isValidIpv6MulticastAddress(value: string): boolean {\n\tif (!/^[0-9a-f:]+$/.test(value)) return false;\n\tconst compressed = value.includes(\"::\");\n\tif (value.indexOf(\"::\") !== value.lastIndexOf(\"::\")) return false;\n\tconst groups = value.split(\":\").filter(Boolean);\n\tif (groups.length === 0 || groups.length > 8) return false;\n\tif (!compressed && groups.length !== 8) return false;\n\treturn groups.every((group) => /^[0-9a-f]{1,4}$/.test(group));\n}\n\nfunction multicastMode(value: string | null): MulticastTransportPreference | undefined {\n\treturn value === \"native\" || value === \"tunnel\" || value === \"auto\" ? value : undefined;\n}\n\nfunction positivePort(value: string | null): number | undefined {\n\tconst port = Number(value);\n\treturn isPositivePort(port) ? port : undefined;\n}\n\nfunction isPositivePort(port: number): boolean {\n\treturn Number.isInteger(port) && port > 0 && port <= 65535;\n}\n", "import type { WindowMulticastSubscription } from \"./types.js\";\n\n/**\n * True when `subscription` delivers packets in batches through the `packets`\n * provider extension (BLO-36599).\n *\n * The pim window-multicast-bridge (0.2.0 and later) advertises the extension\n * in `eventTypes`. It builds only the events some listener asked for, so a\n * consumer that listens to `packets` alone gets one event per reader batch\n * instead of one DOM event per packet. A provider without the field, or one\n * that does not list `packets`, is a v1 provider: listen to `bytes`.\n */\nexport function subscriptionDeliversPacketBatches(\n\tsubscription: Pick<WindowMulticastSubscription, \"eventTypes\">,\n): boolean {\n\treturn subscription.eventTypes?.includes(\"packets\") === true;\n}\n", "import {\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.js\";\nimport { subscriptionDeliversPacketBatches } from \"./packet-batch.js\";\nimport type {\n\tTransportConfig,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscription,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\n\nexport interface MulticastSubscriptionConsumer {\n\tonBytes(data: ArrayBuffer): void;\n\tonState?(state: WindowMulticastSubscriptionState): void;\n\tonError?(error: Error): void;\n}\n\ninterface PoolEntry {\n\tsubscription: WindowMulticastSubscription;\n\tconsumers: Map<symbol, MulticastSubscriptionConsumer>;\n\t/**\n\t * Exactly one packet surface is listened to: `packets` when the provider\n\t * advertises it, `bytes` otherwise (BLO-36599).\n\t */\n\tpacketListener:\n\t\t| { type: \"bytes\"; listener: (event: CustomEvent<ArrayBuffer>) => void }\n\t\t| { type: \"packets\"; listener: (event: CustomEvent<readonly ArrayBuffer[]>) => void };\n\tstateListener: (event: CustomEvent<WindowMulticastSubscriptionState>) => void;\n\terrorListener: (event: CustomEvent<unknown>) => void;\n\tclosed: boolean;\n}\n\ninterface PoolRecord { claims: number; entry: Promise<PoolEntry>; }\n\nexport interface MulticastSubscriptionLease {\n\treadonly key: string;\n\trelease(): Promise<void>;\n}\n\n/** Shares only IWA multicast/AMT subscriptions. Unicast sessions remain per manager. */\nexport class MulticastSubscriptionPool {\n\treadonly #entries = new Map<string, PoolRecord>();\n\treadonly #gatewayIds = new WeakMap<object, number>();\n\t#nextGatewayId = 1;\n\n\tasync acquire(\n\t\tgateway: WindowMulticastGateway,\n\t\tconfig: TransportConfig,\n\t\tconsumer: MulticastSubscriptionConsumer,\n\t): Promise<MulticastSubscriptionLease> {\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t\tamtDiscovery: config.amtDiscovery,\n\t\t});\n\t\tif (!endpoint) throw new Error(\"Invalid multicast endpoint\");\n\t\tif (typeof gateway.subscribe !== \"function\") throw new Error(\"window.multicast.subscribe unavailable\");\n\n\t\tconst key = multicastEndpointKey(endpoint);\n\t\tconst internalKey = `${this.#gatewayId(gateway)}|${key}`;\n\t\tlet record = this.#entries.get(internalKey);\n\t\tif (!record) {\n\t\t\trecord = { claims: 0, entry: Promise.resolve(null as unknown as PoolEntry) };\n\t\t\tconst createdRecord = record;\n\t\t\trecord.entry = this.#createEntry(gateway, endpoint, (entry) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t}).catch((error) => {\n\t\t\t\tif (this.#entries.get(internalKey) === createdRecord) this.#entries.delete(internalKey);\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\tthis.#entries.set(internalKey, record);\n\t\t}\n\n\t\trecord.claims++;\n\t\tlet entry: PoolEntry;\n\t\ttry {\n\t\t\tentry = await record.entry;\n\t\t} catch (error) {\n\t\t\trecord.claims--;\n\t\t\tthrow error;\n\t\t}\n\t\tif (entry.closed) {\n\t\t\trecord.claims--;\n\t\t\treturn this.acquire(gateway, config, consumer);\n\t\t}\n\t\tconst token = Symbol(key);\n\t\tentry.consumers.set(token, consumer);\n\t\tlet released = false;\n\t\treturn {\n\t\t\tkey,\n\t\t\trelease: async () => {\n\t\t\t\tif (released) return;\n\t\t\t\treleased = true;\n\t\t\t\tentry.consumers.delete(token);\n\t\t\t\trecord.claims--;\n\t\t\t\tif (record.claims > 0) return;\n\t\t\t\tif (this.#entries.get(internalKey) === record) this.#entries.delete(internalKey);\n\t\t\t\tthis.#detach(entry);\n\t\t\t\tif (!entry.closed) await entry.subscription.unsubscribe();\n\t\t\t},\n\t\t};\n\t}\n\n\tget size(): number { return this.#entries.size; }\n\n\tasync #createEntry(\n\t\tgateway: WindowMulticastGateway,\n\t\tendpoint: NonNullable<ReturnType<typeof normalizeMulticastEndpoint>>,\n\t\tonClosed: (entry: PoolEntry) => void,\n\t): Promise<PoolEntry> {\n\t\tconst subscription = await gateway.subscribe!(toWindowMulticastSubscribeConfig({\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\t...(endpoint.sourceAddress ? { source: endpoint.sourceAddress } : {}),\n\t\t\tport: endpoint.port,\n\t\t\t...(endpoint.mode ? { mode: endpoint.mode } : {}),\n\t\t\t...(endpoint.amtRelay ? { amtRelay: endpoint.amtRelay } : {}),\n\t\t\t...(endpoint.amtRelayPort ? { amtRelayPort: endpoint.amtRelayPort } : {}),\n\t\t\t...(endpoint.amtDiscovery ? { amtDiscovery: endpoint.amtDiscovery } : {}),\n\t\t}));\n\t\tconst consumers = new Map<symbol, MulticastSubscriptionConsumer>();\n\t\tlet entry: PoolEntry;\n\t\t// A batch is delivered as the same per-packet onBytes calls the bytes\n\t\t// surface makes, in arrival order and with each call isolated, so a\n\t\t// consumer cannot tell the two apart; only the DOM event per packet is\n\t\t// gone. The batch array and its elements are the provider's own, uncopied.\n\t\tconst packetListener: PoolEntry[\"packetListener\"] = subscriptionDeliversPacketBatches(subscription)\n\t\t\t? {\n\t\t\t\t\ttype: \"packets\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tfor (const data of event.detail) {\n\t\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(data));\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\ttype: \"bytes\",\n\t\t\t\t\tlistener: (event) => {\n\t\t\t\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));\n\t\t\t\t\t},\n\t\t\t\t};\n\t\tconst stateListener = (event: CustomEvent<WindowMulticastSubscriptionState>) => {\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onState?.(event.detail));\n\t\t\tif (event.detail === \"closed\" && !entry.closed) {\n\t\t\t\tentry.closed = true;\n\t\t\t\tonClosed(entry);\n\t\t\t}\n\t\t};\n\t\tconst errorListener = (event: CustomEvent<unknown>) => {\n\t\t\tconst error = event.detail instanceof Error ? event.detail : new Error(String(event.detail));\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onError?.(error));\n\t\t};\n\t\tentry = { subscription, consumers, packetListener, stateListener, errorListener, closed: false };\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tsubscription.addEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tsubscription.addEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tsubscription.addEventListener(\"state\", stateListener);\n\t\tsubscription.addEventListener(\"error\", errorListener);\n\t\treturn entry;\n\t}\n\n\t#gatewayId(gateway: WindowMulticastGateway): number {\n\t\tconst object = gateway as object;\n\t\tlet id = this.#gatewayIds.get(object);\n\t\tif (!id) {\n\t\t\tid = this.#nextGatewayId++;\n\t\t\tthis.#gatewayIds.set(object, id);\n\t\t}\n\t\treturn id;\n\t}\n\n\t#detach(entry: PoolEntry): void {\n\t\tconst { packetListener } = entry;\n\t\tif (packetListener.type === \"packets\") {\n\t\t\tentry.subscription.removeEventListener(\"packets\", packetListener.listener);\n\t\t} else {\n\t\t\tentry.subscription.removeEventListener(\"bytes\", packetListener.listener);\n\t\t}\n\t\tentry.subscription.removeEventListener(\"state\", entry.stateListener);\n\t\tentry.subscription.removeEventListener(\"error\", entry.errorListener);\n\t}\n\n\t#dispatch(\n\t\tconsumers: Map<symbol, MulticastSubscriptionConsumer>,\n\t\tcallback: (consumer: MulticastSubscriptionConsumer) => void,\n\t): void {\n\t\tfor (const consumer of consumers.values()) {\n\t\t\ttry { callback(consumer); } catch (error) { console.error(\"[Transport] multicast consumer callback failed\", error); }\n\t\t}\n\t}\n}\n\nexport const multicastSubscriptionPool = new MulticastSubscriptionPool();\n", "/**\n * @blockcast/transport - Transport Manager\n *\n * Unified transport abstraction for MoQ players supporting:\n * - window.multicast gateway subscriptions\n * - AMT proxy tunneling (RFC 7450) with optional DRIAD relay discovery\n * - WebTransport/QUIC (MoQ-lite protocol)\n * - WebSocket fallback\n *\n * Transport promotion hierarchy (WebTransport-first for fast startup):\n * 1. WebTransport - immediate connection for fast channel select\n * 2. Background discovery - check if window.multicast is available\n * 3. Promote to gateway multicast if available\n * 4. WebSocket fallback if WebTransport unavailable\n *\n * Features:\n * - Progressive adaptive bitrate based on network conditions\n * - FEC (Forward Error Correction) for burst loss recovery\n * - Seamless transport switching with session continuity\n * - Unified FEC/ABR pipeline across all transport modes\n */\n\nimport type {\n\tTransportConfig,\n\tTransportConnectConfig,\n\tTransportSubscription,\n\tTransportCapabilities,\n\tTransportState,\n\tQualityLevel,\n\tWindowMulticastCapabilities,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\nimport { AdaptiveBitrateController } from \"./abr-controller.js\";\nimport {\n\tMulticastOrchestrator,\n\ttype MulticastTierPacket,\n\ttype OrchestratorCommand,\n\ttype OrchestratorOptions,\n} from \"./multicast-orchestrator.js\";\nimport type { MulticastTierEndpoint } from \"./multicast-tier.js\";\nimport { multicastSubscriptionPool } from \"./multicast-subscription-pool.js\";\nimport { normalizeMulticastEndpoint } from \"./endpoint-config.js\";\n\n/**\n * Transport Manager\n *\n * Manages transport connections with automatic mode selection:\n * WebTransport -> window.multicast -> AMT proxy -> WebSocket\n */\nexport class TransportManager {\n\tprivate state: TransportState = {\n\t\tmode: \"disconnected\",\n\t\tconnected: false,\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tlastPacketTime: 0,\n\t};\n\n\tprivate capabilities: TransportCapabilities | null = null;\n\tprivate subscription: TransportSubscription | null = null;\n\t/** Built by `connect()` from the caller's ladder; there is no built-in one. */\n\tprivate abr: AdaptiveBitrateController | null = null;\n\tprivate multicastOrchestrator: MulticastOrchestrator | null = null;\n\tprivate onOrchestratorCommand?: (command: OrchestratorCommand) => void;\n\tprivate connectionGeneration = 0;\n\n\tprivate onPacket?: (data: ArrayBuffer) => void;\n\tprivate onError?: (error: Error) => void;\n\tprivate onStateChange?: (state: TransportState) => void;\n\n\tprivate getWindow(): Window | undefined {\n\t\treturn typeof window === \"undefined\" ? undefined : window;\n\t}\n\n\tprivate getWindowMulticastGateway(): WindowMulticastGateway | undefined {\n\t\treturn this.getWindow()?.multicast;\n\t}\n\n\tprivate async getGatewayCapabilities(\n\t\tgateway: WindowMulticastGateway,\n\t): Promise<WindowMulticastCapabilities | undefined> {\n\t\tif (!gateway?.capabilities) return undefined;\n\t\ttry {\n\t\t\treturn typeof gateway.capabilities === \"function\" ?\n\t\t\t\tawait gateway.capabilities() :\n\t\t\t\tgateway.capabilities;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast capabilities failed:\", error);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async gatewaySupportsBytes(gateway: WindowMulticastGateway | undefined): Promise<boolean> {\n\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\t\tif (!gateway.capabilities) return true;\n\n\t\tconst capabilities = await this.getGatewayCapabilities(gateway);\n\t\tif (!capabilities) return false;\n\t\tconst consumeModes = capabilities.consumeModes;\n\t\treturn !consumeModes || consumeModes.includes(\"bytes\");\n\t}\n\n\t/**\n\t * Detect available transport capabilities\n\t */\n\tasync detectCapabilities(): Promise<TransportCapabilities> {\n\t\tif (this.capabilities) return this.capabilities;\n\n\t\tconst gateway = this.getWindowMulticastGateway();\n\t\tconst hasGatewayBytes = await this.gatewaySupportsBytes(gateway);\n\t\tconst win = this.getWindow();\n\n\t\tthis.capabilities = {\n\t\t\tnativeSSM: hasGatewayBytes,\n\t\t\tnativeIGMP: hasGatewayBytes,\n\t\t\tdirectSockets: typeof win?.UDPSocket !== \"undefined\",\n\t\t\tamt: false,\n\t\t\tdriad: hasGatewayBytes, // the gateway runs RFC 8777 discovery\n\t\t\tfecDecode: false,\n\t\t};\n\n\t\tconsole.log(\"[Transport] Capabilities:\", this.capabilities);\n\t\treturn this.capabilities;\n\t}\n\n\t/**\n\t * Connect using best available transport\n\t *\n\t * Promotion hierarchy:\n\t * 1. window.multicast gateway bytes\n\t * 2. AMT: a configured relay via the AMT proxy, else gateway DRIAD discovery\n\t * 3. MOQ WebTransport (final fallback)\n\t *\n\t * The ABR ladder (`abrLadder`) and initial rendition (`initialQuality`)\n\t * are checked first, before anything is torn down. A config that fails is\n\t * refused with the same shape as an invalid endpoint, the error to the\n\t * error callback and `false` resolved; unlike one, it is caught before the\n\t * live subscription closes, so a refused reconnect leaves that subscription,\n\t * its state and any connect already in flight untouched.\n\t */\n\tasync connect(config: TransportConnectConfig): Promise<boolean> {\n\t\tlet abr: AdaptiveBitrateController;\n\t\ttry {\n\t\t\tabr = new AdaptiveBitrateController({\n\t\t\t\tladder: config.abrLadder,\n\t\t\t\tinitialQuality: config.initialQuality?.name,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\t// A refused config is a caller bug. Without an error callback, say so\n\t\t\t// on the console, since `false` alone also means \"no multicast path\";\n\t\t\t// with one, the callback is the report.\n\t\t\tconst refused = error instanceof Error ? error : new Error(String(error));\n\t\t\tif (this.onError) this.onError(refused);\n\t\t\telse console.error(\"[Transport] ABR config refused, not connecting:\", refused);\n\t\t\treturn false;\n\t\t}\n\t\tconst generation = ++this.connectionGeneration;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// The previous connection's ladder ends with it, as in disconnect(): a\n\t\t// reconnect that bails below (invalid endpoint, or superseded while\n\t\t// detecting capabilities) leaves no ladder, and only the success path\n\t\t// installs `abr`.\n\t\tthis.abr = null;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t\tconst endpoint = normalizeMulticastEndpoint({\n\t\t\tgroupAddress: config.group,\n\t\t\tsourceAddress: config.source,\n\t\t\tport: config.port,\n\t\t\tmode: config.mode,\n\t\t\tamtRelay: config.amtRelay,\n\t\t\tamtRelayPort: config.amtRelayPort,\n\t\t});\n\t\tif (!endpoint) {\n\t\t\tconst error = new Error(\"Invalid multicast endpoint configuration\");\n\t\t\tthis.onError?.(error);\n\t\t\treturn false;\n\t\t}\n\t\tconfig = {\n\t\t\t...config,\n\t\t\tgroup: endpoint.groupAddress,\n\t\t\tsource: endpoint.sourceAddress ?? \"\",\n\t\t\tmode: endpoint.mode,\n\t\t\tamtRelay: endpoint.amtRelay,\n\t\t\tamtRelayPort: endpoint.amtRelayPort,\n\t\t};\n\t\tconst caps = await this.detectCapabilities();\n\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t// ABR adapts over the caller's ladder (the catalog's renditions),\n\t\t// validated above before anything was torn down.\n\t\tthis.abr = abr;\n\n\t\tabr.setCallback((quality) => {\n\t\t\tthis.state.quality = quality;\n\t\t\tthis.subscription?.onqualitychange?.(quality);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t});\n\n\t\t// Try transport hierarchy\n\t\tlet connected = false;\n\n\t\t// 1. Try the canonical browser multicast gateway.\n\t\tif (caps.nativeSSM || caps.nativeIGMP) {\n\t\t\tconsole.log(\"[Transport] Attempting window.multicast...\");\n\t\t\tconnected = await this.connectWindowMulticast(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"multicast\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 2. Try AMT (configured relay, else gateway DRIAD discovery)\n\t\tif (!connected) {\n\t\t\tconsole.log(\"[Transport] Attempting AMT tunnel...\");\n\t\t\tconnected = await this.connectAmt(config, generation);\n\t\t\tif (connected) {\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tthis.state.mode = \"amt\";\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback to MOQ\n\t\tconsole.log(\"[Transport] All multicast transports failed, falling back to MOQ\");\n\t\treturn false;\n\t}\n\n\t/**\n\t * Connect via the lowercase window.multicast bytes gateway.\n\t */\n\tprivate async connectWindowMulticast(\n\t\tconfig: TransportConfig,\n\t\tgeneration: number,\n\t\tmode: \"multicast\" | \"amt\" = \"multicast\",\n\t): Promise<boolean> {\n\t\ttry {\n\t\t\tconst gateway = this.getWindowMulticastGateway();\n\t\t\tif (typeof gateway?.subscribe !== \"function\") return false;\n\n\t\t\tconst lease = await multicastSubscriptionPool.acquire(gateway, config, {\n\t\t\t\tonBytes: (data) => {\n\t\t\t\t\tif (generation === this.connectionGeneration) this.handlePacket(data);\n\t\t\t\t},\n\t\t\t\tonState: (state: WindowMulticastSubscriptionState) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tthis.state.connected = state !== \"closed\";\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t},\n\t\t\t\tonError: (error) => {\n\t\t\t\t\tif (generation !== this.connectionGeneration) return;\n\t\t\t\t\tconsole.error(\"[Transport] window.multicast error:\", error);\n\t\t\t\t\tthis.onError?.(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\tawait lease.release();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tthis.subscription = {\n\t\t\t\tid: lease.key,\n\t\t\t\tconfig,\n\t\t\t\tclose: () => { void lease.release(); },\n\t\t\t};\n\n\t\t\tthis.state = {\n\t\t\t\tmode,\n\t\t\t\tconnected: true,\n\t\t\t\tgroup: config.group,\n\t\t\t\tsource: config.source,\n\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\tpacketsReceived: 0,\n\t\t\t\tbytesReceived: 0,\n\t\t\t\tlastPacketTime: 0,\n\t\t\t};\n\n\t\t\tconsole.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);\n\t\t\tthis.onStateChange?.(this.state);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] window.multicast failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via AMT: a configured relay goes through the AMT proxy; with no\n\t * relay, the window.multicast gateway discovers one per RFC 8777 (DRIAD).\n\t */\n\tprivate async connectAmt(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tif (config.amtRelay) return await this.connectAmtProxy(config, config.amtRelay, generation);\n\t\t\tif (config.driadEnabled === false || !config.source || config.mode === \"native\") {\n\t\t\t\tconsole.warn(\"[Transport] No AMT relay available (DRIAD requires source address for SSM)\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconsole.log(`[Transport] Asking the gateway for a DRIAD relay for source ${config.source}...`);\n\t\t\treturn await this.connectWindowMulticast({ ...config, amtDiscovery: \"driad\" }, generation, \"amt\");\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT connection failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Connect via WebSocket-based AMT proxy (browser fallback)\n\t */\n\tprivate async connectAmtProxy(config: TransportConfig, amtRelay: string, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tconst relayHost = amtRelay.includes(\":\") && !amtRelay.startsWith(\"[\") ? `[${amtRelay}]` : amtRelay;\n\t\t\t// amtRelayPort is the relay's RFC 7450 UDP port, not the proxy's HTTPS port.\n\t\t\tconst wsUrl = new URL(`wss://${relayHost}/amt-proxy`);\n\t\t\twsUrl.searchParams.set(\"group\", config.group);\n\t\t\twsUrl.searchParams.set(\"source\", config.source || \"\");\n\t\t\twsUrl.searchParams.set(\"port\", String(config.port));\n\n\t\t\tconst ws = new WebSocket(wsUrl);\n\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tws.binaryType = \"arraybuffer\";\n\t\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\t\tws.close();\n\t\t\t\t\treject(new Error(\"AMT proxy connection timeout\"));\n\t\t\t\t}, 10000);\n\n\t\t\t\tws.onopen = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tws.close();\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);\n\n\t\t\t\t\tthis.subscription = {\n\t\t\t\t\t\tid: `amt-proxy-${Date.now()}`,\n\t\t\t\t\t\tconfig,\n\t\t\t\t\t\tclose: () => ws.close(),\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.state = {\n\t\t\t\t\t\tmode: \"amt\",\n\t\t\t\t\t\tconnected: true,\n\t\t\t\t\t\tgroup: config.group,\n\t\t\t\t\t\tsource: config.source,\n\t\t\t\t\t\tamtRelay,\n\t\t\t\t\t\tquality: this.requireAbr().getCurrentQuality(),\n\t\t\t\t\t\tpacketsReceived: 0,\n\t\t\t\t\t\tbytesReceived: 0,\n\t\t\t\t\t\tlastPacketTime: 0,\n\t\t\t\t\t};\n\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(true);\n\t\t\t\t};\n\n\t\t\t\tws.onmessage = (event) => {\n\t\t\t\t\tif (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {\n\t\t\t\t\t\tthis.handlePacket(event.data);\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tws.onerror = (error) => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.error(\"[Transport] AMT proxy error:\", error);\n\t\t\t\t\treject(error);\n\t\t\t\t};\n\n\t\t\t\tws.onclose = () => {\n\t\t\t\t\tclearTimeout(timeout);\n\t\t\t\t\tif (generation !== this.connectionGeneration) {\n\t\t\t\t\t\tresolve(false);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.state.connected = false;\n\t\t\t\t\tthis.onStateChange?.(this.state);\n\t\t\t\t\tresolve(false);\n\t\t\t\t};\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[Transport] AMT proxy failed:\", error);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Handle received packet\n\t */\n\tprivate handlePacket(data: ArrayBuffer) {\n\t\tthis.state.packetsReceived++;\n\t\tthis.state.bytesReceived += data.byteLength;\n\t\tthis.state.lastPacketTime = Date.now();\n\n\t\t// Update ABR with sample\n\t\t// Packets reach here only through a connect()ed join, which built the ABR.\n\t\tthis.abr?.recordSample(data.byteLength, 0);\n\n\t\t// Forward to callback\n\t\tthis.onPacket?.(data);\n\t}\n\n\t/**\n\t * Get current transport state\n\t */\n\tgetState(): TransportState {\n\t\treturn { ...this.state };\n\t}\n\n\t/**\n\t * The rung of the live connection's `abrLadder` ABR is on; `null` when\n\t * there is no ladder (before `connect(config)` succeeds, after\n\t * `disconnect()`, or after a reconnect that was torn down and bailed).\n\t */\n\tgetQuality(): QualityLevel | null {\n\t\treturn this.abr?.getCurrentQuality() ?? null;\n\t}\n\n\t/**\n\t * Force the rung named `name` of the live connection's `abrLadder`.\n\t * Throws `Error` when there is no ladder (see `getQuality()`), and\n\t * `RangeError` when the ladder has no rung of that name; it never falls\n\t * back to another rung.\n\t */\n\tsetQuality(name: string) {\n\t\tthis.requireAbr().setQuality(name);\n\t}\n\n\t/** The controller `connect()` built; before it there is no ladder to adapt over. */\n\tprivate requireAbr(): AdaptiveBitrateController {\n\t\tif (!this.abr) throw new Error(\"[Transport] ABR has no ladder before connect(config)\");\n\t\treturn this.abr;\n\t}\n\n\t/** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */\n\tconfigureMulticastTiers(\n\t\toptions: OrchestratorOptions,\n\t\tonCommand?: (command: OrchestratorCommand) => void,\n\t): MulticastTierEndpoint | undefined {\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = new MulticastOrchestrator(options);\n\t\tthis.onOrchestratorCommand = onCommand;\n\t\treturn this.multicastOrchestrator.start();\n\t}\n\n\trecordMulticastGoodput(bitrate: number | undefined): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));\n\t}\n\n\trecordMulticastHeadroom(healthy: boolean): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onHeadroom({ healthy }));\n\t}\n\n\trecordMulticastTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));\n\t}\n\n\tpollMulticastTierSwitch(): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());\n\t}\n\n\t/** Abandon the in-flight tier join, e.g. because the host could not join the target. */\n\tcancelMulticastTierJoin(): OrchestratorCommand | undefined {\n\t\treturn this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t}\n\n\t/**\n\t * Set packet callback\n\t */\n\tsetPacketCallback(callback: (data: ArrayBuffer) => void) {\n\t\tthis.onPacket = callback;\n\t}\n\n\t/**\n\t * Set error callback\n\t */\n\tsetErrorCallback(callback: (error: Error) => void) {\n\t\tthis.onError = callback;\n\t}\n\n\t/**\n\t * Set state change callback\n\t */\n\tsetStateCallback(callback: (state: TransportState) => void) {\n\t\tthis.onStateChange = callback;\n\t}\n\n\t/**\n\t * Disconnect\n\t */\n\tdisconnect() {\n\t\tthis.connectionGeneration++;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = null;\n\t\t// No ladder after a disconnect, as before the first connect():\n\t\t// getQuality() is null and setQuality() throws until connect(config).\n\t\tthis.abr = null;\n\t\tthis.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());\n\t\tthis.multicastOrchestrator = null;\n\t\tthis.onOrchestratorCommand = undefined;\n\t\tthis.state = {\n\t\t\tmode: \"disconnected\",\n\t\t\tconnected: false,\n\t\t\tpacketsReceived: 0,\n\t\t\tbytesReceived: 0,\n\t\t\tlastPacketTime: 0,\n\t\t};\n\t\tthis.onStateChange?.(this.state);\n\t}\n\n\tprivate dispatchOrchestratorCommand(command: OrchestratorCommand | undefined): OrchestratorCommand | undefined {\n\t\tif (command) this.onOrchestratorCommand?.(command);\n\t\treturn command;\n\t}\n}\n\n// Export singleton instance\nexport const transportManager = new TransportManager();\n", "const MAX_MOQT_PRIORITY = 0xff;\n\ndeclare const subscriberPriorityBrand: unique symbol;\ndeclare const publisherPriorityBrand: unique symbol;\n\nexport type MoqtSubscriberPriority = number & { readonly [subscriberPriorityBrand]: true };\nexport type MoqtPublisherPriority = number & { readonly [publisherPriorityBrand]: true };\n\nfunction moqtPriority(value: number, field: string): number {\n\tif (!Number.isInteger(value) || value < 0 || value > MAX_MOQT_PRIORITY) {\n\t\tthrow new RangeError(`${field} must be an integer in [0, ${MAX_MOQT_PRIORITY}], got ${value}`);\n\t}\n\treturn value;\n}\n\n/** Validate a protocol-native MOQT subscriber priority without transforming it. */\nexport function moqtSubscriberPriority(value: number): MoqtSubscriberPriority {\n\treturn moqtPriority(value, \"MOQT subscriber priority\") as MoqtSubscriberPriority;\n}\n\n/** Validate a protocol-native MOQT publisher priority without transforming it. */\nexport function moqtPublisherPriority(value: number): MoqtPublisherPriority {\n\treturn moqtPriority(value, \"MOQT publisher priority\") as MoqtPublisherPriority;\n}\n\n/** Shared media subscription policy on MOQT's native lower-number-wins scale. */\nexport const MOQT_SUBSCRIBER_PRIORITY = Object.freeze({\n\tcatalog: moqtSubscriberPriority(0),\n\t/**\n\t * @deprecated A repair FETCH's SUBSCRIBER_PRIORITY is derived per request\n\t * from the catalog source and repair priorities (draft-ramadan-moq-fec\n\t * \u00A711.4.5), not a player constant. Use the `subscriberPriority` that\n\t * `@blockcast/mmt-container`'s `wireRepairFetch` puts on each\n\t * `RepairFetchRequest` (`repairFetchSubscriberPriority`).\n\t */\n\tdeadlineRepair: moqtSubscriberPriority(0),\n\taudio: moqtSubscriberPriority(1),\n\tvideo: moqtSubscriberPriority(2),\n\t/**\n\t * @deprecated See `deadlineRepair`: use the request's catalog-derived\n\t * `RepairFetchRequest.subscriberPriority` (draft-ramadan-moq-fec \u00A711.4.5).\n\t */\n\tbackgroundRepair: moqtSubscriberPriority(3),\n});\n\n/** Publisher priority bands from draft-ramadan-moq-fec section 10. */\nexport const MOQT_PUBLISHER_PRIORITY = Object.freeze({\n\tcatalog: moqtPublisherPriority(32),\n\tsource: moqtPublisherPriority(128),\n\trepair: moqtPublisherPriority(240),\n});\n", "/**\n * @blockcast/transport - AMT Gateway\n *\n * Consolidated AMT (Automatic Multicast Tunneling) implementation\n * supporting both Service Worker and Direct Sockets approaches.\n *\n * RFC 7450: Automatic Multicast Tunneling\n */\n\nimport type { GroupKey, PacketMetadata, AmtRelayInfo } from \"./types.js\";\nimport { DEFAULT_AMT_RELAY_PORT } from \"@blockcast/mmt-base\";\n\n/** AMT message types */\nconst AMT_MESSAGE_TYPES = {\n\tRELAY_DISCOVERY: 1,\n\tRELAY_ADVERTISEMENT: 2,\n\tREQUEST: 3,\n\tMEMBERSHIP_QUERY: 4,\n\tMEMBERSHIP_UPDATE: 5,\n\tMULTICAST_DATA: 6,\n\tTEARDOWN: 7,\n} as const;\n\n/** AMT gateway states */\nconst GATEWAY_STATES = {\n\tIDLE: \"idle\",\n\tDISCOVERING: \"discovering\",\n\tREQUESTING: \"requesting\",\n\tQUERYING: \"querying\",\n\tACTIVE: \"active\",\n\tLEAVING: \"leaving\",\n\tCLOSED: \"closed\",\n} as const;\n\ntype GatewayState = (typeof GATEWAY_STATES)[keyof typeof GATEWAY_STATES];\n\n/** AMT subscription configuration */\nexport interface AMTSubscriptionConfig {\n\tkey: GroupKey;\n\trelayAddress?: string;\n\trelayPort?: number;\n\tonPacket: (data: ArrayBuffer, metadata: PacketMetadata) => void;\n\tonError: (error: Error) => void;\n\t/**\n\t * A bc-provenance protected flow. Refused: this manager stamps the relay,\n\t * not the datagram's sender, so it cannot discard a datagram whose source\n\t * is not S (multicast-02 7.2.7). Join protected flows through window.multicast.\n\t */\n\tprotected?: boolean;\n}\n\n/** AMT packet info */\nexport interface AMTPacketInfo {\n\tdata: Uint8Array;\n\tsrcAddress: string;\n\tdstAddress: string;\n\tsrcPort: number;\n\tdstPort: number;\n\tgroupKey: string;\n\ttimestamp: number;\n}\n\n/** AMT Gateway options */\nexport interface AMTGatewayOptions {\n\trelayAddress: string;\n\trelayPort?: number;\n\tonPacket?: (packet: AMTPacketInfo) => void;\n\tonStateChange?: (newState: GatewayState, oldState: GatewayState) => void;\n\tonError?: (error: Error) => void;\n\tonLog?: (message: string) => void;\n}\n\n/** Active AMT subscription */\ninterface AMTSubscription {\n\tkey: GroupKey;\n\tsubscriptionId: string;\n\tconfig: AMTSubscriptionConfig;\n\trelay?: AmtRelayInfo;\n\thandshakeComplete: boolean;\n\tkeepAliveTimer?: ReturnType<typeof setInterval>;\n\tlastKeepAlive?: number;\n}\n\n/** Check if address is IPv4 */\nfunction isIPv4(address: string): boolean {\n\treturn /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(address);\n}\n\n/**\n * AMT Gateway Manager (Service Worker based)\n *\n * Manages AMT tunnel subscriptions using Service Worker for UDP communication.\n * Handles the AMT handshake and packet decapsulation. It needs a configured\n * relay: DRIAD (RFC 8777) discovery is the window.multicast gateway's job.\n */\nexport class AMTGatewayManager {\n\tprivate subscriptions = new Map<string, AMTSubscription>();\n\tprivate messageHandler?: (event: MessageEvent) => void;\n\tprivate pendingPackets = new Map<string, Array<{ data: ArrayBuffer; remoteAddress: string; remotePort: number }>>();\n\n\tconstructor() {\n\t\t// Set up message handler for packets from service worker\n\t\tthis.messageHandler = (event: MessageEvent) => {\n\t\t\tconst data = event.data as { type: string; subscriptionId?: string; data?: unknown; error?: string; message?: string };\n\t\t\tconsole.log(`[AMT] SW message received:`, data.type);\n\t\t\tif (data.type === \"UDP_PACKET\") {\n\t\t\t\tthis.handlePacket(data);\n\t\t\t} else if (data.type === \"SOCKET_ERROR\") {\n\t\t\t\tthis.handleSocketError(data);\n\t\t\t} else if (data.type === \"DEBUG_LOG\") {\n\t\t\t\tconsole.log(`[AMT SW Debug] ${data.message}`);\n\t\t\t}\n\t\t};\n\n\t\t// Register message handler\n\t\tif (typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.addEventListener(\"message\", this.messageHandler);\n\t\t\tconsole.log(\"[AMT] Registered service worker message handler\");\n\t\t} else {\n\t\t\tconsole.warn(\"[AMT] Service worker not available\");\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe via AMT tunnel\n\t */\n\tasync subscribe(config: AMTSubscriptionConfig): Promise<string> {\n\t\tconst { key } = config;\n\t\tif (config.protected) {\n\t\t\tthrow new Error(\"AMTGatewayManager refuses a protected join: it reports no per-datagram sender (multicast-02 7.2.7)\");\n\t\t}\n\t\tconst keyStr = this.getKeyString(key);\n\n\t\tif (this.subscriptions.has(keyStr)) {\n\t\t\tconsole.log(`[AMT] Reusing existing subscription to ${keyStr}`);\n\t\t\treturn keyStr;\n\t\t}\n\n\t\ttry {\n\t\t\tconst relayAddress = config.relayAddress;\n\t\t\tconst relayPort = config.relayPort || DEFAULT_AMT_RELAY_PORT;\n\t\t\tif (!relayAddress) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`AMT relay address required: this manager does no relay discovery. ` +\n\t\t\t\t\t\t`For RFC 8777 DRIAD discovery, join through window.multicast with amtDiscovery: \"driad\".`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsole.log(`[AMT] Using configured relay ${relayAddress}:${relayPort}`);\n\n\t\t\tconst subscriptionId = keyStr;\n\n\t\t\t// Initialize packet queue for handshake\n\t\t\tthis.pendingPackets.set(subscriptionId, []);\n\n\t\t\t// Create subscription\n\t\t\tconst subscription: AMTSubscription = {\n\t\t\t\tkey,\n\t\t\t\tsubscriptionId,\n\t\t\t\tconfig,\n\t\t\t\thandshakeComplete: false,\n\t\t\t\trelay: {\n\t\t\t\t\taddress: relayAddress,\n\t\t\t\t\tport: relayPort,\n\t\t\t\t\tdiscoveryMethod: \"manual\",\n\t\t\t\t\tdiscoveredAt: Date.now(),\n\t\t\t\t},\n\t\t\t};\n\n\t\t\tthis.subscriptions.set(keyStr, subscription);\n\n\t\t\t// Note: Actual UDP socket creation and handshake would require\n\t\t\t// Service Worker communication - this is a simplified version\n\t\t\tconsole.log(`[AMT] Subscription created for ${keyStr} via ${relayAddress}:${relayPort}`);\n\n\t\t\treturn keyStr;\n\t\t} catch (error) {\n\t\t\tconst subscription = this.subscriptions.get(keyStr);\n\t\t\tif (subscription) {\n\t\t\t\tthis.subscriptions.delete(keyStr);\n\t\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t\t}\n\n\t\t\tthrow new Error(`Failed to subscribe via AMT: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Unsubscribe from AMT tunnel\n\t */\n\tasync unsubscribe(handle: string): Promise<void> {\n\t\tconst subscription = this.subscriptions.get(handle);\n\t\tif (!subscription) {\n\t\t\tthrow new Error(`Subscription ${handle} not found`);\n\t\t}\n\n\t\ttry {\n\t\t\t// Stop keep-alive timer\n\t\t\tif (subscription.keepAliveTimer) {\n\t\t\t\tclearInterval(subscription.keepAliveTimer);\n\t\t\t}\n\n\t\t\tconsole.log(`[AMT] Unsubscribed from ${handle}`);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[AMT] Error during unsubscribe:\", error);\n\t\t} finally {\n\t\t\tthis.subscriptions.delete(handle);\n\t\t\tthis.pendingPackets.delete(subscription.subscriptionId);\n\t\t}\n\t}\n\n\t/**\n\t * Get list of active subscriptions\n\t */\n\tgetSubscriptions(): string[] {\n\t\treturn Array.from(this.subscriptions.keys());\n\t}\n\n\t/**\n\t * Handle packet from service worker\n\t */\n\tprivate handlePacket(message: { subscriptionId?: string; data?: unknown }): void {\n\t\tconst { subscriptionId, data } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received packet for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// During handshake, queue packets\n\t\tif (!subscription.handshakeComplete) {\n\t\t\tconst queue = this.pendingPackets.get(subscriptionId);\n\t\t\tconst packetData = data as { data: ArrayBuffer; remoteAddress: string; remotePort: number };\n\t\t\tif (queue) {\n\t\t\t\tqueue.push({\n\t\t\t\t\tdata: packetData.data,\n\t\t\t\t\tremoteAddress: packetData.remoteAddress,\n\t\t\t\t\tremotePort: packetData.remotePort,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// After handshake, forward to callback\n\t\tconst packetData = data as { data: ArrayBuffer };\n\t\tconst metadata: PacketMetadata = {\n\t\t\tsourceAddress: subscription.relay?.address || \"unknown\",\n\t\t\tsourcePort: subscription.relay?.port || 0,\n\t\t\ttransport: \"tunnel\",\n\t\t\ttimestamp: performance.now(),\n\t\t};\n\n\t\tsubscription.config.onPacket(packetData.data, metadata);\n\t}\n\n\t/**\n\t * Handle socket error from service worker\n\t */\n\tprivate handleSocketError(message: { subscriptionId?: string; error?: string }): void {\n\t\tconst { subscriptionId, error } = message;\n\t\tif (!subscriptionId) return;\n\n\t\tconst subscription = this.subscriptions.get(subscriptionId);\n\t\tif (!subscription) {\n\t\t\tconsole.warn(`[AMT] Received error for unknown subscription: ${subscriptionId}`);\n\t\t\treturn;\n\t\t}\n\n\t\tsubscription.config.onError(new Error(error || \"Unknown error\"));\n\t}\n\n\t/**\n\t * Cleanup on destroy\n\t */\n\tdestroy(): void {\n\t\tif (this.messageHandler && typeof navigator !== \"undefined\" && navigator.serviceWorker) {\n\t\t\tnavigator.serviceWorker.removeEventListener(\"message\", this.messageHandler);\n\t\t}\n\n\t\tconst handles = Array.from(this.subscriptions.keys());\n\t\tfor (const handle of handles) {\n\t\t\tthis.unsubscribe(handle).catch((err) => {\n\t\t\t\tconsole.error(`[AMT] Error unsubscribing ${handle}:`, err);\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Get key string for HashMap\n\t */\n\tprivate getKeyString(key: GroupKey): string {\n\t\tconst source = key.source ? `@${key.source}` : \"\";\n\t\treturn `${key.group}:${key.port}${source}`;\n\t}\n}\n\n/**\n * AMT Gateway Client (Direct Sockets based)\n *\n * Connects to an AMT relay to receive multicast data via unicast tunnel.\n * Implements the gateway side of RFC 7450.\n * Supports multiple (S,G) groups over a single AMT tunnel.\n */\nexport class AMTGateway {\n\tprivate relayAddress: string;\n\tprivate relayPort: number;\n\tprivate groups = new Map<\n\t\tstring,\n\t\t{\n\t\t\tgroupPort: number;\n\t\t\tonPacket: ((packet: AMTPacketInfo) => void) | null;\n\t\t\tjoinedAt: number;\n\t\t}\n\t>();\n\n\t// Callbacks\n\tprivate onPacket: (packet: AMTPacketInfo) => void;\n\tprivate onStateChange: (newState: GatewayState, oldState: GatewayState) => void;\n\tprivate onError: (error: Error) => void;\n\tprivate onLog: (message: string) => void;\n\n\t// State\n\tprivate state: GatewayState = GATEWAY_STATES.IDLE;\n\tprivate nonce: Uint8Array | null = null;\n\tprivate responseMAC: Uint8Array | null = null;\n\tprivate relayConfirmedAddress: string | null = null;\n\tprivate lastDataTime = 0;\n\n\t// Stats\n\tprivate stats = {\n\t\tpacketsReceived: 0,\n\t\tbytesReceived: 0,\n\t\tqueriesReceived: 0,\n\t\tupdatesSet: 0,\n\t};\n\n\tconstructor(options: AMTGatewayOptions) {\n\t\tthis.relayAddress = options.relayAddress;\n\t\tthis.relayPort = options.relayPort || DEFAULT_AMT_RELAY_PORT;\n\n\t\tthis.onPacket = options.onPacket || (() => {});\n\t\tthis.onStateChange = options.onStateChange || (() => {});\n\t\tthis.onError = options.onError || ((err) => console.error(\"AMT Error:\", err));\n\t\tthis.onLog = options.onLog || ((msg) => console.log(\"AMT:\", msg));\n\t}\n\n\t/**\n\t * Generate random nonce\n\t */\n\tprivate generateNonce(): Uint8Array {\n\t\tconst nonce = new Uint8Array(4);\n\t\tcrypto.getRandomValues(nonce);\n\t\treturn nonce;\n\t}\n\n\t/**\n\t * Set gateway state\n\t */\n\tprivate setState(newState: GatewayState): void {\n\t\tconst oldState = this.state;\n\t\tthis.state = newState;\n\t\tthis.onLog(`State: ${oldState} -> ${newState}`);\n\t\tthis.onStateChange(newState, oldState);\n\t}\n\n\t/**\n\t * Join a multicast group\n\t */\n\tasync joinGroup(\n\t\tgroupAddress: string,\n\t\tgroupPort: number,\n\t\tsourceAddress: string | null = null,\n\t\tonPacket: ((packet: AMTPacketInfo) => void) | null = null,\n\t): Promise<string> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group already joined: ${groupKey}`);\n\t\t\treturn groupKey;\n\t\t}\n\n\t\tthis.groups.set(groupKey, {\n\t\t\tgroupPort,\n\t\t\tonPacket,\n\t\t\tjoinedAt: Date.now(),\n\t\t});\n\n\t\tthis.onLog(`Joined group: ${groupKey} (port ${groupPort})`);\n\t\treturn groupKey;\n\t}\n\n\t/**\n\t * Leave a multicast group\n\t */\n\tasync leaveGroup(groupAddress: string, sourceAddress: string | null = null): Promise<boolean> {\n\t\tconst groupKey = `${sourceAddress || \"*\"},${groupAddress}`;\n\n\t\tif (!this.groups.has(groupKey)) {\n\t\t\tthis.onLog(`Group not found: ${groupKey}`);\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.groups.delete(groupKey);\n\t\tthis.onLog(`Left group: ${groupKey} (${this.groups.size} remaining)`);\n\n\t\tif (this.groups.size === 0) {\n\t\t\tthis.onLog(\"No groups remaining - closing AMT tunnel\");\n\t\t\tawait this.close();\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Get list of active groups\n\t */\n\tgetGroups(): Array<{\n\t\tgroupKey: string;\n\t\tgroupAddress: string;\n\t\tsourceAddress: string | null;\n\t\tgroupPort: number;\n\t\tjoinedAt: number;\n\t}> {\n\t\treturn Array.from(this.groups.entries()).map(([key, info]) => {\n\t\t\tconst [source, group] = key.split(\",\");\n\t\t\treturn {\n\t\t\t\tgroupKey: key,\n\t\t\t\tgroupAddress: group,\n\t\t\t\tsourceAddress: source !== \"*\" ? source : null,\n\t\t\t\tgroupPort: info.groupPort,\n\t\t\t\tjoinedAt: info.joinedAt,\n\t\t\t};\n\t\t});\n\t}\n\n\t/**\n\t * Open connection to AMT relay\n\t */\n\tasync open(): Promise<void> {\n\t\tif (this.state !== GATEWAY_STATES.IDLE) {\n\t\t\tthrow new Error(`Cannot open in state: ${this.state}`);\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.DISCOVERING);\n\t\tthis.onLog(`Connecting to AMT relay ${this.relayAddress}:${this.relayPort}`);\n\t\tthis.onLog(`Active groups: ${this.groups.size}`);\n\n\t\t// Generate session nonce\n\t\tthis.nonce = this.generateNonce();\n\t\tthis.onLog(`Generated nonce: ${Array.from(this.nonce).map((b) => b.toString(16).padStart(2, \"0\")).join(\"\")}`);\n\n\t\t// Note: Actual UDPSocket connection would go here\n\t\t// This is a simplified version showing the state machine\n\t}\n\n\t/**\n\t * Close connection\n\t */\n\tasync close(): Promise<void> {\n\t\tif (this.state === GATEWAY_STATES.CLOSED) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState(GATEWAY_STATES.LEAVING);\n\n\t\t// Clear all groups\n\t\tthis.groups.clear();\n\n\t\tthis.setState(GATEWAY_STATES.CLOSED);\n\t\tthis.onLog(\"AMT Gateway closed\");\n\t}\n\n\t/**\n\t * Get current statistics\n\t */\n\tgetStats(): typeof this.stats {\n\t\treturn { ...this.stats };\n\t}\n\n\t/**\n\t * Get current state\n\t */\n\tgetState(): GatewayState {\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Check if gateway is active and receiving data\n\t */\n\tisActive(): boolean {\n\t\treturn this.state === GATEWAY_STATES.ACTIVE;\n\t}\n}\n\n// Export singleton manager instance\nexport const amtGatewayManager = new AMTGatewayManager();\n", "/**\n * Strict v2 multicast delivery catalog: the DASH/HLS/MMTP routes a receiver\n * joins, their SSM endpoints, FEC geometry and MMTP packet-ID bindings.\n *\n * Moved from moqtail-private `libs/moqtail-ts/src/model/catalog/delivery.ts`\n * (BLO-47003) so every player reads one contract. Two rules are stricter than\n * the copy it replaces, both libmmt invariants rather than local choices:\n * `fec.blockProfile` is required and must be `iso-ssbg1-v1` (BLO-35597), and a\n * DASH route's `mmtp.packetId` is refused when it is the signaling flow 0\n * (`isMulticastTrackPacketId`).\n */\nimport { parseFecBlockProfile, type FecBlockProfile } from '@blockcast/mmt-base'\nimport { isMulticastTrackPacketId } from '@blockcast/mmt-transport'\n\nexport type DeliveryPackaging = 'mmtp' | 'dash' | 'hls'\nexport type DeliveryContentType = 'video' | 'audio' | 'text'\n\nexport interface DeliveryEndpoint {\n source: string\n group: string\n port: number\n}\n\nexport interface DeliveryPolicy {\n bufferWindowSegments: number\n requestTimeoutMs: number\n missPolicy: 'fail'\n}\n\nexport interface DeliveryObjectMapping {\n namespace: string\n initObject: string\n mediaObjectTemplate: string\n}\n\nexport interface DeliveryFec {\n K: number\n /** Repair symbols per source block. Zero means source-only delivery. */\n P: number\n T: number\n /** Full FEC block span in milliseconds (draft-ramadan-moq-fec \u00A75.1). */\n interleaveDepthMs: number\n /**\n * The catalog `fec.blockProfile` (BLO-35597). Required and never\n * defaulted; `parseFecBlockProfile` refuses anything but `iso-ssbg1-v1`,\n * including the retired `k-padded-v1`.\n */\n blockProfile: FecBlockProfile\n}\n\nexport interface DeliveryRouteBase {\n id: string\n contentType: DeliveryContentType\n endpointRef: string\n initUri: string\n mediaUriTemplate: string\n objects: DeliveryObjectMapping\n fec: DeliveryFec\n}\n\nexport interface DashDeliveryRoute extends DeliveryRouteBase {\n representationId: string\n /**\n * Authoritative MMTP binding used to assemble the DASH objects carried by\n * this route. The current source accepts MFU-mode MMTP only; the init MPU is\n * the representation init segment. MMTP-backed DASH URI and object templates\n * contain exactly one $Number$ and may also contain $RepresentationID$;\n * $Time$ is rejected because the catalog has no publisher-authored first-time\n * anchor. Publishers explicitly anchor MMTP MPU sequence numbers to DASH\n * $Number$ values so receivers never infer object boundaries from cadence.\n */\n mmtp: {\n trackName: string\n packetId: number\n mode: 'mfu'\n codec: string\n /** fMP4 media timescale used by Shaka's MMTP transmuxer. */\n timescale: number\n /** Exact duration of one MFU in that timescale, from publisher timing. */\n mediaUnitDurationTicks: number\n /** Exact MFU count carried by each numbered DASH media object. */\n mediaUnitsPerObject: number\n /** First publisher MMTP MPU sequence represented by this route. */\n firstMpuSequenceNumber: number\n /** DASH $Number$ containing firstMpuSequenceNumber. */\n firstObjectNumber: number\n /** Consecutive MMTP MPU sequence numbers carried by each DASH object. */\n mpuSequencesPerObject: number\n /** Optional reliable repair-track name under objects.namespace. */\n repairTrack?: string\n }\n}\n\nexport interface HlsDeliveryRoute extends DeliveryRouteBase {\n variantId: string\n trackId: string\n}\n\nexport interface MmtpDeliveryRoute extends DeliveryRouteBase {\n trackName: string\n}\n\nexport interface DashDeliveryConfig {\n manifestUrl: string\n routes: DashDeliveryRoute[]\n}\n\nexport interface HlsDeliveryConfig {\n manifestUrl: string\n routes: HlsDeliveryRoute[]\n}\n\nexport interface MmtpDeliveryConfig {\n routes: MmtpDeliveryRoute[]\n}\n\nexport interface DeliveryMulticastBase {\n endpoints: Record<string, DeliveryEndpoint>\n deliveryPolicy: DeliveryPolicy\n}\n\nexport type DeliveryMulticastV2 = DeliveryMulticastBase &\n (\n | { packaging: 'dash'; dash: DashDeliveryConfig }\n | { packaging: 'hls'; hls: HlsDeliveryConfig }\n | { packaging: 'mmtp'; mmtp: MmtpDeliveryConfig }\n )\n\nexport interface DeliveryCatalogV2 {\n schemaVersion: 2\n channelId: string\n multicast: DeliveryMulticastV2\n}\n\nconst TEMPLATE_VARIABLE = /\\$([A-Za-z][A-Za-z0-9]*)\\$/g\nconst ALLOWED_TEMPLATE_VARIABLES = new Set(['Number', 'Time', 'RepresentationID', 'VariantID', 'TrackID'])\n\nfunction object(value: unknown, path: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new Error(`${path} must be an object`)\n }\n return value as Record<string, unknown>\n}\n\nfunction string(value: unknown, path: string): string {\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new Error(`${path} must be a non-empty string`)\n }\n return value\n}\n\nfunction positiveInteger(value: unknown, path: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new Error(`${path} must be a positive integer`)\n }\n return value\n}\n\nfunction nonNegativeInteger(value: unknown, path: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n throw new Error(`${path} must be a non-negative integer`)\n }\n return value\n}\n\nfunction nonNegativeSafeInteger(value: unknown, path: string): number {\n const parsed = nonNegativeInteger(value, path)\n if (!Number.isSafeInteger(parsed)) throw new Error(`${path} must be a safe integer`)\n return parsed\n}\n\nfunction mpuSequenceNumber(value: unknown, path: string): number {\n const parsed = nonNegativeSafeInteger(value, path)\n if (parsed > 0xffffffff) throw new Error(`${path} must be between 0 and 4294967295`)\n return parsed\n}\n\nfunction port(value: unknown, path: string): number {\n const parsed = positiveInteger(value, path)\n if (parsed > 65535) throw new Error(`${path} must be between 1 and 65535`)\n return parsed\n}\n\nfunction httpUrl(value: unknown, path: string): string {\n const raw = string(value, path)\n let parsed: URL\n try {\n parsed = new URL(raw)\n } catch {\n throw new Error(`${path} must be an absolute HTTP(S) URL`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${path} must be an absolute HTTP(S) URL`)\n }\n return raw\n}\n\nfunction templateVariables(template: string, path: string): Set<string> {\n const variables = new Set<string>()\n for (const match of template.matchAll(TEMPLATE_VARIABLE)) {\n const variable = match[1]\n if (variable === undefined || !ALLOWED_TEMPLATE_VARIABLES.has(variable)) {\n throw new Error(`${path} contains unsupported template variable $${variable ?? ''}$`)\n }\n variables.add(variable)\n }\n if (template.includes('$') && template.replace(TEMPLATE_VARIABLE, '').includes('$')) {\n throw new Error(`${path} contains a malformed template variable`)\n }\n return variables\n}\n\nfunction validateTemplatePair(mediaUriTemplate: string, mediaObjectTemplate: string, path: string): void {\n const uriVariables = templateVariables(mediaUriTemplate, `${path}.mediaUriTemplate`)\n const objectVariables = templateVariables(mediaObjectTemplate, `${path}.objects.mediaObjectTemplate`)\n if (uriVariables.size === 0) {\n throw new Error(`${path}.mediaUriTemplate must contain a segment template variable`)\n }\n if (\n uriVariables.size !== objectVariables.size ||\n [...uriVariables].some((variable) => !objectVariables.has(variable))\n ) {\n throw new Error(`${path} URI and object templates must use the same variables`)\n }\n}\n\nfunction validateMmtpDashTemplate(template: string, path: string): void {\n const variables = [...template.matchAll(TEMPLATE_VARIABLE)].map((match) => match[1])\n const unsupported = variables.find((variable) => variable !== 'Number' && variable !== 'RepresentationID')\n if (unsupported !== undefined) {\n throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported}$`)\n }\n if (variables.filter((variable) => variable === 'Number').length !== 1) {\n throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`)\n }\n}\n\nfunction validateMmtpDashTemplates(route: DeliveryRouteBase, path: string): void {\n validateMmtpDashTemplate(route.mediaUriTemplate, `${path}.mediaUriTemplate`)\n validateMmtpDashTemplate(route.objects.mediaObjectTemplate, `${path}.objects.mediaObjectTemplate`)\n}\n\nfunction validateEndpoint(value: unknown, path: string): DeliveryEndpoint {\n const endpoint = object(value, path)\n return {\n source: string(endpoint['source'], `${path}.source`),\n group: string(endpoint['group'], `${path}.group`),\n port: port(endpoint['port'], `${path}.port`),\n }\n}\n\nfunction validateEndpoints(value: unknown): Record<string, DeliveryEndpoint> {\n const endpoints = object(value, 'multicast.endpoints')\n const entries = Object.entries(endpoints)\n if (entries.length === 0) throw new Error('multicast.endpoints must not be empty')\n return Object.fromEntries(\n entries.map(([id, endpoint]) => [\n string(id, 'multicast endpoint id'),\n validateEndpoint(endpoint, `multicast.endpoints.${id}`),\n ]),\n )\n}\n\nfunction validateDeliveryPolicy(value: unknown): DeliveryPolicy {\n const policy = object(value, 'multicast.deliveryPolicy')\n const missPolicy = policy['missPolicy']\n if (missPolicy !== 'fail') {\n throw new Error(\"multicast.deliveryPolicy.missPolicy must be 'fail'\")\n }\n return {\n bufferWindowSegments: positiveInteger(\n policy['bufferWindowSegments'],\n 'multicast.deliveryPolicy.bufferWindowSegments',\n ),\n requestTimeoutMs: positiveInteger(policy['requestTimeoutMs'], 'multicast.deliveryPolicy.requestTimeoutMs'),\n missPolicy,\n }\n}\n\nfunction validateRouteBase(\n value: unknown,\n path: string,\n endpoints: Record<string, DeliveryEndpoint>,\n): DeliveryRouteBase {\n const route = object(value, path)\n const endpointRef = string(route['endpointRef'], `${path}.endpointRef`)\n if (!(endpointRef in endpoints)) throw new Error(`${path}.endpointRef does not resolve: ${endpointRef}`)\n\n const objects = object(route['objects'], `${path}.objects`)\n const fec = object(route['fec'], `${path}.fec`)\n if ('interleaveDepth' in fec || 'interleaveMs' in fec) {\n throw new Error(`${path}.fec.interleaveDepthMs is the only supported interleave field`)\n }\n const mediaUriTemplate = httpUrl(route['mediaUriTemplate'], `${path}.mediaUriTemplate`)\n const mediaObjectTemplate = string(objects['mediaObjectTemplate'], `${path}.objects.mediaObjectTemplate`)\n validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path)\n\n return {\n id: string(route['id'], `${path}.id`),\n contentType: validateContentType(route['contentType'], `${path}.contentType`),\n endpointRef,\n initUri: httpUrl(route['initUri'], `${path}.initUri`),\n mediaUriTemplate,\n objects: {\n namespace: string(objects['namespace'], `${path}.objects.namespace`),\n initObject: string(objects['initObject'], `${path}.objects.initObject`),\n mediaObjectTemplate,\n },\n fec: {\n K: positiveInteger(fec['K'], `${path}.fec.K`),\n P: nonNegativeInteger(fec['P'], `${path}.fec.P`),\n T: positiveInteger(fec['T'], `${path}.fec.T`),\n interleaveDepthMs: positiveInteger(fec['interleaveDepthMs'], `${path}.fec.interleaveDepthMs`),\n blockProfile: parseFecBlockProfile(fec['blockProfile'], `${path}.fec.blockProfile`),\n },\n }\n}\n\nfunction validateContentType(value: unknown, path: string): DeliveryContentType {\n if (value !== 'video' && value !== 'audio' && value !== 'text') {\n throw new Error(`${path} must be 'video', 'audio', or 'text'`)\n }\n return value\n}\n\nfunction templateShape(template: string): string {\n return template.replace(TEMPLATE_VARIABLE, '$variable$')\n}\n\nfunction validateRouteUniqueness(\n routes: DeliveryRouteBase[],\n selectorKeys: string[],\n rawRoutes: Record<string, unknown>[],\n): void {\n const ids = new Set<string>()\n const selectors = new Set<string>()\n const initUris = new Set<string>()\n const mediaShapes = new Set<string>()\n routes.forEach((route, index) => {\n if (ids.has(route.id)) throw new Error(`duplicate route id: ${route.id}`)\n ids.add(route.id)\n\n const selector = selectorKeys.map((key) => String(rawRoutes[index]?.[key])).join('\\u0000')\n if (selectors.has(selector)) throw new Error(`duplicate route selector: ${selectorKeys.join(', ')}`)\n selectors.add(selector)\n\n if (initUris.has(route.initUri)) throw new Error(`ambiguous init URI mapping: ${route.initUri}`)\n initUris.add(route.initUri)\n\n const mediaShape = templateShape(route.mediaUriTemplate)\n if (mediaShapes.has(mediaShape)) {\n throw new Error(`ambiguous media URI mapping: ${route.mediaUriTemplate}`)\n }\n mediaShapes.add(mediaShape)\n })\n}\n\nfunction validateRoutes(value: unknown, kind: 'dash', endpoints: Record<string, DeliveryEndpoint>): DashDeliveryRoute[]\nfunction validateRoutes(value: unknown, kind: 'hls', endpoints: Record<string, DeliveryEndpoint>): HlsDeliveryRoute[]\nfunction validateRoutes(value: unknown, kind: 'mmtp', endpoints: Record<string, DeliveryEndpoint>): MmtpDeliveryRoute[]\nfunction validateRoutes(\n value: unknown,\n kind: DeliveryPackaging,\n endpoints: Record<string, DeliveryEndpoint>,\n): (DashDeliveryRoute | HlsDeliveryRoute | MmtpDeliveryRoute)[] {\n if (!Array.isArray(value) || value.length === 0) throw new Error(`multicast.${kind}.routes must not be empty`)\n const rawRoutes = value.map((route, index) => object(route, `multicast.${kind}.routes[${index}]`))\n const routes = rawRoutes.map((route, index) => {\n const path = `multicast.${kind}.routes[${index}]`\n const base = validateRouteBase(route, path, endpoints)\n if (kind === 'dash') {\n validateMmtpDashTemplates(base, path)\n const mmtp = object(route['mmtp'], `${path}.mmtp`)\n if (mmtp['mode'] !== 'mfu') throw new Error(`${path}.mmtp.mode must be 'mfu'`)\n const mode = 'mfu' as const\n const packetId = mmtp['packetId']\n if (!isMulticastTrackPacketId(packetId)) {\n throw new Error(`${path}.mmtp.packetId must be an integer from 1 to 65535 (0 is the MMTP signaling flow)`)\n }\n const repairTrack = mmtp['repairTrack']\n if (repairTrack !== undefined && (typeof repairTrack !== 'string' || repairTrack.trim().length === 0)) {\n throw new Error(`${path}.mmtp.repairTrack must be a non-empty string when provided`)\n }\n const mediaUnitsPerObject = positiveInteger(mmtp['mediaUnitsPerObject'], `${path}.mmtp.mediaUnitsPerObject`)\n const firstMpuSequenceNumber = mpuSequenceNumber(\n mmtp['firstMpuSequenceNumber'],\n `${path}.mmtp.firstMpuSequenceNumber`,\n )\n const firstObjectNumber = nonNegativeSafeInteger(mmtp['firstObjectNumber'], `${path}.mmtp.firstObjectNumber`)\n const mpuSequencesPerObject = positiveInteger(mmtp['mpuSequencesPerObject'], `${path}.mmtp.mpuSequencesPerObject`)\n if (mpuSequencesPerObject > mediaUnitsPerObject) {\n throw new Error(`${path}.mmtp.mpuSequencesPerObject must not exceed mediaUnitsPerObject`)\n }\n if (firstMpuSequenceNumber + mpuSequencesPerObject - 1 > 0xffffffff) {\n throw new Error(`${path}.mmtp first object exceeds the MMTP MPU sequence-number range`)\n }\n return {\n ...base,\n representationId: string(route['representationId'], `${path}.representationId`),\n mmtp: {\n trackName: string(mmtp['trackName'], `${path}.mmtp.trackName`),\n packetId,\n mode,\n codec: string(mmtp['codec'], `${path}.mmtp.codec`),\n timescale: positiveInteger(mmtp['timescale'], `${path}.mmtp.timescale`),\n mediaUnitDurationTicks: positiveInteger(\n mmtp['mediaUnitDurationTicks'],\n `${path}.mmtp.mediaUnitDurationTicks`,\n ),\n mediaUnitsPerObject,\n firstMpuSequenceNumber,\n firstObjectNumber,\n mpuSequencesPerObject,\n ...(repairTrack === undefined ? {} : { repairTrack }),\n },\n }\n }\n if (kind === 'hls') {\n return {\n ...base,\n variantId: string(route['variantId'], `${path}.variantId`),\n trackId: string(route['trackId'], `${path}.trackId`),\n }\n }\n return { ...base, trackName: string(route['trackName'], `${path}.trackName`) }\n })\n validateRouteUniqueness(\n routes,\n kind === 'dash' ? ['representationId', 'contentType'] : kind === 'hls' ? ['variantId', 'trackId'] : ['trackName'],\n rawRoutes,\n )\n return routes\n}\n\nfunction validateV2Catalog(value: unknown): DeliveryCatalogV2 {\n const catalog = object(value, 'catalog')\n if (catalog['schemaVersion'] !== 2) throw new Error('catalog.schemaVersion must be 2')\n const multicast = object(catalog['multicast'], 'multicast')\n const packaging = multicast['packaging']\n if (packaging !== 'dash' && packaging !== 'hls' && packaging !== 'mmtp') {\n throw new Error(\"multicast.packaging must be 'dash', 'hls', or 'mmtp'\")\n }\n const endpoints = validateEndpoints(multicast['endpoints'])\n const deliveryPolicy = validateDeliveryPolicy(multicast['deliveryPolicy'])\n const common = {\n schemaVersion: 2 as const,\n channelId: string(catalog['channelId'], 'catalog.channelId'),\n }\n\n if (packaging === 'dash') {\n const dash = object(multicast['dash'], 'multicast.dash')\n return {\n ...common,\n multicast: {\n packaging,\n endpoints,\n deliveryPolicy,\n dash: {\n manifestUrl: httpUrl(dash['manifestUrl'], 'multicast.dash.manifestUrl'),\n routes: validateRoutes(dash['routes'], packaging, endpoints),\n },\n },\n }\n }\n if (packaging === 'hls') {\n const hls = object(multicast['hls'], 'multicast.hls')\n return {\n ...common,\n multicast: {\n packaging,\n endpoints,\n deliveryPolicy,\n hls: {\n manifestUrl: httpUrl(hls['manifestUrl'], 'multicast.hls.manifestUrl'),\n routes: validateRoutes(hls['routes'], packaging, endpoints),\n },\n },\n }\n }\n const mmtp = object(multicast['mmtp'], 'multicast.mmtp')\n return {\n ...common,\n multicast: {\n packaging,\n endpoints,\n deliveryPolicy,\n mmtp: { routes: validateRoutes(mmtp['routes'], packaging, endpoints) },\n },\n }\n}\n\n/** Parse and normalize the strict v2 delivery-catalog contract. */\nexport function normalizeDeliveryCatalog(value: unknown): DeliveryCatalogV2 {\n const catalog = object(value, 'catalog')\n if (catalog['schemaVersion'] === 2) return validateV2Catalog(catalog)\n if (catalog['schemaVersion'] === undefined) throw new Error('catalog.schemaVersion is required')\n throw new Error(`unsupported catalog.schemaVersion: ${String(catalog['schemaVersion'])}`)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["MPU_HEADER_SIZE", "MPU_LENGTH_FIELD_SIZE", "MPU_HEADER_REMAINDER_SIZE", "MPU_HEADER_SIZE", "MMTP_SIGNALING_PACKET_ID", "NTP_SHORT_BITS", "NTP_SHORT_MODULUS", "NTP_SHORT_HALF_RANGE", "FEC_BLOCK_PROFILES", "RUNTIME_FEC_RECEIVER", "MMTP_SIGNALING_PACKET_ID", "port", "entry", "object", "packetData"]
7
7
  }