@blockcast/mmt-container 2.0.0 → 2.0.1-main.57647cd30645
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.
- package/README.md +1 -1
- package/dist/browser-runtime.js +45 -12
- package/dist/browser-runtime.js.map +2 -2
- package/dist/delivery-catalog.js +3 -0
- package/dist/delivery-catalog.js.map +2 -2
- package/dist/mmtp-container-parser.no-modules.js +19 -10
- package/dist/mmtp-container-parser.no-modules.js.map +2 -2
- package/dist/multicast-manager.js +45 -12
- package/dist/multicast-manager.js.map +2 -2
- package/dist/multicast-transport.d.ts +8 -1
- package/dist/multicast-transport.d.ts.map +1 -1
- package/dist/multicast-transport.js +93 -2
- package/dist/multicast-transport.js.map +4 -4
- package/dist/no-modules.js +19 -10
- package/dist/no-modules.js.map +2 -2
- package/package.json +5 -5
- package/src/multicast-transport.ts +43 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../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", "../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/fec-bindings.ts", "../../mmt-manifest-verify/src/bc-provenance-v2.ts", "../src/multicast-transport.ts"],
|
|
4
|
-
"sourcesContent": ["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 * @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", "import { sha256 } from '@noble/hashes/sha256'\n\n/** Fixed-width bytes before a record's digest list. */\nexport const BCPV2_HEADER_LENGTH = 68\nexport const BCPV2_SIGNATURE_LENGTH = 64\nexport const BCPV2_MIN_RECORD_LENGTH = BCPV2_HEADER_LENGTH + 32 + BCPV2_SIGNATURE_LENGTH\nexport const BCPV2_MAX_RECORD_LENGTH = 1316\nexport const BCPV2_MAX_DIGESTS = 37\nexport const BCPV2_HASH_ALG_SHA256 = 1\n\nexport interface BcProvenanceV2Endpoint {\n /** S in the AMBI pseudoheader and signature context. */\n sourceAddress: string\n /** G in the AMBI pseudoheader and signature context. */\n groupAddress: string\n /** UDP source and destination port of protected datagrams and, in band, of their records. */\n port: number\n /** Catalog-declared channel generation. */\n streamId: number\n}\n\nexport interface BcProvenanceV2Record {\n kid: number\n epoch: number\n mseq: bigint\n firstSeq: bigint\n count: number\n hashAlg: number\n flags: number\n sent: number\n ctx: Uint8Array\n digests: Uint8Array[]\n signature: Uint8Array\n /** Record bytes covered by the signature, excluding the signature itself. */\n body: Uint8Array\n /** A defensive copy of the complete wire record. */\n raw: Uint8Array\n}\n\nconst text = new TextEncoder()\nconst MAGIC = Uint8Array.from([0x4d, 0x48, 0x4d, 0x32]) // MHM2\n\n/**\n * multicast-02 7.2.7: a datagram on a protected flow's `port` whose first four\n * octets are \"MHM2\" is a record candidate (step 1); every other one is data\n * (step 2). Nothing else is read to decide.\n */\nexport function isBcpV2RecordCandidate(datagram: Uint8Array): boolean {\n return datagram.byteLength >= 4 &&\n datagram[0] === MAGIC[0] && datagram[1] === MAGIC[1] && datagram[2] === MAGIC[2] && datagram[3] === MAGIC[3]\n}\nconst SIGNING_DOMAIN = text.encode('mcast-manifest-v2\\0')\n\nfunction equalBytes(left: Uint8Array, right: Uint8Array): boolean {\n if (left.byteLength !== right.byteLength) return false\n let different = 0\n for (let index = 0; index < left.byteLength; index++) different |= left[index]! ^ right[index]!\n return different === 0\n}\n\nfunction u16(value: number): Uint8Array {\n const result = new Uint8Array(2)\n new DataView(result.buffer).setUint16(0, value)\n return result\n}\n\nfunction u32(value: number): Uint8Array {\n const result = new Uint8Array(4)\n new DataView(result.buffer).setUint32(0, value)\n return result\n}\n\nfunction validU32(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff\n}\n\nfunction validPort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffff\n}\n\nfunction parseIpv4(address: string): Uint8Array {\n const fields = address.split('.')\n if (fields.length !== 4 || fields.some((field) => !/^\\d+$/.test(field))) {\n throw new Error(`invalid IPv4 address ${address}`)\n }\n const values = fields.map(Number)\n if (values.some((value, index) => value > 255 || String(value) !== fields[index])) {\n throw new Error(`invalid IPv4 address ${address}`)\n }\n return Uint8Array.from(values)\n}\n\nfunction parseIpv6(address: string): Uint8Array {\n const compressed = address.includes('::')\n if ((address.match(/::/g) ?? []).length > 1) throw new Error(`invalid IPv6 address ${address}`)\n const [leftText, rightText = ''] = address.split('::')\n const left = leftText ? leftText.split(':') : []\n const right = rightText ? rightText.split(':') : []\n // RFC 3986 IPv6address: an ls32 dotted tail may end the literal (multicast-02\n // 7.2.6), never precede \"::\"; it stands for the last two h16 groups.\n const tail = compressed ? right : left\n if (tail.length > 0 && tail[tail.length - 1]!.includes('.')) {\n const v4 = parseIpv4(tail.pop()!)\n tail.push(((v4[0]! << 8) | v4[1]!).toString(16), ((v4[2]! << 8) | v4[3]!).toString(16))\n }\n if (left.some((group) => group.includes('.')) || right.some((group) => group.includes('.'))) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const missing = 8 - left.length - right.length\n if (missing < 0 || (!compressed && missing !== 0) || (compressed && missing < 1)) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const groups = [...left, ...Array.from({ length: missing }, () => '0'), ...right]\n if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/i.test(group))) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const result = new Uint8Array(16)\n const view = new DataView(result.buffer)\n groups.forEach((group, index) => view.setUint16(index * 2, Number.parseInt(group, 16)))\n return result\n}\n\n/** Parse an IPv4 or IPv6 address in the native-width form used on the wire. */\nexport function parseBcProvenanceV2Address(address: string): Uint8Array {\n if (address.includes(':')) return parseIpv6(address)\n return parseIpv4(address)\n}\n\nfunction endpointBytes(endpoint: BcProvenanceV2Endpoint): {\n source: Uint8Array\n group: Uint8Array\n} {\n if (!validPort(endpoint.port) || !validU32(endpoint.streamId)) {\n throw new Error('invalid BCPV2 endpoint')\n }\n const source = parseBcProvenanceV2Address(endpoint.sourceAddress)\n const group = parseBcProvenanceV2Address(endpoint.groupAddress)\n if (source.byteLength !== group.byteLength) throw new Error('endpoint address families differ')\n return { source, group }\n}\n\nfunction readU32(view: DataView, offset: number): number {\n return view.getUint32(offset)\n}\n\nfunction readU64(view: DataView, offset: number): bigint {\n return view.getBigUint64(offset)\n}\n\n/**\n * Decode the fixed-layout BCPV2 record from multicast-02 Section 7.2.3.\n * Structural validation happens before callers inspect the variable digest list.\n */\nexport function decodeBcProvenanceV2Record(input: Uint8Array): BcProvenanceV2Record {\n const bytes = Uint8Array.from(input)\n if (\n bytes.byteLength < BCPV2_MIN_RECORD_LENGTH ||\n bytes.byteLength > BCPV2_MAX_RECORD_LENGTH ||\n (bytes.byteLength - 132) % 32 !== 0\n ) {\n throw new Error('invalid BCPV2 record length')\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n if (!equalBytes(bytes.subarray(0, 4), MAGIC)) throw new Error('invalid BCPV2 record magic')\n const count = view.getUint16(28)\n if (count < 1 || count > BCPV2_MAX_DIGESTS || bytes.byteLength !== 132 + count * 32) {\n throw new Error('invalid BCPV2 record count')\n }\n const body = bytes.slice(0, bytes.byteLength - BCPV2_SIGNATURE_LENGTH)\n const digests: Uint8Array[] = []\n for (let index = 0; index < count; index++) {\n digests.push(bytes.slice(BCPV2_HEADER_LENGTH + index * 32, BCPV2_HEADER_LENGTH + (index + 1) * 32))\n }\n return {\n kid: readU32(view, 4),\n epoch: readU32(view, 8),\n mseq: readU64(view, 12),\n firstSeq: readU64(view, 20),\n count,\n hashAlg: bytes[30]!,\n flags: bytes[31]!,\n sent: readU32(view, 32),\n ctx: bytes.slice(36, 68),\n digests,\n signature: bytes.slice(bytes.byteLength - BCPV2_SIGNATURE_LENGTH),\n body,\n raw: bytes,\n }\n}\n\n/** Return the exact Ed25519 preimage from multicast-02 Section 7.2.4: the prefix binds `u16(port)`. */\nexport function bcProvenanceV2SignedMessage(\n record: BcProvenanceV2Record,\n endpoint: BcProvenanceV2Endpoint,\n): Uint8Array {\n const { source, group } = endpointBytes(endpoint)\n const prefix = new Uint8Array(SIGNING_DOMAIN.byteLength + 4 + source.byteLength + group.byteLength + 2)\n let offset = 0\n prefix.set(SIGNING_DOMAIN, offset)\n offset += SIGNING_DOMAIN.byteLength\n prefix.set(u32(endpoint.streamId), offset)\n offset += 4\n prefix.set(source, offset)\n offset += source.byteLength\n prefix.set(group, offset)\n offset += group.byteLength\n prefix.set(u16(endpoint.port), offset)\n const message = new Uint8Array(prefix.byteLength + record.body.byteLength)\n message.set(prefix)\n message.set(record.body, prefix.byteLength)\n return message\n}\n\n/**\n * Compute the AMBI Section 3.4.2 digest for one transmitted datagram.\n * The payload includes the Source FEC Payload ID when one was transmitted.\n */\nexport function bcProvenanceV2DatagramDigest(\n endpoint: BcProvenanceV2Endpoint,\n payload: Uint8Array,\n): Uint8Array {\n if (payload.byteLength > 0xffff) throw new Error('datagram payload exceeds u16 length')\n const { source, group } = endpointBytes(endpoint)\n const pseudoheader = new Uint8Array(source.byteLength + group.byteLength + 2 + 2 + 2 + 2 + 4)\n let offset = 0\n pseudoheader.set(source, offset)\n offset += source.byteLength\n pseudoheader.set(group, offset)\n offset += group.byteLength\n pseudoheader.set([0, 0x11], offset)\n offset += 2\n pseudoheader.set(u16(payload.byteLength), offset)\n offset += 2\n pseudoheader.set(u16(endpoint.port), offset)\n offset += 2\n pseudoheader.set(u16(endpoint.port), offset)\n offset += 2\n pseudoheader.set(u32(endpoint.streamId), offset)\n const input = new Uint8Array(pseudoheader.byteLength + payload.byteLength)\n input.set(pseudoheader)\n input.set(payload, pseudoheader.byteLength)\n return sha256(input)\n}\n\n/** Verify the ordered digest list against the transmitted datagrams. */\nexport function verifyBcProvenanceV2Datagrams(\n record: BcProvenanceV2Record,\n endpoint: BcProvenanceV2Endpoint,\n datagrams: readonly Uint8Array[],\n): boolean {\n return record.hashAlg === BCPV2_HASH_ALG_SHA256 &&\n datagrams.length === record.count &&\n datagrams.every((datagram, index) => equalBytes(\n bcProvenanceV2DatagramDigest(endpoint, datagram),\n record.digests[index]!,\n ))\n}\n\nexport interface BcProvenanceV2Params {\n codec: string\n /** 0 where MOQ-MMT Section 4.4.2 resolves none. */\n timescale: number\n groupDurationTicks: number\n /** The entry's `fec` array (fec-catalog-00 3.1); absent or empty for no FEC. */\n fec?: readonly BcProvenanceV2FecInstance[]\n}\n\nexport interface BcProvenanceV2FecInstance {\n /** Orders the tuples, never hashed; 0 when absent. */\n instanceId?: number\n /** Registered scheme identifier; \"none\" is reserved and rejected. */\n scheme: string\n sourceSymbols: number\n repairSymbols: number\n symbolSize: number\n interleaveDepthMs?: number\n}\n\nfunction concat(parts: Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0))\n let offset = 0\n for (const part of parts) {\n out.set(part, offset)\n offset += part.byteLength\n }\n return out\n}\n\nfunction lp16(value: Uint8Array | string): Uint8Array {\n const bytes = typeof value === 'string' ? text.encode(value) : value\n if (bytes.byteLength > 0xffff) throw new Error('lp16 field exceeds u16 length')\n return concat([u16(bytes.byteLength), bytes])\n}\n\n/**\n * `params_hash` of multicast-02 Section 7.2.4, hash_alg 1: one tuple per FEC\n * instance in ascending `instanceId`, neither the count nor an `instanceId`\n * encoded; no instance hashes as `lp16(\"none\")` then four u32(0).\n */\nexport function bcProvenanceV2ParamsHash(params: BcProvenanceV2Params): Uint8Array {\n const fec = [...(params.fec ?? [])].sort((a, b) => (a.instanceId ?? 0) - (b.instanceId ?? 0))\n if (fec.some((instance) => instance.scheme === 'none')) {\n throw new Error('FEC scheme \"none\" is reserved (fec-core-00 15.2)')\n }\n const tuples = fec.length === 0\n ? [lp16('none'), u32(0), u32(0), u32(0), u32(0)]\n : fec.flatMap((instance) => [\n lp16(instance.scheme),\n u32(instance.sourceSymbols),\n u32(instance.repairSymbols),\n u32(instance.symbolSize),\n u32(instance.interleaveDepthMs ?? 0),\n ])\n return sha256(concat([\n text.encode('BCPV2-params'),\n lp16(params.codec),\n u32(params.timescale),\n u32(params.groupDurationTicks),\n ...tuples,\n ]))\n}\n\n/**\n * `ctx` of multicast-02 Section 7.2.4: the namespace fields in order, the endpoint's\n * packetIds ascending, and `params_hash`. A flow without a MoQ catalog uses 32 zero octets.\n */\nexport function bcProvenanceV2Ctx(\n namespace: readonly (Uint8Array | string)[],\n packetIds: readonly number[],\n params: BcProvenanceV2Params,\n): Uint8Array {\n const ids = [...packetIds].sort((a, b) => a - b)\n if (namespace.length > 0xffff || ids.length > 0xffff) throw new Error('ctx field count exceeds u16')\n return sha256(concat([\n text.encode('BCPV2-ctx'),\n u16(namespace.length),\n ...namespace.map(lp16),\n u16(ids.length),\n ...ids.map(u16),\n bcProvenanceV2ParamsHash(params),\n ]))\n}\n", "/**\n * Thin transport wrapper for SSM multicast \u2014 join, socket, watchdog,\n * takeover/failback. Delegates raw packets to a callback (MmtpRouter\n * or any direct consumer).\n */\n\nimport {\n isMulticastTrackPacketId,\n subscriptionDeliversPacketBatches,\n toWindowMulticastSubscribeConfig,\n type WindowMulticastDelivery,\n type WindowMulticastGateway,\n type WindowMulticastSubscription,\n} from '@blockcast/mmt-transport'\nimport {\n MMTP_PACKET_ID_MAX,\n MMTP_SIGNALING_PACKET_ID,\n readMMTPPacketId,\n} from '@blockcast/mmt-base'\nimport { isBcpV2RecordCandidate } from '@blockcast/mmt-manifest-verify'\n\nexport interface MulticastTransportConfig {\n /** SSM source; omitted on ASM. */\n sourceAddress?: string\n /** ASM sender filter (multicast-contract 1.1.0 `senderAddress`); never beside `sourceAddress`. */\n senderAddress?: string\n groupAddress: string\n port: number\n mode?: 'auto' | 'native' | 'tunnel'\n amtRelay?: string\n amtRelayPort?: number\n /** `'driad'`: with no relay, the gateway discovers one per RFC 8777. */\n amtDiscovery?: 'driad'\n packetIds?: number[]\n tracks?: { name: string; packetId: number }[]\n /** ATSC 3.0 tuning (`catalogAtsc3NetworkSource`); when set the join is the tuner. */\n atsc3?: { frequency: number; plpId?: number; serviceId?: number }\n /**\n * The join is a protected flow (multicast-02 7.2.3): its bc-provenance\n * records ride in band on this `port`, so a datagram whose first four octets\n * are \"MHM2\" passes ahead of the packet_id read and the allow-list, and is\n * never counted toward a packet_id. On an unprotected join it is an\n * unparseable MMTP packet, as before.\n */\n protected?: boolean\n}\n\nexport type ResolvedMulticastTransport = 'native' | 'amt'\n\n/**\n * Why a joined transport handed control back to MoQ.\n *\n * - `silence`: the data plane was live (at least one accepted packet since the\n * join) and then stayed silent for the catalog-derived failback window.\n * - `subscription-error`: the provider reported the subscription failed. This\n * is the provider's own explicit, typed outcome (a `MulticastError`, branch\n * on its `code`), e.g. a bridge whose AMT tunnel it declared dead.\n */\nexport type MulticastFailbackReason =\n | { readonly kind: 'silence'; readonly windowMs: number }\n | { readonly kind: 'subscription-error'; readonly error: unknown }\n\n/**\n * Why a join() resolved false, named by the step at which the join stopped,\n * never by what an error message says.\n *\n * - `disposed`: the transport was disposed before the join started, or while\n * the provider's subscribe() was pending.\n * - `unavailable`: there is no window.multicast gateway with a subscribe().\n * - `superseded`: a newer join() started while this join's subscribe() was\n * pending; the newer join owns the transport.\n * - `invalid-request`: the join config does not make a valid window.multicast\n * request (toWindowMulticastSubscribeConfig refused it), so the provider was\n * never asked.\n * - `provider-rejected`: the provider's subscribe() rejected or threw. The only\n * cause that carries the provider's own verdict (`providerError`).\n * - `invalid-subscription`: subscribe() resolved something that is not a usable\n * subscription: no event surface, a malformed `eventTypes`, an unknown\n * `delivery`, or an `addEventListener` that threw.\n * - `delivery-hook-failed`: the consumer's onDelivery hook threw for the\n * delivery the subscription declared, e.g. 'ordered-relay' to FEC tracks\n * configured without ordered geometry. A local configuration failure, not\n * the provider's.\n */\nexport type MulticastJoinDeclineReason =\n | 'disposed'\n | 'unavailable'\n | 'superseded'\n | 'invalid-request'\n | 'provider-rejected'\n | 'invalid-subscription'\n | 'delivery-hook-failed'\n\n/**\n * The typed part of a provider's subscribe() rejection. `name` and `code` are\n * the provider's own typed fields; the frozen multicast contract makes `code`\n * the one to branch on (e.g. a bridge's `amt_relay_refused`). `message` is\n * free text carried for humans only: never branch on it.\n */\nexport interface MulticastJoinProviderError {\n readonly name: string\n readonly message: string\n /** Present only when the provider's error carries a string `code`. */\n readonly code?: string\n}\n\n/**\n * How a join() resolved: joined, or the typed reason it resolved false.\n * `providerError` is present only for `provider-rejected`, and only when the\n * provider's rejection value carries a string `name` and `message`.\n */\nexport type MulticastJoinOutcome =\n | { readonly ok: true }\n | {\n readonly ok: false\n readonly reason: MulticastJoinDeclineReason\n readonly providerError?: MulticastJoinProviderError\n }\n\n/**\n * Receive counters plus the ingress-filter drop counters.\n *\n * `packetsReceived` / `bytesReceived` count only packets that passed the\n * allow-list and were handed to the page \u2014 a rejected packet must never\n * inflate them. The `dropped*` counters are the other half of that story:\n * without them a filtered PID is indistinguishable from a PID that was never\n * transmitted, which is exactly the ambiguity that makes an all-zero FEC\n * repair count unreadable (a repair leg dropped here and a repair leg the\n * publisher never sent produce byte-identical downstream telemetry).\n */\nexport interface MulticastTransportStats {\n packetsReceived: number\n bytesReceived: number\n /** Parsed cleanly, but the packet ID is absent from the join's allow-list. */\n packetsDroppedUnregisteredPid: number\n /** MMTP header did not parse; the packet ID is unknown, so it cannot be attributed. */\n packetsDroppedUnparseable: number\n /**\n * packet_id 0, the MMTP signaling flow. Expected traffic on a protected\n * session (moq_mmt sends the AL-FEC message on it), so it is counted apart\n * from the unregistered-PID fault counter and never takes a\n * `droppedPacketIds` slot.\n */\n packetsDroppedSignaling: number\n /**\n * From a sender other than the one this join locked to (BLO-50098). An\n * unprotected ASM subscription admits any source, so two senders can share\n * one (G, port, packet_id); the first sender whose packet is accepted owns\n * the sub-flow until the next join, and every other sender is dropped here,\n * before any gap, duplicate or FEC ledger sees it. A join or a silence\n * failback releases the lock.\n */\n packetsDroppedForeignSender: number\n}\n\n/**\n * join() calls by how they resolved, cumulative for the life of the transport:\n * across rejoins, failed joins and dispose. `succeeded` plus every `declined`\n * count is the number of join() calls that resolved, superseded ones\n * included; a join() that rejects counts in none (see\n * MulticastTransport.lastJoinOutcome).\n */\nexport interface MulticastJoinStats {\n /** join() calls that resolved true. */\n readonly succeeded: number\n /** join() calls that resolved false, by the reason each one did. */\n readonly declined: Readonly<Record<MulticastJoinDeclineReason, number>>\n}\n\nconst JOIN_SUCCEEDED: MulticastJoinOutcome = Object.freeze({ ok: true })\n\n/**\n * The typed part of a provider's subscribe() rejection, or undefined when the\n * rejection value is not an error with a string `name` and `message`. Nothing\n * is filled in for a field the provider did not type, and the message is\n * carried, never read.\n */\nfunction providerErrorOf(error: unknown): MulticastJoinProviderError | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const fields = error as { readonly name?: unknown; readonly message?: unknown; readonly code?: unknown }\n let name: unknown\n let message: unknown\n let code: unknown\n try {\n name = fields.name\n message = fields.message\n code = fields.code\n } catch {\n // A rejection value whose fields throw on read (a revoked Proxy) types\n // nothing. Reading it must not turn a declined join into a rejected one.\n return undefined\n }\n if (typeof name !== 'string' || typeof message !== 'string') return undefined\n return Object.freeze(typeof code === 'string' ? { name, message, code } : { name, message })\n}\n\n/**\n * Distinct unregistered PIDs retained for diagnostics. A conformant catalog\n * advertises a handful of PIDs per endpoint, so this only has to be large\n * enough to name every leg of a mis-provisioned one; the cap exists so a\n * malformed or hostile stream cycling the 16-bit PID space cannot grow the map\n * without bound. Drops keep counting in aggregate after the cap.\n */\nconst MAX_TRACKED_DROPPED_PIDS = 16\n\ntype MulticastGlobals = typeof globalThis & {\n multicast?: WindowMulticastGateway\n}\n\nfunction getMulticastGlobals(): MulticastGlobals {\n return globalThis as MulticastGlobals\n}\n\nfunction hasMulticastGateway(): boolean {\n return typeof getMulticastGlobals().multicast?.subscribe === 'function'\n}\n\n/** The `bytes` event's sender (multicast-contract 1.3.0); undefined when unknown. */\nfunction senderOf(event: CustomEvent<unknown>): string | undefined {\n const sourceAddress = (event as { sourceAddress?: unknown }).sourceAddress\n return typeof sourceAddress === 'string' ? sourceAddress : undefined\n}\n\n/**\n * The packet one `bytes` event (or one element of a `packets` batch) carries,\n * as a view of the provider's own bytes. A Uint8Array is used as it is; the\n * bridge hands every packet over as one, so the common path builds nothing.\n */\nfunction normalizeBytesEventDetail(detail: unknown): Uint8Array | null {\n const value =\n detail && typeof detail === 'object' && 'data' in detail\n ? (detail as { data?: unknown }).data\n : detail\n if (value instanceof Uint8Array) return value\n if (value instanceof ArrayBuffer) return new Uint8Array(value)\n if (ArrayBuffer.isView(value)) {\n return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n }\n return null\n}\n\n/**\n * A provider that can replace a live subscription's MMTP packet-ID filter\n * without rejoining its socket: the frozen multicast contract's\n * `Subscription.setPacketIds` (pim `packages/multicast-contract`). It is not\n * declared on every provider this transport meets, so it is detected, never\n * assumed.\n */\ninterface PacketFilterUpdater {\n setPacketIds(packetIds: readonly number[]): Promise<void> | void\n}\n\nfunction packetFilterUpdater(\n subscription: WindowMulticastSubscription,\n): PacketFilterUpdater['setPacketIds'] | undefined {\n const update = (subscription as Partial<PacketFilterUpdater>).setPacketIds\n return typeof update === 'function' ? update.bind(subscription) : undefined\n}\n\nfunction sameIds(a: ReadonlySet<number>, b: ReadonlySet<number>): boolean {\n if (a.size !== b.size) return false\n for (const id of a) if (!b.has(id)) return false\n return true\n}\n\n/**\n * The provider-side packet filter of one live join, and the in-place updates\n * sent to it. It lives exactly as long as the join: a later join or release\n * replaces it, and an acknowledgement for a replaced one is ignored.\n */\ninterface ProviderFilter {\n /**\n * The filter as the newest acknowledged update left it: the packet IDs the\n * provider forwards, or null when it forwards every ID. Until an update is\n * acknowledged it is the join's own filter.\n */\n acknowledged: ReadonlySet<number> | null\n /** Updates sent to the provider, numbered from 1 in send order. */\n sent: number\n /** The number of the newest acknowledged update; 0 for the join's own filter. */\n acknowledgedUpdate: number\n /** Updates sent and not yet settled. */\n inFlight: number\n}\n\n/** Which packet surface a join listens to, and its listener. */\ntype PacketListener =\n | { readonly type: 'bytes'; readonly listener: (event: CustomEvent<unknown>) => void }\n | { readonly type: 'packets'; readonly listener: (event: CustomEvent<readonly unknown[]>) => void }\n\nexport class MulticastTransport<TPacketContext = never> {\n #onPacket: (packet: Uint8Array, context?: TPacketContext) => void\n #subscription: WindowMulticastSubscription | null = null\n #packetListener: PacketListener | null = null\n #resolvedTransport: ResolvedMulticastTransport | undefined\n #videoConfirmed = false\n #audioConfirmed = false\n // One armed no-packets timer per silence window (BLO-36599). Packets only\n // record #lastPacketAtMs; the timer re-arms for the remainder when it wakes\n // inside the window, instead of a clear+set pair on every packet.\n #watchdog: ReturnType<typeof setTimeout> | null = null\n #lastPacketAtMs = 0\n #failbackWindowMs: number | null | undefined\n #onTakeover: (() => void) | null = null\n #onFailback: ((reason: MulticastFailbackReason) => void) | null = null\n // Data-plane-live edge: the first packet this join accepted. Everything\n // before it is join latency, not silence (see #resetWatchdog).\n #dataPlaneLive = false\n #onDataPlaneLive: (() => void) | null = null\n #errorListener: ((event: CustomEvent<unknown>) => void) | null = null\n #onDelivery: ((delivery: WindowMulticastDelivery) => void) | null = null\n #delivery: WindowMulticastDelivery | undefined\n #disposed = false\n #stats: MulticastTransportStats = {\n packetsReceived: 0,\n bytesReceived: 0,\n packetsDroppedUnregisteredPid: 0,\n packetsDroppedUnparseable: 0,\n packetsDroppedSignaling: 0,\n packetsDroppedForeignSender: 0,\n }\n // join() calls by how they resolved; see joinStats.\n #joinsSucceeded = 0\n readonly #joinsDeclined: Record<MulticastJoinDeclineReason, number> = {\n disposed: 0,\n unavailable: 0,\n superseded: 0,\n 'invalid-request': 0,\n 'provider-rejected': 0,\n 'invalid-subscription': 0,\n 'delivery-hook-failed': 0,\n }\n // The newest resolved join's outcome (see lastJoinOutcome) and the join\n // generation it belongs to. Generations are issued in start order, so a\n // join that resolves after a newer join has recorded leaves it standing.\n #lastJoinOutcome: MulticastJoinOutcome | undefined = undefined\n #lastJoinOutcomeGeneration = 0\n #droppedPidCounts = new Map<number, number>()\n // Accepted bytes per MMTP packet_id; see bytesReceivedForPacketIds. Each\n // value is a mutable cell, so the per-packet update is one lookup and an\n // in-place add: no insertion once the key exists, and no re-boxed number\n // per packet once a count outgrows the small-integer range.\n readonly #acceptedBytesByPacketId = new Map<number, { bytes: number }>()\n #packetIds: ReadonlySet<number> | null = null\n #protected = false\n // The sender S this join's accepted packets came from, once known; see\n // packetsDroppedForeignSender. Cleared by every join and by a silence\n // failback, so a replacement publisher is not locked out.\n #sender: string | undefined = undefined\n readonly #lockedTo: (() => string | undefined) | undefined\n // What the live join delivers with each accepted packet. A field, not a\n // listener closure, so an in-place admission change (setContext) applies\n // from the next packet on without rejoining.\n #context: TPacketContext | undefined = undefined\n // The live join's provider-side filter. An in-place widening must reach it,\n // or the provider never delivers the new ID; null without a live join.\n #providerFilter: ProviderFilter | null = null\n // IDs an in-place update withdrew while the provider may still be\n // delivering them. Their late packets are still counted as filtered, but\n // they are the expected tail of a deliberate change, not a provisioning\n // fault, so they are counted apart and do not raise the unadvertised-PID\n // warning.\n readonly #withdrawnPacketIds = new Set<number>()\n #withdrawnPacketIdDrops = 0\n // Distinct packet IDs dropped as provisioning faults: on the wire but in\n // no allow-list the join or an in-place update declared. Capped with\n // droppedPacketIds.\n readonly #unadvertisedPacketIds = new Set<number>()\n #joinGeneration = 0\n\n // The provider to subscribe through; window.multicast when unset.\n readonly #gateway: WindowMulticastGateway | undefined\n\n /**\n * `lockedTo` reads the sender another transport is locked to (a repair\n * group follows its media join, whose publisher sends both). While it reads\n * a sender, this join admits only that one; until then its own lock applies.\n */\n constructor(\n onPacket: (packet: Uint8Array, context?: TPacketContext) => void,\n options: { gateway?: WindowMulticastGateway; lockedTo?: () => string | undefined } = {},\n ) {\n this.#onPacket = onPacket\n this.#gateway = options.gateway\n this.#lockedTo = options.lockedTo\n }\n\n static isAvailable(): boolean {\n return hasMulticastGateway()\n }\n\n async join(config: MulticastTransportConfig, context?: TPacketContext): Promise<boolean> {\n const generation = ++this.#joinGeneration\n this.#releaseSubscription()\n this.#dataPlaneLive = false\n this.#sender = undefined\n\n if (this.#disposed) return this.#declined(generation, 'disposed')\n const gateway = this.#gateway ?? getMulticastGlobals().multicast\n if (typeof gateway?.subscribe !== 'function') {\n console.warn('[MulticastTransport] Multicast API not available')\n return this.#declined(generation, 'unavailable')\n }\n\n // What a throw from the step in progress means. Each step names its\n // failure before it runs, so the catch classifies a decline by where the\n // join stopped, never by what the error says.\n let failure: MulticastJoinDeclineReason = 'invalid-request'\n try {\n const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null\n console.log(\n `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? '*'}, ${config.groupAddress}, ${config.port})`,\n )\n const subscribeConfig = toWindowMulticastSubscribeConfig({\n source: config.sourceAddress,\n senderAddress: config.senderAddress,\n group: config.groupAddress,\n port: config.port,\n mode: config.mode,\n amtRelay: config.amtRelay,\n amtRelayPort: config.amtRelayPort,\n amtDiscovery: config.amtDiscovery,\n packetIds: config.packetIds,\n tracks: config.tracks,\n atsc3: config.atsc3,\n })\n failure = 'provider-rejected'\n const subscription = await gateway.subscribe(subscribeConfig)\n failure = 'invalid-subscription'\n console.log(`[MulticastTransport] window.multicast.subscribe returned:`, !!subscription)\n\n if (this.#disposed || generation !== this.#joinGeneration) {\n void subscription.unsubscribe()\n return this.#declined(generation, this.#displacedReason())\n }\n if (typeof subscription.addEventListener !== 'function') {\n if (typeof subscription.unsubscribe === 'function') {\n void subscription.unsubscribe()\n }\n throw new Error('window.multicast subscription does not support bytes events')\n }\n\n // Own the subscription before anything below can throw, so a failed\n // join's release reaches unsubscribe() instead of leaking it.\n this.#subscription = subscription\n const live = () => !this.#disposed && generation === this.#joinGeneration\n // Exactly one packet surface. A provider that advertises the `packets`\n // extension delivers each reader batch as one event, so the page pays\n // one DOM event and one liveness read per batch instead of per packet\n // (BLO-36599). Every packet still goes through the same filter, counters\n // and callback, in arrival order, as a view of the provider's bytes.\n // A batch carries no per-packet sender, so an any-source join, the one\n // join that can see two senders, listens to `bytes` (BLO-50098). That\n // costs batching on every any-source join, including one on a pre-1.3.0\n // provider that sends no sender and so gets no lock for it; keep it\n // unless the provider can say it carries the sender.\n const anySource = !config.sourceAddress && !config.senderAddress && !config.atsc3\n const packetListener: PacketListener = !anySource && subscriptionDeliversPacketBatches(subscription)\n ? {\n type: 'packets',\n listener: (event) => {\n if (!live()) return\n this.#deliverBatch(event.detail, live)\n },\n }\n : {\n type: 'bytes',\n listener: (event) => {\n if (!live()) return\n if (this.#deliver(event.detail, senderOf(event))) this.#resetWatchdog()\n },\n }\n this.#packetListener = packetListener\n this.#packetIds = packetIds\n this.#protected = config.protected === true\n this.#providerFilter = {\n acknowledged: packetIds,\n sent: 0,\n acknowledgedUpdate: 0,\n inFlight: 0,\n }\n this.#context = context\n // Seed the allow-list's counters so accepted packets never insert. An\n // id already counted by an earlier join keeps its count: the counters\n // are cumulative across rejoins, as bytesReceived is.\n if (packetIds) {\n for (const packetId of packetIds) {\n if (!this.#acceptedBytesByPacketId.has(packetId)) {\n this.#acceptedBytesByPacketId.set(packetId, { bytes: 0 })\n }\n }\n }\n // The request origin is the accepted acquisition contract. The\n // gateway no longer exposes a mutable transport/transportchange\n // surface, so assign this only after subscribe() has resolved and the\n // returned subscription has passed the event-surface check above.\n this.#resolvedTransport = subscribeConfig.origin === 'amt' ? 'amt' : 'native'\n // The provider alone knows whether its packets are independent\n // datagrams or an ordered relay of one socket; undeclared promises no\n // ordering. Announce it before the listener can deliver a packet. A\n // rejected declaration fails the join and releases the subscription.\n const delivery = subscription.delivery ?? 'datagram'\n if (delivery !== 'datagram' && delivery !== 'ordered-relay') {\n throw new Error(\n `window.multicast subscription declared unknown delivery ${String(delivery)}`,\n )\n }\n this.#delivery = delivery\n failure = 'delivery-hook-failed'\n this.#onDelivery?.(delivery)\n failure = 'invalid-subscription'\n // The provider's explicit, terminal subscription failure. Before the\n // first packet this is the only signal that bounds a join, so it must\n // reach the failback path rather than be dropped.\n const onError = (event: CustomEvent<unknown>) => {\n if (this.#disposed || generation !== this.#joinGeneration) return\n this.#failBackOnSubscriptionError(event.detail)\n }\n this.#errorListener = onError\n if (packetListener.type === 'packets') {\n subscription.addEventListener('packets', packetListener.listener)\n } else {\n subscription.addEventListener('bytes', packetListener.listener)\n }\n subscription.addEventListener('error', onError)\n // No watchdog here. A resolved subscribe() is the provider's join\n // outcome, not proof that the data plane is live: over AMT it resolves\n // once the gateway has sent its Membership Update, and the relay's\n // upstream join plus one path RTT still precede the first packet. The\n // silence window is a packet-gap clock for a stream that has flowed;\n // charging join latency to it failed back joins that went on to play.\n // The join phase is bounded by the provider's typed error or the host's\n // takeover deadline (see SessionManager.joinActive). The window starts\n // at the first accepted packet (#resetWatchdog).\n console.log(\n `[MulticastTransport] Joined ${config.groupAddress}:${config.port}`,\n )\n return this.#joined(generation)\n } catch (err) {\n if (this.#disposed || generation !== this.#joinGeneration) {\n return this.#declined(generation, this.#displacedReason())\n }\n this.#releaseSubscription()\n console.error('[MulticastTransport] Failed to join SSM group:', err)\n return this.#declined(\n generation,\n failure,\n failure === 'provider-rejected' ? providerErrorOf(err) : undefined,\n )\n }\n }\n\n /** Record a join that resolved true; returns its result. */\n #joined(generation: number): true {\n this.#joinsSucceeded++\n this.#recordJoinOutcome(generation, JOIN_SUCCEEDED)\n return true\n }\n\n /** Record a join that resolved false, and why; returns its result. */\n #declined(\n generation: number,\n reason: MulticastJoinDeclineReason,\n providerError?: MulticastJoinProviderError,\n ): false {\n this.#joinsDeclined[reason]++\n this.#recordJoinOutcome(\n generation,\n Object.freeze(\n providerError === undefined ? { ok: false, reason } : { ok: false, reason, providerError },\n ),\n )\n return false\n }\n\n /**\n * Why a join that no longer owns the transport resolved false. dispose()\n * advances the join generation too, so disposal is checked first.\n */\n #displacedReason(): 'disposed' | 'superseded' {\n return this.#disposed ? 'disposed' : 'superseded'\n }\n\n #recordJoinOutcome(generation: number, outcome: MulticastJoinOutcome): void {\n if (generation <= this.#lastJoinOutcomeGeneration) return\n this.#lastJoinOutcomeGeneration = generation\n this.#lastJoinOutcome = outcome\n }\n\n /**\n * One packet from the subscription: filter, count, and hand it to the\n * callback. Returns whether it was delivered as liveness (a delivered record\n * candidate is not); the caller notes liveness.\n */\n #deliver(detail: unknown, sourceAddress?: string): boolean {\n const packet = normalizeBytesEventDetail(detail)\n if (!packet) {\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n if (!this.#acceptPacket(packet, sourceAddress)) return false\n const liveness = !this.#isRecordCandidate(packet)\n // Read per packet: a callback that changes the admission (an in-place\n // rendition switch committing on this very packet) takes effect from the\n // next packet, including the rest of a batch.\n this.#onPacket(packet, this.#context)\n return liveness\n }\n\n #isRecordCandidate(packet: Uint8Array): boolean {\n return this.#protected && isBcpV2RecordCandidate(packet)\n }\n\n /**\n * One `packets` event. Each packet is delivered as a `bytes` event would\n * have delivered it, including isolation: a packet whose callback throws\n * does not cost the rest of its batch, as it would not have cost the next\n * `bytes` event. The first failure is rethrown once the batch is done, so\n * it reaches the event-listener error reporting a `bytes` listener's throw\n * reached; any further one is rethrown from its own microtask, which the\n * host reports the same way, so no failure is swallowed. A callback that\n * disposes the transport or starts another join ends the batch there, as\n * the per-packet guard would have ignored the remaining events.\n */\n #deliverBatch(\n batch: readonly unknown[],\n live: () => boolean,\n ): void {\n if (!Array.isArray(batch)) {\n throw new TypeError('[MulticastTransport] packets event detail must be an array of packets')\n }\n let delivered = false\n let failures: unknown[] | null = null\n for (let i = 0; i < batch.length; i++) {\n try {\n if (this.#deliver(batch[i])) delivered = true\n } catch (error) {\n if (failures === null) failures = []\n failures.push(error)\n }\n if (!live()) break\n }\n // One liveness read covers the batch: every packet in it arrived in the\n // same provider task. A batch that delivered nothing, like a rejected\n // bytes event, is not liveness.\n if (delivered && live()) this.#resetWatchdog()\n if (failures !== null) {\n for (let i = 1; i < failures.length; i++) {\n const failure = failures[i]\n queueMicrotask(() => {\n throw failure\n })\n }\n throw failures[0]\n }\n }\n\n /**\n * Feed a raw packet from an external source (e.g., AMT tunnel or ASM\n * subscription). `sourceAddress` is the datagram's sender when the source\n * knows it; see packetsDroppedForeignSender.\n */\n feedPacket(data: ArrayBuffer | Uint8Array, context?: TPacketContext, sourceAddress?: string): void {\n if (this.#disposed) return\n const packet = data instanceof Uint8Array ? data : new Uint8Array(data)\n if (!this.#acceptPacket(packet, sourceAddress)) return\n const liveness = !this.#isRecordCandidate(packet)\n this.#onPacket(packet, context)\n if (liveness) this.#resetWatchdog()\n }\n\n confirmVideo(): void {\n this.#videoConfirmed = true\n this.#checkTakeover()\n }\n\n confirmAudio(): void {\n this.#audioConfirmed = true\n this.#checkTakeover()\n }\n\n onTakeover(cb: () => void): void {\n this.#onTakeover = cb\n }\n\n onFailback(cb: (reason: MulticastFailbackReason) => void): void {\n this.#onFailback = cb\n }\n\n /**\n * Called on every successful join, before the joined subscription can\n * deliver a packet, with how that subscription delivers packets.\n */\n onDelivery(cb: (delivery: WindowMulticastDelivery) => void): void {\n this.#onDelivery = cb\n }\n\n /** How the current subscription delivers packets, once joined. */\n get delivery(): WindowMulticastDelivery | undefined {\n return this.#delivery\n }\n\n /**\n * Fires once per join, at the first packet the join accepts (after the PID\n * filter; a rejected packet is not liveness). This is the data-plane-live\n * edge, where this transport's silence window starts. A join whose source\n * never sends never reaches it, so a host may start a media-arrival budget\n * here only if the provider bounds the join phase with a typed subscription\n * error (see SessionManager.joinActive); otherwise the host starts it when\n * the subscribe settles.\n */\n onDataPlaneLive(cb: () => void): void {\n this.#onDataPlaneLive = cb\n }\n\n /** Whether the current join has accepted a packet (see onDataPlaneLive). */\n get dataPlaneLive(): boolean {\n return this.#dataPlaneLive\n }\n\n setFailbackWindowMs(windowMs: number | null): void {\n if (windowMs !== null && (!Number.isFinite(windowMs) || windowMs <= 0)) {\n throw new Error(\n `[MulticastTransport] failbackWindowMs=${windowMs}. ` +\n `Derive a positive failback window from the stream catalog.`,\n )\n }\n this.#failbackWindowMs = windowMs\n if (windowMs === null) {\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n return\n }\n // Re-enable against the last packet even when the previous policy disabled\n // the watchdog: a silent live stream may never deliver another packet.\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n if (this.#dataPlaneLive && this.#onFailback) {\n const silentMs = performance.now() - this.#lastPacketAtMs\n this.#armWatchdog(Math.max(0, Math.ceil(windowMs - silentMs)))\n }\n }\n\n get stats(): MulticastTransportStats {\n return this.#stats\n }\n\n /**\n * How the newest join() to resolve did: `{ ok: true }`, or `{ ok: false }`\n * with the typed reason it resolved false (and, for `provider-rejected`, the\n * provider's typed error fields). Undefined until a join resolves. Branch on\n * `reason` and `providerError.code`, never on log text or\n * `providerError.message`.\n *\n * Newest is by start order: a join that resolves after a newer join has\n * recorded its outcome does not replace it, so once `await join()` resolves\n * this names that join's outcome unless a newer join has already resolved.\n * It records how a join resolved, not whether it is still joined (see\n * `joined`); a join() that rejects instead of resolving records nothing,\n * the rejection being its report. Frozen: a later outcome replaces the\n * object, never changes it.\n */\n get lastJoinOutcome(): MulticastJoinOutcome | undefined {\n return this.#lastJoinOutcome\n }\n\n /**\n * join() calls by how they resolved (see MulticastJoinStats). A fresh frozen\n * snapshot on every read, so a later join never changes one already read.\n * Kept apart from `stats`, which is the live counter object.\n */\n get joinStats(): MulticastJoinStats {\n return Object.freeze({\n succeeded: this.#joinsSucceeded,\n declined: Object.freeze({ ...this.#joinsDeclined }),\n })\n }\n\n /**\n * Accepted bytes summed over `packetIds`: the `bytesReceived` share of those\n * MMTP packet_ids. Counted by the same rule, on every path that counts\n * `bytesReceived` (only packets that pass the ingress filter; packet_id 0\n * signaling never), and cumulative for the life of the transport, across\n * rejoins, as `bytesReceived` is. An id never accepted contributes 0. An\n * unparseable packet admitted by a join without an allow-list has no\n * packet_id, so it counts toward `bytesReceived` only.\n *\n * Use it to scope an ABR throughput sample to the active tier's source and\n * repair packet_ids when the join admits more than the active tier (a\n * shared group joined across rendition switches); `bytesReceived` is then\n * the whole allow-list's arrival, not the active tier's. A Set, so an id\n * shared by two tracks of the tier is counted once.\n *\n * Monotonic only for a fixed set: each id has counted since it was first\n * admitted, so after a tier switch the new set's sum is not a continuation\n * of the old one. A cumulative consumer takes a fresh baseline for the new\n * set and never differences across sets.\n */\n bytesReceivedForPacketIds(packetIds: ReadonlySet<number>): number {\n let bytes = 0\n for (const packetId of packetIds) {\n bytes += this.#acceptedBytesByPacketId.get(packetId)?.bytes ?? 0\n }\n return bytes\n }\n\n /**\n * Per-PID drop breakdown, newest-capped at MAX_TRACKED_DROPPED_PIDS distinct\n * PIDs. This is what names the offender: `packetsDroppedUnregisteredPid`\n * says the ingress filter fired, this says it fired on 0x8001.\n */\n get droppedPacketIds(): ReadonlyMap<number, number> {\n return this.#droppedPidCounts\n }\n\n /**\n * Distinct packet IDs dropped as provisioning faults: arriving on the wire\n * but in no allow-list the join or an in-place update (setPacketIds)\n * declared, the drops that raise the unadvertised-PID warning. A packet ID\n * an in-place update withdrew is not one (see withdrawnPacketIdDrops).\n * Cumulative across rejoins and capped with droppedPacketIds.\n */\n get unadvertisedPacketIds(): ReadonlySet<number> {\n return this.#unadvertisedPacketIds\n }\n\n /**\n * Packets dropped because an in-place update (setPacketIds) withdrew their\n * packet ID while the provider may still deliver it: the expected tail of\n * a deliberate change. A subset of packetsDroppedUnregisteredPid,\n * cumulative across rejoins.\n */\n get withdrawnPacketIdDrops(): number {\n return this.#withdrawnPacketIdDrops\n }\n\n /** The sender this join is locked to; undefined until one is known. */\n get sender(): string | undefined {\n return this.#sender\n }\n\n /** Whether a subscription is live: joined, and not yet released or replaced. */\n get joined(): boolean {\n return this.#subscription !== null\n }\n\n /**\n * What the live join delivers with every accepted packet (the join's\n * `context`, or the last setContext), or undefined without a live join.\n */\n get context(): TPacketContext | undefined {\n return this.#context\n }\n\n /**\n * The live join's page-side packet-ID allow-list, or null when it admits\n * every packet ID (a join that carried none, or no live join).\n */\n get packetIds(): ReadonlySet<number> | null {\n return this.#packetIds\n }\n\n /**\n * Replace what the live join delivers with each accepted packet, from the\n * next packet on. The subscription (socket, group membership, AMT tunnel)\n * is untouched: this is how an in-place rendition switch moves the router's\n * ingress identities without leaving and re-joining the group.\n */\n setContext(context: TPacketContext): void {\n if (this.#subscription === null) {\n throw new Error(\n '[MulticastTransport] setContext needs a live join; join() carries the first context',\n )\n }\n this.#context = context\n }\n\n /**\n * Replace the live join's packet-ID allow-list in place, without rejoining.\n *\n * The page-side filter changes synchronously, before this returns: the\n * next packet is admitted or dropped by the new list. The provider's own\n * filter is updated through the subscription's `setPacketIds` when it has\n * one; the returned promise settles with the provider's acknowledgement of\n * this update. It resolves at once, telling the provider nothing, only\n * when the provider cannot be told, or when no earlier update is still in\n * flight and the provider already forwards exactly this list.\n *\n * While an update is in flight the provider's filter is not known, so every\n * later update is sent: one skipped because it matched the last\n * acknowledged filter would let the in-flight update land after it, and\n * leave the provider forwarding a list nobody asked for last. Updates are\n * numbered in send order and the newest acknowledged one describes the\n * provider, so an acknowledgement that overtakes an older one is not undone\n * by it.\n *\n * A provider without `setPacketIds` can still be narrowed: its wider filter\n * keeps delivering the dropped IDs and the page-side filter discards them.\n * It cannot be widened, since it would never deliver the added IDs; that is\n * refused with a throw before anything changes. An ID this update withdraws\n * may keep arriving until the provider applies the narrower list; those\n * packets are counted as filtered like any other and in\n * withdrawnPacketIdDrops, but are not unadvertised packet IDs: their\n * arrival is the expected tail of this change rather than a provisioning\n * fault.\n */\n setPacketIds(packetIds: readonly number[]): Promise<void> {\n const subscription = this.#subscription\n const filter = this.#providerFilter\n if (subscription === null || filter === null) {\n throw new Error(\n '[MulticastTransport] setPacketIds needs a live join; join() carries the first allow-list',\n )\n }\n if (!Array.isArray(packetIds) || packetIds.length === 0) {\n throw new RangeError(\n '[MulticastTransport] setPacketIds needs a non-empty packet-ID list: ' +\n 'an in-place update names the packet IDs it admits',\n )\n }\n if (!packetIds.every(isMulticastTrackPacketId)) {\n throw new RangeError(\n `[MulticastTransport] packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n \"packetId 0 is the MMTP signaling flow and is never a track's\",\n )\n }\n const next: ReadonlySet<number> = new Set(packetIds)\n const provider = filter.acknowledged\n const update = packetFilterUpdater(subscription)\n // Without an updater nothing is ever sent, so the join's filter is exact.\n if (update === undefined && provider !== null) {\n const missing = [...next].filter((id) => !provider.has(id))\n if (missing.length > 0) {\n throw new Error(\n '[MulticastTransport] this subscription cannot widen its packet filter in place ' +\n `(the provider exposes no setPacketIds); rejoin to admit ${missing.join(', ')}`,\n )\n }\n }\n const previous = this.#packetIds\n this.#packetIds = next\n for (const id of next) {\n this.#withdrawnPacketIds.delete(id)\n if (!this.#acceptedBytesByPacketId.has(id)) {\n this.#acceptedBytesByPacketId.set(id, { bytes: 0 })\n }\n }\n if (previous !== null) {\n for (const id of previous) if (!next.has(id)) this.#withdrawnPacketIds.add(id)\n }\n if (update === undefined) return Promise.resolve()\n if (filter.inFlight === 0 && provider !== null && sameIds(provider, next)) {\n return Promise.resolve()\n }\n const number = ++filter.sent\n filter.inFlight++\n let applied: Promise<void>\n try {\n applied = Promise.resolve(update([...next]))\n } catch (error) {\n applied = Promise.reject(error)\n }\n return applied.then(\n () => {\n if (this.#providerFilter !== filter) return\n filter.inFlight--\n if (number > filter.acknowledgedUpdate) {\n filter.acknowledgedUpdate = number\n filter.acknowledged = next\n }\n },\n (error: unknown) => {\n // A refused update leaves the provider on the filter it had.\n if (this.#providerFilter === filter) filter.inFlight--\n throw error\n },\n )\n }\n\n get resolvedTransport(): ResolvedMulticastTransport | undefined {\n return this.#resolvedTransport\n }\n\n dispose(): void {\n ++this.#joinGeneration\n this.#disposed = true\n this.#releaseSubscription()\n }\n\n #releaseSubscription(): void {\n if (this.#watchdog) clearTimeout(this.#watchdog)\n this.#watchdog = null\n if (this.#subscription) {\n const packetListener = this.#packetListener\n if (packetListener?.type === 'packets') {\n this.#subscription.removeEventListener('packets', packetListener.listener)\n } else if (packetListener) {\n this.#subscription.removeEventListener('bytes', packetListener.listener)\n }\n if (this.#errorListener) {\n this.#subscription.removeEventListener('error', this.#errorListener)\n }\n void this.#subscription.unsubscribe()\n }\n this.#subscription = null\n this.#packetListener = null\n this.#errorListener = null\n this.#resolvedTransport = undefined\n this.#delivery = undefined\n this.#packetIds = null\n this.#protected = false\n this.#providerFilter = null\n this.#context = undefined\n this.#withdrawnPacketIds.clear()\n }\n\n /** The ingress filter; an accepted packet is counted here, once. */\n #acceptPacket(packet: Uint8Array, sourceAddress?: string): boolean {\n // Absent means unknown (a pre-1.3.0 provider, a tuner): never a new sender.\n const owner = this.#lockedTo?.() ?? this.#sender\n if (sourceAddress !== undefined && owner !== undefined && sourceAddress !== owner) {\n this.#stats.packetsDroppedForeignSender++\n return false\n }\n // A record candidate of a protected flow carries no packet_id: the router\n // splits it off before anything parses it (7.2.7). Step 1 has not run on\n // it yet, so it neither takes the sender lock nor counts as liveness\n // (#isRecordCandidate, #deliver): records alone must not hold off\n // silence failback.\n if (this.#isRecordCandidate(packet)) {\n this.#countAccepted(packet, null, undefined)\n return true\n }\n // This early read is intentional: rejected PIDs must not affect page\n // delivery, receive statistics, or the failback liveness watchdog. It\n // refuses exactly what parseMMTPHeader refuses but builds no header\n // object; the router parses the header once, later (BLO-36599).\n const packetId = readMMTPPacketId(packet)\n if (packetId === null) {\n // Without an allow-list there is no filter to enforce, so an unparseable\n // packet keeps flowing to the router as before.\n if (!this.#packetIds) {\n this.#countAccepted(packet, null, sourceAddress)\n return true\n }\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n // packet_id 0 is the MMTP signaling flow and never a track's. Drop it even\n // when the join carried no allow-list (a legacy catalog whose tracks omit\n // packetId): the discard rule must not depend on how the catalog was written.\n if (packetId === MMTP_SIGNALING_PACKET_ID) {\n this.#stats.packetsDroppedSignaling++\n return false\n }\n if (!this.#packetIds || this.#packetIds.has(packetId)) {\n this.#countAccepted(packet, packetId, sourceAddress)\n return true\n }\n this.#recordUnregisteredPidDrop(packetId)\n return false\n }\n\n /** Receive counters for one accepted packet; `packetId` is null when it has none. */\n #countAccepted(packet: Uint8Array, packetId: number | null, sourceAddress: string | undefined): void {\n this.#sender ??= sourceAddress\n this.#stats.packetsReceived++\n this.#stats.bytesReceived += packet.byteLength\n if (packetId === null) return\n const counter = this.#acceptedBytesByPacketId.get(packetId)\n if (counter !== undefined) {\n counter.bytes += packet.byteLength\n } else {\n // First sight of this id on a join with no allow-list (an allow-list's\n // ids are seeded at join). Bounded by the 16-bit packet_id space.\n this.#acceptedBytesByPacketId.set(packetId, { bytes: packet.byteLength })\n }\n }\n\n #recordUnregisteredPidDrop(packetId: number): void {\n this.#stats.packetsDroppedUnregisteredPid++\n const withdrawn = this.#withdrawnPacketIds.has(packetId)\n if (withdrawn) this.#withdrawnPacketIdDrops++\n const seen = this.#droppedPidCounts.get(packetId)\n if (seen !== undefined) {\n this.#droppedPidCounts.set(packetId, seen + 1)\n return\n }\n if (this.#droppedPidCounts.size >= MAX_TRACKED_DROPPED_PIDS) return\n this.#droppedPidCounts.set(packetId, 1)\n if (withdrawn) return\n this.#unadvertisedPacketIds.add(packetId)\n // Warn once per distinct PID. A PID arriving on the wire but missing from\n // the catalog's advertised set is a provisioning fault, not stream noise,\n // and it is silent in every downstream counter.\n console.warn(\n `[MulticastTransport] Dropping packetId=${packetId} (0x${packetId.toString(16)}): ` +\n `not in the advertised set [${[...(this.#packetIds ?? [])].join(', ')}]. ` +\n `If this is an FEC repair leg, the catalog is not advertising its packetId.`,\n )\n }\n\n #checkTakeover(): void {\n if (this.#videoConfirmed && this.#audioConfirmed && this.#onTakeover) {\n const cb = this.#onTakeover\n this.#onTakeover = null // fire exactly once\n cb()\n }\n }\n\n /**\n * Note liveness now; arm the no-packets timer if none is armed.\n *\n * Called only for accepted packets, so the first call of a join is the\n * data-plane-live edge and is what first arms the silence window: the window\n * measures silence of a flowing stream, which is undefined before the stream\n * has flowed.\n */\n #resetWatchdog(): void {\n this.#lastPacketAtMs = performance.now()\n if (!this.#dataPlaneLive) {\n this.#dataPlaneLive = true\n this.#onDataPlaneLive?.()\n }\n if (this.#watchdog !== null) return\n if (!this.#onFailback) return\n if (this.#failbackWindowMs === null) return\n if (this.#failbackWindowMs === undefined) {\n throw new Error(\n '[MulticastTransport] Failback watchdog requires a catalog-derived window',\n )\n }\n this.#armWatchdog(this.#failbackWindowMs)\n }\n\n #armWatchdog(delayMs: number): void {\n this.#watchdog = setTimeout(() => {\n this.#watchdog = null\n if (this.#disposed || !this.#onFailback || this.#failbackWindowMs == null) return\n const silentMs = performance.now() - this.#lastPacketAtMs\n if (silentMs < this.#failbackWindowMs) {\n this.#armWatchdog(Math.ceil(this.#failbackWindowMs - silentMs))\n return\n }\n console.warn(\n `[MulticastTransport] No packets for ${this.#failbackWindowMs}ms; failing back to MoQ`,\n )\n // The sub-flow's owner went silent: a sender that resumes after this,\n // the same one or a replacement publisher, owns the sub-flow anew.\n this.#sender = undefined\n this.#onFailback({ kind: 'silence', windowMs: this.#failbackWindowMs })\n }, delayMs)\n }\n\n /** Hand control back to MoQ for a provider-reported subscription failure. */\n #failBackOnSubscriptionError(error: unknown): void {\n // The subscription is dead; a silence timer armed against it would only\n // report the same failure a second time.\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n console.warn('[MulticastTransport] Subscription failed; failing back to MoQ:', error)\n this.#onFailback?.({ kind: 'subscription-error', error })\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["entry", "packetData", "MPU_HEADER_SIZE", "MPU_LENGTH_FIELD_SIZE", "MPU_HEADER_REMAINDER_SIZE", "MPU_HEADER_SIZE", "MMTP_SIGNALING_PACKET_ID", "MMTP_PACKET_ID_MAX", "NTP_SHORT_BITS", "NTP_SHORT_MODULUS", "NTP_SHORT_HALF_RANGE", "FEC_BLOCK_PROFILES", "RUNTIME_FEC_RECEIVER", "MMTP_PACKET_ID_MAX", "MMTP_SIGNALING_PACKET_ID"]
|
|
3
|
+
"sources": ["../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", "../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/ip-address.ts", "../../mmt-base/src/fec-bindings.ts", "../../mmt-manifest-verify/src/bc-provenance-v2.ts", "../src/multicast-transport.ts"],
|
|
4
|
+
"sourcesContent": ["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 * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nexport const SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// fec_code_id_for_repair_flow code points (ISO/IEC 23008-10 Table 1;\n// draft-ramadan-moq-mmt-01 Section 8.3).\nexport const FEC_CODE_REED_SOLOMON = 1\nexport const FEC_CODE_RAPTORQ = 3\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n hasOverlayFecId: boolean\n}\n\n/** Options for extractMFUPayload and explainMFURejection. */\nexport interface MFUExtractOptions {\n /**\n * When true, do NOT subtract the Source FEC Payload ID from the payload\n * boundary, even if fecType >= 1. For FEC-recovered symbols whose FEC PID\n * was already stripped before WASM decoding.\n */\n skipFecId?: boolean\n /**\n * The catalog declares a keyframe overlay (`scope: \"keyframe\"` repair track)\n * for this packet's source. A RAP packet then carries two Source FEC Payload\n * IDs, base first and overlay last (BLO-32719 Clarification 7). This is a\n * catalog fact, never inferred: the header has no overlay bit, and a payload\n * four bytes shorter is byte-identical to a missing overlay. Mirrors\n * mmt-core `split_payload_and_source_fec_trailers(overlay_declared)`.\n */\n overlayDeclared?: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: MFUExtractOptions,\n rapFlag = false,\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false, hasOverlayFecId: false }\n : null\n }\n\n const overlay = options?.overlayDeclared === true && rapFlag\n if (fecType === 1) {\n const fecStart = payload.length - sourceFecTrailerSize(overlay)\n if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true, hasOverlayFecId: overlay }\n }\n // An overlay ID rides behind a base ID the header says is absent.\n if (fecType !== 0 || overlay) return null\n\n if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false, hasOverlayFecId: false }\n }\n\n return null\n}\n\nfunction sourceFecTrailerSize(overlay: boolean): number {\n return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n /** Keyframe-overlay Source FEC Payload ID: RAP packets with `overlayDeclared` only. */\n overlayFecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. The legacy\n * authenticator suffix and the Source FEC trailer are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-01 \u00A73.1).\n * @param options.overlayDeclared The catalog declares a keyframe overlay for this\n * source, so a RAP packet carries an 8-byte trailer (see MFUExtractOptions).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n header.rapFlag,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag)\n if (observed !== expected) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${expected} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact legacy authenticator trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const trailerStart =\n mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId)\n const ssIdAt = (offset: number) => ({\n ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false),\n })\n const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null\n const overlayFecPayloadId = trailerLayout.hasOverlayFecId\n ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE)\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n checkRsId = true,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.ssbLength === 0 ||\n (checkRsId && repairId.rsId >= repairId.rsbLength) ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n/**\n * Why a repair object of an mmtp track is discarded on admission:\n * `repairLength` -- the object is not exactly 12 + 13 + T bytes with a\n * 12-byte MMTP header (C = 0, X = 0; draft-ramadan-moq-mmt-02 7.3, judged\n * before any Repair FEC Payload ID field is read); `repairRsId` -- RS_ID is not\n * below RSB_length, under every scheme; `repairSchemeBound` -- the object\n * breaks its scheme's bound: reed-solomon SSB_length + RSB_length > 255, which\n * also caps RS_ID at 253, or raptorq SSB_length + RSB_length - 1 > 2^24 - 1\n * (draft-ramadan-moq-fec-core-00 7.5 and 10.3, -block-codes-00 7).\n */\nexport type RepairObjectReject = 'repairLength' | 'repairRsId' | 'repairSchemeBound'\n\n/**\n * Admission of one mmtp repair object under its instance's T and scheme. The\n * caller names the instance (by the repair track it subscribed); nothing here\n * infers it from the fields. `repairRsId` and `repairSchemeBound` carry the\n * frame so the caller can first route an RSB_length below a held layer's\n * O_i + P_i to the 7.3.2 stop, which takes precedence over the discard.\n * `null`: not an iso repair packet, or SSB_length 0.\n */\nexport function admitMmtpRepairObject(\n packet: Uint8Array,\n header: MMTPHeader,\n symbolSize: number,\n algorithm: string | undefined,\n): { frame: MoqRepairFrame; reject?: Exclude<RepairObjectReject, 'repairLength'> } | { reject: 'repairLength' } | null {\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n if (header.headerLength !== MMTP_HEADER_SIZE || packet.length !== MMTP_HEADER_SIZE + REPAIR_FEC_PID_SIZE + symbolSize) {\n return { reject: 'repairLength' }\n }\n const frame = moqRepairFrameFrom(packet, readRepairFecPayloadIdFields(packet, header), false)\n if (!frame) return null\n if (frame.rsId >= frame.rsbLength) return { frame, reject: 'repairRsId' }\n const blockSymbols = frame.ssbLength + frame.rsbLength\n if (algorithm === 'reed-solomon' ? blockSymbols > 255 : algorithm === 'raptorq' && blockSymbols - 1 > 0xffffff) {\n return { frame, reject: 'repairSchemeBound' }\n }\n return { frame }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "/**\n * IP address literals as draft-ramadan-moq-multicast-02 7.2.6 requires them of\n * `sourceAddress`, `groupAddress` and `senderAddress`: the IPv4address or\n * IPv6address form of RFC 3986 3.2.2. Dotted decimal with no leading zero in\n * any dec-octet (the ls32 tail of an IPv6 literal included); IPv6 h16 groups\n * may carry leading zeros in either hex case; no brackets, no zone identifier,\n * never a name. Draft: Blockcast/moqcast-draft@5f49172.\n *\n * Addresses compare as IP addresses, never as strings, and an address of one\n * family never matches one of the other: an IPv4-mapped IPv6 address is of the\n * IPv6 family, so it parses to 16 octets and never equals its IPv4 address.\n */\n\nconst DEC_OCTET = /^(?:0|[1-9]\\d{0,2})$/;\nconst H16 = /^[0-9A-Fa-f]{1,4}$/;\n\nfunction parseIpv4(text: string): number[] | undefined {\n\tconst fields = text.split(\".\");\n\tif (fields.length !== 4) return undefined;\n\tconst octets: number[] = [];\n\tfor (const field of fields) {\n\t\tif (!DEC_OCTET.test(field)) return undefined;\n\t\tconst value = Number(field);\n\t\tif (value > 255) return undefined;\n\t\toctets.push(value);\n\t}\n\treturn octets;\n}\n\nfunction parseIpv6(text: string): Uint8Array | undefined {\n\tconst halves = text.split(\"::\");\n\tif (halves.length > 2) return undefined;\n\tconst groupsOf = (part: string) => (part === \"\" ? [] : part.split(\":\"));\n\tconst left = groupsOf(halves[0]!);\n\tconst right = halves.length === 2 ? groupsOf(halves[1]!) : [];\n\tconst words: number[][] = [];\n\tconst all = [...left, ...right];\n\t// The ls32 dotted tail ends the literal: the last group, never before \"::\".\n\tconst last = halves.length === 2 && right.length === 0 ? -1 : all.length - 1;\n\tfor (let index = 0; index < all.length; index++) {\n\t\tconst group = all[index]!;\n\t\tif (index === last && group.includes(\".\")) {\n\t\t\tconst v4 = parseIpv4(group);\n\t\t\tif (!v4) return undefined;\n\t\t\twords.push([(v4[0]! << 8) | v4[1]!, (v4[2]! << 8) | v4[3]!]);\n\t\t} else if (H16.test(group)) {\n\t\t\twords.push([Number.parseInt(group, 16)]);\n\t\t} else {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tconst leftWords = words.slice(0, left.length).flat();\n\tconst rightWords = words.slice(left.length).flat();\n\tconst missing = 8 - leftWords.length - rightWords.length;\n\tif (halves.length === 2 ? missing < 1 : missing !== 0) return undefined;\n\tconst out = new Uint8Array(16);\n\t[...leftWords, ...new Array<number>(missing).fill(0), ...rightWords].forEach((word, index) => {\n\t\tout[index * 2] = word >> 8;\n\t\tout[index * 2 + 1] = word & 0xff;\n\t});\n\treturn out;\n}\n\n/** The octets of an RFC 3986 IPv4address (4) or IPv6address (16), or `undefined`. */\nexport function parseIpAddressLiteral(text: unknown): Uint8Array | undefined {\n\tif (typeof text !== \"string\") return undefined;\n\tif (text.includes(\":\")) return parseIpv6(text);\n\tconst v4 = parseIpv4(text);\n\treturn v4 ? Uint8Array.from(v4) : undefined;\n}\n\n/** Whether two address literals name one address of one family. Unparseable never matches. */\nexport function sameIpAddress(a: unknown, b: unknown): boolean {\n\tconst x = parseIpAddressLiteral(a);\n\tconst y = parseIpAddressLiteral(b);\n\treturn x !== undefined && y !== undefined && x.length === y.length && x.every((octet, index) => octet === y[index]);\n}\n\n/**\n * One spelling per address: dotted decimal for IPv4, RFC 5952 for IPv6\n * (lowercase, no leading zeros, the longest run of two or more zero groups,\n * leftmost on a tie, as \"::\"; an IPv4-mapped address in hex like any other).\n * `undefined` for text that is not an address literal.\n */\nexport function canonicalIpAddress(text: unknown): string | undefined {\n\tconst bytes = parseIpAddressLiteral(text);\n\tif (!bytes) return undefined;\n\tif (bytes.length === 4) return bytes.join(\".\");\n\tconst words = Array.from({ length: 8 }, (_, index) => (bytes[index * 2]! << 8) | bytes[index * 2 + 1]!);\n\tlet bestStart = -1;\n\tlet bestLength = 1;\n\tfor (let start = 0; start < 8; ) {\n\t\tlet end = start;\n\t\twhile (end < 8 && words[end] === 0) end++;\n\t\tif (end - start > bestLength) {\n\t\t\tbestStart = start;\n\t\t\tbestLength = end - start;\n\t\t}\n\t\tstart = end === start ? start + 1 : end;\n\t}\n\tconst hex = words.map(word => word.toString(16));\n\tif (bestStart < 0) return hex.join(\":\");\n\treturn `${hex.slice(0, bestStart).join(\":\")}::${hex.slice(bestStart + bestLength).join(\":\")}`;\n}\n", "/**\n * 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 { sha256 } from '@noble/hashes/sha256'\n\n/** Fixed-width bytes before a record's digest list. */\nexport const BCPV2_HEADER_LENGTH = 68\nexport const BCPV2_SIGNATURE_LENGTH = 64\nexport const BCPV2_MIN_RECORD_LENGTH = BCPV2_HEADER_LENGTH + 32 + BCPV2_SIGNATURE_LENGTH\nexport const BCPV2_MAX_RECORD_LENGTH = 1316\nexport const BCPV2_MAX_DIGESTS = 37\nexport const BCPV2_HASH_ALG_SHA256 = 1\n\nexport interface BcProvenanceV2Endpoint {\n /** S in the AMBI pseudoheader and signature context. */\n sourceAddress: string\n /** G in the AMBI pseudoheader and signature context. */\n groupAddress: string\n /** UDP source and destination port of protected datagrams and, in band, of their records. */\n port: number\n /** Catalog-declared channel generation. */\n streamId: number\n}\n\nexport interface BcProvenanceV2Record {\n kid: number\n epoch: number\n mseq: bigint\n firstSeq: bigint\n count: number\n hashAlg: number\n flags: number\n sent: number\n ctx: Uint8Array\n digests: Uint8Array[]\n signature: Uint8Array\n /** Record bytes covered by the signature, excluding the signature itself. */\n body: Uint8Array\n /** A defensive copy of the complete wire record. */\n raw: Uint8Array\n}\n\nconst text = new TextEncoder()\nconst MAGIC = Uint8Array.from([0x4d, 0x48, 0x4d, 0x32]) // MHM2\n\n/**\n * multicast-02 7.2.7: a datagram on a protected flow's `port` whose first four\n * octets are \"MHM2\" is a record candidate (step 1); every other one is data\n * (step 2). Nothing else is read to decide.\n */\nexport function isBcpV2RecordCandidate(datagram: Uint8Array): boolean {\n return datagram.byteLength >= 4 &&\n datagram[0] === MAGIC[0] && datagram[1] === MAGIC[1] && datagram[2] === MAGIC[2] && datagram[3] === MAGIC[3]\n}\nconst SIGNING_DOMAIN = text.encode('mcast-manifest-v2\\0')\n\nfunction equalBytes(left: Uint8Array, right: Uint8Array): boolean {\n if (left.byteLength !== right.byteLength) return false\n let different = 0\n for (let index = 0; index < left.byteLength; index++) different |= left[index]! ^ right[index]!\n return different === 0\n}\n\nfunction u16(value: number): Uint8Array {\n const result = new Uint8Array(2)\n new DataView(result.buffer).setUint16(0, value)\n return result\n}\n\nfunction u32(value: number): Uint8Array {\n const result = new Uint8Array(4)\n new DataView(result.buffer).setUint32(0, value)\n return result\n}\n\nfunction validU32(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff\n}\n\nfunction validPort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffff\n}\n\nfunction parseIpv4(address: string): Uint8Array {\n const fields = address.split('.')\n if (fields.length !== 4 || fields.some((field) => !/^\\d+$/.test(field))) {\n throw new Error(`invalid IPv4 address ${address}`)\n }\n const values = fields.map(Number)\n if (values.some((value, index) => value > 255 || String(value) !== fields[index])) {\n throw new Error(`invalid IPv4 address ${address}`)\n }\n return Uint8Array.from(values)\n}\n\nfunction parseIpv6(address: string): Uint8Array {\n const compressed = address.includes('::')\n if ((address.match(/::/g) ?? []).length > 1) throw new Error(`invalid IPv6 address ${address}`)\n const [leftText, rightText = ''] = address.split('::')\n const left = leftText ? leftText.split(':') : []\n const right = rightText ? rightText.split(':') : []\n // RFC 3986 IPv6address: an ls32 dotted tail may end the literal (multicast-02\n // 7.2.6), never precede \"::\"; it stands for the last two h16 groups.\n const tail = compressed ? right : left\n if (tail.length > 0 && tail[tail.length - 1]!.includes('.')) {\n const v4 = parseIpv4(tail.pop()!)\n tail.push(((v4[0]! << 8) | v4[1]!).toString(16), ((v4[2]! << 8) | v4[3]!).toString(16))\n }\n if (left.some((group) => group.includes('.')) || right.some((group) => group.includes('.'))) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const missing = 8 - left.length - right.length\n if (missing < 0 || (!compressed && missing !== 0) || (compressed && missing < 1)) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const groups = [...left, ...Array.from({ length: missing }, () => '0'), ...right]\n if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/i.test(group))) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const result = new Uint8Array(16)\n const view = new DataView(result.buffer)\n groups.forEach((group, index) => view.setUint16(index * 2, Number.parseInt(group, 16)))\n return result\n}\n\n/** Parse an IPv4 or IPv6 address in the native-width form used on the wire. */\nexport function parseBcProvenanceV2Address(address: string): Uint8Array {\n if (address.includes(':')) return parseIpv6(address)\n return parseIpv4(address)\n}\n\nfunction endpointBytes(endpoint: BcProvenanceV2Endpoint): {\n source: Uint8Array\n group: Uint8Array\n} {\n if (!validPort(endpoint.port) || !validU32(endpoint.streamId)) {\n throw new Error('invalid BCPV2 endpoint')\n }\n const source = parseBcProvenanceV2Address(endpoint.sourceAddress)\n const group = parseBcProvenanceV2Address(endpoint.groupAddress)\n if (source.byteLength !== group.byteLength) throw new Error('endpoint address families differ')\n return { source, group }\n}\n\nfunction readU32(view: DataView, offset: number): number {\n return view.getUint32(offset)\n}\n\nfunction readU64(view: DataView, offset: number): bigint {\n return view.getBigUint64(offset)\n}\n\n/**\n * Decode the fixed-layout BCPV2 record from multicast-02 Section 7.2.3.\n * Structural validation happens before callers inspect the variable digest list.\n */\nexport function decodeBcProvenanceV2Record(input: Uint8Array): BcProvenanceV2Record {\n const bytes = Uint8Array.from(input)\n if (\n bytes.byteLength < BCPV2_MIN_RECORD_LENGTH ||\n bytes.byteLength > BCPV2_MAX_RECORD_LENGTH ||\n (bytes.byteLength - 132) % 32 !== 0\n ) {\n throw new Error('invalid BCPV2 record length')\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n if (!equalBytes(bytes.subarray(0, 4), MAGIC)) throw new Error('invalid BCPV2 record magic')\n const count = view.getUint16(28)\n if (count < 1 || count > BCPV2_MAX_DIGESTS || bytes.byteLength !== 132 + count * 32) {\n throw new Error('invalid BCPV2 record count')\n }\n const body = bytes.slice(0, bytes.byteLength - BCPV2_SIGNATURE_LENGTH)\n const digests: Uint8Array[] = []\n for (let index = 0; index < count; index++) {\n digests.push(bytes.slice(BCPV2_HEADER_LENGTH + index * 32, BCPV2_HEADER_LENGTH + (index + 1) * 32))\n }\n return {\n kid: readU32(view, 4),\n epoch: readU32(view, 8),\n mseq: readU64(view, 12),\n firstSeq: readU64(view, 20),\n count,\n hashAlg: bytes[30]!,\n flags: bytes[31]!,\n sent: readU32(view, 32),\n ctx: bytes.slice(36, 68),\n digests,\n signature: bytes.slice(bytes.byteLength - BCPV2_SIGNATURE_LENGTH),\n body,\n raw: bytes,\n }\n}\n\n/** Return the exact Ed25519 preimage from multicast-02 Section 7.2.4: the prefix binds `u16(port)`. */\nexport function bcProvenanceV2SignedMessage(\n record: BcProvenanceV2Record,\n endpoint: BcProvenanceV2Endpoint,\n): Uint8Array {\n const { source, group } = endpointBytes(endpoint)\n const prefix = new Uint8Array(SIGNING_DOMAIN.byteLength + 4 + source.byteLength + group.byteLength + 2)\n let offset = 0\n prefix.set(SIGNING_DOMAIN, offset)\n offset += SIGNING_DOMAIN.byteLength\n prefix.set(u32(endpoint.streamId), offset)\n offset += 4\n prefix.set(source, offset)\n offset += source.byteLength\n prefix.set(group, offset)\n offset += group.byteLength\n prefix.set(u16(endpoint.port), offset)\n const message = new Uint8Array(prefix.byteLength + record.body.byteLength)\n message.set(prefix)\n message.set(record.body, prefix.byteLength)\n return message\n}\n\n/**\n * Compute the AMBI Section 3.4.2 digest for one transmitted datagram.\n * The payload includes the Source FEC Payload ID when one was transmitted.\n */\nexport function bcProvenanceV2DatagramDigest(\n endpoint: BcProvenanceV2Endpoint,\n payload: Uint8Array,\n): Uint8Array {\n if (payload.byteLength > 0xffff) throw new Error('datagram payload exceeds u16 length')\n const { source, group } = endpointBytes(endpoint)\n const pseudoheader = new Uint8Array(source.byteLength + group.byteLength + 2 + 2 + 2 + 2 + 4)\n let offset = 0\n pseudoheader.set(source, offset)\n offset += source.byteLength\n pseudoheader.set(group, offset)\n offset += group.byteLength\n pseudoheader.set([0, 0x11], offset)\n offset += 2\n pseudoheader.set(u16(payload.byteLength), offset)\n offset += 2\n pseudoheader.set(u16(endpoint.port), offset)\n offset += 2\n pseudoheader.set(u16(endpoint.port), offset)\n offset += 2\n pseudoheader.set(u32(endpoint.streamId), offset)\n const input = new Uint8Array(pseudoheader.byteLength + payload.byteLength)\n input.set(pseudoheader)\n input.set(payload, pseudoheader.byteLength)\n return sha256(input)\n}\n\n/** Verify the ordered digest list against the transmitted datagrams. */\nexport function verifyBcProvenanceV2Datagrams(\n record: BcProvenanceV2Record,\n endpoint: BcProvenanceV2Endpoint,\n datagrams: readonly Uint8Array[],\n): boolean {\n return record.hashAlg === BCPV2_HASH_ALG_SHA256 &&\n datagrams.length === record.count &&\n datagrams.every((datagram, index) => equalBytes(\n bcProvenanceV2DatagramDigest(endpoint, datagram),\n record.digests[index]!,\n ))\n}\n\nexport interface BcProvenanceV2Params {\n codec: string\n /** 0 where MOQ-MMT Section 4.4.2 resolves none. */\n timescale: number\n groupDurationTicks: number\n /** The entry's `fec` array (fec-catalog-00 3.1); absent or empty for no FEC. */\n fec?: readonly BcProvenanceV2FecInstance[]\n}\n\nexport interface BcProvenanceV2FecInstance {\n /** Orders the tuples, never hashed; 0 when absent. */\n instanceId?: number\n /** Registered scheme identifier; \"none\" is reserved and rejected. */\n scheme: string\n sourceSymbols: number\n repairSymbols: number\n symbolSize: number\n interleaveDepthMs?: number\n}\n\nfunction concat(parts: Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0))\n let offset = 0\n for (const part of parts) {\n out.set(part, offset)\n offset += part.byteLength\n }\n return out\n}\n\nfunction lp16(value: Uint8Array | string): Uint8Array {\n const bytes = typeof value === 'string' ? text.encode(value) : value\n if (bytes.byteLength > 0xffff) throw new Error('lp16 field exceeds u16 length')\n return concat([u16(bytes.byteLength), bytes])\n}\n\n/**\n * `params_hash` of multicast-02 Section 7.2.4, hash_alg 1: one tuple per FEC\n * instance in ascending `instanceId`, neither the count nor an `instanceId`\n * encoded; no instance hashes as `lp16(\"none\")` then four u32(0).\n */\nexport function bcProvenanceV2ParamsHash(params: BcProvenanceV2Params): Uint8Array {\n const fec = [...(params.fec ?? [])].sort((a, b) => (a.instanceId ?? 0) - (b.instanceId ?? 0))\n if (fec.some((instance) => instance.scheme === 'none')) {\n throw new Error('FEC scheme \"none\" is reserved (fec-core-00 15.2)')\n }\n const tuples = fec.length === 0\n ? [lp16('none'), u32(0), u32(0), u32(0), u32(0)]\n : fec.flatMap((instance) => [\n lp16(instance.scheme),\n u32(instance.sourceSymbols),\n u32(instance.repairSymbols),\n u32(instance.symbolSize),\n u32(instance.interleaveDepthMs ?? 0),\n ])\n return sha256(concat([\n text.encode('BCPV2-params'),\n lp16(params.codec),\n u32(params.timescale),\n u32(params.groupDurationTicks),\n ...tuples,\n ]))\n}\n\n/**\n * `ctx` of multicast-02 Section 7.2.4: the namespace fields in order, the endpoint's\n * packetIds ascending, and `params_hash`. A flow without a MoQ catalog uses 32 zero octets.\n */\nexport function bcProvenanceV2Ctx(\n namespace: readonly (Uint8Array | string)[],\n packetIds: readonly number[],\n params: BcProvenanceV2Params,\n): Uint8Array {\n const ids = [...packetIds].sort((a, b) => a - b)\n if (namespace.length > 0xffff || ids.length > 0xffff) throw new Error('ctx field count exceeds u16')\n return sha256(concat([\n text.encode('BCPV2-ctx'),\n u16(namespace.length),\n ...namespace.map(lp16),\n u16(ids.length),\n ...ids.map(u16),\n bcProvenanceV2ParamsHash(params),\n ]))\n}\n", "/**\n * Thin transport wrapper for SSM multicast \u2014 join, socket, watchdog,\n * takeover/failback. Delegates raw packets to a callback (MmtpRouter\n * or any direct consumer).\n */\n\nimport {\n isMulticastTrackPacketId,\n subscriptionDeliversPacketBatches,\n toWindowMulticastSubscribeConfig,\n type WindowMulticastDelivery,\n type WindowMulticastGateway,\n type WindowMulticastSubscription,\n} from '@blockcast/mmt-transport'\nimport {\n MMTP_PACKET_ID_MAX,\n MMTP_SIGNALING_PACKET_ID,\n readMMTPPacketId,\n sameIpAddress,\n} from '@blockcast/mmt-base'\nimport { isBcpV2RecordCandidate } from '@blockcast/mmt-manifest-verify'\n\nexport interface MulticastTransportConfig {\n /** SSM source; omitted on ASM. */\n sourceAddress?: string\n /** ASM sender filter (multicast-contract 1.1.0 `senderAddress`); never beside `sourceAddress`. */\n senderAddress?: string\n groupAddress: string\n port: number\n mode?: 'auto' | 'native' | 'tunnel'\n amtRelay?: string\n amtRelayPort?: number\n /** `'driad'`: with no relay, the gateway discovers one per RFC 8777. */\n amtDiscovery?: 'driad'\n packetIds?: number[]\n tracks?: { name: string; packetId: number }[]\n /** ATSC 3.0 tuning (`catalogAtsc3NetworkSource`); when set the join is the tuner. */\n atsc3?: { frequency: number; plpId?: number; serviceId?: number }\n /**\n * The join is a protected flow (multicast-02 7.2.3): its bc-provenance\n * records ride in band on this `port`, so a datagram whose first four octets\n * are \"MHM2\" passes ahead of the packet_id read and the allow-list, and is\n * never counted toward a packet_id. On an unprotected join it is an\n * unparseable MMTP packet, as before.\n *\n * A protected join admits only datagrams whose reported sender is the\n * flow's S (`sourceAddress`, or `senderAddress` on ASM), compared as IP\n * addresses, before anything hashes them (multicast-02 7.2.7). A datagram\n * with no reported sender is dropped, and a join that cannot report one\n * (no S, or an ATSC 3.0 tuner) is declined `invalid-request`.\n */\n protected?: boolean\n}\n\nexport type ResolvedMulticastTransport = 'native' | 'amt'\n\n/**\n * Why a joined transport handed control back to MoQ.\n *\n * - `silence`: the data plane was live (at least one accepted packet since the\n * join) and then stayed silent for the catalog-derived failback window.\n * - `subscription-error`: the provider reported the subscription failed. This\n * is the provider's own explicit, typed outcome (a `MulticastError`, branch\n * on its `code`), e.g. a bridge whose AMT tunnel it declared dead.\n */\nexport type MulticastFailbackReason =\n | { readonly kind: 'silence'; readonly windowMs: number }\n | { readonly kind: 'subscription-error'; readonly error: unknown }\n\n/**\n * Why a join() resolved false, named by the step at which the join stopped,\n * never by what an error message says.\n *\n * - `disposed`: the transport was disposed before the join started, or while\n * the provider's subscribe() was pending.\n * - `unavailable`: there is no window.multicast gateway with a subscribe().\n * - `superseded`: a newer join() started while this join's subscribe() was\n * pending; the newer join owns the transport.\n * - `invalid-request`: the join config does not make a valid window.multicast\n * request (toWindowMulticastSubscribeConfig refused it), so the provider was\n * never asked.\n * - `provider-rejected`: the provider's subscribe() rejected or threw. The only\n * cause that carries the provider's own verdict (`providerError`).\n * - `invalid-subscription`: subscribe() resolved something that is not a usable\n * subscription: no event surface, a malformed `eventTypes`, an unknown\n * `delivery`, or an `addEventListener` that threw.\n * - `delivery-hook-failed`: the consumer's onDelivery hook threw for the\n * delivery the subscription declared, e.g. 'ordered-relay' to FEC tracks\n * configured without ordered geometry. A local configuration failure, not\n * the provider's.\n */\nexport type MulticastJoinDeclineReason =\n | 'disposed'\n | 'unavailable'\n | 'superseded'\n | 'invalid-request'\n | 'provider-rejected'\n | 'invalid-subscription'\n | 'delivery-hook-failed'\n\n/**\n * The typed part of a provider's subscribe() rejection. `name` and `code` are\n * the provider's own typed fields; the frozen multicast contract makes `code`\n * the one to branch on (e.g. a bridge's `amt_relay_refused`). `message` is\n * free text carried for humans only: never branch on it.\n */\nexport interface MulticastJoinProviderError {\n readonly name: string\n readonly message: string\n /** Present only when the provider's error carries a string `code`. */\n readonly code?: string\n}\n\n/**\n * How a join() resolved: joined, or the typed reason it resolved false.\n * `providerError` is present only for `provider-rejected`, and only when the\n * provider's rejection value carries a string `name` and `message`.\n */\nexport type MulticastJoinOutcome =\n | { readonly ok: true }\n | {\n readonly ok: false\n readonly reason: MulticastJoinDeclineReason\n readonly providerError?: MulticastJoinProviderError\n }\n\n/**\n * Receive counters plus the ingress-filter drop counters.\n *\n * `packetsReceived` / `bytesReceived` count only packets that passed the\n * allow-list and were handed to the page \u2014 a rejected packet must never\n * inflate them. The `dropped*` counters are the other half of that story:\n * without them a filtered PID is indistinguishable from a PID that was never\n * transmitted, which is exactly the ambiguity that makes an all-zero FEC\n * repair count unreadable (a repair leg dropped here and a repair leg the\n * publisher never sent produce byte-identical downstream telemetry).\n */\nexport interface MulticastTransportStats {\n packetsReceived: number\n bytesReceived: number\n /** Parsed cleanly, but the packet ID is absent from the join's allow-list. */\n packetsDroppedUnregisteredPid: number\n /** MMTP header did not parse; the packet ID is unknown, so it cannot be attributed. */\n packetsDroppedUnparseable: number\n /**\n * packet_id 0, the MMTP signaling flow. Expected traffic on a protected\n * session (moq_mmt sends the AL-FEC message on it), so it is counted apart\n * from the unregistered-PID fault counter and never takes a\n * `droppedPacketIds` slot.\n */\n packetsDroppedSignaling: number\n /**\n * From a sender other than the one this join locked to (BLO-50098). An\n * unprotected ASM subscription admits any source, so two senders can share\n * one (G, port, packet_id); the first sender whose packet is accepted owns\n * the sub-flow until the next join, and every other sender is dropped here,\n * before any gap, duplicate or FEC ledger sees it. A join or a silence\n * failback releases the lock. On a protected join it also counts every\n * datagram whose sender is not the flow's S, or is not reported.\n */\n packetsDroppedForeignSender: number\n}\n\n/**\n * join() calls by how they resolved, cumulative for the life of the transport:\n * across rejoins, failed joins and dispose. `succeeded` plus every `declined`\n * count is the number of join() calls that resolved, superseded ones\n * included; a join() that rejects counts in none (see\n * MulticastTransport.lastJoinOutcome).\n */\nexport interface MulticastJoinStats {\n /** join() calls that resolved true. */\n readonly succeeded: number\n /** join() calls that resolved false, by the reason each one did. */\n readonly declined: Readonly<Record<MulticastJoinDeclineReason, number>>\n}\n\nconst JOIN_SUCCEEDED: MulticastJoinOutcome = Object.freeze({ ok: true })\n\n/**\n * The typed part of a provider's subscribe() rejection, or undefined when the\n * rejection value is not an error with a string `name` and `message`. Nothing\n * is filled in for a field the provider did not type, and the message is\n * carried, never read.\n */\nfunction providerErrorOf(error: unknown): MulticastJoinProviderError | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const fields = error as { readonly name?: unknown; readonly message?: unknown; readonly code?: unknown }\n let name: unknown\n let message: unknown\n let code: unknown\n try {\n name = fields.name\n message = fields.message\n code = fields.code\n } catch {\n // A rejection value whose fields throw on read (a revoked Proxy) types\n // nothing. Reading it must not turn a declined join into a rejected one.\n return undefined\n }\n if (typeof name !== 'string' || typeof message !== 'string') return undefined\n return Object.freeze(typeof code === 'string' ? { name, message, code } : { name, message })\n}\n\n/**\n * Distinct unregistered PIDs retained for diagnostics. A conformant catalog\n * advertises a handful of PIDs per endpoint, so this only has to be large\n * enough to name every leg of a mis-provisioned one; the cap exists so a\n * malformed or hostile stream cycling the 16-bit PID space cannot grow the map\n * without bound. Drops keep counting in aggregate after the cap.\n */\nconst MAX_TRACKED_DROPPED_PIDS = 16\n\ntype MulticastGlobals = typeof globalThis & {\n multicast?: WindowMulticastGateway\n}\n\nfunction getMulticastGlobals(): MulticastGlobals {\n return globalThis as MulticastGlobals\n}\n\nfunction hasMulticastGateway(): boolean {\n return typeof getMulticastGlobals().multicast?.subscribe === 'function'\n}\n\n/**\n * The `bytes` event's sender (multicast-contract 1.3.0); undefined when unknown.\n *\n * The provider filters (S, G, port) and owns the multicast-02 7.2.7\n * destination MUST: the event carries no destination address, so libmmt\n * cannot discard a datagram whose IP destination is not G (pim BLO-51627).\n * On a protected flow libmmt checks the sender itself, against S.\n */\nfunction senderOf(event: CustomEvent<unknown>): string | undefined {\n const sourceAddress = (event as { sourceAddress?: unknown }).sourceAddress\n return typeof sourceAddress === 'string' ? sourceAddress : undefined\n}\n\n/**\n * The packet one `bytes` event (or one element of a `packets` batch) carries,\n * as a view of the provider's own bytes. A Uint8Array is used as it is; the\n * bridge hands every packet over as one, so the common path builds nothing.\n */\nfunction normalizeBytesEventDetail(detail: unknown): Uint8Array | null {\n const value =\n detail && typeof detail === 'object' && 'data' in detail\n ? (detail as { data?: unknown }).data\n : detail\n if (value instanceof Uint8Array) return value\n if (value instanceof ArrayBuffer) return new Uint8Array(value)\n if (ArrayBuffer.isView(value)) {\n return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n }\n return null\n}\n\n/**\n * A provider that can replace a live subscription's MMTP packet-ID filter\n * without rejoining its socket: the frozen multicast contract's\n * `Subscription.setPacketIds` (pim `packages/multicast-contract`). It is not\n * declared on every provider this transport meets, so it is detected, never\n * assumed.\n */\ninterface PacketFilterUpdater {\n setPacketIds(packetIds: readonly number[]): Promise<void> | void\n}\n\nfunction packetFilterUpdater(\n subscription: WindowMulticastSubscription,\n): PacketFilterUpdater['setPacketIds'] | undefined {\n const update = (subscription as Partial<PacketFilterUpdater>).setPacketIds\n return typeof update === 'function' ? update.bind(subscription) : undefined\n}\n\nfunction sameIds(a: ReadonlySet<number>, b: ReadonlySet<number>): boolean {\n if (a.size !== b.size) return false\n for (const id of a) if (!b.has(id)) return false\n return true\n}\n\n/**\n * The provider-side packet filter of one live join, and the in-place updates\n * sent to it. It lives exactly as long as the join: a later join or release\n * replaces it, and an acknowledgement for a replaced one is ignored.\n */\ninterface ProviderFilter {\n /**\n * The filter as the newest acknowledged update left it: the packet IDs the\n * provider forwards, or null when it forwards every ID. Until an update is\n * acknowledged it is the join's own filter.\n */\n acknowledged: ReadonlySet<number> | null\n /** Updates sent to the provider, numbered from 1 in send order. */\n sent: number\n /** The number of the newest acknowledged update; 0 for the join's own filter. */\n acknowledgedUpdate: number\n /** Updates sent and not yet settled. */\n inFlight: number\n}\n\n/** Which packet surface a join listens to, and its listener. */\ntype PacketListener =\n | { readonly type: 'bytes'; readonly listener: (event: CustomEvent<unknown>) => void }\n | { readonly type: 'packets'; readonly listener: (event: CustomEvent<readonly unknown[]>) => void }\n\nexport class MulticastTransport<TPacketContext = never> {\n #onPacket: (packet: Uint8Array, context?: TPacketContext) => void\n #subscription: WindowMulticastSubscription | null = null\n #packetListener: PacketListener | null = null\n #resolvedTransport: ResolvedMulticastTransport | undefined\n #videoConfirmed = false\n #audioConfirmed = false\n // One armed no-packets timer per silence window (BLO-36599). Packets only\n // record #lastPacketAtMs; the timer re-arms for the remainder when it wakes\n // inside the window, instead of a clear+set pair on every packet.\n #watchdog: ReturnType<typeof setTimeout> | null = null\n #lastPacketAtMs = 0\n #failbackWindowMs: number | null | undefined\n #onTakeover: (() => void) | null = null\n #onFailback: ((reason: MulticastFailbackReason) => void) | null = null\n // Data-plane-live edge: the first packet this join accepted. Everything\n // before it is join latency, not silence (see #resetWatchdog).\n #dataPlaneLive = false\n #onDataPlaneLive: (() => void) | null = null\n #errorListener: ((event: CustomEvent<unknown>) => void) | null = null\n #onDelivery: ((delivery: WindowMulticastDelivery) => void) | null = null\n #delivery: WindowMulticastDelivery | undefined\n #disposed = false\n #stats: MulticastTransportStats = {\n packetsReceived: 0,\n bytesReceived: 0,\n packetsDroppedUnregisteredPid: 0,\n packetsDroppedUnparseable: 0,\n packetsDroppedSignaling: 0,\n packetsDroppedForeignSender: 0,\n }\n // join() calls by how they resolved; see joinStats.\n #joinsSucceeded = 0\n readonly #joinsDeclined: Record<MulticastJoinDeclineReason, number> = {\n disposed: 0,\n unavailable: 0,\n superseded: 0,\n 'invalid-request': 0,\n 'provider-rejected': 0,\n 'invalid-subscription': 0,\n 'delivery-hook-failed': 0,\n }\n // The newest resolved join's outcome (see lastJoinOutcome) and the join\n // generation it belongs to. Generations are issued in start order, so a\n // join that resolves after a newer join has recorded leaves it standing.\n #lastJoinOutcome: MulticastJoinOutcome | undefined = undefined\n #lastJoinOutcomeGeneration = 0\n #droppedPidCounts = new Map<number, number>()\n // Accepted bytes per MMTP packet_id; see bytesReceivedForPacketIds. Each\n // value is a mutable cell, so the per-packet update is one lookup and an\n // in-place add: no insertion once the key exists, and no re-boxed number\n // per packet once a count outgrows the small-integer range.\n readonly #acceptedBytesByPacketId = new Map<number, { bytes: number }>()\n #packetIds: ReadonlySet<number> | null = null\n #protected = false\n // A protected join's S, and the last sender spelling that matched it, so a\n // matching sender parses once per spelling rather than per datagram.\n #protectedSource: string | undefined = undefined\n #matchedSource: string | undefined = undefined\n // The sender S this join's accepted packets came from, once known; see\n // packetsDroppedForeignSender. Cleared by every join and by a silence\n // failback, so a replacement publisher is not locked out.\n #sender: string | undefined = undefined\n readonly #lockedTo: (() => string | undefined) | undefined\n // What the live join delivers with each accepted packet. A field, not a\n // listener closure, so an in-place admission change (setContext) applies\n // from the next packet on without rejoining.\n #context: TPacketContext | undefined = undefined\n // The live join's provider-side filter. An in-place widening must reach it,\n // or the provider never delivers the new ID; null without a live join.\n #providerFilter: ProviderFilter | null = null\n // IDs an in-place update withdrew while the provider may still be\n // delivering them. Their late packets are still counted as filtered, but\n // they are the expected tail of a deliberate change, not a provisioning\n // fault, so they are counted apart and do not raise the unadvertised-PID\n // warning.\n readonly #withdrawnPacketIds = new Set<number>()\n #withdrawnPacketIdDrops = 0\n // Distinct packet IDs dropped as provisioning faults: on the wire but in\n // no allow-list the join or an in-place update declared. Capped with\n // droppedPacketIds.\n readonly #unadvertisedPacketIds = new Set<number>()\n #joinGeneration = 0\n\n // The provider to subscribe through; window.multicast when unset.\n readonly #gateway: WindowMulticastGateway | undefined\n\n /**\n * `lockedTo` reads the sender another transport is locked to (a repair\n * group follows its media join, whose publisher sends both). While it reads\n * a sender, this join admits only that one; until then its own lock applies.\n */\n constructor(\n onPacket: (packet: Uint8Array, context?: TPacketContext) => void,\n options: { gateway?: WindowMulticastGateway; lockedTo?: () => string | undefined } = {},\n ) {\n this.#onPacket = onPacket\n this.#gateway = options.gateway\n this.#lockedTo = options.lockedTo\n }\n\n static isAvailable(): boolean {\n return hasMulticastGateway()\n }\n\n async join(config: MulticastTransportConfig, context?: TPacketContext): Promise<boolean> {\n const generation = ++this.#joinGeneration\n this.#releaseSubscription()\n this.#dataPlaneLive = false\n this.#sender = undefined\n\n if (this.#disposed) return this.#declined(generation, 'disposed')\n const gateway = this.#gateway ?? getMulticastGlobals().multicast\n if (typeof gateway?.subscribe !== 'function') {\n console.warn('[MulticastTransport] Multicast API not available')\n return this.#declined(generation, 'unavailable')\n }\n\n // What a throw from the step in progress means. Each step names its\n // failure before it runs, so the catch classifies a decline by where the\n // join stopped, never by what the error says.\n let failure: MulticastJoinDeclineReason = 'invalid-request'\n const protectedSource = config.protected === true ? config.sourceAddress ?? config.senderAddress : undefined\n if (config.protected === true && (protectedSource === undefined || config.atsc3)) {\n console.warn('[MulticastTransport] protected join refused: the path reports no per-datagram sender to check against S')\n return this.#declined(generation, 'invalid-request')\n }\n try {\n const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null\n console.log(\n `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? '*'}, ${config.groupAddress}, ${config.port})`,\n )\n const subscribeConfig = toWindowMulticastSubscribeConfig({\n source: config.sourceAddress,\n senderAddress: config.senderAddress,\n group: config.groupAddress,\n port: config.port,\n mode: config.mode,\n amtRelay: config.amtRelay,\n amtRelayPort: config.amtRelayPort,\n amtDiscovery: config.amtDiscovery,\n packetIds: config.packetIds,\n tracks: config.tracks,\n atsc3: config.atsc3,\n })\n failure = 'provider-rejected'\n const subscription = await gateway.subscribe(subscribeConfig)\n failure = 'invalid-subscription'\n console.log(`[MulticastTransport] window.multicast.subscribe returned:`, !!subscription)\n\n if (this.#disposed || generation !== this.#joinGeneration) {\n void subscription.unsubscribe()\n return this.#declined(generation, this.#displacedReason())\n }\n if (typeof subscription.addEventListener !== 'function') {\n if (typeof subscription.unsubscribe === 'function') {\n void subscription.unsubscribe()\n }\n throw new Error('window.multicast subscription does not support bytes events')\n }\n\n // Own the subscription before anything below can throw, so a failed\n // join's release reaches unsubscribe() instead of leaking it.\n this.#subscription = subscription\n const live = () => !this.#disposed && generation === this.#joinGeneration\n // Exactly one packet surface. A provider that advertises the `packets`\n // extension delivers each reader batch as one event, so the page pays\n // one DOM event and one liveness read per batch instead of per packet\n // (BLO-36599). Every packet still goes through the same filter, counters\n // and callback, in arrival order, as a view of the provider's bytes.\n // A batch carries no per-packet sender, so an any-source join, the one\n // join that can see two senders, listens to `bytes` (BLO-50098). That\n // costs batching on every any-source join, including one on a pre-1.3.0\n // provider that sends no sender and so gets no lock for it; keep it\n // unless the provider can say it carries the sender.\n // A protected join checks every datagram's sender, so it listens to `bytes` too.\n const anySource = !config.sourceAddress && !config.senderAddress && !config.atsc3\n const packetListener: PacketListener = !anySource && protectedSource === undefined && subscriptionDeliversPacketBatches(subscription)\n ? {\n type: 'packets',\n listener: (event) => {\n if (!live()) return\n this.#deliverBatch(event.detail, live)\n },\n }\n : {\n type: 'bytes',\n listener: (event) => {\n if (!live()) return\n if (this.#deliver(event.detail, senderOf(event))) this.#resetWatchdog()\n },\n }\n this.#packetListener = packetListener\n this.#packetIds = packetIds\n this.#protected = config.protected === true\n this.#protectedSource = protectedSource\n this.#matchedSource = undefined\n this.#providerFilter = {\n acknowledged: packetIds,\n sent: 0,\n acknowledgedUpdate: 0,\n inFlight: 0,\n }\n this.#context = context\n // Seed the allow-list's counters so accepted packets never insert. An\n // id already counted by an earlier join keeps its count: the counters\n // are cumulative across rejoins, as bytesReceived is.\n if (packetIds) {\n for (const packetId of packetIds) {\n if (!this.#acceptedBytesByPacketId.has(packetId)) {\n this.#acceptedBytesByPacketId.set(packetId, { bytes: 0 })\n }\n }\n }\n // The request origin is the accepted acquisition contract. The\n // gateway no longer exposes a mutable transport/transportchange\n // surface, so assign this only after subscribe() has resolved and the\n // returned subscription has passed the event-surface check above.\n this.#resolvedTransport = subscribeConfig.origin === 'amt' ? 'amt' : 'native'\n // The provider alone knows whether its packets are independent\n // datagrams or an ordered relay of one socket; undeclared promises no\n // ordering. Announce it before the listener can deliver a packet. A\n // rejected declaration fails the join and releases the subscription.\n const delivery = subscription.delivery ?? 'datagram'\n if (delivery !== 'datagram' && delivery !== 'ordered-relay') {\n throw new Error(\n `window.multicast subscription declared unknown delivery ${String(delivery)}`,\n )\n }\n this.#delivery = delivery\n failure = 'delivery-hook-failed'\n this.#onDelivery?.(delivery)\n failure = 'invalid-subscription'\n // The provider's explicit, terminal subscription failure. Before the\n // first packet this is the only signal that bounds a join, so it must\n // reach the failback path rather than be dropped.\n const onError = (event: CustomEvent<unknown>) => {\n if (this.#disposed || generation !== this.#joinGeneration) return\n this.#failBackOnSubscriptionError(event.detail)\n }\n this.#errorListener = onError\n if (packetListener.type === 'packets') {\n subscription.addEventListener('packets', packetListener.listener)\n } else {\n subscription.addEventListener('bytes', packetListener.listener)\n }\n subscription.addEventListener('error', onError)\n // No watchdog here. A resolved subscribe() is the provider's join\n // outcome, not proof that the data plane is live: over AMT it resolves\n // once the gateway has sent its Membership Update, and the relay's\n // upstream join plus one path RTT still precede the first packet. The\n // silence window is a packet-gap clock for a stream that has flowed;\n // charging join latency to it failed back joins that went on to play.\n // The join phase is bounded by the provider's typed error or the host's\n // takeover deadline (see SessionManager.joinActive). The window starts\n // at the first accepted packet (#resetWatchdog).\n console.log(\n `[MulticastTransport] Joined ${config.groupAddress}:${config.port}`,\n )\n return this.#joined(generation)\n } catch (err) {\n if (this.#disposed || generation !== this.#joinGeneration) {\n return this.#declined(generation, this.#displacedReason())\n }\n this.#releaseSubscription()\n console.error('[MulticastTransport] Failed to join SSM group:', err)\n return this.#declined(\n generation,\n failure,\n failure === 'provider-rejected' ? providerErrorOf(err) : undefined,\n )\n }\n }\n\n /** Record a join that resolved true; returns its result. */\n #joined(generation: number): true {\n this.#joinsSucceeded++\n this.#recordJoinOutcome(generation, JOIN_SUCCEEDED)\n return true\n }\n\n /** Record a join that resolved false, and why; returns its result. */\n #declined(\n generation: number,\n reason: MulticastJoinDeclineReason,\n providerError?: MulticastJoinProviderError,\n ): false {\n this.#joinsDeclined[reason]++\n this.#recordJoinOutcome(\n generation,\n Object.freeze(\n providerError === undefined ? { ok: false, reason } : { ok: false, reason, providerError },\n ),\n )\n return false\n }\n\n /**\n * Why a join that no longer owns the transport resolved false. dispose()\n * advances the join generation too, so disposal is checked first.\n */\n #displacedReason(): 'disposed' | 'superseded' {\n return this.#disposed ? 'disposed' : 'superseded'\n }\n\n #recordJoinOutcome(generation: number, outcome: MulticastJoinOutcome): void {\n if (generation <= this.#lastJoinOutcomeGeneration) return\n this.#lastJoinOutcomeGeneration = generation\n this.#lastJoinOutcome = outcome\n }\n\n /**\n * One packet from the subscription: filter, count, and hand it to the\n * callback. Returns whether it was delivered as liveness (a delivered record\n * candidate is not); the caller notes liveness.\n */\n #deliver(detail: unknown, sourceAddress?: string): boolean {\n const packet = normalizeBytesEventDetail(detail)\n if (!packet) {\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n if (!this.#acceptPacket(packet, sourceAddress)) return false\n const liveness = !this.#isRecordCandidate(packet)\n // Read per packet: a callback that changes the admission (an in-place\n // rendition switch committing on this very packet) takes effect from the\n // next packet, including the rest of a batch.\n this.#onPacket(packet, this.#context)\n return liveness\n }\n\n #isRecordCandidate(packet: Uint8Array): boolean {\n return this.#protected && isBcpV2RecordCandidate(packet)\n }\n\n /**\n * One `packets` event. Each packet is delivered as a `bytes` event would\n * have delivered it, including isolation: a packet whose callback throws\n * does not cost the rest of its batch, as it would not have cost the next\n * `bytes` event. The first failure is rethrown once the batch is done, so\n * it reaches the event-listener error reporting a `bytes` listener's throw\n * reached; any further one is rethrown from its own microtask, which the\n * host reports the same way, so no failure is swallowed. A callback that\n * disposes the transport or starts another join ends the batch there, as\n * the per-packet guard would have ignored the remaining events.\n */\n #deliverBatch(\n batch: readonly unknown[],\n live: () => boolean,\n ): void {\n if (!Array.isArray(batch)) {\n throw new TypeError('[MulticastTransport] packets event detail must be an array of packets')\n }\n let delivered = false\n let failures: unknown[] | null = null\n for (let i = 0; i < batch.length; i++) {\n try {\n if (this.#deliver(batch[i])) delivered = true\n } catch (error) {\n if (failures === null) failures = []\n failures.push(error)\n }\n if (!live()) break\n }\n // One liveness read covers the batch: every packet in it arrived in the\n // same provider task. A batch that delivered nothing, like a rejected\n // bytes event, is not liveness.\n if (delivered && live()) this.#resetWatchdog()\n if (failures !== null) {\n for (let i = 1; i < failures.length; i++) {\n const failure = failures[i]\n queueMicrotask(() => {\n throw failure\n })\n }\n throw failures[0]\n }\n }\n\n /**\n * Feed a raw packet from an external source (e.g., AMT tunnel or ASM\n * subscription). `sourceAddress` is the datagram's sender when the source\n * knows it; see packetsDroppedForeignSender.\n */\n feedPacket(data: ArrayBuffer | Uint8Array, context?: TPacketContext, sourceAddress?: string): void {\n if (this.#disposed) return\n const packet = data instanceof Uint8Array ? data : new Uint8Array(data)\n if (!this.#acceptPacket(packet, sourceAddress)) return\n const liveness = !this.#isRecordCandidate(packet)\n this.#onPacket(packet, context)\n if (liveness) this.#resetWatchdog()\n }\n\n confirmVideo(): void {\n this.#videoConfirmed = true\n this.#checkTakeover()\n }\n\n confirmAudio(): void {\n this.#audioConfirmed = true\n this.#checkTakeover()\n }\n\n onTakeover(cb: () => void): void {\n this.#onTakeover = cb\n }\n\n onFailback(cb: (reason: MulticastFailbackReason) => void): void {\n this.#onFailback = cb\n }\n\n /**\n * Called on every successful join, before the joined subscription can\n * deliver a packet, with how that subscription delivers packets.\n */\n onDelivery(cb: (delivery: WindowMulticastDelivery) => void): void {\n this.#onDelivery = cb\n }\n\n /** How the current subscription delivers packets, once joined. */\n get delivery(): WindowMulticastDelivery | undefined {\n return this.#delivery\n }\n\n /**\n * Fires once per join, at the first packet the join accepts (after the PID\n * filter; a rejected packet is not liveness). This is the data-plane-live\n * edge, where this transport's silence window starts. A join whose source\n * never sends never reaches it, so a host may start a media-arrival budget\n * here only if the provider bounds the join phase with a typed subscription\n * error (see SessionManager.joinActive); otherwise the host starts it when\n * the subscribe settles.\n */\n onDataPlaneLive(cb: () => void): void {\n this.#onDataPlaneLive = cb\n }\n\n /** Whether the current join has accepted a packet (see onDataPlaneLive). */\n get dataPlaneLive(): boolean {\n return this.#dataPlaneLive\n }\n\n setFailbackWindowMs(windowMs: number | null): void {\n if (windowMs !== null && (!Number.isFinite(windowMs) || windowMs <= 0)) {\n throw new Error(\n `[MulticastTransport] failbackWindowMs=${windowMs}. ` +\n `Derive a positive failback window from the stream catalog.`,\n )\n }\n this.#failbackWindowMs = windowMs\n if (windowMs === null) {\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n return\n }\n // Re-enable against the last packet even when the previous policy disabled\n // the watchdog: a silent live stream may never deliver another packet.\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n if (this.#dataPlaneLive && this.#onFailback) {\n const silentMs = performance.now() - this.#lastPacketAtMs\n this.#armWatchdog(Math.max(0, Math.ceil(windowMs - silentMs)))\n }\n }\n\n get stats(): MulticastTransportStats {\n return this.#stats\n }\n\n /**\n * How the newest join() to resolve did: `{ ok: true }`, or `{ ok: false }`\n * with the typed reason it resolved false (and, for `provider-rejected`, the\n * provider's typed error fields). Undefined until a join resolves. Branch on\n * `reason` and `providerError.code`, never on log text or\n * `providerError.message`.\n *\n * Newest is by start order: a join that resolves after a newer join has\n * recorded its outcome does not replace it, so once `await join()` resolves\n * this names that join's outcome unless a newer join has already resolved.\n * It records how a join resolved, not whether it is still joined (see\n * `joined`); a join() that rejects instead of resolving records nothing,\n * the rejection being its report. Frozen: a later outcome replaces the\n * object, never changes it.\n */\n get lastJoinOutcome(): MulticastJoinOutcome | undefined {\n return this.#lastJoinOutcome\n }\n\n /**\n * join() calls by how they resolved (see MulticastJoinStats). A fresh frozen\n * snapshot on every read, so a later join never changes one already read.\n * Kept apart from `stats`, which is the live counter object.\n */\n get joinStats(): MulticastJoinStats {\n return Object.freeze({\n succeeded: this.#joinsSucceeded,\n declined: Object.freeze({ ...this.#joinsDeclined }),\n })\n }\n\n /**\n * Accepted bytes summed over `packetIds`: the `bytesReceived` share of those\n * MMTP packet_ids. Counted by the same rule, on every path that counts\n * `bytesReceived` (only packets that pass the ingress filter; packet_id 0\n * signaling never), and cumulative for the life of the transport, across\n * rejoins, as `bytesReceived` is. An id never accepted contributes 0. An\n * unparseable packet admitted by a join without an allow-list has no\n * packet_id, so it counts toward `bytesReceived` only.\n *\n * Use it to scope an ABR throughput sample to the active tier's source and\n * repair packet_ids when the join admits more than the active tier (a\n * shared group joined across rendition switches); `bytesReceived` is then\n * the whole allow-list's arrival, not the active tier's. A Set, so an id\n * shared by two tracks of the tier is counted once.\n *\n * Monotonic only for a fixed set: each id has counted since it was first\n * admitted, so after a tier switch the new set's sum is not a continuation\n * of the old one. A cumulative consumer takes a fresh baseline for the new\n * set and never differences across sets.\n */\n bytesReceivedForPacketIds(packetIds: ReadonlySet<number>): number {\n let bytes = 0\n for (const packetId of packetIds) {\n bytes += this.#acceptedBytesByPacketId.get(packetId)?.bytes ?? 0\n }\n return bytes\n }\n\n /**\n * Per-PID drop breakdown, newest-capped at MAX_TRACKED_DROPPED_PIDS distinct\n * PIDs. This is what names the offender: `packetsDroppedUnregisteredPid`\n * says the ingress filter fired, this says it fired on 0x8001.\n */\n get droppedPacketIds(): ReadonlyMap<number, number> {\n return this.#droppedPidCounts\n }\n\n /**\n * Distinct packet IDs dropped as provisioning faults: arriving on the wire\n * but in no allow-list the join or an in-place update (setPacketIds)\n * declared, the drops that raise the unadvertised-PID warning. A packet ID\n * an in-place update withdrew is not one (see withdrawnPacketIdDrops).\n * Cumulative across rejoins and capped with droppedPacketIds.\n */\n get unadvertisedPacketIds(): ReadonlySet<number> {\n return this.#unadvertisedPacketIds\n }\n\n /**\n * Packets dropped because an in-place update (setPacketIds) withdrew their\n * packet ID while the provider may still deliver it: the expected tail of\n * a deliberate change. A subset of packetsDroppedUnregisteredPid,\n * cumulative across rejoins.\n */\n get withdrawnPacketIdDrops(): number {\n return this.#withdrawnPacketIdDrops\n }\n\n /** The sender this join is locked to; undefined until one is known. */\n get sender(): string | undefined {\n return this.#sender\n }\n\n /** Whether a subscription is live: joined, and not yet released or replaced. */\n get joined(): boolean {\n return this.#subscription !== null\n }\n\n /**\n * What the live join delivers with every accepted packet (the join's\n * `context`, or the last setContext), or undefined without a live join.\n */\n get context(): TPacketContext | undefined {\n return this.#context\n }\n\n /**\n * The live join's page-side packet-ID allow-list, or null when it admits\n * every packet ID (a join that carried none, or no live join).\n */\n get packetIds(): ReadonlySet<number> | null {\n return this.#packetIds\n }\n\n /**\n * Replace what the live join delivers with each accepted packet, from the\n * next packet on. The subscription (socket, group membership, AMT tunnel)\n * is untouched: this is how an in-place rendition switch moves the router's\n * ingress identities without leaving and re-joining the group.\n */\n setContext(context: TPacketContext): void {\n if (this.#subscription === null) {\n throw new Error(\n '[MulticastTransport] setContext needs a live join; join() carries the first context',\n )\n }\n this.#context = context\n }\n\n /**\n * Replace the live join's packet-ID allow-list in place, without rejoining.\n *\n * The page-side filter changes synchronously, before this returns: the\n * next packet is admitted or dropped by the new list. The provider's own\n * filter is updated through the subscription's `setPacketIds` when it has\n * one; the returned promise settles with the provider's acknowledgement of\n * this update. It resolves at once, telling the provider nothing, only\n * when the provider cannot be told, or when no earlier update is still in\n * flight and the provider already forwards exactly this list.\n *\n * While an update is in flight the provider's filter is not known, so every\n * later update is sent: one skipped because it matched the last\n * acknowledged filter would let the in-flight update land after it, and\n * leave the provider forwarding a list nobody asked for last. Updates are\n * numbered in send order and the newest acknowledged one describes the\n * provider, so an acknowledgement that overtakes an older one is not undone\n * by it.\n *\n * A provider without `setPacketIds` can still be narrowed: its wider filter\n * keeps delivering the dropped IDs and the page-side filter discards them.\n * It cannot be widened, since it would never deliver the added IDs; that is\n * refused with a throw before anything changes. An ID this update withdraws\n * may keep arriving until the provider applies the narrower list; those\n * packets are counted as filtered like any other and in\n * withdrawnPacketIdDrops, but are not unadvertised packet IDs: their\n * arrival is the expected tail of this change rather than a provisioning\n * fault.\n */\n setPacketIds(packetIds: readonly number[]): Promise<void> {\n const subscription = this.#subscription\n const filter = this.#providerFilter\n if (subscription === null || filter === null) {\n throw new Error(\n '[MulticastTransport] setPacketIds needs a live join; join() carries the first allow-list',\n )\n }\n if (!Array.isArray(packetIds) || packetIds.length === 0) {\n throw new RangeError(\n '[MulticastTransport] setPacketIds needs a non-empty packet-ID list: ' +\n 'an in-place update names the packet IDs it admits',\n )\n }\n if (!packetIds.every(isMulticastTrackPacketId)) {\n throw new RangeError(\n `[MulticastTransport] packetId must be an integer from 1 to ${MMTP_PACKET_ID_MAX}; ` +\n \"packetId 0 is the MMTP signaling flow and is never a track's\",\n )\n }\n const next: ReadonlySet<number> = new Set(packetIds)\n const provider = filter.acknowledged\n const update = packetFilterUpdater(subscription)\n // Without an updater nothing is ever sent, so the join's filter is exact.\n if (update === undefined && provider !== null) {\n const missing = [...next].filter((id) => !provider.has(id))\n if (missing.length > 0) {\n throw new Error(\n '[MulticastTransport] this subscription cannot widen its packet filter in place ' +\n `(the provider exposes no setPacketIds); rejoin to admit ${missing.join(', ')}`,\n )\n }\n }\n const previous = this.#packetIds\n this.#packetIds = next\n for (const id of next) {\n this.#withdrawnPacketIds.delete(id)\n if (!this.#acceptedBytesByPacketId.has(id)) {\n this.#acceptedBytesByPacketId.set(id, { bytes: 0 })\n }\n }\n if (previous !== null) {\n for (const id of previous) if (!next.has(id)) this.#withdrawnPacketIds.add(id)\n }\n if (update === undefined) return Promise.resolve()\n if (filter.inFlight === 0 && provider !== null && sameIds(provider, next)) {\n return Promise.resolve()\n }\n const number = ++filter.sent\n filter.inFlight++\n let applied: Promise<void>\n try {\n applied = Promise.resolve(update([...next]))\n } catch (error) {\n applied = Promise.reject(error)\n }\n return applied.then(\n () => {\n if (this.#providerFilter !== filter) return\n filter.inFlight--\n if (number > filter.acknowledgedUpdate) {\n filter.acknowledgedUpdate = number\n filter.acknowledged = next\n }\n },\n (error: unknown) => {\n // A refused update leaves the provider on the filter it had.\n if (this.#providerFilter === filter) filter.inFlight--\n throw error\n },\n )\n }\n\n get resolvedTransport(): ResolvedMulticastTransport | undefined {\n return this.#resolvedTransport\n }\n\n dispose(): void {\n ++this.#joinGeneration\n this.#disposed = true\n this.#releaseSubscription()\n }\n\n #releaseSubscription(): void {\n if (this.#watchdog) clearTimeout(this.#watchdog)\n this.#watchdog = null\n if (this.#subscription) {\n const packetListener = this.#packetListener\n if (packetListener?.type === 'packets') {\n this.#subscription.removeEventListener('packets', packetListener.listener)\n } else if (packetListener) {\n this.#subscription.removeEventListener('bytes', packetListener.listener)\n }\n if (this.#errorListener) {\n this.#subscription.removeEventListener('error', this.#errorListener)\n }\n void this.#subscription.unsubscribe()\n }\n this.#subscription = null\n this.#packetListener = null\n this.#errorListener = null\n this.#resolvedTransport = undefined\n this.#delivery = undefined\n this.#packetIds = null\n this.#protected = false\n this.#protectedSource = undefined\n this.#matchedSource = undefined\n this.#providerFilter = null\n this.#context = undefined\n this.#withdrawnPacketIds.clear()\n }\n\n /** The ingress filter; an accepted packet is counted here, once. */\n #acceptPacket(packet: Uint8Array, sourceAddress?: string): boolean {\n const source = this.#protectedSource\n if (source !== undefined && (sourceAddress === undefined || sourceAddress !== this.#matchedSource)) {\n if (sourceAddress === undefined || !sameIpAddress(sourceAddress, source)) {\n this.#stats.packetsDroppedForeignSender++\n return false\n }\n this.#matchedSource = sourceAddress\n }\n // Absent means unknown (a pre-1.3.0 provider, a tuner): never a new sender.\n // A protected flow's S check above replaces the first-seen lock, which\n // compares spellings and would refuse S written another way.\n const owner = source === undefined ? this.#lockedTo?.() ?? this.#sender : undefined\n if (sourceAddress !== undefined && owner !== undefined && sourceAddress !== owner) {\n this.#stats.packetsDroppedForeignSender++\n return false\n }\n // A record candidate of a protected flow carries no packet_id: the router\n // splits it off before anything parses it (7.2.7). Step 1 has not run on\n // it yet, so it neither takes the sender lock nor counts as liveness\n // (#isRecordCandidate, #deliver): records alone must not hold off\n // silence failback.\n if (this.#isRecordCandidate(packet)) {\n this.#countAccepted(packet, null, undefined)\n return true\n }\n // This early read is intentional: rejected PIDs must not affect page\n // delivery, receive statistics, or the failback liveness watchdog. It\n // refuses exactly what parseMMTPHeader refuses but builds no header\n // object; the router parses the header once, later (BLO-36599).\n const packetId = readMMTPPacketId(packet)\n if (packetId === null) {\n // Without an allow-list there is no filter to enforce, so an unparseable\n // packet keeps flowing to the router as before.\n if (!this.#packetIds) {\n this.#countAccepted(packet, null, sourceAddress)\n return true\n }\n this.#stats.packetsDroppedUnparseable++\n return false\n }\n // packet_id 0 is the MMTP signaling flow and never a track's. Drop it even\n // when the join carried no allow-list (a legacy catalog whose tracks omit\n // packetId): the discard rule must not depend on how the catalog was written.\n if (packetId === MMTP_SIGNALING_PACKET_ID) {\n this.#stats.packetsDroppedSignaling++\n return false\n }\n if (!this.#packetIds || this.#packetIds.has(packetId)) {\n this.#countAccepted(packet, packetId, sourceAddress)\n return true\n }\n this.#recordUnregisteredPidDrop(packetId)\n return false\n }\n\n /** Receive counters for one accepted packet; `packetId` is null when it has none. */\n #countAccepted(packet: Uint8Array, packetId: number | null, sourceAddress: string | undefined): void {\n this.#sender ??= sourceAddress\n this.#stats.packetsReceived++\n this.#stats.bytesReceived += packet.byteLength\n if (packetId === null) return\n const counter = this.#acceptedBytesByPacketId.get(packetId)\n if (counter !== undefined) {\n counter.bytes += packet.byteLength\n } else {\n // First sight of this id on a join with no allow-list (an allow-list's\n // ids are seeded at join). Bounded by the 16-bit packet_id space.\n this.#acceptedBytesByPacketId.set(packetId, { bytes: packet.byteLength })\n }\n }\n\n #recordUnregisteredPidDrop(packetId: number): void {\n this.#stats.packetsDroppedUnregisteredPid++\n const withdrawn = this.#withdrawnPacketIds.has(packetId)\n if (withdrawn) this.#withdrawnPacketIdDrops++\n const seen = this.#droppedPidCounts.get(packetId)\n if (seen !== undefined) {\n this.#droppedPidCounts.set(packetId, seen + 1)\n return\n }\n if (this.#droppedPidCounts.size >= MAX_TRACKED_DROPPED_PIDS) return\n this.#droppedPidCounts.set(packetId, 1)\n if (withdrawn) return\n this.#unadvertisedPacketIds.add(packetId)\n // Warn once per distinct PID. A PID arriving on the wire but missing from\n // the catalog's advertised set is a provisioning fault, not stream noise,\n // and it is silent in every downstream counter.\n console.warn(\n `[MulticastTransport] Dropping packetId=${packetId} (0x${packetId.toString(16)}): ` +\n `not in the advertised set [${[...(this.#packetIds ?? [])].join(', ')}]. ` +\n `If this is an FEC repair leg, the catalog is not advertising its packetId.`,\n )\n }\n\n #checkTakeover(): void {\n if (this.#videoConfirmed && this.#audioConfirmed && this.#onTakeover) {\n const cb = this.#onTakeover\n this.#onTakeover = null // fire exactly once\n cb()\n }\n }\n\n /**\n * Note liveness now; arm the no-packets timer if none is armed.\n *\n * Called only for accepted packets, so the first call of a join is the\n * data-plane-live edge and is what first arms the silence window: the window\n * measures silence of a flowing stream, which is undefined before the stream\n * has flowed.\n */\n #resetWatchdog(): void {\n this.#lastPacketAtMs = performance.now()\n if (!this.#dataPlaneLive) {\n this.#dataPlaneLive = true\n this.#onDataPlaneLive?.()\n }\n if (this.#watchdog !== null) return\n if (!this.#onFailback) return\n if (this.#failbackWindowMs === null) return\n if (this.#failbackWindowMs === undefined) {\n throw new Error(\n '[MulticastTransport] Failback watchdog requires a catalog-derived window',\n )\n }\n this.#armWatchdog(this.#failbackWindowMs)\n }\n\n #armWatchdog(delayMs: number): void {\n this.#watchdog = setTimeout(() => {\n this.#watchdog = null\n if (this.#disposed || !this.#onFailback || this.#failbackWindowMs == null) return\n const silentMs = performance.now() - this.#lastPacketAtMs\n if (silentMs < this.#failbackWindowMs) {\n this.#armWatchdog(Math.ceil(this.#failbackWindowMs - silentMs))\n return\n }\n console.warn(\n `[MulticastTransport] No packets for ${this.#failbackWindowMs}ms; failing back to MoQ`,\n )\n // The sub-flow's owner went silent: a sender that resumes after this,\n // the same one or a replacement publisher, owns the sub-flow anew.\n this.#sender = undefined\n this.#onFailback({ kind: 'silence', windowMs: this.#failbackWindowMs })\n }, delayMs)\n }\n\n /** Hand control back to MoQ for a provider-reported subscription failure. */\n #failBackOnSubscriptionError(error: unknown): void {\n // The subscription is dead; a silence timer armed against it would only\n // report the same failure a second time.\n if (this.#watchdog !== null) clearTimeout(this.#watchdog)\n this.#watchdog = null\n console.warn('[MulticastTransport] Subscription failed; failing back to MoQ:', error)\n this.#onFailback?.({ kind: 'subscription-error', error })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["entry", "packetData", "MPU_HEADER_SIZE", "MPU_LENGTH_FIELD_SIZE", "MPU_HEADER_REMAINDER_SIZE", "MPU_HEADER_SIZE", "MMTP_SIGNALING_PACKET_ID", "MMTP_PACKET_ID_MAX", "NTP_SHORT_BITS", "NTP_SHORT_MODULUS", "NTP_SHORT_HALF_RANGE", "FEC_BLOCK_PROFILES", "text", "parseIpAddressLiteral", "RUNTIME_FEC_RECEIVER", "MMTP_PACKET_ID_MAX", "MMTP_SIGNALING_PACKET_ID"]
|
|
7
7
|
}
|