@blockcast/mmt-transport 0.2.2-main.cde7600fad6a → 0.2.2-main.ce3c8aeb7f68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/multicast-tier.ts", "../src/abr-controller.ts", "../node_modules/@blockcast/mmt-alta-parse/src/index.ts", "../node_modules/@blockcast/mmt-base/src/mmtp.ts", "../src/endpoint-config.ts", "../src/multicast-orchestrator.ts", "../src/multicast-signaling-reconciler.ts", "../src/multicast-catalog-policy.ts", "../src/multicast-subscription-pool.ts", "../src/moqt-priority.ts", "../src/stall-detector.ts", "../src/takeover.ts", "../src/driad-discovery.ts", "../src/transport-manager.ts", "../src/unicast-fallback.ts", "../src/no-modules.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 * Per draft-ramadan-moq-fec-00, ABR decisions should use the **effective loss rate**\n * after FEC recovery, not the raw transport-level loss rate. This allows FEC to\n * mask burst losses without unnecessarily downgrading quality.\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 * Standard quality levels for progressive ABR\n */\nexport const QUALITY_LEVELS: QualityLevel[] = [\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];\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 for ABR decisions per draft-ramadan-moq-fec-00\n * - Progressive quality upgrades (waits for stability)\n * - Fast quality downgrades on congestion\n */\nexport class AdaptiveBitrateController {\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(initialQuality?: QualityLevel) {\n\t\tthis.currentQualityIndex = initialQuality ? QUALITY_LEVELS.findIndex((q) => q.name === initialQuality.name) : 0;\n\n\t\tif (this.currentQualityIndex < 0) this.currentQualityIndex = 0;\n\t}\n\n\tsetCallback(callback: (quality: QualityLevel) => void) {\n\t\tthis.onQualityChange = callback;\n\t}\n\n\tgetCurrentQuality(): QualityLevel {\n\t\treturn QUALITY_LEVELS[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// Per draft-ramadan-moq-fec-00: 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 < QUALITY_LEVELS.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 * Per draft-ramadan-moq-fec-00, ABR should use the effective loss rate\n\t * (after FEC recovery) rather than 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 >= QUALITY_LEVELS.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 * Force quality level (for testing)\n\t */\n\tsetQuality(name: string) {\n\t\tconst index = QUALITY_LEVELS.findIndex((q) => q.name === name);\n\t\tif (index >= 0) {\n\t\t\tthis.currentQualityIndex = index;\n\t\t\tthis.lastQualityChange = Date.now();\n\t\t\tthis.onQualityChange?.(this.getCurrentQuality());\n\t\t}\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 * Zero-dependency helper for extracting the detached ALTA authenticator trailer\n * from an MMTP source packet.\n *\n * Lives in its own leaf package so both `@blockcast/mmt-container` (wire-path\n * parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share\n * one implementation without creating a package cycle between them.\n *\n * Spec: ISO/IEC 23008-1:2023 + draft-ramadan-moq-mmt \u00A712 (detached trailer, Path 2).\n */\n\n// --- Private MMTP/MPU/MFU constants (ISO/IEC 23008-1 \u00A79.2.2\u20139.2.3) ---\n// Duplicated here as immutable spec values so this package stays a leaf with\n// zero `@blockcast/*` dependencies. Re-deriving from `mmt-container` would\n// reintroduce the package cycle this split exists to avoid.\n\nconst MMTP_HEADER_SIZE = 12\nconst MPU_HEADER_SIZE = 8\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nconst TIMED_MFU_DU_HEADER_SIZE = 14\nconst NONTIMED_MFU_DU_HEADER_SIZE = 4\nconst PACKET_TYPE_MPU = 0x00\n\n// --- Public ALTA trailer constants ---\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\nexport const MMTP_EXT_TYPE_ALTA_AUTH = 0xf0a1\n\n/** Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 BE). */\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = 4\n\n/** Result of extracting the detached ALTA authenticator from an MMTP packet. */\nexport interface AltaTrailer {\n /** Authenticator bytes (no ext_type/ext_length prefix, no trailing padding). */\n auth: Uint8Array\n /**\n * Wire-packet byte offset where the signed payload ends, i.e. where the\n * trailer begins. Caller uses this to produce\n * `payload = wirePacket.subarray(0, signedEnd)` for verify_detached.\n */\n signedEnd: number\n}\n\n/**\n * Extract the detached ALTA authenticator trailer from an MMTP source packet.\n *\n * Wire layout:\n * `[MMTP hdr][length(2)][remaining MPU hdr(6)][DU?][media][TRAILER][SS_ID?]`\n * `payload_length = 6 + DU bytes + media bytes`\n *\n * Returns `null` when:\n * - the packet is too small / malformed,\n * - `payload_length` cannot contain the remaining six MPU-header bytes,\n * - the declared payload overruns the packet,\n * - no bytes remain between `payload_length` and the SS_ID,\n * - the trailer's `ext_type` isn't {@link MMTP_EXT_TYPE_ALTA_AUTH},\n * - the declared `ext_length` would extend past the SS_ID boundary.\n *\n * Non-MPU packets (repair, signaling) always return `null`.\n *\n * @param options.skipFecId When true, do NOT reserve 4 trailing bytes for the\n * Source FEC Payload ID. Use on FEC-recovered symbols where the PID has\n * already been stripped upstream.\n */\nexport function extractAltaTrailer(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): AltaTrailer | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n // ISO/IEC 23008-1:2023 Figure 8 places V=0 packet_type in the low six bits.\n const packetType = mmtpPacket[1]! & 0x3f\n if (packetType !== PACKET_TYPE_MPU) return null\n\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n let headerLength = MMTP_HEADER_SIZE\n if (fecType < 2 && (mmtpPacket[0]! & 0x20) !== 0) headerLength += 4\n if (fecType < 2 && (mmtpPacket[0]! & 0x02) !== 0) {\n if (headerLength + 4 > mmtpPacket.length) return null\n const extensionLength = new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + headerLength + 2,\n 2,\n ).getUint16(0, false)\n headerLength += 4 + extensionLength\n }\n if (headerLength + MPU_HEADER_SIZE > mmtpPacket.length) return null\n\n const payload = mmtpPacket.subarray(headerLength)\n\n // Inline a minimal MPU-header read \u2014 only the fields extractAltaTrailer needs.\n // payloadLength: 16 BE at bytes 0-1.\n // Byte 2 packs FT(4) | T(1) | FI(2) | A(1).\n const payloadLength = (payload[0]! << 8) | payload[1]!\n const flagsByte = payload[2]!\n const fragmentType = (flagsByte >> 4) & 0x0f\n const timed = ((flagsByte >> 3) & 0x01) === 1\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n\n // Only MFU packets (FT=2) carry the detached trailer.\n if (fragmentType !== 2) return null\n\n // FI=0/1 structurally carries a DU header; FI=2/3 does not. Require at\n // least one media byte after that deterministic layout so the bytes at the\n // declared boundary cannot impersonate ALTA for a header-only fragment.\n const dataUnitHeaderSize = fragmentationIndicator <= 1\n ? (timed ? TIMED_MFU_DU_HEADER_SIZE : NONTIMED_MFU_DU_HEADER_SIZE)\n : 0\n if (payloadLength <= MPU_HEADER_REMAINDER_SIZE + dataUnitHeaderSize) return null\n\n if (fecType > 1) return null\n const hasFecId = !options?.skipFecId && fecType === 1\n const payloadEnd = hasFecId ? payload.length - 4 : payload.length\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (declaredEnd > payloadEnd) return null\n\n if (declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE > payloadEnd) return null\n const extType = (payload[declaredEnd]! << 8) | payload[declaredEnd + 1]!\n if (extType !== MMTP_EXT_TYPE_ALTA_AUTH) return null\n const extLen = (payload[declaredEnd + 2]! << 8) | payload[declaredEnd + 3]!\n\n const authStart = declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE\n if (authStart + extLen !== payloadEnd) return null\n\n return {\n auth: payload.subarray(authStart, payloadEnd),\n signedEnd: headerLength + declaredEnd,\n }\n}\n", "import {\n MMTP_EXT_TYPE_ALTA_AUTH as _LEAF_MMTP_EXT_TYPE_ALTA_AUTH,\n MMTP_EXT_TRAILER_HEADER_SIZE as _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE,\n extractAltaTrailer as _leafExtractAltaTrailer,\n type AltaTrailer as _LeafAltaTrailer,\n} from '@blockcast/mmt-alta-parse'\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\nconst 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 IDs\nexport const FEC_CODE_XOR = 1\nexport const FEC_CODE_RAPTORQ = 2\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 * 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\nfunction isEmptyOrExactAltaTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + _LEAF_MMTP_EXT_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) !== _LEAF_MMTP_EXT_TYPE_ALTA_AUTH) return false\n\n const authLength = view.getUint16(2, false)\n return start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: { skipFecId?: boolean },\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false }\n : null\n }\n\n if (fecType === 1) {\n const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n if (fecStart < declaredEnd || !isEmptyOrExactAltaTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false }\n }\n\n return null\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. ALTA and Source FEC\n * trailers 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-00 \u00A73.1).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\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 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 if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n if (observed !== SOURCE_FEC_PAYLOAD_ID_SIZE) {\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 ${SOURCE_FEC_PAYLOAD_ID_SIZE} ` +\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 ALTA 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?: { skipFecId?: boolean },\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?: { skipFecId?: boolean },\n): MFUExtraction | null {\n const result = planMfuExtraction(mmtpPacket, options)\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const fecPayloadId = trailerLayout.hasFecId\n ? {\n ssId: new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE,\n SOURCE_FEC_PAYLOAD_ID_SIZE,\n ).getUint32(0, false),\n }\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n if (!hasSourceFecPayloadId(mmtpPacket)) return null\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n const view = new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + off,\n SOURCE_FEC_PAYLOAD_ID_SIZE,\n )\n return { ssId: view.getUint32(0, false) }\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 if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return false\n\n let header: MMTPHeader\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return false\n }\n if (header.packetType !== PACKET_TYPE_MPU) return false\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return false\n const mpu = parseMPUHeader(payload)\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) return false\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n return resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId ?? false\n}\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\n// Re-export the ALTA trailer helper from the zero-dep leaf package. The leaf\n// owns the implementation so container and FEC consumers share one parser\n// without creating a package cycle.\nexport const MMTP_EXT_TYPE_ALTA_AUTH = _LEAF_MMTP_EXT_TYPE_ALTA_AUTH\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE\nexport type AltaTrailer = _LeafAltaTrailer\nexport const extractAltaTrailer = _leafExtractAltaTrailer\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/** Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header. */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\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 view = new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + off, REPAIR_FEC_PID_SIZE)\n const ssStart = view.getUint32(0, false) // 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 const repairId = readRepairFecPayloadIdFields(frame)\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.rsbLength === 0 ||\n repairId.ssbLength === 0 ||\n 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 * AL-FEC signaling configuration parsed from ISO/IEC 23008-1:2023 Table C.3 message.\n */\nexport interface AlFecConfig {\n fecCodeId: number\n fecPayloadIdMode: 0\n repairSymbolSize: number\n maximumK: number\n maximumP: number\n interleaveDepth: number\n oti: Uint8Array\n}\n\nfunction canonicalRaptorQOtiK(oti: Uint8Array, repairSymbolSize: number): number | null {\n if (oti.length !== 12 || oti[5] !== 0) return null\n const transferLength = (oti[0]! * 0x1_0000_0000) +\n (((oti[1]! << 24) | (oti[2]! << 16) | (oti[3]! << 8) | oti[4]!) >>> 0)\n const symbolSize = (oti[6]! << 8) | oti[7]!\n const sourceBlocks = oti[8]!\n const subBlocks = (oti[9]! << 8) | oti[10]!\n const alignment = oti[11]!\n if (!Number.isSafeInteger(transferLength) || transferLength <= 0 ||\n symbolSize !== repairSymbolSize || symbolSize === 0 || symbolSize % 8 !== 0 ||\n sourceBlocks !== 1 || subBlocks !== 1 || alignment !== 8 ||\n transferLength % symbolSize !== 0) {\n return null\n }\n const k = transferLength / symbolSize\n return Number.isInteger(k) && k >= 1 && k <= 56_403 ? k : null\n}\n\n/**\n * Parse AL-FEC signaling message from a full MMTP signaling packet.\n *\n * Wire format (ISO/IEC 23008-1:2023 Table C.3, Amendment 1:2025):\n * [12-byte MMTP header (packet_type=0x02)]\n * message_id(2) = 0x0203\n * version(1)\n * message_length(2) = length of remaining payload\n * flags(1) = fec_flag(1) | private_fec_flag(1) | reserved(6)\n * fec_flow_descriptor:\n * length(2)\n * num_flows(1)\n * per flow:\n * fec_flow_id(1), source_flow_id(1), num_assets(1), packet_ids(2*N),\n * coding_params(1), repair_symbol_size(2),\n * [one-stage]: repair_flow_id(2), fec_code_id(1),\n * [private_fec_flag]: private_header(1){flag(1)|len(7)},\n * current RaptorQ profile OTI(12) + interleave_depth(1)\n * max_k(3), max_p(3), protection_window_time(4), protection_window_size(4)\n *\n * @param mmtpPacket Full MMTP packet including 12-byte header\n * @returns Parsed config or null if not an AL-FEC signaling message\n */\nexport function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecConfig | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + 9) return null\n let header: MMTPHeader\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n if (header.packetType !== PACKET_TYPE_SIGNALING || header.fecType !== 0) return null\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < 9) return null\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n let off = 0\n\n const messageId = view.getUint16(off, false); off += 2\n if (messageId !== ALFEC_MESSAGE_ID) return null\n const version = payload[off]!; off += 1\n const messageLength = view.getUint16(off, false); off += 2\n if (version !== 1 || messageLength === 0 || messageLength !== payload.length - off) return null\n\n const flags = payload[off]!; off += 1\n const fecFlag = (flags >> 7) & 1\n const privateFecFlag = (flags >> 6) & 1\n if (fecFlag !== 1 || privateFecFlag !== 1 || (flags & 0x3f) !== 0x3f) return null\n\n if (off + 2 > payload.length) return null\n const descriptorLength = view.getUint16(off, false); off += 2\n const descriptorEnd = off + descriptorLength\n if (descriptorLength === 0 || descriptorEnd !== payload.length) return null\n\n const numFlows = payload[off]!; off += 1\n if (numFlows !== 1 || off + 3 > descriptorEnd) return null\n\n off += 2 // fec_flow_id, source_flow_id\n const numAssets = payload[off]!; off += 1\n if (numAssets === 0 || off + (numAssets * 2) > descriptorEnd) return null\n off += numAssets * 2\n\n if (off + 3 > descriptorEnd) return null\n const codingParams = payload[off]!; off += 1\n const fecCodingStructure = (codingParams >> 4) & 0x0f\n const ssbgMode = (codingParams >> 2) & 0x03\n const ffsrptsFlag = (codingParams >> 1) & 0x01\n const fecPayloadIdMode = codingParams & 0x01\n if (fecCodingStructure !== 1 || ssbgMode !== 0 || ffsrptsFlag !== 0 || fecPayloadIdMode !== 0) {\n return null\n }\n const repairSymbolSize = view.getUint16(off, false); off += 2\n if (repairSymbolSize === 0 || repairSymbolSize % 8 !== 0) return null\n\n if (off + 4 > descriptorEnd) return null\n off += 2 // Amendment 1:2025 replaces Table C.3 with a 16-bit repair_flow_id\n const fecCodeId = payload[off]!; off += 1\n if (fecCodeId !== FEC_CODE_RAPTORQ) return null\n const privateHeader = payload[off]!; off += 1\n const privateFlag = privateHeader >> 7\n const privateFieldLength = privateHeader & 0x7f\n if (privateFlag !== 1 || privateFieldLength !== 13 || off + 13 > descriptorEnd) return null\n const oti = payload.slice(off, off + 12); off += 12\n const interleaveDepth = payload[off]!; off += 1\n const otiK = canonicalRaptorQOtiK(oti, repairSymbolSize)\n if (otiK === null || interleaveDepth === 0) return null\n\n if (off + 14 !== descriptorEnd) return null\n const maximumK = (payload[off]! << 16) | (payload[off + 1]! << 8) | payload[off + 2]!\n off += 3\n const maximumP = (payload[off]! << 16) | (payload[off + 1]! << 8) | payload[off + 2]!\n off += 3\n if (maximumK !== otiK || maximumP === 0 || maximumK + maximumP - 1 > 0x00ff_ffff) {\n return null\n }\n off += 8 // protection_window_time, protection_window_size\n if (off !== descriptorEnd) return null\n\n return { fecCodeId, fecPayloadIdMode, repairSymbolSize, maximumK, maximumP, interleaveDepth, oti }\n}\n", "import { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID } from \"@blockcast/mmt-base\";\nimport type {\n\tWindowMulticastSubscribeConfig,\n\tWindowMulticastSubscribeConfigBytes,\n} from \"./types.js\";\n\nexport type MulticastTransportPreference = \"auto\" | \"native\" | \"tunnel\";\n\nexport interface MulticastEndpointConfig {\n\tgroupAddress: string;\n\tsourceAddress?: string;\n\tport: number;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\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\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tpacketIds?: readonly number[];\n\ttracks?: ReadonlyArray<{ name: string; packetId: 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 */\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 base = {\n\t\tgroup: input.group,\n\t\tport: input.port,\n\t\t...(input.source?.trim() ? { source: input.source.trim() } : {}),\n\t\tconsume: \"bytes\" as const,\n\t\t...(input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}),\n\t};\n\n\tconst relay = input.amtRelay?.trim();\n\tconst wantsAmt = input.mode === \"tunnel\" ||\n\t\t(input.mode !== \"native\" && relay !== undefined && relay !== \"\");\n\tif (!wantsAmt) {\n\t\tconst native: WindowMulticastSubscribeConfigBytes = {\n\t\t\t...base,\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 (!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\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 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\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};\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]);\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\nexport function isUsableMulticastEndpoint(\n\tendpoint: { groupAddress?: string; port?: unknown } | undefined,\n): boolean {\n\tconst port = Number(endpoint?.port);\n\treturn isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port);\n}\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\") || params.get(\"amt_relay\") || undefined;\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\") || params.get(\"amt_relay_port\"));\n\tconst requestedMode = params.get(\"mc_mode\");\n\tconst mode = amtRelay ? \"tunnel\" : multicastMode(requestedMode);\n\n\treturn 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}\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 { 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\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#lastCommittedTimestamp = Number.NEGATIVE_INFINITY;\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 || this.options.adaptive === false) return undefined;\n\t\tif (this.#state === \"joining\") return this.poll();\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\tonTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\tif (!this.#current) return undefined;\n\t\tif (packet.endpointKey === endpointKey(this.#current) && Number.isFinite(packet.timestamp)) {\n\t\t\tthis.#lastCommittedTimestamp = Math.max(this.#lastCommittedTimestamp, packet.timestamp);\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\t!Number.isFinite(packet.timestamp) ||\n\t\t\tpacket.timestamp < this.#lastCommittedTimestamp\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.#lastCommittedTimestamp = packet.timestamp;\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#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", "import { isMulticastTrackPacketId, isValidMulticastGroupAddress } from \"./endpoint-config.js\";\n\nexport interface MulticastSignalingEndpoint {\n\tendpointId: string;\n\tendpointVersion: number;\n\tprotocol: \"ssm\" | \"asm\" | \"amt\";\n\tsourceAddress?: string;\n\tgroupAddress: string;\n\tport: number;\n\tvalidUntil?: number;\n\ttracks: Array<{ name: string; packetId?: number; [key: string]: unknown }>;\n\t[key: string]: unknown;\n}\n\ntype Catalog = Record<string, any>;\ntype Signal = Record<string, any>;\n\nexport function createMulticastSignalingReconciler(catalog: Catalog, options: { catalog?: Catalog } = {}) {\n\tconst initial = normalizeCatalogEndpoints(catalog);\n\tconst endpoints = new Map(initial.map(endpoint => [endpoint.endpointId, copyEndpoint(endpoint)]));\n\tconst versions = new Map(initial.map(endpoint => [endpoint.endpointId, endpoint.endpointVersion]));\n\tconst seen = new Set<string>();\n\tconst dormant = new Set<string>();\n\tlet catalogVersion = Number(catalog?.multicast?.version ?? catalog?.version ?? 0) || 0;\n\tconst snapshot = (now = Date.now()) => ({\n\t\tcatalogVersion,\n\t\tendpoints: [...endpoints.values()].filter(endpoint => !dormant.has(endpoint.endpointId) && !isExpired(endpoint, now)).map(copyEndpoint),\n\t\tdormantEndpointIds: [...dormant].sort(),\n\t});\n\n\treturn {\n\t\tapply(signal: Signal, now = Date.now()) {\n\t\t\tif (!isObject(signal) || typeof signal.type !== \"string\" || typeof signal.msgId !== \"string\") return { applied: false, reason: \"invalid-message\" };\n\t\t\tif (seen.has(signal.msgId)) return { applied: false, reason: \"duplicate-message\" };\n\t\t\tif (signal.expiresAt != null && Number(signal.expiresAt) < now) return { applied: false, reason: \"expired\" };\n\t\t\tconst signalCatalogVersion = Number(signal.catalogVersion ?? 0);\n\t\t\tif (!Number.isFinite(signalCatalogVersion)) return { applied: false, reason: \"invalid-message\" };\n\t\t\tif (signalCatalogVersion < catalogVersion) return { applied: false, reason: \"stale-catalog\" };\n\t\t\tconst result = applyFresh(signal, now);\n\t\t\tif (result.applied) {\n\t\t\t\tseen.add(signal.msgId);\n\t\t\t\t// A heartbeat proves liveness but carries no catalog state. Ratcheting\n\t\t\t\t// here would incorrectly reject the next real snapshot as stale.\n\t\t\t\tif (signal.type !== \"heartbeat\") {\n\t\t\t\t\tcatalogVersion = Math.max(catalogVersion, signalCatalogVersion);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\t\tsnapshot,\n\t\ttoCatalog(now = Date.now()) {\n\t\t\tconst current = snapshot(now);\n\t\t\treturn { ...catalog, multicast: { ...catalog.multicast, version: current.catalogVersion, endpoints: current.endpoints } };\n\t\t},\n\t};\n\n\tfunction applyFresh(signal: Signal, now: number): { applied: boolean; reason?: string; action?: string } {\n\t\tswitch (signal.type) {\n\t\t\tcase \"snapshot\": {\n\t\t\t\tconst next = signal.payload?.endpoints;\n\t\t\t\tif (!Array.isArray(next)) return { applied: false, reason: \"invalid-message\" };\n\t\t\t\tconst normalized = next.map((endpoint, index) => normalizeEndpoint(endpoint, index));\n\t\t\t\tif (normalized.some(endpoint => !endpoint || !isValidEndpoint(endpoint, options.catalog))) return { applied: false, reason: \"invalid-endpoint\" };\n\t\t\t\tconst ids = new Set(normalized.map(endpoint => endpoint!.endpointId));\n\t\t\t\tif (ids.size !== normalized.length) return { applied: false, reason: \"duplicate-endpoint\" };\n\t\t\t\tendpoints.clear(); versions.clear(); dormant.clear();\n\t\t\t\tfor (const endpoint of normalized as MulticastSignalingEndpoint[]) {\n\t\t\t\t\tversions.set(endpoint.endpointId, endpoint.endpointVersion);\n\t\t\t\t\tif (!isExpired(endpoint, now)) endpoints.set(endpoint.endpointId, copyEndpoint(endpoint));\n\t\t\t\t}\n\t\t\t\treturn { applied: true, action: \"snapshot\" };\n\t\t\t}\n\t\t\tcase \"add\": return upsert(signal.payload?.endpoint, now);\n\t\t\tcase \"update\": {\n\t\t\t\tconst id = signal.payload?.endpointId;\n\t\t\t\tconst fields = signal.payload?.fields;\n\t\t\t\tif (!endpoints.has(id)) return { applied: false, reason: \"unknown-endpoint\" };\n\t\t\t\tif (isStale(id, signal.payload?.endpointVersion)) return { applied: false, reason: \"stale-endpoint\" };\n\t\t\t\tif (!isObject(fields) || [\"protocol\", \"sourceAddress\", \"groupAddress\", \"port\"].some(key => Object.hasOwn(fields, key))) return { applied: false, reason: \"invalid-update\" };\n\t\t\t\treturn upsert({ ...endpoints.get(id), ...fields, endpointVersion: signal.payload.endpointVersion }, now);\n\t\t\t}\n\t\t\tcase \"remove\": {\n\t\t\t\tconst id = signal.payload?.endpointId;\n\t\t\t\tif (!endpoints.has(id)) return { applied: false, reason: \"unknown-endpoint\" };\n\t\t\t\tconst endpointVersion = Number(signal.payload?.endpointVersion);\n\t\t\t\tif (!Number.isFinite(endpointVersion)) return { applied: false, reason: \"invalid-message\" };\n\t\t\t\tif (isStale(id, endpointVersion)) return { applied: false, reason: \"stale-endpoint\" };\n\t\t\t\tendpoints.delete(id); dormant.delete(id); versions.set(id, endpointVersion);\n\t\t\t\treturn { applied: true, action: \"drop\" };\n\t\t\t}\n\t\t\tcase \"invalidate\": {\n\t\t\t\tconst id = signal.payload?.endpointId;\n\t\t\t\tif (!endpoints.has(id)) return { applied: false, reason: \"unknown-endpoint\" };\n\t\t\t\tdormant.add(id);\n\t\t\t\treturn { applied: true, action: \"drop\" };\n\t\t\t}\n\t\t\tcase \"heartbeat\": return { applied: true, action: \"heartbeat\" };\n\t\t\tdefault: return { applied: false, reason: \"unknown-message-type\" };\n\t\t}\n\t}\n\n\tfunction upsert(value: unknown, now: number) {\n\t\tconst endpoint = normalizeEndpoint(value, endpoints.size);\n\t\tif (!endpoint || !isValidEndpoint(endpoint, options.catalog)) return { applied: false, reason: \"invalid-endpoint\" };\n\t\tif (isStale(endpoint.endpointId, endpoint.endpointVersion)) return { applied: false, reason: \"stale-endpoint\" };\n\t\tversions.set(endpoint.endpointId, endpoint.endpointVersion);\n\t\tdormant.delete(endpoint.endpointId);\n\t\tif (isExpired(endpoint, now)) endpoints.delete(endpoint.endpointId);\n\t\telse endpoints.set(endpoint.endpointId, copyEndpoint(endpoint));\n\t\treturn { applied: true, action: isExpired(endpoint, now) ? \"drop\" : \"candidate\" };\n\t}\n\n\tfunction isStale(id: string, version: unknown) {\n\t\tconst current = versions.get(id);\n\t\treturn current !== undefined && Number(version) <= current;\n\t}\n}\n\nexport function normalizeCatalogEndpoints(catalog: Catalog): MulticastSignalingEndpoint[] {\n\tconst endpoints = catalog?.multicast?.endpoints;\n\tif (!Array.isArray(endpoints)) return [];\n\treturn endpoints\n\t\t.map(normalizeEndpoint)\n\t\t.filter((endpoint): endpoint is MulticastSignalingEndpoint => !!endpoint && isValidEndpoint(endpoint, catalog));\n}\n\nfunction normalizeEndpoint(value: unknown, index: number): MulticastSignalingEndpoint | undefined {\n\tif (!isObject(value)) return undefined;\n\tconst endpoint = value as Record<string, any>;\n\tconst tracks = Array.isArray(endpoint.tracks) ? endpoint.tracks.map(track => typeof track === \"string\" ? { name: track } : { ...track }).filter(track => track.name) : [];\n\treturn { ...endpoint, endpointId: endpoint.endpointId || `${endpoint.sourceAddress || \"\"}|${endpoint.groupAddress || \"\"}|${endpoint.port || \"\"}|${index}`, endpointVersion: Number(endpoint.endpointVersion ?? 0), tracks } as MulticastSignalingEndpoint;\n}\n\nfunction isValidEndpoint(endpoint: MulticastSignalingEndpoint, catalog?: Catalog): boolean {\n\tif (!endpoint.endpointId || !Number.isFinite(endpoint.endpointVersion)) return false;\n\tif (![\"ssm\", \"asm\", \"amt\"].includes(endpoint.protocol)) return false;\n\tif (endpoint.protocol === \"ssm\" && (typeof endpoint.sourceAddress !== \"string\" || endpoint.sourceAddress.trim().length === 0)) return false;\n\tif (!isValidMulticastGroupAddress(endpoint.groupAddress) || !Number.isInteger(Number(endpoint.port)) || endpoint.port <= 0 || endpoint.port > 65535 || !endpoint.tracks.length) return false;\n\tconst catalogTracks = new Set([...Object.keys(catalog?.video?.renditions ?? {}), ...Object.keys(catalog?.audio?.renditions ?? {})]);\n\t// Every track must carry a valid packet_id (isMulticastTrackPacketId); one\n\t// that does not -- including packet_id 0, the MMTP signaling flow --\n\t// rejects the whole endpoint rather than silently dropping that track.\n\treturn endpoint.tracks.every(track => !!track.name && isMulticastTrackPacketId(track.packetId) && (catalogTracks.size === 0 || catalogTracks.has(track.name) || catalogTracks.has(track.name.replace(/\\/repair$/, \"\"))));\n}\n\nfunction copyEndpoint(endpoint: MulticastSignalingEndpoint): MulticastSignalingEndpoint {\n\treturn { ...endpoint, tracks: endpoint.tracks.map(track => ({ ...track })) };\n}\n\nfunction isExpired(endpoint: MulticastSignalingEndpoint, now: number): boolean {\n\treturn endpoint.validUntil != null && Number(endpoint.validUntil) < now;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\";\n}\n", "import {\n\tnormalizeCatalogEndpoints,\n\ttype MulticastSignalingEndpoint,\n} from \"./multicast-signaling-reconciler.js\";\nimport {\n\tisMulticastTrackPacketId,\n\tisUsableMulticastEndpoint,\n\tisValidMulticastGroupAddress,\n} from \"./endpoint-config.js\";\nimport type { MulticastTierEndpoint } from \"./multicast-tier.js\";\n\ntype Catalog = Record<string, any>;\n\nexport interface PlayerMulticastConfig {\n\tgroup: string;\n\tsource: string;\n\tport: number;\n\tmode?: string;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tamtNetworkSource?: unknown;\n\tpacketIds?: number[];\n}\n\nexport interface MulticastTrackSelection {\n\tcontainerKind?: string;\n\trendition?: string;\n}\n\nexport function hasMulticastCatalogBaseline(catalog: Catalog): boolean {\n\tconst multicast = catalog?.multicast;\n\treturn normalizeCatalogEndpoints(catalog).length > 0 || Boolean(multicast?.groupAddress && multicast?.port);\n}\n\nexport function hasUnicastMediaRenditions(catalog: Catalog): boolean {\n\treturn Object.keys(catalog?.video?.renditions ?? {}).length > 0 ||\n\t\tObject.keys(catalog?.audio?.renditions ?? {}).length > 0;\n}\n\nexport function isMulticastOnlyCatalog(catalog: Catalog): boolean {\n\treturn hasMulticastCatalogBaseline(catalog) && !hasUnicastMediaRenditions(catalog);\n}\n\nexport function requiresMulticastCapability(\n\tcatalog: Catalog,\n\tcapabilities: { multicast?: boolean } = {},\n): boolean {\n\treturn isMulticastOnlyCatalog(catalog) && !capabilities.multicast;\n}\n\nexport function isRecoverableMulticastSignalReadError(error: unknown): boolean {\n\tconst candidate = error as { name?: unknown; message?: unknown } | null;\n\tconst name = String(candidate?.name ?? \"\");\n\tconst message = String(candidate?.message ?? error ?? \"\");\n\treturn name === \"SyntaxError\" ||\n\t\t/not valid JSON|unexpected token|JSON\\.parse|encoded data was not valid|malformed/i.test(message);\n}\n\nexport function withMulticastEndpoints(\n\tcatalog: Catalog,\n\tendpoints: MulticastSignalingEndpoint[],\n\tversion: number,\n): Catalog {\n\tif (!catalog?.multicast) return catalog;\n\treturn {\n\t\t...catalog,\n\t\tmulticast: {\n\t\t\t...catalog.multicast,\n\t\t\tversion,\n\t\t\tendpoints: endpoints.map(copyEndpoint),\n\t\t},\n\t};\n}\n\nexport function selectMulticastCandidate(\n\tcatalog: Catalog,\n\tendpoints = normalizeCatalogEndpoints(catalog),\n\tsearch: string | URLSearchParams = currentSearchParams(),\n): MulticastSignalingEndpoint | null {\n\tconst params = search instanceof URLSearchParams ? search : new URLSearchParams(search);\n\treturn selectMulticastCandidateForTracks(catalog, endpoints, {\n\t\tcontainerKind: params.get(\"packaging\") || params.get(\"container\") || \"mmtp\",\n\t\trendition: params.get(\"rendition\") || undefined,\n\t});\n}\n\nexport function selectMulticastCandidateForTracks(\n\tcatalog: Catalog,\n\tendpoints = normalizeCatalogEndpoints(catalog),\n\tselection: MulticastTrackSelection = {},\n): MulticastSignalingEndpoint | null {\n\tconst packaging = selection.containerKind || \"mmtp\";\n\tconst renditionTrack = selection.rendition || \"\";\n\tconst preferredTrack = packaging === \"mmtp\" ? \"video\" : `video.${packaging}`;\n\tconst usableEndpoints = endpoints.filter(isUsableMulticastEndpoint);\n\tconst defaultRenditionTrack = renditionTrack ? \"\" : lowestServedVideoRendition(catalog, usableEndpoints);\n\treturn (renditionTrack && usableEndpoints.find(endpoint =>\n\t\tendpoint.tracks?.some(track => matchesPreferredTrack(track.name, renditionTrack)))) ||\n\t\t(defaultRenditionTrack && usableEndpoints.find(endpoint =>\n\t\t\tendpoint.tracks?.some(track => matchesPreferredTrack(track.name, defaultRenditionTrack)))) ||\n\t\tusableEndpoints.find(endpoint =>\n\t\t\tendpoint.tracks?.some(track => matchesPreferredTrack(track.name, preferredTrack))) ||\n\t\tusableEndpoints[0] || null;\n}\n\nexport function endpointToMulticastConfig(\n\tendpoint: MulticastSignalingEndpoint | Record<string, any> | null | undefined,\n\tcatalog: Catalog,\n\tselection?: MulticastTrackSelection,\n): PlayerMulticastConfig | null {\n\tif (!endpoint || !isUsableMulticastEndpoint(endpoint) || advertisesInvalidPacketId(endpoint)) return null;\n\tconst config: PlayerMulticastConfig = {\n\t\tgroup: endpoint.groupAddress,\n\t\tsource: endpoint.sourceAddress || \"\",\n\t\tport: Number(endpoint.port),\n\t};\n\tconst networkSource = endpoint.networkSource ?? catalog?.multicast?.networkSource;\n\tconst amt = findAmtRelay(networkSource);\n\tif (endpoint.protocol === \"amt\" || amt) {\n\t\tconfig.mode = \"tunnel\";\n\t\tif (amt) {\n\t\t\tconfig.amtRelay = amt.relay;\n\t\t\tif (amt.port) config.amtRelayPort = amt.port;\n\t\t}\n\t}\n\tconst packetIds = selection === undefined ?\n\t\tselectEndpointPacketIds(endpoint, catalog) :\n\t\tselectEndpointPacketIdsForTracks(endpoint, catalog, selection);\n\tif (packetIds === null) return null;\n\tif (packetIds.length > 0) config.packetIds = packetIds;\n\treturn config;\n}\n\nexport function selectEndpointPacketIds(\n\tendpoint: MulticastSignalingEndpoint | Record<string, any> | null | undefined,\n\tcatalog: Catalog,\n\tsearch: string | URLSearchParams = currentSearchParams(),\n): number[] | null {\n\tconst params = search instanceof URLSearchParams ? search : new URLSearchParams(search);\n\treturn selectEndpointPacketIdsForTracks(endpoint, catalog, {\n\t\tcontainerKind: params.get(\"packaging\") || params.get(\"container\") || \"mmtp\",\n\t\trendition: params.get(\"rendition\") || undefined,\n\t});\n}\n\nexport function selectEndpointPacketIdsForTracks(\n\tendpoint: MulticastSignalingEndpoint | Record<string, any> | null | undefined,\n\tcatalog: Catalog,\n\tselection: MulticastTrackSelection = {},\n): number[] | null {\n\tif (!endpoint || !Array.isArray(endpoint.tracks)) return [];\n\t// An empty result means \"no packet_id filter\" to the join, so an invalid\n\t// packet_id must fail here rather than be dropped: dropping the only track\n\t// would widen the filter to every packet_id, the signaling flow included.\n\t// null (\"this endpoint is unusable\") matches endpointToMulticastConfig on\n\t// the same input, and the widened return type is the signal to callers.\n\tif (advertisesInvalidPacketId(endpoint)) return null;\n\tconst tracks = endpoint.tracks\n\t\t.map((track: any) => ({\n\t\t\tname: typeof track?.name === \"string\" ? track.name : \"\",\n\t\t\tpacketId: validPacketId(track?.packetId),\n\t\t}))\n\t\t.filter((track: { name: string; packetId?: number }): track is { name: string; packetId: number } =>\n\t\t\ttrack.name.length > 0 && track.packetId !== undefined);\n\tif (tracks.length === 0) return [];\n\n\tconst packaging = selection.containerKind || \"mmtp\";\n\tconst renditionTrack = selection.rendition || \"\";\n\tconst videoRenditions = catalogRenditions(catalog, \"video\");\n\tconst audioRenditions = catalogRenditions(catalog, \"audio\");\n\tconst videoTracks = tracks.filter(track => Object.hasOwn(videoRenditions, track.name));\n\tconst audioTracks = tracks.filter(track => Object.hasOwn(audioRenditions, track.name));\n\tconst preferredVideo = packaging === \"mmtp\" ? \"video\" : `video.${packaging}`;\n\tconst preferredAudio = packaging === \"mmtp\" ? \"audio\" : `audio.${packaging}`;\n\tconst video =\n\t\t(renditionTrack ? videoTracks.find(track => track.name === renditionTrack) : undefined) ??\n\t\tvideoTracks.find(track => track.name === preferredVideo) ??\n\t\t[...videoTracks].sort((a, b) =>\n\t\t\tvideoRenditionRank(videoRenditions, a.name) - videoRenditionRank(videoRenditions, b.name))[0];\n\tconst audio = audioTracks.find(track => track.name === preferredAudio) ?? audioTracks[0];\n\tconst selected = [video, audio].filter(\n\t\t(track): track is { name: string; packetId: number } => track !== undefined,\n\t);\n\tif (selected.length === 0) return normalizePacketIds(tracks.map(track => track.packetId));\n\n\t// Every repair layer and overlay of a selected track is a repair track of\n\t// that track (draft-ramadan-moq-fec \u00A76.3), so a layer listed on the endpoint\n\t// is selected alongside layer 0 rather than treated as unknown.\n\tconst selectedRepairNames = new Set(selected.flatMap(track => catalogRepairTracks(catalog, track.name)));\n\tconst selectedRepairTracks = tracks.filter(track => selectedRepairNames.has(track.name));\n\treturn normalizePacketIds([...selected, ...selectedRepairTracks].map(track => track.packetId));\n}\n\nexport function readPlayerMulticastConfig(\n\tsearch: string | URLSearchParams = currentSearchParams(),\n): PlayerMulticastConfig | null {\n\tconst params = search instanceof URLSearchParams ? search : new URLSearchParams(search || \"\");\n\tconst group = params.get(\"mc_group\") || \"\";\n\tif (!isValidMulticastGroupAddress(group)) return null;\n\tconst port = positivePort(params.get(\"mc_port\"));\n\tif (!port) return null;\n\tconst mode = params.get(\"mc_mode\") || \"\";\n\tconst amtRelay = params.get(\"mc_amt_relay\") || params.get(\"amt_relay\") || \"\";\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\") || params.get(\"amt_relay_port\"));\n\tconst config: PlayerMulticastConfig = {\n\t\tgroup,\n\t\tsource: params.get(\"mc_source\") || \"\",\n\t\tport,\n\t};\n\tif (mode) config.mode = mode;\n\tif (amtRelay) {\n\t\tconfig.amtRelay = amtRelay;\n\t\tconfig.mode = \"tunnel\";\n\t}\n\tif (amtRelayPort) config.amtRelayPort = amtRelayPort;\n\treturn config;\n}\n\nexport function normalizeMulticastConfig(\n\tconfig: Partial<PlayerMulticastConfig> | null | undefined,\n): PlayerMulticastConfig | null {\n\tif (!config?.group) return null;\n\t// A join list naming an invalid packet_id is refused whole: dropping the\n\t// entry could leave the list empty, which the join reads as \"no filter\".\n\tif (Array.isArray(config.packetIds) && !config.packetIds.every(isMulticastTrackPacketId)) return null;\n\tconst amt = findAmtRelay(config.amtNetworkSource);\n\tconst amtRelay = config.amtRelay || amt?.relay || \"\";\n\tconst amtRelayPort = config.amtRelayPort ?? amt?.port;\n\tconst packetIds = normalizePacketIds(config.packetIds);\n\treturn {\n\t\tgroup: config.group,\n\t\tsource: config.source || \"\",\n\t\tport: Number(config.port),\n\t\tmode: amtRelay ? \"tunnel\" : (config.mode || \"auto\"),\n\t\tamtRelay,\n\t\t...(amtRelayPort ? { amtRelayPort } : {}),\n\t\t...(packetIds.length > 0 ? { packetIds } : {}),\n\t};\n}\n\nexport function sameMulticastConfig(\n\ta: Partial<PlayerMulticastConfig> | null | undefined,\n\tb: Partial<PlayerMulticastConfig> | null | undefined,\n): boolean {\n\tconst left = normalizeMulticastConfig(a);\n\tconst right = normalizeMulticastConfig(b);\n\treturn Boolean(left && right &&\n\t\tleft.group === right.group &&\n\t\tleft.source === right.source &&\n\t\tleft.port === right.port &&\n\t\tleft.mode === right.mode &&\n\t\tleft.amtRelay === right.amtRelay &&\n\t\t(left.amtRelayPort ?? 0) === (right.amtRelayPort ?? 0) &&\n\t\tJSON.stringify(left.packetIds ?? []) === JSON.stringify(right.packetIds ?? []));\n}\n\nexport function urlOverrideBlocksMulticastCandidate(\n\tcandidateConfig: Partial<PlayerMulticastConfig> | null | undefined,\n\turlConfig: PlayerMulticastConfig | null = readPlayerMulticastConfig(),\n): boolean {\n\tif (!urlConfig) return false;\n\tconst candidate = normalizeMulticastConfig(candidateConfig);\n\tconst explicit = normalizeMulticastConfig(urlConfig);\n\treturn !candidate || !explicit ||\n\t\tcandidate.group !== explicit.group ||\n\t\tcandidate.source !== explicit.source ||\n\t\tcandidate.port !== explicit.port;\n}\n\n/** Preserve catalog track selection while applying explicit URL transport policy. */\nexport function mergeUrlMulticastPolicy(\n\tcandidateConfig: Partial<PlayerMulticastConfig> | null | undefined,\n\turlConfig: PlayerMulticastConfig | null = readPlayerMulticastConfig(),\n): PlayerMulticastConfig | null {\n\tconst candidate = normalizeMulticastConfig(candidateConfig);\n\tif (!candidate || !urlConfig || urlOverrideBlocksMulticastCandidate(candidate, urlConfig)) return candidate;\n\n\tconst merged = { ...candidate };\n\tif (urlConfig.mode !== undefined) {\n\t\tmerged.mode = urlConfig.mode;\n\t\tif (urlConfig.mode !== \"tunnel\" && urlConfig.amtRelay === undefined) {\n\t\t\tmerged.amtRelay = \"\";\n\t\t\tdelete merged.amtRelayPort;\n\t\t}\n\t}\n\tif (urlConfig.amtRelay !== undefined) {\n\t\tmerged.amtRelay = urlConfig.amtRelay;\n\t\tmerged.mode = \"tunnel\";\n\t}\n\tif (urlConfig.amtRelayPort !== undefined) merged.amtRelayPort = urlConfig.amtRelayPort;\n\treturn normalizeMulticastConfig(merged);\n}\n\nfunction currentSearchParams(): URLSearchParams {\n\treturn typeof location === \"undefined\" ?\n\t\tnew URLSearchParams() :\n\t\tnew URLSearchParams(location.search);\n}\n\nfunction positivePort(value: string | null): number | null {\n\tconst port = Number(value);\n\treturn Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;\n}\n\nfunction copyEndpoint(endpoint: MulticastSignalingEndpoint): MulticastSignalingEndpoint {\n\treturn { ...endpoint, tracks: endpoint.tracks.map(track => ({ ...track })) };\n}\n\nfunction findAmtRelay(networkSource: unknown): { relay: string; port?: number } | undefined {\n\tconst sources = Array.isArray(networkSource) ? networkSource : networkSource ? [networkSource] : [];\n\tconst source = sources.find(candidate =>\n\t\tcandidate?.type === \"amt\" && typeof candidate?.relay === \"string\" && candidate.relay.trim());\n\tif (!source) return undefined;\n\tconst port = positivePort(String(source.port ?? \"\"));\n\tconst relay = source.relay.trim();\n\treturn port ? { relay, port } : { relay };\n}\n\nfunction validPacketId(value: unknown): number | undefined {\n\treturn isMulticastTrackPacketId(value) ? value : undefined;\n}\n\n/** True when some endpoint track advertises a `packetId` that is present but\n * not a valid track packet_id. A track that omits `packetId` (the legacy\n * string form) advertises none and is not invalid. */\nfunction advertisesInvalidPacketId(endpoint: { tracks?: unknown }): boolean {\n\treturn Array.isArray(endpoint.tracks) && endpoint.tracks.some((track: any) =>\n\t\ttrack?.packetId !== undefined && !isMulticastTrackPacketId(track.packetId));\n}\n\nfunction normalizePacketIds(packetIds: unknown): number[] {\n\tif (!Array.isArray(packetIds)) return [];\n\treturn [...new Set(packetIds.map(validPacketId).filter((id): id is number => id !== undefined))].sort(\n\t\t(a, b) => a - b,\n\t);\n}\n\nfunction matchesPreferredTrack(trackName: string, preferredTrack: string): boolean {\n\treturn trackName === preferredTrack || trackName.startsWith(`${preferredTrack}/`);\n}\n\nfunction catalogRenditions(catalog: Catalog, role: \"video\" | \"audio\"): Record<string, any> {\n\tconst renditions = { ...(catalog?.[role]?.renditions ?? {}) };\n\tfor (const track of Array.isArray(catalog?.tracks) ? catalog.tracks : []) {\n\t\tif (track?.role === role && typeof track.name === \"string\" && track.name.length > 0) {\n\t\t\trenditions[track.name] = track;\n\t\t}\n\t}\n\treturn renditions;\n}\n\n/** Repair tracks a source track (or rendition) depends on for its FEC: layer 0\n * named by `fec.repairTrack`, then every self-describing repair track that\n * `depends` on it (MSF `tracks[]` entries with `packaging: \"fec-repair\"`, or\n * hang `repair.renditions` entries), per draft-ramadan-moq-fec \u00A76.3. Names\n * carry no meaning: membership comes from the pointer and from `depends`,\n * never from a name suffix. Empty when the track carries no FEC, including\n * `fec.algorithm: \"none\"`, which the container-side derivations\n * (`carp-catalog` and `catalog-fec-params`) also read as no FEC. */\nfunction catalogRepairTracks(catalog: Catalog, name: string): string[] {\n\tconst tracks: any[] = Array.isArray(catalog?.tracks) ? catalog.tracks : [];\n\tconst track = tracks.find((candidate: any) => candidate?.name === name);\n\tconst rendition = catalog?.video?.renditions?.[name] ?? catalog?.audio?.renditions?.[name];\n\tconst fec = track?.fec ?? rendition?.fec;\n\tif (!fec || fec.algorithm === \"none\") return [];\n\tconst names = new Set<string>();\n\tif (typeof fec.repairTrack === \"string\" && fec.repairTrack.length > 0) names.add(fec.repairTrack);\n\tconst dependsOn = (entry: any): boolean =>\n\t\tArray.isArray(entry?.depends) && entry.depends.length === 1 && entry.depends[0] === name;\n\tconst common = catalog?.commonTrackFields ?? {};\n\tfor (const candidate of tracks) {\n\t\tconst packaging = candidate?.packaging ?? common?.packaging;\n\t\tif (packaging === \"fec-repair\" && typeof candidate?.name === \"string\" && candidate.name.length > 0 && dependsOn(candidate)) {\n\t\t\tnames.add(candidate.name);\n\t\t}\n\t}\n\tconst repairRenditions = catalog?.repair?.renditions;\n\tif (repairRenditions && typeof repairRenditions === \"object\") {\n\t\tfor (const [repairName, entry] of Object.entries(repairRenditions)) {\n\t\t\tif (repairName.length > 0 && dependsOn(entry)) names.add(repairName);\n\t\t}\n\t}\n\treturn [...names];\n}\n\n/** Every self-describing repair track name in the catalog, in either dialect:\n * MSF `tracks[]` entries with `packaging: \"fec-repair\"` and hang\n * `repair.renditions` keys. A repair track describes itself, so membership\n * needs no source resolution and never comes from a name suffix. */\nfunction catalogRepairTrackNames(catalog: Catalog): Set<string> {\n\tconst names = new Set<string>();\n\tconst common = catalog?.commonTrackFields ?? {};\n\tfor (const track of Array.isArray(catalog?.tracks) ? catalog.tracks : []) {\n\t\tconst packaging = track?.packaging ?? common?.packaging;\n\t\tif (packaging === \"fec-repair\" && typeof track?.name === \"string\" && track.name.length > 0) {\n\t\t\tnames.add(track.name);\n\t\t}\n\t}\n\tconst repairRenditions = catalog?.repair?.renditions;\n\tif (repairRenditions && typeof repairRenditions === \"object\") {\n\t\tfor (const name of Object.keys(repairRenditions)) {\n\t\t\tif (name.length > 0) names.add(name);\n\t\t}\n\t}\n\treturn names;\n}\n\n/** True when an endpoint carries at least one track that is not a repair\n * track, i.e. it can serve media on its own. A repair-layer endpoint carries\n * only `fec-repair` tracks and is joined by the loss-driven layer policy, so\n * it must be excluded from the bandwidth ladder (`selectMulticastTier`).\n * An endpoint listing no tracks is treated as carrying media: that is the\n * pre-layer signalling shape, and a naming-era catalog with no repair tracks\n * declared classifies every endpoint as media. */\nexport function endpointCarriesMedia(\n\tcatalog: Catalog,\n\tendpoint: { tracks?: { name?: string }[] } | null | undefined,\n): boolean {\n\tconst tracks = endpoint?.tracks;\n\tif (!Array.isArray(tracks) || tracks.length === 0) return true;\n\tconst repairNames = catalogRepairTrackNames(catalog);\n\tif (repairNames.size === 0) return true;\n\treturn tracks.some(track => typeof track?.name === \"string\" && !repairNames.has(track.name));\n}\n\n/** Catalog signalling endpoints as bandwidth-ladder endpoints, classified.\n *\n * Every ladder caller must build its `MulticastTierEndpoint[]` through this\n * rather than hand-shaping them from the catalog: `carriesMedia` is optional\n * and an unset one is treated as carrying media, so an endpoint that was\n * never classified stays selectable as a tier and joining it would leave the\n * receiver with repair symbols and no media. Classifying at the one place\n * endpoints are converted is what makes that impossible to forget.\n *\n * `bandwidth` is carried through only when the endpoint declares a positive\n * finite number and is never synthesized, so an endpoint that omits it stays\n * unranked exactly as before. Only the fields the ladder reads are copied;\n * signalling-only keys stay behind. */\nexport function catalogTierEndpoints(\n\tcatalog: Catalog,\n\tendpoints: MulticastSignalingEndpoint[] = normalizeCatalogEndpoints(catalog),\n): MulticastTierEndpoint[] {\n\treturn endpoints.map(endpoint => {\n\t\tconst tier: MulticastTierEndpoint = {\n\t\t\tgroupAddress: endpoint.groupAddress,\n\t\t\tport: Number(endpoint.port),\n\t\t\ttracks: endpoint.tracks.map(track =>\n\t\t\t\ttrack.packetId === undefined ? { name: track.name } : { name: track.name, packetId: track.packetId }),\n\t\t\tcarriesMedia: endpointCarriesMedia(catalog, endpoint),\n\t\t};\n\t\tif (endpoint.sourceAddress) tier.sourceAddress = endpoint.sourceAddress;\n\t\tif (endpoint.protocol) tier.protocol = endpoint.protocol;\n\t\tconst bandwidth = (endpoint as Record<string, unknown>).bandwidth;\n\t\tif (typeof bandwidth === \"number\" && Number.isFinite(bandwidth) && bandwidth > 0) tier.bandwidth = bandwidth;\n\t\treturn tier;\n\t});\n}\n\nfunction videoRenditionRank(config: Record<string, any>, name: string): number {\n\tif (typeof config?.bitrate === \"number\" && config.bitrate > 0) return config.bitrate;\n\tconst width = typeof config?.codedWidth === \"number\" ? config.codedWidth : 0;\n\tconst height = typeof config?.codedHeight === \"number\" ? config.codedHeight : 0;\n\tif (width > 0 && height > 0) return width * height;\n\tconst match = /(?:^|[/_.-])(\\d{3,4})p(?:$|[/_.-])/i.exec(name);\n\treturn match ? Number(match[1]) : Number.POSITIVE_INFINITY;\n}\n\nfunction lowestServedVideoRendition(\n\tcatalog: Catalog,\n\tendpoints: MulticastSignalingEndpoint[],\n): string {\n\tconst renditions = catalog?.video?.renditions ?? {};\n\tconst servedTracks = new Set(endpoints.flatMap(endpoint =>\n\t\tendpoint.tracks?.map(track => track.name) ?? []));\n\tlet selected = \"\";\n\tlet selectedRank = Number.POSITIVE_INFINITY;\n\tfor (const [name, config] of Object.entries(renditions)) {\n\t\tif (!name || !servedTracks.has(name)) continue;\n\t\tconst rank = videoRenditionRank(config as Record<string, any>, name);\n\t\tif (!selected || rank < selectedRank) {\n\t\t\tselected = name;\n\t\t\tselectedRank = rank;\n\t\t}\n\t}\n\treturn selected;\n}\n", "import {\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.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\tbytesListener: (event: CustomEvent<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});\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}));\n\t\tconst consumers = new Map<symbol, MulticastSubscriptionConsumer>();\n\t\tlet entry: PoolEntry;\n\t\tconst bytesListener = (event: CustomEvent<ArrayBuffer>) => {\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));\n\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, bytesListener, stateListener, errorListener, closed: false };\n\t\tsubscription.addEventListener(\"bytes\", bytesListener);\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\tentry.subscription.removeEventListener(\"bytes\", entry.bytesListener);\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", "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\tdeadlineRepair: moqtSubscriberPriority(0),\n\taudio: moqtSubscriberPriority(1),\n\tvideo: moqtSubscriberPriority(2),\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", "export interface FecProgressSnapshot {\n\tsourceSymbols: number;\n\trepairSymbols: number;\n\tblocksComplete: number;\n\tblocksRecovered: number;\n\tmediaFrames?: number;\n}\n\nexport interface StallDetectorConfig {\n\twindowMs: number;\n\tonStarvation(): void;\n\tonRecovery(): void;\n\tnow?: () => number;\n}\n\nexport interface PlayerStallSnapshot {\n\tliveQ?: number | null;\n\tdecodeCallsDelta?: number | null;\n\tfps?: number | null;\n}\n\nexport interface PlayerStallClassification {\n\tverdict: \"DECODE-BOUND\" | \"PIPELINE-STARVED\" | \"STARVED-OR-SLOW\" | \"INDETERMINATE\";\n\treason: string;\n}\n\nexport function classifyStall({\n\tliveQ = null,\n\tdecodeCallsDelta = null,\n\tfps = null,\n}: PlayerStallSnapshot = {}): PlayerStallClassification {\n\tif (liveQ != null && liveQ >= 20) {\n\t\treturn {\n\t\t\tverdict: \"DECODE-BOUND\",\n\t\t\treason: `decode queue backlogged now (liveQ=${liveQ})`,\n\t\t};\n\t}\n\tif (liveQ != null && liveQ <= 2 && decodeCallsDelta != null && decodeCallsDelta <= 1) {\n\t\treturn {\n\t\t\tverdict: \"PIPELINE-STARVED\",\n\t\t\treason: `queue empty (liveQ=${liveQ}) and decode flatlined (\u0394calls=${decodeCallsDelta}) \u2014 nothing reaching the decoder; suspect delivery/FEC/failback, not decode perf`,\n\t\t};\n\t}\n\tif (fps != null && fps < 45) {\n\t\treturn {\n\t\t\tverdict: \"STARVED-OR-SLOW\",\n\t\t\treason: `low decode rate (fps=${fps}) without a queue/flatline signal to disambiguate`,\n\t\t};\n\t}\n\treturn { verdict: \"INDETERMINATE\", reason: \"no signal supports a verdict\" };\n}\n\nexport class TransportStallDetector {\n\t#last: FecProgressSnapshot | null = null;\n\t#lastSampleAt = 0;\n\t#starvedSince: number | null = null;\n\t#latched = false;\n\treadonly #now: () => number;\n\n\tconstructor(private readonly config: StallDetectorConfig) {\n\t\tif (config.windowMs <= 0) throw new Error(\"Stall window must be positive and catalog-derived\");\n\t\tthis.#now = config.now ?? Date.now;\n\t}\n\n\tget isStarved(): boolean { return this.#latched; }\n\n\tupdate(snapshot: FecProgressSnapshot): void {\n\t\tconst previous = this.#last;\n\t\tconst previousAt = this.#lastSampleAt;\n\t\tconst now = this.#now();\n\t\tthis.#last = { ...snapshot };\n\t\tthis.#lastSampleAt = now;\n\t\tif (!previous) return;\n\n\t\tconst blockDelta = snapshot.blocksComplete - previous.blocksComplete +\n\t\t\tsnapshot.blocksRecovered - previous.blocksRecovered;\n\t\tconst mediaDelta = (snapshot.mediaFrames ?? 0) - (previous.mediaFrames ?? 0);\n\n\t\tif (blockDelta <= 0 && mediaDelta <= 0) {\n\t\t\tthis.#starvedSince ??= previousAt;\n\t\t\tif (!this.#latched && now - this.#starvedSince >= this.config.windowMs) {\n\t\t\t\tthis.#latched = true;\n\t\t\t\tthis.config.onStarvation();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#starvedSince = null;\n\t\tif (this.#latched) {\n\t\t\tthis.#latched = false;\n\t\t\tthis.config.onRecovery();\n\t\t}\n\t}\n\n\treset(): void {\n\t\tthis.#last = null;\n\t\tthis.#starvedSince = null;\n\t\tthis.#latched = false;\n\t}\n}\n", "export interface ActivatableSessions { activateMulticast(): void; }\n\nexport function makeSourceSwitchHandler(options: {\n\tmulticastBridgeAvailable: boolean;\n\tsessionsReady: Promise<ActivatableSessions | null>;\n\tonMulticastConfirmed?(): void;\n\tonAbortTakeover?(): void;\n\tlog?(message: string): void;\n\twarn?(message: string): void;\n}): (source: string) => Promise<void> {\n\tconst log = options.log ?? console.log;\n\tconst warn = options.warn ?? console.warn;\n\treturn async (source) => {\n\t\tif (source !== \"multicast\") return;\n\t\tif (!options.multicastBridgeAvailable) {\n\t\t\twarn(\"[MoqWatch] MediaRouter requested multicast without bridge; keeping MoQ active\");\n\t\t\treturn;\n\t\t}\n\t\tconst sessions = await options.sessionsReady;\n\t\tif (!sessions) {\n\t\t\twarn(\"[MoqWatch] SessionManager unavailable; aborting multicast takeover, staying on MoQ\");\n\t\t\toptions.onAbortTakeover?.();\n\t\t\treturn;\n\t\t}\n\t\tlog(\"[MoqWatch] MediaRouter -> multicast; activating via SessionManager\");\n\t\tsessions.activateMulticast();\n\t\toptions.onMulticastConfirmed?.();\n\t};\n}\n\nexport interface LossArcSession {\n\t/** TrackSession state. Optional so non-TrackSession hosts keep the generic API. */\n\treadonly state?: string;\n\tlossDetected(): void;\n\tlossRecovered(): void;\n}\nexport interface FecStarvationSnapshot {\n\tsourceSymbols: number;\n\trepairSymbols: number;\n\tblocksComplete: number;\n\tblocksRecovered: number;\n\tmediaFrames?: number;\n}\nexport interface StarvationDetectorLike { update(snapshot: FecStarvationSnapshot): void; reset(): void; }\nexport type StallDetectorLike = StarvationDetectorLike;\n\nexport function wireStallDetector(args: {\n\tsession: LossArcSession | undefined;\n\twindowMs: number;\n\tmakeDetector(config: { windowMs: number; onStarvation(): void; onRecovery(): void }): StallDetectorLike;\n}): StallDetectorLike | null {\n\tif (!args.session || args.windowMs <= 0) return null;\n\treturn args.makeDetector({\n\t\twindowMs: args.windowMs,\n\t\tonStarvation: () => args.session!.lossDetected(),\n\t\tonRecovery: () => args.session!.lossRecovered(),\n\t});\n}\n\nexport async function wireStarvationDetector(args: {\n\tsession: LossArcSession | undefined;\n\ttrackName: string;\n\tfecInterleaveMs: number;\n\tfecK: number;\n\tdeliveryWindowMs?: number;\n\tmakeDetector(config: { windowMs: number; onStarvation(): void; onRecovery(): void }): StarvationDetectorLike;\n\twarn?(message: string): void;\n}): Promise<StarvationDetectorLike | null> {\n\tif (!args.session || args.fecInterleaveMs <= 0 || args.fecK <= 0) return null;\n\tconst windowMs = args.deliveryWindowMs ?? Math.ceil(args.fecInterleaveMs * 3);\n\tif (!Number.isFinite(windowMs) || windowMs <= 0) {\n\t\targs.warn?.(`Unable to create starvation detector for ${args.trackName}: invalid delivery window ${windowMs}`);\n\t\treturn null;\n\t}\n\tconst detector = wireStallDetector({\n\t\tsession: args.session,\n\t\twindowMs,\n\t\tmakeDetector: args.makeDetector,\n\t});\n\tif (!detector) args.warn?.(`Unable to create starvation detector for ${args.trackName}`);\n\tif (!detector || typeof args.session.state !== \"string\") return detector;\n\n\t// Stats flow while TrackSession is still `joining`. Feeding them directly can\n\t// age a starvation episode past its window before the first multicast RAP,\n\t// causing the first post-cutover sample to flip a healthy session to\n\t// `failing`. Start with a fresh baseline only once multicast/AMT is primary;\n\t// retain the detector through `failing` so it can observe recovery.\n\tlet active = false;\n\treturn {\n\t\tupdate(snapshot) {\n\t\t\tconst state = args.session!.state;\n\t\t\tconst shouldObserve = state === \"multicast\" || state === \"amt\" || state === \"failing\";\n\t\t\tif (!shouldObserve) {\n\t\t\t\tif (active) detector.reset();\n\t\t\t\tactive = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!active) {\n\t\t\t\tdetector.reset();\n\t\t\t\tactive = true;\n\t\t\t}\n\t\t\tdetector.update(snapshot);\n\t\t},\n\t\treset() {\n\t\t\tactive = false;\n\t\t\tdetector.reset();\n\t\t},\n\t};\n}\n", "/**\n * @blockcast/transport - DRIAD Discovery (RFC 8777)\n *\n * DRIAD (DNS Reverse IP AMT Discovery) discovers AMT relays based on the\n * **source address**, NOT the multicast group. The source network operator\n * configures DNS records for their source IPs to advertise which AMT relay(s)\n * can tunnel their traffic.\n *\n * Example: For source 69.25.95.10 sending to group 232.0.0.1:\n * Query: 10.95.25.69.amt.in-addr.arpa (source-based, NOT group-based)\n */\n\nimport type { DRIADRelay } from \"./types.js\";\n\n/**\n * DRIAD (DNS Reverse IP AMT Discovery) - RFC 8777\n * Discovers AMT relays based on source address via DNS lookups\n */\nexport class DRIADDiscovery {\n\tprivate cache = new Map<string, DRIADRelay[]>();\n\tprivate cacheExpiry = 300_000; // 5 minutes\n\n\t/**\n\t * Discover AMT relays for a multicast source address (RFC 8777)\n\t *\n\t * @param source - The multicast source IP address (NOT the group!)\n\t * @returns Array of discovered AMT relays that can tunnel traffic from this source\n\t */\n\tasync discoverRelays(source: string): Promise<DRIADRelay[]> {\n\t\t// Check cache first\n\t\tconst cached = this.cache.get(source);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\ttry {\n\t\t\t// Convert source to DRIAD format (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryDomain = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconsole.log(`[DRIAD] Discovering relays for source ${source} via ${queryDomain}`);\n\n\t\t\t// In browser, we use a DRIAD proxy service or DNS-over-HTTPS\n\t\t\tconst relays = await this.queryDRIADProxy(source);\n\n\t\t\tif (relays.length > 0) {\n\t\t\t\tthis.cache.set(source, relays);\n\t\t\t\tsetTimeout(() => this.cache.delete(source), this.cacheExpiry);\n\t\t\t}\n\n\t\t\treturn relays;\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[DRIAD] Discovery failed for source ${source}:`, error);\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/**\n\t * Query DRIAD proxy service (browser-compatible)\n\t */\n\tprivate async queryDRIADProxy(source: string): Promise<DRIADRelay[]> {\n\t\t// List of known DRIAD proxy endpoints\n\t\tconst proxyEndpoints = [\n\t\t\t\"https://driad.amt.net/api/v1/relays\",\n\t\t\t\"https://multicast-gateway.cloudflare.com/driad\",\n\t\t];\n\n\t\tfor (const endpoint of proxyEndpoints) {\n\t\t\ttry {\n\t\t\t\t// Query by source address per RFC 8777\n\t\t\t\tconst response = await fetch(`${endpoint}?source=${encodeURIComponent(source)}`, {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t\t});\n\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = (await response.json()) as { relays: DRIADRelay[] };\n\t\t\t\t\treturn data.relays || [];\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Try next proxy endpoint\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: Try DNS-over-HTTPS query\n\t\tconst dohRelay = await this.queryDNSOverHTTPS(source);\n\t\tif (dohRelay) {\n\t\t\treturn [dohRelay];\n\t\t}\n\n\t\treturn [];\n\t}\n\n\t/**\n\t * Query DNS-over-HTTPS for DRIAD relay based on source address (RFC 8777)\n\t */\n\tprivate async queryDNSOverHTTPS(source: string): Promise<DRIADRelay | null> {\n\t\ttry {\n\t\t\t// Build DRIAD query name from source address (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryName = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconst dohUrl = `https://cloudflare-dns.com/dns-query?name=${queryName}&type=A`;\n\t\t\tconst response = await fetch(dohUrl, {\n\t\t\t\theaders: { Accept: \"application/dns-json\" },\n\t\t\t\tsignal: AbortSignal.timeout(3000),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst result = (await response.json()) as { Answer?: Array<{ data: string }> };\n\n\t\t\t// Extract first A/AAAA record\n\t\t\tif (result.Answer && result.Answer.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\thost: result.Answer[0].data,\n\t\t\t\t\tport: 2268,\n\t\t\t\t\tpriority: 10,\n\t\t\t\t\tweight: 100,\n\t\t\t\t\tasn: 0,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[DRIAD] DNS-over-HTTPS query failed:\", error);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Select best relay based on RTT measurements\n\t */\n\tasync selectBestRelay(relays: DRIADRelay[]): Promise<DRIADRelay | null> {\n\t\tif (relays.length === 0) return null;\n\n\t\t// Measure RTT to each relay\n\t\tconst measured = await Promise.all(\n\t\t\trelays.map(async (relay) => {\n\t\t\t\tconst rtt = await this.measureRTT(relay.host, relay.port);\n\t\t\t\treturn { ...relay, rtt };\n\t\t\t}),\n\t\t);\n\n\t\t// Sort by priority, then by RTT\n\t\tmeasured.sort((a, b) => {\n\t\t\tif (a.priority !== b.priority) return a.priority - b.priority;\n\t\t\treturn (a.rtt || Infinity) - (b.rtt || Infinity);\n\t\t});\n\n\t\tconsole.log(`[DRIAD] Selected relay: ${measured[0].host} (RTT: ${measured[0].rtt}ms)`);\n\t\treturn measured[0];\n\t}\n\n\tprivate async measureRTT(host: string, _port: number): Promise<number> {\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\t// Use HTTP/HTTPS probe since we can't do UDP from browser\n\t\t\tawait fetch(`https://${host}/health`, {\n\t\t\t\tmethod: \"HEAD\",\n\t\t\t\tsignal: AbortSignal.timeout(2000),\n\t\t\t\tmode: \"no-cors\",\n\t\t\t});\n\t\t\treturn performance.now() - start;\n\t\t} catch {\n\t\t\treturn Infinity;\n\t\t}\n\t}\n\n\t/**\n\t * Clear the relay cache\n\t */\n\tclearCache(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Get cached relays for a source address\n\t */\n\tgetCached(source: string): DRIADRelay[] | undefined {\n\t\treturn this.cache.get(source);\n\t}\n}\n\n// Export singleton instance\nexport const driadDiscovery = new DRIADDiscovery();\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\tTransportSubscription,\n\tTransportCapabilities,\n\tTransportState,\n\tQualityLevel,\n\tWindowMulticastCapabilities,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\nimport { DRIADDiscovery } from \"./driad-discovery.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\tprivate driad = new DRIADDiscovery();\n\tprivate abr: AdaptiveBitrateController;\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\tconstructor() {\n\t\tthis.abr = new AdaptiveBitrateController();\n\t}\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: true, // DRIAD uses HTTP proxy, always available\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 proxy (specified or discovered via DRIAD)\n\t * 3. MOQ WebTransport (final fallback)\n\t */\n\tasync connect(config: TransportConfig): Promise<boolean> {\n\t\tconst generation = ++this.connectionGeneration;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = 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// Initialize ABR with requested quality\n\t\tif (config.initialQuality) {\n\t\t\tthis.abr = new AdaptiveBitrateController(config.initialQuality);\n\t\t}\n\n\t\tthis.abr.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 proxy (with optional 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(config: TransportConfig, generation: number): 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: \"multicast\",\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.abr.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 proxy (with optional DRIAD discovery)\n\t */\n\tprivate async connectAmt(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tlet amtRelay = config.amtRelay;\n\n\t\t\t// Use DRIAD to discover relay if not specified (RFC 8777)\n\t\t\t// DRIAD discovers relays based on SOURCE address, not group\n\t\t\tif (!amtRelay && config.driadEnabled !== false && config.source) {\n\t\t\t\tconsole.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);\n\t\t\t\tconst relays = await this.driad.discoverRelays(config.source);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tconst bestRelay = await this.driad.selectBestRelay(relays);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t\t\tif (bestRelay) {\n\t\t\t\t\tamtRelay = bestRelay.host;\n\t\t\t\t\tthis.state.amtRelay = amtRelay;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!amtRelay) {\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\n\t\t\t// Use the WebSocket-based AMT proxy when a relay is known.\n\t\t\treturn await this.connectAmtProxy(config, amtRelay, generation);\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\tconst relayAuthority = config.amtRelayPort ? `${relayHost}:${config.amtRelayPort}` : relayHost;\n\t\t\tconst wsUrl = new URL(`wss://${relayAuthority}/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.abr.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\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 * Get current quality level\n\t */\n\tgetQuality(): QualityLevel {\n\t\treturn this.abr.getCurrentQuality();\n\t}\n\n\t/**\n\t * Force quality level\n\t */\n\tsetQuality(name: string) {\n\t\tthis.abr.setQuality(name);\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/**\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\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", "export interface FallbackTrack { name: string; type: \"video\" | \"audio\"; }\nexport interface FallbackGroup { readFrame(): Promise<Uint8Array | undefined>; close(): void; }\nexport interface FallbackSubscription { nextGroup(): Promise<FallbackGroup | undefined>; close(): void; }\nexport interface FallbackBroadcast { subscribe(name: string, priority: number): FallbackSubscription; }\nexport interface FallbackSessions { enterUnicastFallback(): void; }\nexport type FallbackGroupLike = FallbackGroup;\nexport type FallbackSubLike = FallbackSubscription;\nexport type FallbackBroadcastLike = FallbackBroadcast;\nexport interface FallbackSessionManagerLike extends FallbackSessions { hasActiveFallback(): boolean; }\n\nexport function runUnicastFallback(input: {\n\ttracks: FallbackTrack[];\n\tbroadcast: FallbackBroadcast;\n\tsessions: FallbackSessions;\n\tonPacket(packet: Uint8Array): void;\n\tpriority: number;\n\tsignal: AbortSignal;\n\tonError?(trackName: string, error: unknown): void;\n}): { done: Promise<void> } {\n\tinput.sessions.enterUnicastFallback();\n\tconst tracks = Array.from(new Map(input.tracks.map((track) => [track.name, track])).values());\n\tlet resolveAbort: (value: undefined) => void;\n\tconst aborted = new Promise<undefined>((resolve) => {\n\t\tresolveAbort = resolve;\n\t\tif (input.signal.aborted) resolve(undefined);\n\t});\n\tconst onAbort = () => resolveAbort(undefined);\n\tif (!input.signal.aborted) input.signal.addEventListener(\"abort\", onAbort, { once: true });\n\n\tconst drainers = tracks.map(async (track) => {\n\t\tconst subscription = input.broadcast.subscribe(track.name, input.priority);\n\t\ttry {\n\t\t\twhile (!input.signal.aborted) {\n\t\t\t\tconst nextGroup = subscription.nextGroup();\n\t\t\t\tconst group = await Promise.race([nextGroup, aborted]);\n\t\t\t\tif (!group && input.signal.aborted) void nextGroup.then((lateGroup) => lateGroup?.close());\n\t\t\t\tif (!group) break;\n\t\t\t\ttry {\n\t\t\t\t\twhile (!input.signal.aborted) {\n\t\t\t\t\t\tconst frame = await Promise.race([group.readFrame(), aborted]);\n\t\t\t\t\t\tif (!frame) break;\n\t\t\t\t\t\tinput.onPacket(frame);\n\t\t\t\t\t}\n\t\t\t\t} finally { group.close(); }\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!input.signal.aborted) {\n\t\t\t\tif (input.onError) input.onError(track.name, error);\n\t\t\t\telse console.warn(`[unicast-fallback] drainer error on ${track.name}:`, error);\n\t\t\t}\n\t\t} finally { subscription.close(); }\n\t});\n\tconst done = Promise.all(drainers).then(() => undefined).finally(() => {\n\t\tinput.signal.removeEventListener(\"abort\", onAbort);\n\t});\n\treturn { done };\n}\n", "import { AdaptiveBitrateController } from \"./abr-controller.js\";\nimport {\n\tisMulticastExplicitlyDisabled,\n\tisUsableMulticastEndpoint,\n\tisValidMulticastGroupAddress,\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\treadUrlMulticastConfig,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.js\";\nimport { MulticastOrchestrator } from \"./multicast-orchestrator.js\";\nimport {\n\tendpointToMulticastConfig,\n\thasMulticastCatalogBaseline,\n\thasUnicastMediaRenditions,\n\tisMulticastOnlyCatalog,\n\tisRecoverableMulticastSignalReadError,\n\tmergeUrlMulticastPolicy,\n\tnormalizeMulticastConfig,\n\treadPlayerMulticastConfig,\n\trequiresMulticastCapability,\n\tsameMulticastConfig,\n\tselectEndpointPacketIds,\n\tselectMulticastCandidate,\n\turlOverrideBlocksMulticastCandidate,\n\twithMulticastEndpoints,\n} from \"./multicast-catalog-policy.js\";\nimport {\n\tcreateMulticastSignalingReconciler,\n\tnormalizeCatalogEndpoints,\n} from \"./multicast-signaling-reconciler.js\";\nimport { MulticastSubscriptionPool } from \"./multicast-subscription-pool.js\";\nimport { endpointKey, selectMulticastTier } from \"./multicast-tier.js\";\nimport {\n\tMOQT_PUBLISHER_PRIORITY,\n\tMOQT_SUBSCRIBER_PRIORITY,\n\tmoqtPublisherPriority,\n\tmoqtSubscriberPriority,\n} from \"./moqt-priority.js\";\nimport { classifyStall, TransportStallDetector } from \"./stall-detector.js\";\nimport { makeSourceSwitchHandler, wireStarvationDetector } from \"./takeover.js\";\nimport { TransportManager } from \"./transport-manager.js\";\nimport { runUnicastFallback } from \"./unicast-fallback.js\";\n\nconst api = {\n\tAdaptiveBitrateController,\n\tclassifyStall,\n\tcreateMulticastSignalingReconciler,\n\tendpointToMulticastConfig,\n\thasMulticastCatalogBaseline,\n\thasUnicastMediaRenditions,\n\tisRecoverableMulticastSignalReadError,\n\tisMulticastOnlyCatalog,\n\tMulticastOrchestrator,\n\tMulticastSubscriptionPool,\n\tMOQT_PUBLISHER_PRIORITY,\n\tMOQT_SUBSCRIBER_PRIORITY,\n\tmoqtPublisherPriority,\n\tmoqtSubscriberPriority,\n\tTransportManager,\n\tTransportStallDetector,\n\tendpointKey,\n\tisMulticastExplicitlyDisabled,\n\tisUsableMulticastEndpoint,\n\tisValidMulticastGroupAddress,\n\tmakeSourceSwitchHandler,\n\tmergeUrlMulticastPolicy,\n\tmulticastEndpointKey,\n\tnormalizeCatalogEndpoints,\n\tnormalizeMulticastConfig,\n\tnormalizeMulticastEndpoint,\n\treadPlayerMulticastConfig,\n\treadUrlMulticastConfig,\n\ttoWindowMulticastSubscribeConfig,\n\trequiresMulticastCapability,\n\trunUnicastFallback,\n\tsameMulticastConfig,\n\tselectEndpointPacketIds,\n\tselectMulticastCandidate,\n\tselectMulticastTier,\n\turlOverrideBlocksMulticastCandidate,\n\twithMulticastEndpoints,\n\twireStarvationDetector,\n};\n\nObject.defineProperty(globalThis, \"__MmtTransport\", {\n\tconfigurable: true,\n\tenumerable: false,\n\twritable: false,\n\tvalue: Object.freeze(api),\n});\n\nexport default api;\n"],
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 * Per draft-ramadan-moq-fec-00, ABR decisions should use the **effective loss rate**\n * after FEC recovery, not the raw transport-level loss rate. This allows FEC to\n * mask burst losses without unnecessarily downgrading quality.\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 * Standard quality levels for progressive ABR\n */\nexport const QUALITY_LEVELS: QualityLevel[] = [\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];\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 for ABR decisions per draft-ramadan-moq-fec-00\n * - Progressive quality upgrades (waits for stability)\n * - Fast quality downgrades on congestion\n */\nexport class AdaptiveBitrateController {\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(initialQuality?: QualityLevel) {\n\t\tthis.currentQualityIndex = initialQuality ? QUALITY_LEVELS.findIndex((q) => q.name === initialQuality.name) : 0;\n\n\t\tif (this.currentQualityIndex < 0) this.currentQualityIndex = 0;\n\t}\n\n\tsetCallback(callback: (quality: QualityLevel) => void) {\n\t\tthis.onQualityChange = callback;\n\t}\n\n\tgetCurrentQuality(): QualityLevel {\n\t\treturn QUALITY_LEVELS[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// Per draft-ramadan-moq-fec-00: 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 < QUALITY_LEVELS.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 * Per draft-ramadan-moq-fec-00, ABR should use the effective loss rate\n\t * (after FEC recovery) rather than 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 >= QUALITY_LEVELS.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 * Force quality level (for testing)\n\t */\n\tsetQuality(name: string) {\n\t\tconst index = QUALITY_LEVELS.findIndex((q) => q.name === name);\n\t\tif (index >= 0) {\n\t\t\tthis.currentQualityIndex = index;\n\t\t\tthis.lastQualityChange = Date.now();\n\t\t\tthis.onQualityChange?.(this.getCurrentQuality());\n\t\t}\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 * Zero-dependency helper for extracting the detached ALTA authenticator trailer\n * from an MMTP source packet.\n *\n * Lives in its own leaf package so both `@blockcast/mmt-container` (wire-path\n * parsing) and `@blockcast/fec-worker` (FEC-recovered-source parsing) can share\n * one implementation without creating a package cycle between them.\n *\n * Spec: ISO/IEC 23008-1:2023 + draft-ramadan-moq-mmt \u00A712 (detached trailer, Path 2).\n */\n\n// --- Private MMTP/MPU/MFU constants (ISO/IEC 23008-1 \u00A79.2.2\u20139.2.3) ---\n// Duplicated here as immutable spec values so this package stays a leaf with\n// zero `@blockcast/*` dependencies. Re-deriving from `mmt-container` would\n// reintroduce the package cycle this split exists to avoid.\n\nconst MMTP_HEADER_SIZE = 12\nconst MPU_HEADER_SIZE = 8\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nconst TIMED_MFU_DU_HEADER_SIZE = 14\nconst NONTIMED_MFU_DU_HEADER_SIZE = 4\nconst PACKET_TYPE_MPU = 0x00\n\n// --- Public ALTA trailer constants ---\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\nexport const MMTP_EXT_TYPE_ALTA_AUTH = 0xf0a1\n\n/** Fixed size of the trailer's `[ext_type][ext_length]` prefix (2 + 2 BE). */\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = 4\n\n/** Result of extracting the detached ALTA authenticator from an MMTP packet. */\nexport interface AltaTrailer {\n /** Authenticator bytes (no ext_type/ext_length prefix, no trailing padding). */\n auth: Uint8Array\n /**\n * Wire-packet byte offset where the signed payload ends, i.e. where the\n * trailer begins. Caller uses this to produce\n * `payload = wirePacket.subarray(0, signedEnd)` for verify_detached.\n */\n signedEnd: number\n}\n\n/**\n * Extract the detached ALTA authenticator trailer from an MMTP source packet.\n *\n * Wire layout:\n * `[MMTP hdr][length(2)][remaining MPU hdr(6)][DU?][media][TRAILER][SS_ID?]`\n * `payload_length = 6 + DU bytes + media bytes`\n *\n * Returns `null` when:\n * - the packet is too small / malformed,\n * - `payload_length` cannot contain the remaining six MPU-header bytes,\n * - the declared payload overruns the packet,\n * - no bytes remain between `payload_length` and the SS_ID,\n * - the trailer's `ext_type` isn't {@link MMTP_EXT_TYPE_ALTA_AUTH},\n * - the declared `ext_length` would extend past the SS_ID boundary.\n *\n * Non-MPU packets (repair, signaling) always return `null`.\n *\n * @param options.skipFecId When true, do NOT reserve 4 trailing bytes for the\n * Source FEC Payload ID. Use on FEC-recovered symbols where the PID has\n * already been stripped upstream.\n */\nexport function extractAltaTrailer(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n): AltaTrailer | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n // ISO/IEC 23008-1:2023 Figure 8 places V=0 packet_type in the low six bits.\n const packetType = mmtpPacket[1]! & 0x3f\n if (packetType !== PACKET_TYPE_MPU) return null\n\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n let headerLength = MMTP_HEADER_SIZE\n if (fecType < 2 && (mmtpPacket[0]! & 0x20) !== 0) headerLength += 4\n if (fecType < 2 && (mmtpPacket[0]! & 0x02) !== 0) {\n if (headerLength + 4 > mmtpPacket.length) return null\n const extensionLength = new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + headerLength + 2,\n 2,\n ).getUint16(0, false)\n headerLength += 4 + extensionLength\n }\n if (headerLength + MPU_HEADER_SIZE > mmtpPacket.length) return null\n\n const payload = mmtpPacket.subarray(headerLength)\n\n // Inline a minimal MPU-header read \u2014 only the fields extractAltaTrailer needs.\n // payloadLength: 16 BE at bytes 0-1.\n // Byte 2 packs FT(4) | T(1) | FI(2) | A(1).\n const payloadLength = (payload[0]! << 8) | payload[1]!\n const flagsByte = payload[2]!\n const fragmentType = (flagsByte >> 4) & 0x0f\n const timed = ((flagsByte >> 3) & 0x01) === 1\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n\n // Only MFU packets (FT=2) carry the detached trailer.\n if (fragmentType !== 2) return null\n\n // FI=0/1 structurally carries a DU header; FI=2/3 does not. Require at\n // least one media byte after that deterministic layout so the bytes at the\n // declared boundary cannot impersonate ALTA for a header-only fragment.\n const dataUnitHeaderSize = fragmentationIndicator <= 1\n ? (timed ? TIMED_MFU_DU_HEADER_SIZE : NONTIMED_MFU_DU_HEADER_SIZE)\n : 0\n if (payloadLength <= MPU_HEADER_REMAINDER_SIZE + dataUnitHeaderSize) return null\n\n if (fecType > 1) return null\n const hasFecId = !options?.skipFecId && fecType === 1\n const payloadEnd = hasFecId ? payload.length - 4 : payload.length\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (declaredEnd > payloadEnd) return null\n\n if (declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE > payloadEnd) return null\n const extType = (payload[declaredEnd]! << 8) | payload[declaredEnd + 1]!\n if (extType !== MMTP_EXT_TYPE_ALTA_AUTH) return null\n const extLen = (payload[declaredEnd + 2]! << 8) | payload[declaredEnd + 3]!\n\n const authStart = declaredEnd + MMTP_EXT_TRAILER_HEADER_SIZE\n if (authStart + extLen !== payloadEnd) return null\n\n return {\n auth: payload.subarray(authStart, payloadEnd),\n signedEnd: headerLength + declaredEnd,\n }\n}\n", "import {\n MMTP_EXT_TYPE_ALTA_AUTH as _LEAF_MMTP_EXT_TYPE_ALTA_AUTH,\n MMTP_EXT_TRAILER_HEADER_SIZE as _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE,\n extractAltaTrailer as _leafExtractAltaTrailer,\n type AltaTrailer as _LeafAltaTrailer,\n} from '@blockcast/mmt-alta-parse'\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\nconst 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 IDs\nexport const FEC_CODE_XOR = 1\nexport const FEC_CODE_RAPTORQ = 2\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\nfunction isEmptyOrExactAltaTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + _LEAF_MMTP_EXT_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) !== _LEAF_MMTP_EXT_TYPE_ALTA_AUTH) return false\n\n const authLength = view.getUint16(2, false)\n return start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: { skipFecId?: boolean },\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false }\n : null\n }\n\n if (fecType === 1) {\n const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n if (fecStart < declaredEnd || !isEmptyOrExactAltaTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true }\n }\n if (fecType !== 0) return null\n\n if (isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false }\n }\n\n return null\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. ALTA and Source FEC\n * trailers 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-00 \u00A73.1).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: { skipFecId?: boolean },\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n if (observed !== SOURCE_FEC_PAYLOAD_ID_SIZE) {\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 ${SOURCE_FEC_PAYLOAD_ID_SIZE} ` +\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 ALTA 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?: { skipFecId?: boolean },\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?: { skipFecId?: boolean },\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?: { skipFecId?: boolean },\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const fecPayloadId = trailerLayout.hasFecId\n ? {\n ssId: new DataView(\n mmtpPacket.buffer,\n mmtpPacket.byteOffset + mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE,\n SOURCE_FEC_PAYLOAD_ID_SIZE,\n ).getUint32(0, false),\n }\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/**\n * MMTP header-extension ext_type assigned to the detached ALTA authenticator\n * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`\n * block appended after the MPU's declared payload bytes, before the 4-byte\n * Source FEC Payload ID. MMTP X-bit stays 0 \u2014 non-ALTA MMTP receivers treat\n * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1\n * \u00A79.2.2 \"may be discarded without impacting correct processing\").\n */\n// Re-export the ALTA trailer helper from the zero-dep leaf package. The leaf\n// owns the implementation so container and FEC consumers share one parser\n// without creating a package cycle.\nexport const MMTP_EXT_TYPE_ALTA_AUTH = _LEAF_MMTP_EXT_TYPE_ALTA_AUTH\nexport const MMTP_EXT_TRAILER_HEADER_SIZE = _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE\nexport type AltaTrailer = _LeafAltaTrailer\nexport const extractAltaTrailer = _leafExtractAltaTrailer\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): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.rsbLength === 0 ||\n repairId.ssbLength === 0 ||\n 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 * AL-FEC signaling configuration parsed from ISO/IEC 23008-1:2023 Table C.3 message.\n */\nexport interface AlFecConfig {\n fecCodeId: number\n fecPayloadIdMode: 0\n repairSymbolSize: number\n maximumK: number\n maximumP: number\n interleaveDepth: number\n oti: Uint8Array\n}\n\nfunction canonicalRaptorQOtiK(oti: Uint8Array, repairSymbolSize: number): number | null {\n if (oti.length !== 12 || oti[5] !== 0) return null\n const transferLength = (oti[0]! * 0x1_0000_0000) +\n (((oti[1]! << 24) | (oti[2]! << 16) | (oti[3]! << 8) | oti[4]!) >>> 0)\n const symbolSize = (oti[6]! << 8) | oti[7]!\n const sourceBlocks = oti[8]!\n const subBlocks = (oti[9]! << 8) | oti[10]!\n const alignment = oti[11]!\n if (!Number.isSafeInteger(transferLength) || transferLength <= 0 ||\n symbolSize !== repairSymbolSize || symbolSize === 0 || symbolSize % 8 !== 0 ||\n sourceBlocks !== 1 || subBlocks !== 1 || alignment !== 8 ||\n transferLength % symbolSize !== 0) {\n return null\n }\n const k = transferLength / symbolSize\n return Number.isInteger(k) && k >= 1 && k <= 56_403 ? k : null\n}\n\n/**\n * Parse AL-FEC signaling message from a full MMTP signaling packet.\n *\n * Wire format (ISO/IEC 23008-1:2023 Table C.3, Amendment 1:2025):\n * [12-byte MMTP header (packet_type=0x02)]\n * message_id(2) = 0x0203\n * version(1)\n * message_length(2) = length of remaining payload\n * flags(1) = fec_flag(1) | private_fec_flag(1) | reserved(6)\n * fec_flow_descriptor:\n * length(2)\n * num_flows(1)\n * per flow:\n * fec_flow_id(1), source_flow_id(1), num_assets(1), packet_ids(2*N),\n * coding_params(1), repair_symbol_size(2),\n * [one-stage]: repair_flow_id(2), fec_code_id(1),\n * [private_fec_flag]: private_header(1){flag(1)|len(7)},\n * current RaptorQ profile OTI(12) + interleave_depth(1)\n * max_k(3), max_p(3), protection_window_time(4), protection_window_size(4)\n *\n * @param mmtpPacket Full MMTP packet including 12-byte header\n * @returns Parsed config or null if not an AL-FEC signaling message\n */\nexport function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecConfig | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + 9) return null\n let header: MMTPHeader\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n if (header.packetType !== PACKET_TYPE_SIGNALING || header.fecType !== 0) return null\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < 9) return null\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n let off = 0\n\n const messageId = view.getUint16(off, false); off += 2\n if (messageId !== ALFEC_MESSAGE_ID) return null\n const version = payload[off]!; off += 1\n const messageLength = view.getUint16(off, false); off += 2\n if (version !== 1 || messageLength === 0 || messageLength !== payload.length - off) return null\n\n const flags = payload[off]!; off += 1\n const fecFlag = (flags >> 7) & 1\n const privateFecFlag = (flags >> 6) & 1\n if (fecFlag !== 1 || privateFecFlag !== 1 || (flags & 0x3f) !== 0x3f) return null\n\n if (off + 2 > payload.length) return null\n const descriptorLength = view.getUint16(off, false); off += 2\n const descriptorEnd = off + descriptorLength\n if (descriptorLength === 0 || descriptorEnd !== payload.length) return null\n\n const numFlows = payload[off]!; off += 1\n if (numFlows !== 1 || off + 3 > descriptorEnd) return null\n\n off += 2 // fec_flow_id, source_flow_id\n const numAssets = payload[off]!; off += 1\n if (numAssets === 0 || off + (numAssets * 2) > descriptorEnd) return null\n off += numAssets * 2\n\n if (off + 3 > descriptorEnd) return null\n const codingParams = payload[off]!; off += 1\n const fecCodingStructure = (codingParams >> 4) & 0x0f\n const ssbgMode = (codingParams >> 2) & 0x03\n const ffsrptsFlag = (codingParams >> 1) & 0x01\n const fecPayloadIdMode = codingParams & 0x01\n if (fecCodingStructure !== 1 || ssbgMode !== 0 || ffsrptsFlag !== 0 || fecPayloadIdMode !== 0) {\n return null\n }\n const repairSymbolSize = view.getUint16(off, false); off += 2\n if (repairSymbolSize === 0 || repairSymbolSize % 8 !== 0) return null\n\n if (off + 4 > descriptorEnd) return null\n off += 2 // Amendment 1:2025 replaces Table C.3 with a 16-bit repair_flow_id\n const fecCodeId = payload[off]!; off += 1\n if (fecCodeId !== FEC_CODE_RAPTORQ) return null\n const privateHeader = payload[off]!; off += 1\n const privateFlag = privateHeader >> 7\n const privateFieldLength = privateHeader & 0x7f\n if (privateFlag !== 1 || privateFieldLength !== 13 || off + 13 > descriptorEnd) return null\n const oti = payload.slice(off, off + 12); off += 12\n const interleaveDepth = payload[off]!; off += 1\n const otiK = canonicalRaptorQOtiK(oti, repairSymbolSize)\n if (otiK === null || interleaveDepth === 0) return null\n\n if (off + 14 !== descriptorEnd) return null\n const maximumK = (payload[off]! << 16) | (payload[off + 1]! << 8) | payload[off + 2]!\n off += 3\n const maximumP = (payload[off]! << 16) | (payload[off + 1]! << 8) | payload[off + 2]!\n off += 3\n if (maximumK !== otiK || maximumP === 0 || maximumK + maximumP - 1 > 0x00ff_ffff) {\n return null\n }\n off += 8 // protection_window_time, protection_window_size\n if (off !== descriptorEnd) return null\n\n return { fecCodeId, fecPayloadIdMode, repairSymbolSize, maximumK, maximumP, interleaveDepth, oti }\n}\n", "import { MMTP_PACKET_ID_MAX, MMTP_SIGNALING_PACKET_ID } from \"@blockcast/mmt-base\";\nimport type {\n\tWindowMulticastSubscribeConfig,\n\tWindowMulticastSubscribeConfigBytes,\n} from \"./types.js\";\n\nexport type MulticastTransportPreference = \"auto\" | \"native\" | \"tunnel\";\n\nexport interface MulticastEndpointConfig {\n\tgroupAddress: string;\n\tsourceAddress?: string;\n\tport: number;\n\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\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\tmode?: MulticastTransportPreference;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tpacketIds?: readonly number[];\n\ttracks?: ReadonlyArray<{ name: string; packetId: 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 */\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 base = {\n\t\tgroup: input.group,\n\t\tport: input.port,\n\t\t...(input.source?.trim() ? { source: input.source.trim() } : {}),\n\t\tconsume: \"bytes\" as const,\n\t\t...(input.packetIds?.length ? { packetIds: [...input.packetIds] } : {}),\n\t};\n\n\tconst relay = input.amtRelay?.trim();\n\tconst wantsAmt = input.mode === \"tunnel\" ||\n\t\t(input.mode !== \"native\" && relay !== undefined && relay !== \"\");\n\tif (!wantsAmt) {\n\t\tconst native: WindowMulticastSubscribeConfigBytes = {\n\t\t\t...base,\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 (!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\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 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\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};\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]);\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\nexport function isUsableMulticastEndpoint(\n\tendpoint: { groupAddress?: string; port?: unknown } | undefined,\n): boolean {\n\tconst port = Number(endpoint?.port);\n\treturn isValidMulticastGroupAddress(endpoint?.groupAddress) && isPositivePort(port);\n}\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\") || params.get(\"amt_relay\") || undefined;\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\") || params.get(\"amt_relay_port\"));\n\tconst requestedMode = params.get(\"mc_mode\");\n\tconst mode = amtRelay ? \"tunnel\" : multicastMode(requestedMode);\n\n\treturn 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}\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 { 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\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#lastCommittedTimestamp = Number.NEGATIVE_INFINITY;\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 || this.options.adaptive === false) return undefined;\n\t\tif (this.#state === \"joining\") return this.poll();\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\tonTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {\n\t\tif (!this.#current) return undefined;\n\t\tif (packet.endpointKey === endpointKey(this.#current) && Number.isFinite(packet.timestamp)) {\n\t\t\tthis.#lastCommittedTimestamp = Math.max(this.#lastCommittedTimestamp, packet.timestamp);\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\t!Number.isFinite(packet.timestamp) ||\n\t\t\tpacket.timestamp < this.#lastCommittedTimestamp\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.#lastCommittedTimestamp = packet.timestamp;\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#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", "import { isMulticastTrackPacketId, isValidMulticastGroupAddress } from \"./endpoint-config.js\";\n\nexport interface MulticastSignalingEndpoint {\n\tendpointId: string;\n\tendpointVersion: number;\n\tprotocol: \"ssm\" | \"asm\" | \"amt\";\n\tsourceAddress?: string;\n\tgroupAddress: string;\n\tport: number;\n\tvalidUntil?: number;\n\ttracks: Array<{ name: string; packetId?: number; [key: string]: unknown }>;\n\t[key: string]: unknown;\n}\n\ntype Catalog = Record<string, any>;\ntype Signal = Record<string, any>;\n\nexport function createMulticastSignalingReconciler(catalog: Catalog, options: { catalog?: Catalog } = {}) {\n\tconst initial = normalizeCatalogEndpoints(catalog);\n\tconst endpoints = new Map(initial.map(endpoint => [endpoint.endpointId, copyEndpoint(endpoint)]));\n\tconst versions = new Map(initial.map(endpoint => [endpoint.endpointId, endpoint.endpointVersion]));\n\tconst seen = new Set<string>();\n\tconst dormant = new Set<string>();\n\tlet catalogVersion = Number(catalog?.multicast?.version ?? catalog?.version ?? 0) || 0;\n\tconst snapshot = (now = Date.now()) => ({\n\t\tcatalogVersion,\n\t\tendpoints: [...endpoints.values()].filter(endpoint => !dormant.has(endpoint.endpointId) && !isExpired(endpoint, now)).map(copyEndpoint),\n\t\tdormantEndpointIds: [...dormant].sort(),\n\t});\n\n\treturn {\n\t\tapply(signal: Signal, now = Date.now()) {\n\t\t\tif (!isObject(signal) || typeof signal.type !== \"string\" || typeof signal.msgId !== \"string\") return { applied: false, reason: \"invalid-message\" };\n\t\t\tif (seen.has(signal.msgId)) return { applied: false, reason: \"duplicate-message\" };\n\t\t\tif (signal.expiresAt != null && Number(signal.expiresAt) < now) return { applied: false, reason: \"expired\" };\n\t\t\tconst signalCatalogVersion = Number(signal.catalogVersion ?? 0);\n\t\t\tif (!Number.isFinite(signalCatalogVersion)) return { applied: false, reason: \"invalid-message\" };\n\t\t\tif (signalCatalogVersion < catalogVersion) return { applied: false, reason: \"stale-catalog\" };\n\t\t\tconst result = applyFresh(signal, now);\n\t\t\tif (result.applied) {\n\t\t\t\tseen.add(signal.msgId);\n\t\t\t\t// A heartbeat proves liveness but carries no catalog state. Ratcheting\n\t\t\t\t// here would incorrectly reject the next real snapshot as stale.\n\t\t\t\tif (signal.type !== \"heartbeat\") {\n\t\t\t\t\tcatalogVersion = Math.max(catalogVersion, signalCatalogVersion);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\t\tsnapshot,\n\t\ttoCatalog(now = Date.now()) {\n\t\t\tconst current = snapshot(now);\n\t\t\treturn { ...catalog, multicast: { ...catalog.multicast, version: current.catalogVersion, endpoints: current.endpoints } };\n\t\t},\n\t};\n\n\tfunction applyFresh(signal: Signal, now: number): { applied: boolean; reason?: string; action?: string } {\n\t\tswitch (signal.type) {\n\t\t\tcase \"snapshot\": {\n\t\t\t\tconst next = signal.payload?.endpoints;\n\t\t\t\tif (!Array.isArray(next)) return { applied: false, reason: \"invalid-message\" };\n\t\t\t\tconst normalized = next.map((endpoint, index) => normalizeEndpoint(endpoint, index));\n\t\t\t\tif (normalized.some(endpoint => !endpoint || !isValidEndpoint(endpoint, options.catalog))) return { applied: false, reason: \"invalid-endpoint\" };\n\t\t\t\tconst ids = new Set(normalized.map(endpoint => endpoint!.endpointId));\n\t\t\t\tif (ids.size !== normalized.length) return { applied: false, reason: \"duplicate-endpoint\" };\n\t\t\t\tendpoints.clear(); versions.clear(); dormant.clear();\n\t\t\t\tfor (const endpoint of normalized as MulticastSignalingEndpoint[]) {\n\t\t\t\t\tversions.set(endpoint.endpointId, endpoint.endpointVersion);\n\t\t\t\t\tif (!isExpired(endpoint, now)) endpoints.set(endpoint.endpointId, copyEndpoint(endpoint));\n\t\t\t\t}\n\t\t\t\treturn { applied: true, action: \"snapshot\" };\n\t\t\t}\n\t\t\tcase \"add\": return upsert(signal.payload?.endpoint, now);\n\t\t\tcase \"update\": {\n\t\t\t\tconst id = signal.payload?.endpointId;\n\t\t\t\tconst fields = signal.payload?.fields;\n\t\t\t\tif (!endpoints.has(id)) return { applied: false, reason: \"unknown-endpoint\" };\n\t\t\t\tif (isStale(id, signal.payload?.endpointVersion)) return { applied: false, reason: \"stale-endpoint\" };\n\t\t\t\tif (!isObject(fields) || [\"protocol\", \"sourceAddress\", \"groupAddress\", \"port\"].some(key => Object.hasOwn(fields, key))) return { applied: false, reason: \"invalid-update\" };\n\t\t\t\treturn upsert({ ...endpoints.get(id), ...fields, endpointVersion: signal.payload.endpointVersion }, now);\n\t\t\t}\n\t\t\tcase \"remove\": {\n\t\t\t\tconst id = signal.payload?.endpointId;\n\t\t\t\tif (!endpoints.has(id)) return { applied: false, reason: \"unknown-endpoint\" };\n\t\t\t\tconst endpointVersion = Number(signal.payload?.endpointVersion);\n\t\t\t\tif (!Number.isFinite(endpointVersion)) return { applied: false, reason: \"invalid-message\" };\n\t\t\t\tif (isStale(id, endpointVersion)) return { applied: false, reason: \"stale-endpoint\" };\n\t\t\t\tendpoints.delete(id); dormant.delete(id); versions.set(id, endpointVersion);\n\t\t\t\treturn { applied: true, action: \"drop\" };\n\t\t\t}\n\t\t\tcase \"invalidate\": {\n\t\t\t\tconst id = signal.payload?.endpointId;\n\t\t\t\tif (!endpoints.has(id)) return { applied: false, reason: \"unknown-endpoint\" };\n\t\t\t\tdormant.add(id);\n\t\t\t\treturn { applied: true, action: \"drop\" };\n\t\t\t}\n\t\t\tcase \"heartbeat\": return { applied: true, action: \"heartbeat\" };\n\t\t\tdefault: return { applied: false, reason: \"unknown-message-type\" };\n\t\t}\n\t}\n\n\tfunction upsert(value: unknown, now: number) {\n\t\tconst endpoint = normalizeEndpoint(value, endpoints.size);\n\t\tif (!endpoint || !isValidEndpoint(endpoint, options.catalog)) return { applied: false, reason: \"invalid-endpoint\" };\n\t\tif (isStale(endpoint.endpointId, endpoint.endpointVersion)) return { applied: false, reason: \"stale-endpoint\" };\n\t\tversions.set(endpoint.endpointId, endpoint.endpointVersion);\n\t\tdormant.delete(endpoint.endpointId);\n\t\tif (isExpired(endpoint, now)) endpoints.delete(endpoint.endpointId);\n\t\telse endpoints.set(endpoint.endpointId, copyEndpoint(endpoint));\n\t\treturn { applied: true, action: isExpired(endpoint, now) ? \"drop\" : \"candidate\" };\n\t}\n\n\tfunction isStale(id: string, version: unknown) {\n\t\tconst current = versions.get(id);\n\t\treturn current !== undefined && Number(version) <= current;\n\t}\n}\n\nexport function normalizeCatalogEndpoints(catalog: Catalog): MulticastSignalingEndpoint[] {\n\tconst endpoints = catalog?.multicast?.endpoints;\n\tif (!Array.isArray(endpoints)) return [];\n\treturn endpoints\n\t\t.map(normalizeEndpoint)\n\t\t.filter((endpoint): endpoint is MulticastSignalingEndpoint => !!endpoint && isValidEndpoint(endpoint, catalog));\n}\n\nfunction normalizeEndpoint(value: unknown, index: number): MulticastSignalingEndpoint | undefined {\n\tif (!isObject(value)) return undefined;\n\tconst endpoint = value as Record<string, any>;\n\tconst tracks = Array.isArray(endpoint.tracks) ? endpoint.tracks.map(track => typeof track === \"string\" ? { name: track } : { ...track }).filter(track => track.name) : [];\n\treturn { ...endpoint, endpointId: endpoint.endpointId || `${endpoint.sourceAddress || \"\"}|${endpoint.groupAddress || \"\"}|${endpoint.port || \"\"}|${index}`, endpointVersion: Number(endpoint.endpointVersion ?? 0), tracks } as MulticastSignalingEndpoint;\n}\n\nfunction isValidEndpoint(endpoint: MulticastSignalingEndpoint, catalog?: Catalog): boolean {\n\tif (!endpoint.endpointId || !Number.isFinite(endpoint.endpointVersion)) return false;\n\tif (![\"ssm\", \"asm\", \"amt\"].includes(endpoint.protocol)) return false;\n\tif (endpoint.protocol === \"ssm\" && (typeof endpoint.sourceAddress !== \"string\" || endpoint.sourceAddress.trim().length === 0)) return false;\n\tif (!isValidMulticastGroupAddress(endpoint.groupAddress) || !Number.isInteger(Number(endpoint.port)) || endpoint.port <= 0 || endpoint.port > 65535 || !endpoint.tracks.length) return false;\n\tconst catalogTracks = new Set([...Object.keys(catalog?.video?.renditions ?? {}), ...Object.keys(catalog?.audio?.renditions ?? {})]);\n\t// Every track must carry a valid packet_id (isMulticastTrackPacketId); one\n\t// that does not -- including packet_id 0, the MMTP signaling flow --\n\t// rejects the whole endpoint rather than silently dropping that track.\n\treturn endpoint.tracks.every(track => !!track.name && isMulticastTrackPacketId(track.packetId) && (catalogTracks.size === 0 || catalogTracks.has(track.name) || catalogTracks.has(track.name.replace(/\\/repair$/, \"\"))));\n}\n\nfunction copyEndpoint(endpoint: MulticastSignalingEndpoint): MulticastSignalingEndpoint {\n\treturn { ...endpoint, tracks: endpoint.tracks.map(track => ({ ...track })) };\n}\n\nfunction isExpired(endpoint: MulticastSignalingEndpoint, now: number): boolean {\n\treturn endpoint.validUntil != null && Number(endpoint.validUntil) < now;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn value !== null && typeof value === \"object\";\n}\n", "import {\n\tnormalizeCatalogEndpoints,\n\ttype MulticastSignalingEndpoint,\n} from \"./multicast-signaling-reconciler.js\";\nimport {\n\tisMulticastTrackPacketId,\n\tisUsableMulticastEndpoint,\n\tisValidMulticastGroupAddress,\n} from \"./endpoint-config.js\";\nimport type { MulticastTierEndpoint } from \"./multicast-tier.js\";\n\ntype Catalog = Record<string, any>;\n\nexport interface PlayerMulticastConfig {\n\tgroup: string;\n\tsource: string;\n\tport: number;\n\tmode?: string;\n\tamtRelay?: string;\n\tamtRelayPort?: number;\n\tamtNetworkSource?: unknown;\n\tpacketIds?: number[];\n}\n\nexport interface MulticastTrackSelection {\n\tcontainerKind?: string;\n\trendition?: string;\n}\n\nexport function hasMulticastCatalogBaseline(catalog: Catalog): boolean {\n\tconst multicast = catalog?.multicast;\n\treturn normalizeCatalogEndpoints(catalog).length > 0 || Boolean(multicast?.groupAddress && multicast?.port);\n}\n\nexport function hasUnicastMediaRenditions(catalog: Catalog): boolean {\n\treturn Object.keys(catalog?.video?.renditions ?? {}).length > 0 ||\n\t\tObject.keys(catalog?.audio?.renditions ?? {}).length > 0;\n}\n\nexport function isMulticastOnlyCatalog(catalog: Catalog): boolean {\n\treturn hasMulticastCatalogBaseline(catalog) && !hasUnicastMediaRenditions(catalog);\n}\n\nexport function requiresMulticastCapability(\n\tcatalog: Catalog,\n\tcapabilities: { multicast?: boolean } = {},\n): boolean {\n\treturn isMulticastOnlyCatalog(catalog) && !capabilities.multicast;\n}\n\nexport function isRecoverableMulticastSignalReadError(error: unknown): boolean {\n\tconst candidate = error as { name?: unknown; message?: unknown } | null;\n\tconst name = String(candidate?.name ?? \"\");\n\tconst message = String(candidate?.message ?? error ?? \"\");\n\treturn name === \"SyntaxError\" ||\n\t\t/not valid JSON|unexpected token|JSON\\.parse|encoded data was not valid|malformed/i.test(message);\n}\n\nexport function withMulticastEndpoints(\n\tcatalog: Catalog,\n\tendpoints: MulticastSignalingEndpoint[],\n\tversion: number,\n): Catalog {\n\tif (!catalog?.multicast) return catalog;\n\treturn {\n\t\t...catalog,\n\t\tmulticast: {\n\t\t\t...catalog.multicast,\n\t\t\tversion,\n\t\t\tendpoints: endpoints.map(copyEndpoint),\n\t\t},\n\t};\n}\n\nexport function selectMulticastCandidate(\n\tcatalog: Catalog,\n\tendpoints = normalizeCatalogEndpoints(catalog),\n\tsearch: string | URLSearchParams = currentSearchParams(),\n): MulticastSignalingEndpoint | null {\n\tconst params = search instanceof URLSearchParams ? search : new URLSearchParams(search);\n\treturn selectMulticastCandidateForTracks(catalog, endpoints, {\n\t\tcontainerKind: params.get(\"packaging\") || params.get(\"container\") || \"mmtp\",\n\t\trendition: params.get(\"rendition\") || undefined,\n\t});\n}\n\nexport function selectMulticastCandidateForTracks(\n\tcatalog: Catalog,\n\tendpoints = normalizeCatalogEndpoints(catalog),\n\tselection: MulticastTrackSelection = {},\n): MulticastSignalingEndpoint | null {\n\tconst packaging = selection.containerKind || \"mmtp\";\n\tconst renditionTrack = selection.rendition || \"\";\n\tconst preferredTrack = packaging === \"mmtp\" ? \"video\" : `video.${packaging}`;\n\tconst usableEndpoints = endpoints.filter(isUsableMulticastEndpoint);\n\tconst defaultRenditionTrack = renditionTrack ? \"\" : lowestServedVideoRendition(catalog, usableEndpoints);\n\treturn (renditionTrack && usableEndpoints.find(endpoint =>\n\t\tendpoint.tracks?.some(track => matchesPreferredTrack(track.name, renditionTrack)))) ||\n\t\t(defaultRenditionTrack && usableEndpoints.find(endpoint =>\n\t\t\tendpoint.tracks?.some(track => matchesPreferredTrack(track.name, defaultRenditionTrack)))) ||\n\t\tusableEndpoints.find(endpoint =>\n\t\t\tendpoint.tracks?.some(track => matchesPreferredTrack(track.name, preferredTrack))) ||\n\t\tusableEndpoints[0] || null;\n}\n\nexport function endpointToMulticastConfig(\n\tendpoint: MulticastSignalingEndpoint | Record<string, any> | null | undefined,\n\tcatalog: Catalog,\n\tselection?: MulticastTrackSelection,\n): PlayerMulticastConfig | null {\n\tif (!endpoint || !isUsableMulticastEndpoint(endpoint) || advertisesInvalidPacketId(endpoint)) return null;\n\tconst config: PlayerMulticastConfig = {\n\t\tgroup: endpoint.groupAddress,\n\t\tsource: endpoint.sourceAddress || \"\",\n\t\tport: Number(endpoint.port),\n\t};\n\tconst networkSource = endpoint.networkSource ?? catalog?.multicast?.networkSource;\n\tconst amt = findAmtRelay(networkSource);\n\tif (endpoint.protocol === \"amt\" || amt) {\n\t\tconfig.mode = \"tunnel\";\n\t\tif (amt) {\n\t\t\tconfig.amtRelay = amt.relay;\n\t\t\tif (amt.port) config.amtRelayPort = amt.port;\n\t\t}\n\t}\n\tconst packetIds = selection === undefined ?\n\t\tselectEndpointPacketIds(endpoint, catalog) :\n\t\tselectEndpointPacketIdsForTracks(endpoint, catalog, selection);\n\tif (packetIds === null) return null;\n\tif (packetIds.length > 0) config.packetIds = packetIds;\n\treturn config;\n}\n\nexport function selectEndpointPacketIds(\n\tendpoint: MulticastSignalingEndpoint | Record<string, any> | null | undefined,\n\tcatalog: Catalog,\n\tsearch: string | URLSearchParams = currentSearchParams(),\n): number[] | null {\n\tconst params = search instanceof URLSearchParams ? search : new URLSearchParams(search);\n\treturn selectEndpointPacketIdsForTracks(endpoint, catalog, {\n\t\tcontainerKind: params.get(\"packaging\") || params.get(\"container\") || \"mmtp\",\n\t\trendition: params.get(\"rendition\") || undefined,\n\t});\n}\n\nexport function selectEndpointPacketIdsForTracks(\n\tendpoint: MulticastSignalingEndpoint | Record<string, any> | null | undefined,\n\tcatalog: Catalog,\n\tselection: MulticastTrackSelection = {},\n): number[] | null {\n\tif (!endpoint || !Array.isArray(endpoint.tracks)) return [];\n\t// An empty result means \"no packet_id filter\" to the join, so an invalid\n\t// packet_id must fail here rather than be dropped: dropping the only track\n\t// would widen the filter to every packet_id, the signaling flow included.\n\t// null (\"this endpoint is unusable\") matches endpointToMulticastConfig on\n\t// the same input, and the widened return type is the signal to callers.\n\tif (advertisesInvalidPacketId(endpoint)) return null;\n\tconst tracks = endpoint.tracks\n\t\t.map((track: any) => ({\n\t\t\tname: typeof track?.name === \"string\" ? track.name : \"\",\n\t\t\tpacketId: validPacketId(track?.packetId),\n\t\t}))\n\t\t.filter((track: { name: string; packetId?: number }): track is { name: string; packetId: number } =>\n\t\t\ttrack.name.length > 0 && track.packetId !== undefined);\n\tif (tracks.length === 0) return [];\n\n\tconst packaging = selection.containerKind || \"mmtp\";\n\tconst renditionTrack = selection.rendition || \"\";\n\tconst videoRenditions = catalogRenditions(catalog, \"video\");\n\tconst audioRenditions = catalogRenditions(catalog, \"audio\");\n\tconst videoTracks = tracks.filter(track => Object.hasOwn(videoRenditions, track.name));\n\tconst audioTracks = tracks.filter(track => Object.hasOwn(audioRenditions, track.name));\n\tconst preferredVideo = packaging === \"mmtp\" ? \"video\" : `video.${packaging}`;\n\tconst preferredAudio = packaging === \"mmtp\" ? \"audio\" : `audio.${packaging}`;\n\tconst video =\n\t\t(renditionTrack ? videoTracks.find(track => track.name === renditionTrack) : undefined) ??\n\t\tvideoTracks.find(track => track.name === preferredVideo) ??\n\t\t[...videoTracks].sort((a, b) =>\n\t\t\tvideoRenditionRank(videoRenditions, a.name) - videoRenditionRank(videoRenditions, b.name))[0];\n\tconst audio = audioTracks.find(track => track.name === preferredAudio) ?? audioTracks[0];\n\tconst selected = [video, audio].filter(\n\t\t(track): track is { name: string; packetId: number } => track !== undefined,\n\t);\n\tif (selected.length === 0) return normalizePacketIds(tracks.map(track => track.packetId));\n\n\t// Every repair layer and overlay of a selected track is a repair track of\n\t// that track (draft-ramadan-moq-fec \u00A76.3), so a layer listed on the endpoint\n\t// is selected alongside layer 0 rather than treated as unknown.\n\tconst selectedRepairNames = new Set(selected.flatMap(track => catalogRepairTracks(catalog, track.name)));\n\tconst selectedRepairTracks = tracks.filter(track => selectedRepairNames.has(track.name));\n\treturn normalizePacketIds([...selected, ...selectedRepairTracks].map(track => track.packetId));\n}\n\nexport function readPlayerMulticastConfig(\n\tsearch: string | URLSearchParams = currentSearchParams(),\n): PlayerMulticastConfig | null {\n\tconst params = search instanceof URLSearchParams ? search : new URLSearchParams(search || \"\");\n\tconst group = params.get(\"mc_group\") || \"\";\n\tif (!isValidMulticastGroupAddress(group)) return null;\n\tconst port = positivePort(params.get(\"mc_port\"));\n\tif (!port) return null;\n\tconst mode = params.get(\"mc_mode\") || \"\";\n\tconst amtRelay = params.get(\"mc_amt_relay\") || params.get(\"amt_relay\") || \"\";\n\tconst amtRelayPort = positivePort(params.get(\"mc_amt_relay_port\") || params.get(\"amt_relay_port\"));\n\tconst config: PlayerMulticastConfig = {\n\t\tgroup,\n\t\tsource: params.get(\"mc_source\") || \"\",\n\t\tport,\n\t};\n\tif (mode) config.mode = mode;\n\tif (amtRelay) {\n\t\tconfig.amtRelay = amtRelay;\n\t\tconfig.mode = \"tunnel\";\n\t}\n\tif (amtRelayPort) config.amtRelayPort = amtRelayPort;\n\treturn config;\n}\n\nexport function normalizeMulticastConfig(\n\tconfig: Partial<PlayerMulticastConfig> | null | undefined,\n): PlayerMulticastConfig | null {\n\tif (!config?.group) return null;\n\t// A join list naming an invalid packet_id is refused whole: dropping the\n\t// entry could leave the list empty, which the join reads as \"no filter\".\n\tif (Array.isArray(config.packetIds) && !config.packetIds.every(isMulticastTrackPacketId)) return null;\n\tconst amt = findAmtRelay(config.amtNetworkSource);\n\tconst amtRelay = config.amtRelay || amt?.relay || \"\";\n\tconst amtRelayPort = config.amtRelayPort ?? amt?.port;\n\tconst packetIds = normalizePacketIds(config.packetIds);\n\treturn {\n\t\tgroup: config.group,\n\t\tsource: config.source || \"\",\n\t\tport: Number(config.port),\n\t\tmode: amtRelay ? \"tunnel\" : (config.mode || \"auto\"),\n\t\tamtRelay,\n\t\t...(amtRelayPort ? { amtRelayPort } : {}),\n\t\t...(packetIds.length > 0 ? { packetIds } : {}),\n\t};\n}\n\nexport function sameMulticastConfig(\n\ta: Partial<PlayerMulticastConfig> | null | undefined,\n\tb: Partial<PlayerMulticastConfig> | null | undefined,\n): boolean {\n\tconst left = normalizeMulticastConfig(a);\n\tconst right = normalizeMulticastConfig(b);\n\treturn Boolean(left && right &&\n\t\tleft.group === right.group &&\n\t\tleft.source === right.source &&\n\t\tleft.port === right.port &&\n\t\tleft.mode === right.mode &&\n\t\tleft.amtRelay === right.amtRelay &&\n\t\t(left.amtRelayPort ?? 0) === (right.amtRelayPort ?? 0) &&\n\t\tJSON.stringify(left.packetIds ?? []) === JSON.stringify(right.packetIds ?? []));\n}\n\nexport function urlOverrideBlocksMulticastCandidate(\n\tcandidateConfig: Partial<PlayerMulticastConfig> | null | undefined,\n\turlConfig: PlayerMulticastConfig | null = readPlayerMulticastConfig(),\n): boolean {\n\tif (!urlConfig) return false;\n\tconst candidate = normalizeMulticastConfig(candidateConfig);\n\tconst explicit = normalizeMulticastConfig(urlConfig);\n\treturn !candidate || !explicit ||\n\t\tcandidate.group !== explicit.group ||\n\t\tcandidate.source !== explicit.source ||\n\t\tcandidate.port !== explicit.port;\n}\n\n/** Preserve catalog track selection while applying explicit URL transport policy. */\nexport function mergeUrlMulticastPolicy(\n\tcandidateConfig: Partial<PlayerMulticastConfig> | null | undefined,\n\turlConfig: PlayerMulticastConfig | null = readPlayerMulticastConfig(),\n): PlayerMulticastConfig | null {\n\tconst candidate = normalizeMulticastConfig(candidateConfig);\n\tif (!candidate || !urlConfig || urlOverrideBlocksMulticastCandidate(candidate, urlConfig)) return candidate;\n\n\tconst merged = { ...candidate };\n\tif (urlConfig.mode !== undefined) {\n\t\tmerged.mode = urlConfig.mode;\n\t\tif (urlConfig.mode !== \"tunnel\" && urlConfig.amtRelay === undefined) {\n\t\t\tmerged.amtRelay = \"\";\n\t\t\tdelete merged.amtRelayPort;\n\t\t}\n\t}\n\tif (urlConfig.amtRelay !== undefined) {\n\t\tmerged.amtRelay = urlConfig.amtRelay;\n\t\tmerged.mode = \"tunnel\";\n\t}\n\tif (urlConfig.amtRelayPort !== undefined) merged.amtRelayPort = urlConfig.amtRelayPort;\n\treturn normalizeMulticastConfig(merged);\n}\n\nfunction currentSearchParams(): URLSearchParams {\n\treturn typeof location === \"undefined\" ?\n\t\tnew URLSearchParams() :\n\t\tnew URLSearchParams(location.search);\n}\n\nfunction positivePort(value: string | null): number | null {\n\tconst port = Number(value);\n\treturn Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;\n}\n\nfunction copyEndpoint(endpoint: MulticastSignalingEndpoint): MulticastSignalingEndpoint {\n\treturn { ...endpoint, tracks: endpoint.tracks.map(track => ({ ...track })) };\n}\n\nfunction findAmtRelay(networkSource: unknown): { relay: string; port?: number } | undefined {\n\tconst sources = Array.isArray(networkSource) ? networkSource : networkSource ? [networkSource] : [];\n\tconst source = sources.find(candidate =>\n\t\tcandidate?.type === \"amt\" && typeof candidate?.relay === \"string\" && candidate.relay.trim());\n\tif (!source) return undefined;\n\tconst port = positivePort(String(source.port ?? \"\"));\n\tconst relay = source.relay.trim();\n\treturn port ? { relay, port } : { relay };\n}\n\nfunction validPacketId(value: unknown): number | undefined {\n\treturn isMulticastTrackPacketId(value) ? value : undefined;\n}\n\n/** True when some endpoint track advertises a `packetId` that is present but\n * not a valid track packet_id. A track that omits `packetId` (the legacy\n * string form) advertises none and is not invalid. */\nfunction advertisesInvalidPacketId(endpoint: { tracks?: unknown }): boolean {\n\treturn Array.isArray(endpoint.tracks) && endpoint.tracks.some((track: any) =>\n\t\ttrack?.packetId !== undefined && !isMulticastTrackPacketId(track.packetId));\n}\n\nfunction normalizePacketIds(packetIds: unknown): number[] {\n\tif (!Array.isArray(packetIds)) return [];\n\treturn [...new Set(packetIds.map(validPacketId).filter((id): id is number => id !== undefined))].sort(\n\t\t(a, b) => a - b,\n\t);\n}\n\nfunction matchesPreferredTrack(trackName: string, preferredTrack: string): boolean {\n\treturn trackName === preferredTrack || trackName.startsWith(`${preferredTrack}/`);\n}\n\nfunction catalogRenditions(catalog: Catalog, role: \"video\" | \"audio\"): Record<string, any> {\n\tconst renditions = { ...(catalog?.[role]?.renditions ?? {}) };\n\tfor (const track of Array.isArray(catalog?.tracks) ? catalog.tracks : []) {\n\t\tif (track?.role === role && typeof track.name === \"string\" && track.name.length > 0) {\n\t\t\trenditions[track.name] = track;\n\t\t}\n\t}\n\treturn renditions;\n}\n\n/** Repair tracks a source track (or rendition) depends on for its FEC: layer 0\n * named by `fec.repairTrack`, then every self-describing repair track that\n * `depends` on it (MSF `tracks[]` entries with `packaging: \"fec-repair\"`, or\n * hang `repair.renditions` entries), per draft-ramadan-moq-fec \u00A76.3. Names\n * carry no meaning: membership comes from the pointer and from `depends`,\n * never from a name suffix. Empty when the track carries no FEC, including\n * `fec.algorithm: \"none\"`, which the container-side derivations\n * (`carp-catalog` and `catalog-fec-params`) also read as no FEC. */\nfunction catalogRepairTracks(catalog: Catalog, name: string): string[] {\n\tconst tracks: any[] = Array.isArray(catalog?.tracks) ? catalog.tracks : [];\n\tconst track = tracks.find((candidate: any) => candidate?.name === name);\n\tconst rendition = catalog?.video?.renditions?.[name] ?? catalog?.audio?.renditions?.[name];\n\tconst fec = track?.fec ?? rendition?.fec;\n\tif (!fec || fec.algorithm === \"none\") return [];\n\tconst names = new Set<string>();\n\tif (typeof fec.repairTrack === \"string\" && fec.repairTrack.length > 0) names.add(fec.repairTrack);\n\tconst dependsOn = (entry: any): boolean =>\n\t\tArray.isArray(entry?.depends) && entry.depends.length === 1 && entry.depends[0] === name;\n\tconst common = catalog?.commonTrackFields ?? {};\n\tfor (const candidate of tracks) {\n\t\tconst packaging = candidate?.packaging ?? common?.packaging;\n\t\tif (packaging === \"fec-repair\" && typeof candidate?.name === \"string\" && candidate.name.length > 0 && dependsOn(candidate)) {\n\t\t\tnames.add(candidate.name);\n\t\t}\n\t}\n\tconst repairRenditions = catalog?.repair?.renditions;\n\tif (repairRenditions && typeof repairRenditions === \"object\") {\n\t\tfor (const [repairName, entry] of Object.entries(repairRenditions)) {\n\t\t\tif (repairName.length > 0 && dependsOn(entry)) names.add(repairName);\n\t\t}\n\t}\n\treturn [...names];\n}\n\n/** Every self-describing repair track name in the catalog, in either dialect:\n * MSF `tracks[]` entries with `packaging: \"fec-repair\"` and hang\n * `repair.renditions` keys. A repair track describes itself, so membership\n * needs no source resolution and never comes from a name suffix. */\nfunction catalogRepairTrackNames(catalog: Catalog): Set<string> {\n\tconst names = new Set<string>();\n\tconst common = catalog?.commonTrackFields ?? {};\n\tfor (const track of Array.isArray(catalog?.tracks) ? catalog.tracks : []) {\n\t\tconst packaging = track?.packaging ?? common?.packaging;\n\t\tif (packaging === \"fec-repair\" && typeof track?.name === \"string\" && track.name.length > 0) {\n\t\t\tnames.add(track.name);\n\t\t}\n\t}\n\tconst repairRenditions = catalog?.repair?.renditions;\n\tif (repairRenditions && typeof repairRenditions === \"object\") {\n\t\tfor (const name of Object.keys(repairRenditions)) {\n\t\t\tif (name.length > 0) names.add(name);\n\t\t}\n\t}\n\treturn names;\n}\n\n/** True when an endpoint carries at least one track that is not a repair\n * track, i.e. it can serve media on its own. A repair-layer endpoint carries\n * only `fec-repair` tracks and is joined by the loss-driven layer policy, so\n * it must be excluded from the bandwidth ladder (`selectMulticastTier`).\n * An endpoint listing no tracks is treated as carrying media: that is the\n * pre-layer signalling shape, and a naming-era catalog with no repair tracks\n * declared classifies every endpoint as media. */\nexport function endpointCarriesMedia(\n\tcatalog: Catalog,\n\tendpoint: { tracks?: { name?: string }[] } | null | undefined,\n): boolean {\n\tconst tracks = endpoint?.tracks;\n\tif (!Array.isArray(tracks) || tracks.length === 0) return true;\n\tconst repairNames = catalogRepairTrackNames(catalog);\n\tif (repairNames.size === 0) return true;\n\treturn tracks.some(track => typeof track?.name === \"string\" && !repairNames.has(track.name));\n}\n\n/** Catalog signalling endpoints as bandwidth-ladder endpoints, classified.\n *\n * Every ladder caller must build its `MulticastTierEndpoint[]` through this\n * rather than hand-shaping them from the catalog: `carriesMedia` is optional\n * and an unset one is treated as carrying media, so an endpoint that was\n * never classified stays selectable as a tier and joining it would leave the\n * receiver with repair symbols and no media. Classifying at the one place\n * endpoints are converted is what makes that impossible to forget.\n *\n * `bandwidth` is carried through only when the endpoint declares a positive\n * finite number and is never synthesized, so an endpoint that omits it stays\n * unranked exactly as before. Only the fields the ladder reads are copied;\n * signalling-only keys stay behind. */\nexport function catalogTierEndpoints(\n\tcatalog: Catalog,\n\tendpoints: MulticastSignalingEndpoint[] = normalizeCatalogEndpoints(catalog),\n): MulticastTierEndpoint[] {\n\treturn endpoints.map(endpoint => {\n\t\tconst tier: MulticastTierEndpoint = {\n\t\t\tgroupAddress: endpoint.groupAddress,\n\t\t\tport: Number(endpoint.port),\n\t\t\ttracks: endpoint.tracks.map(track =>\n\t\t\t\ttrack.packetId === undefined ? { name: track.name } : { name: track.name, packetId: track.packetId }),\n\t\t\tcarriesMedia: endpointCarriesMedia(catalog, endpoint),\n\t\t};\n\t\tif (endpoint.sourceAddress) tier.sourceAddress = endpoint.sourceAddress;\n\t\tif (endpoint.protocol) tier.protocol = endpoint.protocol;\n\t\tconst bandwidth = (endpoint as Record<string, unknown>).bandwidth;\n\t\tif (typeof bandwidth === \"number\" && Number.isFinite(bandwidth) && bandwidth > 0) tier.bandwidth = bandwidth;\n\t\treturn tier;\n\t});\n}\n\nfunction videoRenditionRank(config: Record<string, any>, name: string): number {\n\tif (typeof config?.bitrate === \"number\" && config.bitrate > 0) return config.bitrate;\n\tconst width = typeof config?.codedWidth === \"number\" ? config.codedWidth : 0;\n\tconst height = typeof config?.codedHeight === \"number\" ? config.codedHeight : 0;\n\tif (width > 0 && height > 0) return width * height;\n\tconst match = /(?:^|[/_.-])(\\d{3,4})p(?:$|[/_.-])/i.exec(name);\n\treturn match ? Number(match[1]) : Number.POSITIVE_INFINITY;\n}\n\nfunction lowestServedVideoRendition(\n\tcatalog: Catalog,\n\tendpoints: MulticastSignalingEndpoint[],\n): string {\n\tconst renditions = catalog?.video?.renditions ?? {};\n\tconst servedTracks = new Set(endpoints.flatMap(endpoint =>\n\t\tendpoint.tracks?.map(track => track.name) ?? []));\n\tlet selected = \"\";\n\tlet selectedRank = Number.POSITIVE_INFINITY;\n\tfor (const [name, config] of Object.entries(renditions)) {\n\t\tif (!name || !servedTracks.has(name)) continue;\n\t\tconst rank = videoRenditionRank(config as Record<string, any>, name);\n\t\tif (!selected || rank < selectedRank) {\n\t\t\tselected = name;\n\t\t\tselectedRank = rank;\n\t\t}\n\t}\n\treturn selected;\n}\n", "import {\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.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\tbytesListener: (event: CustomEvent<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});\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}));\n\t\tconst consumers = new Map<symbol, MulticastSubscriptionConsumer>();\n\t\tlet entry: PoolEntry;\n\t\tconst bytesListener = (event: CustomEvent<ArrayBuffer>) => {\n\t\t\tthis.#dispatch(consumers, (consumer) => consumer.onBytes(event.detail));\n\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, bytesListener, stateListener, errorListener, closed: false };\n\t\tsubscription.addEventListener(\"bytes\", bytesListener);\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\tentry.subscription.removeEventListener(\"bytes\", entry.bytesListener);\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", "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\tdeadlineRepair: moqtSubscriberPriority(0),\n\taudio: moqtSubscriberPriority(1),\n\tvideo: moqtSubscriberPriority(2),\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", "export interface FecProgressSnapshot {\n\tsourceSymbols: number;\n\trepairSymbols: number;\n\tblocksComplete: number;\n\tblocksRecovered: number;\n\tmediaFrames?: number;\n}\n\nexport interface StallDetectorConfig {\n\twindowMs: number;\n\tonStarvation(): void;\n\tonRecovery(): void;\n\tnow?: () => number;\n}\n\nexport interface PlayerStallSnapshot {\n\tliveQ?: number | null;\n\tdecodeCallsDelta?: number | null;\n\tfps?: number | null;\n}\n\nexport interface PlayerStallClassification {\n\tverdict: \"DECODE-BOUND\" | \"PIPELINE-STARVED\" | \"STARVED-OR-SLOW\" | \"INDETERMINATE\";\n\treason: string;\n}\n\nexport function classifyStall({\n\tliveQ = null,\n\tdecodeCallsDelta = null,\n\tfps = null,\n}: PlayerStallSnapshot = {}): PlayerStallClassification {\n\tif (liveQ != null && liveQ >= 20) {\n\t\treturn {\n\t\t\tverdict: \"DECODE-BOUND\",\n\t\t\treason: `decode queue backlogged now (liveQ=${liveQ})`,\n\t\t};\n\t}\n\tif (liveQ != null && liveQ <= 2 && decodeCallsDelta != null && decodeCallsDelta <= 1) {\n\t\treturn {\n\t\t\tverdict: \"PIPELINE-STARVED\",\n\t\t\treason: `queue empty (liveQ=${liveQ}) and decode flatlined (\u0394calls=${decodeCallsDelta}) \u2014 nothing reaching the decoder; suspect delivery/FEC/failback, not decode perf`,\n\t\t};\n\t}\n\tif (fps != null && fps < 45) {\n\t\treturn {\n\t\t\tverdict: \"STARVED-OR-SLOW\",\n\t\t\treason: `low decode rate (fps=${fps}) without a queue/flatline signal to disambiguate`,\n\t\t};\n\t}\n\treturn { verdict: \"INDETERMINATE\", reason: \"no signal supports a verdict\" };\n}\n\nexport class TransportStallDetector {\n\t#last: FecProgressSnapshot | null = null;\n\t#lastSampleAt = 0;\n\t#starvedSince: number | null = null;\n\t#latched = false;\n\treadonly #now: () => number;\n\n\tconstructor(private readonly config: StallDetectorConfig) {\n\t\tif (config.windowMs <= 0) throw new Error(\"Stall window must be positive and catalog-derived\");\n\t\tthis.#now = config.now ?? Date.now;\n\t}\n\n\tget isStarved(): boolean { return this.#latched; }\n\n\tupdate(snapshot: FecProgressSnapshot): void {\n\t\tconst previous = this.#last;\n\t\tconst previousAt = this.#lastSampleAt;\n\t\tconst now = this.#now();\n\t\tthis.#last = { ...snapshot };\n\t\tthis.#lastSampleAt = now;\n\t\tif (!previous) return;\n\n\t\tconst blockDelta = snapshot.blocksComplete - previous.blocksComplete +\n\t\t\tsnapshot.blocksRecovered - previous.blocksRecovered;\n\t\tconst mediaDelta = (snapshot.mediaFrames ?? 0) - (previous.mediaFrames ?? 0);\n\n\t\tif (blockDelta <= 0 && mediaDelta <= 0) {\n\t\t\tthis.#starvedSince ??= previousAt;\n\t\t\tif (!this.#latched && now - this.#starvedSince >= this.config.windowMs) {\n\t\t\t\tthis.#latched = true;\n\t\t\t\tthis.config.onStarvation();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#starvedSince = null;\n\t\tif (this.#latched) {\n\t\t\tthis.#latched = false;\n\t\t\tthis.config.onRecovery();\n\t\t}\n\t}\n\n\treset(): void {\n\t\tthis.#last = null;\n\t\tthis.#starvedSince = null;\n\t\tthis.#latched = false;\n\t}\n}\n", "export interface ActivatableSessions { activateMulticast(): void; }\n\nexport function makeSourceSwitchHandler(options: {\n\tmulticastBridgeAvailable: boolean;\n\tsessionsReady: Promise<ActivatableSessions | null>;\n\tonMulticastConfirmed?(): void;\n\tonAbortTakeover?(): void;\n\tlog?(message: string): void;\n\twarn?(message: string): void;\n}): (source: string) => Promise<void> {\n\tconst log = options.log ?? console.log;\n\tconst warn = options.warn ?? console.warn;\n\treturn async (source) => {\n\t\tif (source !== \"multicast\") return;\n\t\tif (!options.multicastBridgeAvailable) {\n\t\t\twarn(\"[MoqWatch] MediaRouter requested multicast without bridge; keeping MoQ active\");\n\t\t\treturn;\n\t\t}\n\t\tconst sessions = await options.sessionsReady;\n\t\tif (!sessions) {\n\t\t\twarn(\"[MoqWatch] SessionManager unavailable; aborting multicast takeover, staying on MoQ\");\n\t\t\toptions.onAbortTakeover?.();\n\t\t\treturn;\n\t\t}\n\t\tlog(\"[MoqWatch] MediaRouter -> multicast; activating via SessionManager\");\n\t\tsessions.activateMulticast();\n\t\toptions.onMulticastConfirmed?.();\n\t};\n}\n\nexport interface LossArcSession {\n\t/** TrackSession state. Optional so non-TrackSession hosts keep the generic API. */\n\treadonly state?: string;\n\tlossDetected(): void;\n\tlossRecovered(): void;\n}\nexport interface FecStarvationSnapshot {\n\tsourceSymbols: number;\n\trepairSymbols: number;\n\tblocksComplete: number;\n\tblocksRecovered: number;\n\tmediaFrames?: number;\n}\nexport interface StarvationDetectorLike { update(snapshot: FecStarvationSnapshot): void; reset(): void; }\nexport type StallDetectorLike = StarvationDetectorLike;\n\nexport function wireStallDetector(args: {\n\tsession: LossArcSession | undefined;\n\twindowMs: number;\n\tmakeDetector(config: { windowMs: number; onStarvation(): void; onRecovery(): void }): StallDetectorLike;\n}): StallDetectorLike | null {\n\tif (!args.session || args.windowMs <= 0) return null;\n\treturn args.makeDetector({\n\t\twindowMs: args.windowMs,\n\t\tonStarvation: () => args.session!.lossDetected(),\n\t\tonRecovery: () => args.session!.lossRecovered(),\n\t});\n}\n\nexport async function wireStarvationDetector(args: {\n\tsession: LossArcSession | undefined;\n\ttrackName: string;\n\tfecInterleaveMs: number;\n\tfecK: number;\n\tdeliveryWindowMs?: number;\n\tmakeDetector(config: { windowMs: number; onStarvation(): void; onRecovery(): void }): StarvationDetectorLike;\n\twarn?(message: string): void;\n}): Promise<StarvationDetectorLike | null> {\n\tif (!args.session || args.fecInterleaveMs <= 0 || args.fecK <= 0) return null;\n\tconst windowMs = args.deliveryWindowMs ?? Math.ceil(args.fecInterleaveMs * 3);\n\tif (!Number.isFinite(windowMs) || windowMs <= 0) {\n\t\targs.warn?.(`Unable to create starvation detector for ${args.trackName}: invalid delivery window ${windowMs}`);\n\t\treturn null;\n\t}\n\tconst detector = wireStallDetector({\n\t\tsession: args.session,\n\t\twindowMs,\n\t\tmakeDetector: args.makeDetector,\n\t});\n\tif (!detector) args.warn?.(`Unable to create starvation detector for ${args.trackName}`);\n\tif (!detector || typeof args.session.state !== \"string\") return detector;\n\n\t// Stats flow while TrackSession is still `joining`. Feeding them directly can\n\t// age a starvation episode past its window before the first multicast RAP,\n\t// causing the first post-cutover sample to flip a healthy session to\n\t// `failing`. Start with a fresh baseline only once multicast/AMT is primary;\n\t// retain the detector through `failing` so it can observe recovery.\n\tlet active = false;\n\treturn {\n\t\tupdate(snapshot) {\n\t\t\tconst state = args.session!.state;\n\t\t\tconst shouldObserve = state === \"multicast\" || state === \"amt\" || state === \"failing\";\n\t\t\tif (!shouldObserve) {\n\t\t\t\tif (active) detector.reset();\n\t\t\t\tactive = false;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!active) {\n\t\t\t\tdetector.reset();\n\t\t\t\tactive = true;\n\t\t\t}\n\t\t\tdetector.update(snapshot);\n\t\t},\n\t\treset() {\n\t\t\tactive = false;\n\t\t\tdetector.reset();\n\t\t},\n\t};\n}\n", "/**\n * @blockcast/transport - DRIAD Discovery (RFC 8777)\n *\n * DRIAD (DNS Reverse IP AMT Discovery) discovers AMT relays based on the\n * **source address**, NOT the multicast group. The source network operator\n * configures DNS records for their source IPs to advertise which AMT relay(s)\n * can tunnel their traffic.\n *\n * Example: For source 69.25.95.10 sending to group 232.0.0.1:\n * Query: 10.95.25.69.amt.in-addr.arpa (source-based, NOT group-based)\n */\n\nimport type { DRIADRelay } from \"./types.js\";\n\n/**\n * DRIAD (DNS Reverse IP AMT Discovery) - RFC 8777\n * Discovers AMT relays based on source address via DNS lookups\n */\nexport class DRIADDiscovery {\n\tprivate cache = new Map<string, DRIADRelay[]>();\n\tprivate cacheExpiry = 300_000; // 5 minutes\n\n\t/**\n\t * Discover AMT relays for a multicast source address (RFC 8777)\n\t *\n\t * @param source - The multicast source IP address (NOT the group!)\n\t * @returns Array of discovered AMT relays that can tunnel traffic from this source\n\t */\n\tasync discoverRelays(source: string): Promise<DRIADRelay[]> {\n\t\t// Check cache first\n\t\tconst cached = this.cache.get(source);\n\t\tif (cached) {\n\t\t\treturn cached;\n\t\t}\n\n\t\ttry {\n\t\t\t// Convert source to DRIAD format (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryDomain = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconsole.log(`[DRIAD] Discovering relays for source ${source} via ${queryDomain}`);\n\n\t\t\t// In browser, we use a DRIAD proxy service or DNS-over-HTTPS\n\t\t\tconst relays = await this.queryDRIADProxy(source);\n\n\t\t\tif (relays.length > 0) {\n\t\t\t\tthis.cache.set(source, relays);\n\t\t\t\tsetTimeout(() => this.cache.delete(source), this.cacheExpiry);\n\t\t\t}\n\n\t\t\treturn relays;\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[DRIAD] Discovery failed for source ${source}:`, error);\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/**\n\t * Query DRIAD proxy service (browser-compatible)\n\t */\n\tprivate async queryDRIADProxy(source: string): Promise<DRIADRelay[]> {\n\t\t// List of known DRIAD proxy endpoints\n\t\tconst proxyEndpoints = [\n\t\t\t\"https://driad.amt.net/api/v1/relays\",\n\t\t\t\"https://multicast-gateway.cloudflare.com/driad\",\n\t\t];\n\n\t\tfor (const endpoint of proxyEndpoints) {\n\t\t\ttry {\n\t\t\t\t// Query by source address per RFC 8777\n\t\t\t\tconst response = await fetch(`${endpoint}?source=${encodeURIComponent(source)}`, {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t\t});\n\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = (await response.json()) as { relays: DRIADRelay[] };\n\t\t\t\t\treturn data.relays || [];\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Try next proxy endpoint\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: Try DNS-over-HTTPS query\n\t\tconst dohRelay = await this.queryDNSOverHTTPS(source);\n\t\tif (dohRelay) {\n\t\t\treturn [dohRelay];\n\t\t}\n\n\t\treturn [];\n\t}\n\n\t/**\n\t * Query DNS-over-HTTPS for DRIAD relay based on source address (RFC 8777)\n\t */\n\tprivate async queryDNSOverHTTPS(source: string): Promise<DRIADRelay | null> {\n\t\ttry {\n\t\t\t// Build DRIAD query name from source address (reversed octets)\n\t\t\tconst reversedSource = source.split(\".\").reverse().join(\".\");\n\t\t\tconst queryName = `${reversedSource}.amt.in-addr.arpa`;\n\n\t\t\tconst dohUrl = `https://cloudflare-dns.com/dns-query?name=${queryName}&type=A`;\n\t\t\tconst response = await fetch(dohUrl, {\n\t\t\t\theaders: { Accept: \"application/dns-json\" },\n\t\t\t\tsignal: AbortSignal.timeout(3000),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst result = (await response.json()) as { Answer?: Array<{ data: string }> };\n\n\t\t\t// Extract first A/AAAA record\n\t\t\tif (result.Answer && result.Answer.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\thost: result.Answer[0].data,\n\t\t\t\t\tport: 2268,\n\t\t\t\t\tpriority: 10,\n\t\t\t\t\tweight: 100,\n\t\t\t\t\tasn: 0,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[DRIAD] DNS-over-HTTPS query failed:\", error);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Select best relay based on RTT measurements\n\t */\n\tasync selectBestRelay(relays: DRIADRelay[]): Promise<DRIADRelay | null> {\n\t\tif (relays.length === 0) return null;\n\n\t\t// Measure RTT to each relay\n\t\tconst measured = await Promise.all(\n\t\t\trelays.map(async (relay) => {\n\t\t\t\tconst rtt = await this.measureRTT(relay.host, relay.port);\n\t\t\t\treturn { ...relay, rtt };\n\t\t\t}),\n\t\t);\n\n\t\t// Sort by priority, then by RTT\n\t\tmeasured.sort((a, b) => {\n\t\t\tif (a.priority !== b.priority) return a.priority - b.priority;\n\t\t\treturn (a.rtt || Infinity) - (b.rtt || Infinity);\n\t\t});\n\n\t\tconsole.log(`[DRIAD] Selected relay: ${measured[0].host} (RTT: ${measured[0].rtt}ms)`);\n\t\treturn measured[0];\n\t}\n\n\tprivate async measureRTT(host: string, _port: number): Promise<number> {\n\t\tconst start = performance.now();\n\t\ttry {\n\t\t\t// Use HTTP/HTTPS probe since we can't do UDP from browser\n\t\t\tawait fetch(`https://${host}/health`, {\n\t\t\t\tmethod: \"HEAD\",\n\t\t\t\tsignal: AbortSignal.timeout(2000),\n\t\t\t\tmode: \"no-cors\",\n\t\t\t});\n\t\t\treturn performance.now() - start;\n\t\t} catch {\n\t\t\treturn Infinity;\n\t\t}\n\t}\n\n\t/**\n\t * Clear the relay cache\n\t */\n\tclearCache(): void {\n\t\tthis.cache.clear();\n\t}\n\n\t/**\n\t * Get cached relays for a source address\n\t */\n\tgetCached(source: string): DRIADRelay[] | undefined {\n\t\treturn this.cache.get(source);\n\t}\n}\n\n// Export singleton instance\nexport const driadDiscovery = new DRIADDiscovery();\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\tTransportSubscription,\n\tTransportCapabilities,\n\tTransportState,\n\tQualityLevel,\n\tWindowMulticastCapabilities,\n\tWindowMulticastGateway,\n\tWindowMulticastSubscriptionState,\n} from \"./types.js\";\nimport { DRIADDiscovery } from \"./driad-discovery.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\tprivate driad = new DRIADDiscovery();\n\tprivate abr: AdaptiveBitrateController;\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\tconstructor() {\n\t\tthis.abr = new AdaptiveBitrateController();\n\t}\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: true, // DRIAD uses HTTP proxy, always available\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 proxy (specified or discovered via DRIAD)\n\t * 3. MOQ WebTransport (final fallback)\n\t */\n\tasync connect(config: TransportConfig): Promise<boolean> {\n\t\tconst generation = ++this.connectionGeneration;\n\t\tthis.subscription?.close();\n\t\tthis.subscription = 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// Initialize ABR with requested quality\n\t\tif (config.initialQuality) {\n\t\t\tthis.abr = new AdaptiveBitrateController(config.initialQuality);\n\t\t}\n\n\t\tthis.abr.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 proxy (with optional 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(config: TransportConfig, generation: number): 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: \"multicast\",\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.abr.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 proxy (with optional DRIAD discovery)\n\t */\n\tprivate async connectAmt(config: TransportConfig, generation: number): Promise<boolean> {\n\t\ttry {\n\t\t\tlet amtRelay = config.amtRelay;\n\n\t\t\t// Use DRIAD to discover relay if not specified (RFC 8777)\n\t\t\t// DRIAD discovers relays based on SOURCE address, not group\n\t\t\tif (!amtRelay && config.driadEnabled !== false && config.source) {\n\t\t\t\tconsole.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);\n\t\t\t\tconst relays = await this.driad.discoverRelays(config.source);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\t\t\t\tconst bestRelay = await this.driad.selectBestRelay(relays);\n\t\t\t\tif (generation !== this.connectionGeneration) return false;\n\n\t\t\t\tif (bestRelay) {\n\t\t\t\t\tamtRelay = bestRelay.host;\n\t\t\t\t\tthis.state.amtRelay = amtRelay;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!amtRelay) {\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\n\t\t\t// Use the WebSocket-based AMT proxy when a relay is known.\n\t\t\treturn await this.connectAmtProxy(config, amtRelay, generation);\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\tconst relayAuthority = config.amtRelayPort ? `${relayHost}:${config.amtRelayPort}` : relayHost;\n\t\t\tconst wsUrl = new URL(`wss://${relayAuthority}/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.abr.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\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 * Get current quality level\n\t */\n\tgetQuality(): QualityLevel {\n\t\treturn this.abr.getCurrentQuality();\n\t}\n\n\t/**\n\t * Force quality level\n\t */\n\tsetQuality(name: string) {\n\t\tthis.abr.setQuality(name);\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/**\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\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", "export interface FallbackTrack { name: string; type: \"video\" | \"audio\"; }\nexport interface FallbackGroup { readFrame(): Promise<Uint8Array | undefined>; close(): void; }\nexport interface FallbackSubscription { nextGroup(): Promise<FallbackGroup | undefined>; close(): void; }\nexport interface FallbackBroadcast { subscribe(name: string, priority: number): FallbackSubscription; }\nexport interface FallbackSessions { enterUnicastFallback(): void; }\nexport type FallbackGroupLike = FallbackGroup;\nexport type FallbackSubLike = FallbackSubscription;\nexport type FallbackBroadcastLike = FallbackBroadcast;\nexport interface FallbackSessionManagerLike extends FallbackSessions { hasActiveFallback(): boolean; }\n\nexport function runUnicastFallback(input: {\n\ttracks: FallbackTrack[];\n\tbroadcast: FallbackBroadcast;\n\tsessions: FallbackSessions;\n\tonPacket(packet: Uint8Array): void;\n\tpriority: number;\n\tsignal: AbortSignal;\n\tonError?(trackName: string, error: unknown): void;\n}): { done: Promise<void> } {\n\tinput.sessions.enterUnicastFallback();\n\tconst tracks = Array.from(new Map(input.tracks.map((track) => [track.name, track])).values());\n\tlet resolveAbort: (value: undefined) => void;\n\tconst aborted = new Promise<undefined>((resolve) => {\n\t\tresolveAbort = resolve;\n\t\tif (input.signal.aborted) resolve(undefined);\n\t});\n\tconst onAbort = () => resolveAbort(undefined);\n\tif (!input.signal.aborted) input.signal.addEventListener(\"abort\", onAbort, { once: true });\n\n\tconst drainers = tracks.map(async (track) => {\n\t\tconst subscription = input.broadcast.subscribe(track.name, input.priority);\n\t\ttry {\n\t\t\twhile (!input.signal.aborted) {\n\t\t\t\tconst nextGroup = subscription.nextGroup();\n\t\t\t\tconst group = await Promise.race([nextGroup, aborted]);\n\t\t\t\tif (!group && input.signal.aborted) void nextGroup.then((lateGroup) => lateGroup?.close());\n\t\t\t\tif (!group) break;\n\t\t\t\ttry {\n\t\t\t\t\twhile (!input.signal.aborted) {\n\t\t\t\t\t\tconst frame = await Promise.race([group.readFrame(), aborted]);\n\t\t\t\t\t\tif (!frame) break;\n\t\t\t\t\t\tinput.onPacket(frame);\n\t\t\t\t\t}\n\t\t\t\t} finally { group.close(); }\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!input.signal.aborted) {\n\t\t\t\tif (input.onError) input.onError(track.name, error);\n\t\t\t\telse console.warn(`[unicast-fallback] drainer error on ${track.name}:`, error);\n\t\t\t}\n\t\t} finally { subscription.close(); }\n\t});\n\tconst done = Promise.all(drainers).then(() => undefined).finally(() => {\n\t\tinput.signal.removeEventListener(\"abort\", onAbort);\n\t});\n\treturn { done };\n}\n", "import { AdaptiveBitrateController } from \"./abr-controller.js\";\nimport {\n\tisMulticastExplicitlyDisabled,\n\tisUsableMulticastEndpoint,\n\tisValidMulticastGroupAddress,\n\tmulticastEndpointKey,\n\tnormalizeMulticastEndpoint,\n\treadUrlMulticastConfig,\n\ttoWindowMulticastSubscribeConfig,\n} from \"./endpoint-config.js\";\nimport { MulticastOrchestrator } from \"./multicast-orchestrator.js\";\nimport {\n\tendpointToMulticastConfig,\n\thasMulticastCatalogBaseline,\n\thasUnicastMediaRenditions,\n\tisMulticastOnlyCatalog,\n\tisRecoverableMulticastSignalReadError,\n\tmergeUrlMulticastPolicy,\n\tnormalizeMulticastConfig,\n\treadPlayerMulticastConfig,\n\trequiresMulticastCapability,\n\tsameMulticastConfig,\n\tselectEndpointPacketIds,\n\tselectMulticastCandidate,\n\turlOverrideBlocksMulticastCandidate,\n\twithMulticastEndpoints,\n} from \"./multicast-catalog-policy.js\";\nimport {\n\tcreateMulticastSignalingReconciler,\n\tnormalizeCatalogEndpoints,\n} from \"./multicast-signaling-reconciler.js\";\nimport { MulticastSubscriptionPool } from \"./multicast-subscription-pool.js\";\nimport { endpointKey, selectMulticastTier } from \"./multicast-tier.js\";\nimport {\n\tMOQT_PUBLISHER_PRIORITY,\n\tMOQT_SUBSCRIBER_PRIORITY,\n\tmoqtPublisherPriority,\n\tmoqtSubscriberPriority,\n} from \"./moqt-priority.js\";\nimport { classifyStall, TransportStallDetector } from \"./stall-detector.js\";\nimport { makeSourceSwitchHandler, wireStarvationDetector } from \"./takeover.js\";\nimport { TransportManager } from \"./transport-manager.js\";\nimport { runUnicastFallback } from \"./unicast-fallback.js\";\n\nconst api = {\n\tAdaptiveBitrateController,\n\tclassifyStall,\n\tcreateMulticastSignalingReconciler,\n\tendpointToMulticastConfig,\n\thasMulticastCatalogBaseline,\n\thasUnicastMediaRenditions,\n\tisRecoverableMulticastSignalReadError,\n\tisMulticastOnlyCatalog,\n\tMulticastOrchestrator,\n\tMulticastSubscriptionPool,\n\tMOQT_PUBLISHER_PRIORITY,\n\tMOQT_SUBSCRIBER_PRIORITY,\n\tmoqtPublisherPriority,\n\tmoqtSubscriberPriority,\n\tTransportManager,\n\tTransportStallDetector,\n\tendpointKey,\n\tisMulticastExplicitlyDisabled,\n\tisUsableMulticastEndpoint,\n\tisValidMulticastGroupAddress,\n\tmakeSourceSwitchHandler,\n\tmergeUrlMulticastPolicy,\n\tmulticastEndpointKey,\n\tnormalizeCatalogEndpoints,\n\tnormalizeMulticastConfig,\n\tnormalizeMulticastEndpoint,\n\treadPlayerMulticastConfig,\n\treadUrlMulticastConfig,\n\ttoWindowMulticastSubscribeConfig,\n\trequiresMulticastCapability,\n\trunUnicastFallback,\n\tsameMulticastConfig,\n\tselectEndpointPacketIds,\n\tselectMulticastCandidate,\n\tselectMulticastTier,\n\turlOverrideBlocksMulticastCandidate,\n\twithMulticastEndpoints,\n\twireStarvationDetector,\n};\n\nObject.defineProperty(globalThis, \"__MmtTransport\", {\n\tconfigurable: true,\n\tenumerable: false,\n\twritable: false,\n\tvalue: Object.freeze(api),\n});\n\nexport default api;\n"],
5
5
  "mappings": "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",
6
6
  "names": ["MPU_HEADER_SIZE", "MPU_LENGTH_FIELD_SIZE", "MPU_HEADER_REMAINDER_SIZE", "MPU_HEADER_SIZE", "copyEndpoint", "positivePort", "entry"]
7
7
  }