@blockcast/mmt-container 2.0.1 → 2.1.0-main.36792a9d6614

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.
@@ -7457,11 +7457,33 @@ var AdaptiveBitrateController = class {
7457
7457
  };
7458
7458
 
7459
7459
  // node_modules/@blockcast/mmt-base/dist/mmtp.js
7460
+ var MMTP_HEADER_SIZE2 = 12;
7460
7461
  var MPU_HEADER_SIZE2 = 8;
7461
7462
  var MPU_LENGTH_FIELD_SIZE2 = 2;
7462
7463
  var MPU_HEADER_REMAINDER_SIZE2 = MPU_HEADER_SIZE2 - MPU_LENGTH_FIELD_SIZE2;
7463
7464
  var MMTP_SIGNALING_PACKET_ID2 = 0;
7464
7465
  var MMTP_PACKET_ID_MAX2 = 65535;
7466
+ function readMMTPPacketId2(buffer) {
7467
+ if (buffer.length < MMTP_HEADER_SIZE2)
7468
+ return null;
7469
+ const flags = buffer[0];
7470
+ if ((flags >> 6 & 3) !== 0)
7471
+ return null;
7472
+ let offset = MMTP_HEADER_SIZE2;
7473
+ if ((flags & 32) !== 0) {
7474
+ if (offset + 4 > buffer.length)
7475
+ return null;
7476
+ offset += 4;
7477
+ }
7478
+ if ((flags & 2) !== 0) {
7479
+ if (offset + 4 > buffer.length)
7480
+ return null;
7481
+ const length = buffer[offset + 2] << 8 | buffer[offset + 3];
7482
+ if (offset + 4 + length > buffer.length)
7483
+ return null;
7484
+ }
7485
+ return buffer[2] << 8 | buffer[3];
7486
+ }
7465
7487
 
7466
7488
  // node_modules/@blockcast/mmt-base/dist/net-constants.js
7467
7489
  var DEFAULT_AMT_RELAY_PORT2 = 2268;
@@ -8447,6 +8469,205 @@ var TransportManager = class {
8447
8469
  };
8448
8470
  var transportManager = new TransportManager();
8449
8471
 
8472
+ // node_modules/@blockcast/mmt-transport/dist/multicast-receiver-gateway.js
8473
+ var REGISTRY = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/native-provider-registry/v1");
8474
+ var USER_AGENT_VERIFIED = /* @__PURE__ */ Symbol.for("@blockcast/mmt-transport/user-agent-verified/v1");
8475
+ var INTEGRITY_SCHEME = "manifest-ed25519-2";
8476
+ var MulticastError = class extends Error {
8477
+ code;
8478
+ constructor(code, message, cause) {
8479
+ super(message, cause === void 0 ? void 0 : { cause });
8480
+ this.name = "MulticastError";
8481
+ this.code = code;
8482
+ }
8483
+ };
8484
+ function datagramsOf(chunk) {
8485
+ const list = Array.isArray(chunk) ? chunk : chunk?.datagrams ?? [];
8486
+ const out = [];
8487
+ for (const d of list) {
8488
+ const data = d instanceof Uint8Array || d instanceof ArrayBuffer ? d : d?.data;
8489
+ if (data instanceof Uint8Array)
8490
+ out.push(data);
8491
+ else if (data instanceof ArrayBuffer)
8492
+ out.push(new Uint8Array(data));
8493
+ }
8494
+ return out;
8495
+ }
8496
+ function reasonOf(value) {
8497
+ return typeof value === "object" && value !== null && "reason" in value ? value.reason : value;
8498
+ }
8499
+ function registry() {
8500
+ const g = globalThis;
8501
+ if (g[REGISTRY] === void 0) {
8502
+ const providers = /* @__PURE__ */ new WeakSet();
8503
+ const byScope = /* @__PURE__ */ new WeakMap();
8504
+ Object.defineProperty(g, REGISTRY, {
8505
+ value: Object.freeze({
8506
+ isProvider: (x) => typeof x === "object" && x !== null && providers.has(x),
8507
+ providerFor: (scope) => {
8508
+ let provider = byScope.get(scope);
8509
+ if (provider === void 0) {
8510
+ provider = createProvider(scope);
8511
+ providers.add(provider);
8512
+ byScope.set(scope, provider);
8513
+ }
8514
+ return provider;
8515
+ }
8516
+ }),
8517
+ writable: false,
8518
+ configurable: false,
8519
+ enumerable: false
8520
+ });
8521
+ }
8522
+ return g[REGISTRY];
8523
+ }
8524
+ function resolveMulticastGateway(scope = globalThis) {
8525
+ const s = scope;
8526
+ if (typeof s.MulticastReceiver === "function")
8527
+ return registry().providerFor(scope);
8528
+ if (typeof s.multicast?.subscribe === "function")
8529
+ return s.multicast;
8530
+ return void 0;
8531
+ }
8532
+ function isUserAgentVerifiedGateway(gateway) {
8533
+ return registry().isProvider(gateway);
8534
+ }
8535
+ function userAgentVerifiedIngress(ingress, gateway) {
8536
+ if (isUserAgentVerifiedGateway(gateway))
8537
+ return { ...ingress, admittedBy: USER_AGENT_VERIFIED };
8538
+ if (!("admittedBy" in ingress))
8539
+ return ingress;
8540
+ const { admittedBy: _, ...rest } = ingress;
8541
+ return rest;
8542
+ }
8543
+ function unsupported(message, cause) {
8544
+ return new MulticastError("capability-unsupported", message, cause);
8545
+ }
8546
+ function createProvider(scope) {
8547
+ const Receiver = scope.MulticastReceiver;
8548
+ let gate = null;
8549
+ return {
8550
+ capabilities: () => Promise.resolve({ consumeModes: ["bytes"] }),
8551
+ subscribe: async (config) => {
8552
+ const c = config;
8553
+ if (c.consume !== "bytes")
8554
+ throw unsupported(`consume mode ${String(c.consume)} is not supported`);
8555
+ const origin = c.origin ?? "ip-multicast";
8556
+ if (origin !== "ip-multicast" && !(origin === "amt" && c.discovery === "driad")) {
8557
+ throw unsupported(`origin ${String(origin)} is not supported by MulticastReceiver`);
8558
+ }
8559
+ if (c.relay !== void 0)
8560
+ throw unsupported("MulticastReceiver picks its own AMT relay");
8561
+ if (!c.source)
8562
+ throw unsupported("MulticastReceiver needs an SSM source");
8563
+ if (c.senderAddress !== void 0 && c.senderAddress !== c.source) {
8564
+ throw unsupported("MulticastReceiver filters by source only");
8565
+ }
8566
+ const options = { sourceAddress: c.source, groupAddress: c.group, localPort: c.port };
8567
+ if (gate !== null)
8568
+ await gate;
8569
+ let rx;
8570
+ try {
8571
+ rx = new Receiver(options);
8572
+ } catch (error) {
8573
+ throw unsupported("MulticastReceiver refused the request", error);
8574
+ }
8575
+ const opened = rx.opened;
8576
+ if (gate === null)
8577
+ gate = opened.then(() => void 0, () => void 0);
8578
+ return openSubscription(rx, opened, options, c.packetIds);
8579
+ }
8580
+ };
8581
+ }
8582
+ async function openSubscription(rx, openedPromise, options, packetIds) {
8583
+ let opened;
8584
+ try {
8585
+ opened = await openedPromise;
8586
+ } catch (error) {
8587
+ throw new MulticastError("subscription-failed", "MulticastReceiver did not open", error);
8588
+ }
8589
+ if (rx.integrityScheme !== INTEGRITY_SCHEME) {
8590
+ await rx.close();
8591
+ throw unsupported(`integrity scheme ${String(rx.integrityScheme)} is not ${INTEGRITY_SCHEME}`);
8592
+ }
8593
+ const events = new EventTarget();
8594
+ let state = "connected";
8595
+ let filter = packetIds?.length ? new Set(packetIds) : null;
8596
+ let closing = false;
8597
+ const setState = (next) => {
8598
+ if (state === next)
8599
+ return;
8600
+ state = next;
8601
+ events.dispatchEvent(new CustomEvent("state", { detail: next }));
8602
+ };
8603
+ const sourceAddress = options.sourceAddress;
8604
+ const subscription = {
8605
+ id: `${options.sourceAddress}|${options.groupAddress}|${options.localPort}`,
8606
+ get state() {
8607
+ return state;
8608
+ },
8609
+ delivery: "datagram",
8610
+ eventTypes: Object.freeze(["bytes", "packets", "state", "error"]),
8611
+ setPacketIds(ids) {
8612
+ filter = ids.length ? new Set(ids) : null;
8613
+ },
8614
+ addEventListener: events.addEventListener.bind(events),
8615
+ removeEventListener: events.removeEventListener.bind(events),
8616
+ async unsubscribe() {
8617
+ closing = true;
8618
+ await rx.close();
8619
+ setState("closed");
8620
+ }
8621
+ };
8622
+ void (async () => {
8623
+ const reader = opened.readable.getReader();
8624
+ try {
8625
+ for (; ; ) {
8626
+ const { value, done } = await reader.read();
8627
+ if (done)
8628
+ break;
8629
+ const packets = [];
8630
+ for (const data of datagramsOf(value)) {
8631
+ if (filter !== null) {
8632
+ const id = readMMTPPacketId2(data);
8633
+ if (id === null || !filter.has(id))
8634
+ continue;
8635
+ }
8636
+ const copy = data.slice().buffer;
8637
+ packets.push(copy);
8638
+ events.dispatchEvent(Object.assign(new CustomEvent("bytes", { detail: copy }), { sourceAddress }));
8639
+ }
8640
+ if (packets.length > 0)
8641
+ events.dispatchEvent(new CustomEvent("packets", { detail: packets }));
8642
+ }
8643
+ } catch {
8644
+ }
8645
+ })();
8646
+ void rx.closed.then((value) => {
8647
+ const reason = reasonOf(value);
8648
+ if (!closing) {
8649
+ if (reason === "consent-withdrawn") {
8650
+ events.dispatchEvent(new CustomEvent("error", {
8651
+ detail: new MulticastError("origin-not-trusted", "multicast consent withdrawn")
8652
+ }));
8653
+ } else if (reason === "oversubscribed" || reason === "network") {
8654
+ events.dispatchEvent(new CustomEvent("error", {
8655
+ detail: new MulticastError("subscription-failed", `MulticastReceiver closed: ${reason}`)
8656
+ }));
8657
+ }
8658
+ }
8659
+ setState("closed");
8660
+ }, (error) => {
8661
+ if (!closing) {
8662
+ events.dispatchEvent(new CustomEvent("error", {
8663
+ detail: new MulticastError("subscription-failed", "MulticastReceiver failed", error)
8664
+ }));
8665
+ }
8666
+ setState("closed");
8667
+ });
8668
+ return subscription;
8669
+ }
8670
+
8450
8671
  // node_modules/@blockcast/mmt-transport/dist/moqt-priority.js
8451
8672
  var MAX_MOQT_PRIORITY = 255;
8452
8673
  function moqtPriority(value, field) {
@@ -8733,9 +8954,9 @@ function validateTemplatePair(mediaUriTemplate, mediaObjectTemplate, path) {
8733
8954
  }
8734
8955
  function validateMmtpDashTemplate(template, path) {
8735
8956
  const variables = [...template.matchAll(TEMPLATE_VARIABLE)].map((match) => match[1]);
8736
- const unsupported = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
8737
- if (unsupported !== void 0) {
8738
- throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported}$`);
8957
+ const unsupported2 = variables.find((variable) => variable !== "Number" && variable !== "RepresentationID");
8958
+ if (unsupported2 !== void 0) {
8959
+ throw new Error(`${path} contains unsupported MMTP-backed DASH template variable $${unsupported2}$`);
8739
8960
  }
8740
8961
  if (variables.filter((variable) => variable === "Number").length !== 1) {
8741
8962
  throw new Error(`${path} must contain exactly one $Number$ for MMTP-backed DASH`);
@@ -17098,17 +17319,17 @@ var FECManager = class _FECManager {
17098
17319
 
17099
17320
  // src/mfu-reassembly-policy.ts
17100
17321
  function validateReliableMoqOptionShape(value) {
17101
- const unsupported = Object.keys(value).filter(
17322
+ const unsupported2 = Object.keys(value).filter(
17102
17323
  (key) => key !== "transport" && key !== "startsAtRandomAccessGroup"
17103
17324
  );
17104
- if (unsupported.length > 0) {
17105
- if (unsupported.includes("maxRacingGroups")) {
17325
+ if (unsupported2.length > 0) {
17326
+ if (unsupported2.includes("maxRacingGroups")) {
17106
17327
  throw new Error(
17107
17328
  "[MFU reassembly] maxRacingGroups was removed; reliable MoQ completion is transport-owned"
17108
17329
  );
17109
17330
  }
17110
17331
  throw new Error(
17111
- `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported.join(", ")}`
17332
+ `[MFU reassembly] unsupported reliable MoQ option(s): ${unsupported2.join(", ")}`
17112
17333
  );
17113
17334
  }
17114
17335
  const startsAtRandomAccessGroup = value["startsAtRandomAccessGroup"];
@@ -17152,12 +17373,12 @@ function assertValidMfuReassemblyPolicy(policy) {
17152
17373
  return;
17153
17374
  }
17154
17375
  if (value["transport"] === "datagram") {
17155
- const unsupported = Object.keys(value).filter(
17376
+ const unsupported2 = Object.keys(value).filter(
17156
17377
  (key) => key !== "transport" && key !== "deadlineMs"
17157
17378
  );
17158
- if (unsupported.length > 0) {
17379
+ if (unsupported2.length > 0) {
17159
17380
  throw new Error(
17160
- `[MFU reassembly] unsupported datagram option(s): ${unsupported.join(", ")}`
17381
+ `[MFU reassembly] unsupported datagram option(s): ${unsupported2.join(", ")}`
17161
17382
  );
17162
17383
  }
17163
17384
  assertValidDatagramDeadline(value["deadlineMs"]);
@@ -22431,6 +22652,14 @@ function isRouterDebugEnabled() {
22431
22652
  function debugRouter(message) {
22432
22653
  if (isRouterDebugEnabled()) console.debug(message);
22433
22654
  }
22655
+ var KEYFRAME_OVERLAY_EXTENSION_TYPE = 35585;
22656
+ function keyframeOverlaySsId(header) {
22657
+ const ext = header.extension;
22658
+ if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22659
+ return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22660
+ }
22661
+ var OVERLAY_OFFSET_MPUS = 16;
22662
+ var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22434
22663
  function assertStreamIdentity(role, stream) {
22435
22664
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
22436
22665
  throw new Error(
@@ -22647,6 +22876,26 @@ var MmtpRouter = class {
22647
22876
  #packetIdRoles = /* @__PURE__ */ new Map();
22648
22877
  /** Explicit catalog ownership for dedicated repair-track packet IDs. */
22649
22878
  #repairOwnersByPacketId = /* @__PURE__ */ new Map();
22879
+ /** Keyframe-overlay repair PID → the source its FEC instance protects
22880
+ * (draft-ramadan-mmt-fec-02 7.4.1). Never an owner in
22881
+ * #repairOwnersByPacketId: the overlay is decoded as its own instance. */
22882
+ #overlayLegs = /* @__PURE__ */ new Map();
22883
+ /** Keyframe-overlay repair track names, which name a leg too: a catalog
22884
+ * may state no PID for it (learned at subscribe time). */
22885
+ #overlayRepairNames = /* @__PURE__ */ new Map();
22886
+ /** Installed overlay instances, by overlay repair PID (opt-in only). */
22887
+ #overlays = /* @__PURE__ */ new Map();
22888
+ #overlayRecoveredObservers = /* @__PURE__ */ new Set();
22889
+ /** Streams whose recovered sequence the base path ever re-stamped. */
22890
+ #restampedStreams = /* @__PURE__ */ new Set();
22891
+ #overlayStats = {
22892
+ repairDropped: 0,
22893
+ ownerRefused: 0,
22894
+ recovered: 0,
22895
+ restarts: 0,
22896
+ unsequencedDropped: 0,
22897
+ protectedDropped: 0
22898
+ };
22650
22899
  /** Callback fired when a delta-track FEC block completes or fails. */
22651
22900
  #onDeltaBlockCompleteCb = null;
22652
22901
  // Debug counters
@@ -22843,6 +23092,7 @@ var MmtpRouter = class {
22843
23092
  this.#fecManager.onSourceDiscontinuity = (packetId, catalogTrackName) => {
22844
23093
  this.#parser.markFecDiscontinuity(packetId, catalogTrackName);
22845
23094
  this.#requestFecPoke(packetId, catalogTrackName);
23095
+ this.resetKeyframeOverlays(packetId, catalogTrackName);
22846
23096
  };
22847
23097
  this.#fecManager.onBlockComplete = (sbn, packetId, catalogTrackName) => {
22848
23098
  this.#mseAssemblerFor(packetId, catalogTrackName)?.completeBlock(sbn);
@@ -23165,13 +23415,287 @@ var MmtpRouter = class {
23165
23415
  /** Replace catalog-derived repair ownership before a catalog reconfiguration. */
23166
23416
  clearRepairPacketIdOwners() {
23167
23417
  this.#repairOwnersByPacketId.clear();
23418
+ this.#overlayLegs.clear();
23419
+ this.#overlayRepairNames.clear();
23168
23420
  }
23169
- /** Bind one dedicated repair-track PID to its authoritative media PID. */
23421
+ /** Bind one dedicated repair-track PID to its authoritative media PID. A
23422
+ * known keyframe-overlay PID is refused, and counted: its symbols belong to
23423
+ * the overlay instance, never to the source's base decoder (mmt-02 7.4.4). */
23170
23424
  setRepairPacketIdOwner(repairPacketId, ownerPacketId) {
23425
+ if (this.#overlayLegs.has(repairPacketId)) {
23426
+ this.#overlayStats.ownerRefused++;
23427
+ return;
23428
+ }
23171
23429
  const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? /* @__PURE__ */ new Set();
23172
23430
  owners.add(ownerPacketId);
23173
23431
  this.#repairOwnersByPacketId.set(repairPacketId, owners);
23174
23432
  }
23433
+ /**
23434
+ * Record a keyframe-overlay repair leg (draft-ramadan-mmt-fec-02 7.4): its
23435
+ * PID, the source it protects, and its repair track name. Either one names
23436
+ * the leg, so a catalog that states no PID (learned at subscribe time) is
23437
+ * covered by the name. Overlay repair is then never resolved to a base
23438
+ * owner, on multicast or MoQ. Cleared with the catalog owners
23439
+ * (`clearRepairPacketIdOwners`).
23440
+ */
23441
+ setKeyframeOverlayLeg(repairPacketId, leg) {
23442
+ if (leg.repairTrackName !== void 0) this.#overlayRepairNames.set(leg.repairTrackName, { ...leg });
23443
+ if (repairPacketId === void 0) return;
23444
+ this.#repairOwnersByPacketId.delete(repairPacketId);
23445
+ this.#overlayLegs.set(repairPacketId, { ...leg });
23446
+ }
23447
+ /** The keyframe-overlay leg registered on `repairPacketId`, if any. */
23448
+ keyframeOverlayLeg(repairPacketId) {
23449
+ return this.#overlayLegs.get(repairPacketId);
23450
+ }
23451
+ /**
23452
+ * Overlay counters: repair dropped for want of an installed instance, base
23453
+ * owners refused on an overlay PID, overlay blocks recovered, overlay
23454
+ * restarts, and recovered packets dropped for want of a wire sequence or
23455
+ * for a MoQ symbol on a protected flow (multicast-02 7.2.7).
23456
+ */
23457
+ get keyframeOverlayStats() {
23458
+ return { ...this.#overlayStats };
23459
+ }
23460
+ #isOverlayRepair(repairPacketId, repairTrackName) {
23461
+ return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23462
+ }
23463
+ /**
23464
+ * Install the keyframe overlay decoded on `fec` for a known overlay leg
23465
+ * (draft-ramadan-mmt-fec-02 7.4.4: decoded independently of the base). `fec`
23466
+ * holds the overlay stream under `repairPacketId` and `streamName`. Replaces
23467
+ * an earlier install on the same leg; the returned disposer removes only
23468
+ * this one.
23469
+ */
23470
+ installKeyframeOverlay(repairPacketId, fec, k, streamName) {
23471
+ const leg = this.#overlayLegs.get(repairPacketId);
23472
+ if (!leg) throw new Error(`[MmtpRouter] packetId=${repairPacketId} is no known keyframe-overlay leg`);
23473
+ const inst = {
23474
+ repairPacketId,
23475
+ streamName,
23476
+ leg: { ...leg },
23477
+ fec,
23478
+ k,
23479
+ offsets: /* @__PURE__ */ new Map(),
23480
+ moqBlocks: /* @__PURE__ */ new Set()
23481
+ };
23482
+ this.#overlays.set(repairPacketId, inst);
23483
+ return () => {
23484
+ if (this.#overlays.get(repairPacketId) === inst) this.#overlays.delete(repairPacketId);
23485
+ };
23486
+ }
23487
+ /**
23488
+ * Observe each base block whose RAP MFU fragments a keyframe-overlay
23489
+ * recovery supplied, with the keyframe's FT=0 and FT=1 held
23490
+ * (fec-multicast-00 §6.6). The block is named by the recovered packets'
23491
+ * restored wire sequence through the base SS_ID offset, never by an overlay
23492
+ * coordinate. Called synchronously from the recovered-block route.
23493
+ */
23494
+ addKeyframeOverlayRecoveredObserver(observer) {
23495
+ this.#overlayRecoveredObservers.add(observer);
23496
+ return () => {
23497
+ this.#overlayRecoveredObservers.delete(observer);
23498
+ };
23499
+ }
23500
+ /** Whether an overlay is installed on `repairPacketId`. */
23501
+ hasKeyframeOverlay(repairPacketId) {
23502
+ return this.#overlays.has(repairPacketId);
23503
+ }
23504
+ /**
23505
+ * Start a new epoch of every overlay protecting `sourcePacketId` (its
23506
+ * source restarted): empty overlay stream, no coverage, no offsets.
23507
+ */
23508
+ resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23509
+ for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23510
+ }
23511
+ #resetOverlay(inst) {
23512
+ inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23513
+ inst.last = void 0;
23514
+ inst.offsets.clear();
23515
+ inst.moqBlocks.clear();
23516
+ }
23517
+ #overlaysForSource(sourcePacketId, catalogTrackName) {
23518
+ return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
23519
+ }
23520
+ /**
23521
+ * The overlay instance a repair packet belongs to, by the track it arrived
23522
+ * on (7.4.2: its PID or its repair track name); null for overlay repair
23523
+ * with no instance installed; undefined for repair of no overlay.
23524
+ */
23525
+ #overlayRepairTarget(repairPacketId, repairTrackName) {
23526
+ const inst = this.#overlays.get(repairPacketId) ?? (repairTrackName === void 0 ? void 0 : [...this.#overlays.values()].find(({ leg }) => leg.repairTrackName === repairTrackName));
23527
+ if (inst) return inst;
23528
+ return this.#isOverlayRepair(repairPacketId, repairTrackName) ? null : void 0;
23529
+ }
23530
+ /** Overlay repair goes to its own instance, never to a base owner, locator
23531
+ * or decoder; with none installed it is dropped and counted. */
23532
+ #routeOverlayRepair(inst, repairPacketId, packet, header, source) {
23533
+ const repair = inst ? parseMoqRepairFrameWithHeader(packet, header) : null;
23534
+ if (!inst || !repair) {
23535
+ this.#overlayStats.repairDropped++;
23536
+ debugRouter(`[MmtpRouter] Dropping keyframe-overlay repair pid=${repairPacketId}: no overlay instance`);
23537
+ return;
23538
+ }
23539
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23540
+ if (!fec.admitRepairBlockLength(pid, repair.ssbLength, repair.rsbLength, repair.rsId)) return;
23541
+ if (!fec.admitRepairSymbolSize(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData.byteLength)) return;
23542
+ const placement = fec.placeIsoRepair(pid, name, repair.ssStart, repair.ssbLength);
23543
+ if (placement.status !== "placed") return;
23544
+ const blockStart = placement.block.start;
23545
+ if (placement.span !== void 0) this.#replayOverlaySources(inst, placement.span.block.start, placement.span.placed);
23546
+ this.#replayOverlaySources(inst, blockStart, placement.placed);
23547
+ if (source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
23548
+ const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
23549
+ const recovered = fec.feedRepairSymbol(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData, name);
23550
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
23551
+ }
23552
+ #replayOverlaySources(inst, blockStart, placed) {
23553
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23554
+ for (const { ssId, payload } of placed) {
23555
+ if (payload.source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
23556
+ if (payload.oversized) continue;
23557
+ const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
23558
+ const recovered = fec.feedSourceSymbol(pid, ssId, payload.symbolData, name);
23559
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
23560
+ }
23561
+ }
23562
+ /**
23563
+ * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23564
+ * every overlay installed for its source, at that SS_ID, with the bytes the
23565
+ * base protects: the packet minus its 4-byte SS_ID trailer, extension
23566
+ * included (7.1, 7.4.3).
23567
+ */
23568
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
23569
+ if (this.#overlays.size === 0) return;
23570
+ const ssId = keyframeOverlaySsId(header);
23571
+ if (ssId === void 0) return;
23572
+ for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23573
+ this.#noteOverlaySsId(inst, ssId, sequenceNumber);
23574
+ const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23575
+ if (identity !== null) {
23576
+ inst.offsets.delete(identity.mpuSequenceNumber);
23577
+ inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
23578
+ if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23579
+ }
23580
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23581
+ const symbolData = packet.subarray(0, packet.length - 4);
23582
+ const oversized = !fec.isoSourceFits(pid, symbolData.length);
23583
+ const coords = fec.locateIsoSource(pid, name, ssId);
23584
+ if (coords === null) {
23585
+ fec.holdIsoSource(pid, name, ssId, {
23586
+ symbolData,
23587
+ source,
23588
+ oversized,
23589
+ evidence: sourceSymbolEvidence(packet, header, rapFlag, sequenceNumber)
23590
+ });
23591
+ continue;
23592
+ }
23593
+ if (source === "moq") this.#noteOverlayMoqBlock(inst, coords.sbn);
23594
+ if (oversized) continue;
23595
+ const alreadyCompleted = fec.isBlockCompleted(coords.sbn, pid, name);
23596
+ const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23597
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23598
+ }
23599
+ }
23600
+ #noteOverlayMoqBlock(inst, blockStart) {
23601
+ inst.moqBlocks.add(blockStart);
23602
+ const newest = inst.last?.ssId;
23603
+ if (newest === void 0) return;
23604
+ for (const start of inst.moqBlocks) {
23605
+ if ((newest - start | 0) > OVERLAY_MOQ_HORIZON_BLOCKS * inst.k) inst.moqBlocks.delete(start);
23606
+ }
23607
+ }
23608
+ /**
23609
+ * An overlay SS_ID inconsistent with the source's packet_sequence_number
23610
+ * advance since the last one is an overlay restart (an FFmpeg muxer start,
23611
+ * a bridge swap that keeps psn continuous): it moved the other way, or
23612
+ * further than the psn advance plus K_overlay. Reordering keeps the two
23613
+ * consistent and is no restart.
23614
+ */
23615
+ #noteOverlaySsId(inst, ssId, psn) {
23616
+ const last = inst.last;
23617
+ if (last !== void 0) {
23618
+ const dSs = ssId - last.ssId | 0;
23619
+ const dPsn = psn - last.psn | 0;
23620
+ const consistent = dSs === 0 ? dPsn === 0 : Math.sign(dSs) === Math.sign(dPsn) && Math.abs(dSs) <= Math.abs(dPsn) + inst.k;
23621
+ if (!consistent) {
23622
+ this.#overlayStats.restarts++;
23623
+ this.#resetOverlay(inst);
23624
+ } else if (dPsn < 0) {
23625
+ return;
23626
+ }
23627
+ }
23628
+ inst.last = { ssId, psn };
23629
+ }
23630
+ /**
23631
+ * Route an overlay-recovered block's packets as source media of the
23632
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23633
+ * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23634
+ * base re-stamp, no block advance. Each packet's wire sequence is restored
23635
+ * from its keyframe's own offset; without one, a stream the base never
23636
+ * re-stamped keeps the header's sequence and any other drops the packet.
23637
+ */
23638
+ #routeOverlayRecovered(inst, blockStart, recovered) {
23639
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23640
+ fec.markBlockCompleted(blockStart, pid, name);
23641
+ const sourcePacketId = inst.leg.sourcePacketId;
23642
+ const sourceName = inst.leg.sourceCatalogTrackName;
23643
+ if (this.#classifyPacketId(sourcePacketId) === null) return;
23644
+ const receiver = this.#provenanceFor(sourcePacketId, sourceName);
23645
+ if (receiver !== void 0 && inst.moqBlocks.has(blockStart)) {
23646
+ this.#overlayStats.protectedDropped++;
23647
+ return;
23648
+ }
23649
+ const blockProfile = fec.blockProfileForPacket(pid);
23650
+ const symbolSize = fec.symbolSizeForPacket(pid);
23651
+ if (blockProfile === void 0 || symbolSize === void 0 || !(symbolSize > 0)) return;
23652
+ const result = processFecBlock(recovered, { symbolSize, ownerPacketId: sourcePacketId, blockProfile });
23653
+ const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23654
+ const restamped = this.#restampedStreams.has(streamKey);
23655
+ const restored = /* @__PURE__ */ new Set();
23656
+ const symbols = result.symbols.flatMap((sym) => {
23657
+ const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23658
+ if (offset !== void 0) {
23659
+ const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
23660
+ restored.add(packetSequenceNumber);
23661
+ return [{ ...sym, packetSequenceNumber }];
23662
+ }
23663
+ if (!restamped) return [sym];
23664
+ this.#overlayStats.unsequencedDropped++;
23665
+ return [];
23666
+ });
23667
+ this.#overlayStats.recovered++;
23668
+ this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23669
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
23670
+ });
23671
+ this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
23672
+ }
23673
+ /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23674
+ * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
23675
+ * packet whose wire sequence was not restored names no base block. */
23676
+ #noteOverlayRecoveredRaps(packetId, catalogTrackName, streamKey, symbols) {
23677
+ const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(streamKey);
23678
+ if (ssIdSeqOffset === void 0 || this.#overlayRecoveredObservers.size === 0) return;
23679
+ const withMeta = (type) => new Set(symbols.filter((sym) => sym.fragmentType === type).map((sym) => sym.fragment.mpuSequenceNumber));
23680
+ const ft0 = withMeta(0);
23681
+ const ft1 = withMeta(1);
23682
+ const rapMfu = (fi) => new Set(symbols.filter((sym) => sym.fragmentType === 2 && sym.fragment.rapFlag && sym.fragment.fragmentationIndicator === fi).map((sym) => sym.fragment.mpuSequenceNumber));
23683
+ const single = rapMfu(0);
23684
+ const first = rapMfu(1);
23685
+ const last = rapMfu(3);
23686
+ const blocks = /* @__PURE__ */ new Set();
23687
+ for (const sym of symbols) {
23688
+ const mpu = sym.fragment.mpuSequenceNumber;
23689
+ if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
23690
+ if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
23691
+ const ssId = sym.packetSequenceNumber + ssIdSeqOffset >>> 0;
23692
+ const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
23693
+ if (sbn !== void 0) blocks.add(sbn);
23694
+ }
23695
+ for (const sbn of blocks) {
23696
+ for (const observer of this.#overlayRecoveredObservers) observer(packetId, catalogTrackName, sbn);
23697
+ }
23698
+ }
23175
23699
  /**
23176
23700
  * Register callback fired when a delta-track FEC block completes or fails.
23177
23701
  *
@@ -23276,10 +23800,11 @@ var MmtpRouter = class {
23276
23800
  if (!entry) return;
23277
23801
  entry.receiver.release();
23278
23802
  entry.protected = false;
23803
+ entry.userAgent = false;
23279
23804
  this.#assistBlocks.clear();
23280
23805
  this.#assistFailures.clear();
23281
23806
  }
23282
- /** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
23807
+ /** Packets of a track a protected flow lists, dropped unread: a MoQ copy (duplicate, overflow, or listed by more than one flow) or a multicast source datagram that names no flow, of a track a user-agent-admitted flow lists. */
23283
23808
  get unadmittedListedDropCount() {
23284
23809
  return this.#unadmittedListedDrops;
23285
23810
  }
@@ -23320,6 +23845,13 @@ var MmtpRouter = class {
23320
23845
  }
23321
23846
  return false;
23322
23847
  }
23848
+ /** Whether a protected flow the user agent admitted lists `packetId`. */
23849
+ #userAgentGoverns(packetId) {
23850
+ for (const entry of this.#provenance.values()) {
23851
+ if (entry.protected && entry.userAgent && ingressHasPacketId(entry.ingress, packetId)) return true;
23852
+ }
23853
+ return false;
23854
+ }
23323
23855
  /**
23324
23856
  * A MoQ repair symbol no protected flow lists, owned by a source track one
23325
23857
  * does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
@@ -23410,7 +23942,7 @@ var MmtpRouter = class {
23410
23942
  fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
23411
23943
  fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
23412
23944
  fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
23413
- })) {
23945
+ }, true)) {
23414
23946
  this.#unlistedAssistedRecoveries++;
23415
23947
  this.#assistBlocks.delete(key);
23416
23948
  return;
@@ -23595,6 +24127,11 @@ var MmtpRouter = class {
23595
24127
  return;
23596
24128
  }
23597
24129
  const repairPacketId = header.packetId;
24130
+ const overlay = this.#overlayRepairTarget(repairPacketId, repairTrackName);
24131
+ if (overlay !== void 0) {
24132
+ this.#routeOverlayRepair(overlay, repairPacketId, packet, header, "moq");
24133
+ return;
24134
+ }
23598
24135
  const hintedOwner = ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null ? ownerPacketIdHint : null;
23599
24136
  const ownerPacketId = hintedOwner ?? (this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
23600
24137
  if (ownerPacketId === null) {
@@ -23884,6 +24421,8 @@ var MmtpRouter = class {
23884
24421
  }
23885
24422
  }
23886
24423
  this.#parser.resetTrack(packetId, catalogTrackName);
24424
+ this.resetKeyframeOverlays(packetId, catalogTrackName);
24425
+ this.#restampedStreams.delete(this.#mediaStreamKey(packetId, catalogTrackName));
23887
24426
  const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
23888
24427
  for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
23889
24428
  for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
@@ -23970,6 +24509,17 @@ var MmtpRouter = class {
23970
24509
  this.#packetIdRoles.delete(packetId);
23971
24510
  this.#catalogTrackNamesByPacketId.delete(packetId);
23972
24511
  this.#repairOwnersByPacketId.delete(packetId);
24512
+ this.#overlayLegs.delete(packetId);
24513
+ for (const [repairPacketId, leg] of [...this.#overlayLegs]) {
24514
+ if (leg.sourcePacketId === packetId) this.#overlayLegs.delete(repairPacketId);
24515
+ }
24516
+ for (const [name, leg] of [...this.#overlayRepairNames]) {
24517
+ if (leg.sourcePacketId === packetId) this.#overlayRepairNames.delete(name);
24518
+ }
24519
+ this.#overlays.delete(packetId);
24520
+ for (const [repairPacketId, inst] of [...this.#overlays]) {
24521
+ if (inst.leg.sourcePacketId === packetId) this.#overlays.delete(repairPacketId);
24522
+ }
23973
24523
  delete this.#pidCounts[packetId];
23974
24524
  delete this.#repairPidCounts[packetId];
23975
24525
  delete this.#moqRepairPidCounts[packetId];
@@ -24208,6 +24758,9 @@ var MmtpRouter = class {
24208
24758
  this.#parserDiagnostics.clear();
24209
24759
  this.#catalogTrackNamesByPacketId.clear();
24210
24760
  this.#repairOwnersByPacketId.clear();
24761
+ this.#overlayLegs.clear();
24762
+ this.#overlayRepairNames.clear();
24763
+ this.#overlays.clear();
24211
24764
  for (const { receiver } of this.#provenance.values()) receiver.release();
24212
24765
  this.#provenance.clear();
24213
24766
  this.#provenanceStreamFlow.clear();
@@ -24545,6 +25098,12 @@ var MmtpRouter = class {
24545
25098
  debugRouter(`[MmtpRouter] Dropping multicast packet pid=${info.packetId}: flow ${ingress.flow} has no receiver`);
24546
25099
  return;
24547
25100
  }
25101
+ if (ingress.admittedBy === USER_AGENT_VERIFIED) {
25102
+ this.#noteFlowAdmitted(ingress.flow);
25103
+ protectedFlow.userAgent = true;
25104
+ this.#processMMTP(packet, source, true, ingress, overlapCopy);
25105
+ return;
25106
+ }
24548
25107
  const { verdict } = protectedFlow.receiver.onDatagram(packet, "multicast");
24549
25108
  if (verdict === "admitted") {
24550
25109
  this.#noteFlowAdmitted(ingress.flow);
@@ -24562,7 +25121,17 @@ var MmtpRouter = class {
24562
25121
  return;
24563
25122
  }
24564
25123
  }
25124
+ const flowless = source === "multicast" && !manifestAdmitted && ingress?.flow === void 0;
25125
+ if (flowless && !info.isRepair && this.#userAgentGoverns(info.packetId)) {
25126
+ this.#unadmittedListedDrops++;
25127
+ return;
25128
+ }
24565
25129
  if (info.isRepair) {
25130
+ const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
25131
+ if (overlay !== void 0) {
25132
+ this.#routeOverlayRepair(overlay, info.packetId, packet, parsed.header, source);
25133
+ return;
25134
+ }
24566
25135
  const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
24567
25136
  if (ownerPacketId != null && this.#fecManager && hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
24568
25137
  const repair = this.#readRepairObject(
@@ -24574,7 +25143,7 @@ var MmtpRouter = class {
24574
25143
  );
24575
25144
  if (!repair) return;
24576
25145
  if (ownerPacketId != null && this.#fecManager) {
24577
- if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
25146
+ if ((flowless && this.#userAgentGoverns(ownerPacketId) || source === "moq" && !manifestAdmitted) && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
24578
25147
  this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
24579
25148
  const count = this.#repairPidCounts[info.packetId];
24580
25149
  if (count <= 2 || count % 500 === 0) {
@@ -24769,6 +25338,17 @@ var MmtpRouter = class {
24769
25338
  });
24770
25339
  }
24771
25340
  }
25341
+ if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
25342
+ this.#feedOverlaySource(
25343
+ info.packetId,
25344
+ catalogTrackName,
25345
+ packet,
25346
+ parsed.header,
25347
+ info.sequenceNumber,
25348
+ info.rapFlag,
25349
+ source
25350
+ );
25351
+ }
24772
25352
  }
24773
25353
  }
24774
25354
  /**
@@ -25142,7 +25722,7 @@ var MmtpRouter = class {
25142
25722
  }
25143
25723
  // ── Private: FEC recovery routing ───────────────────────────────
25144
25724
  /** Route WASM-recovered block through appropriate path based on container kind. */
25145
- #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete) {
25725
+ #routeRecoveredBlock(blockId, recovered, ownerPacketId, catalogTrackName, complete, assisted = false) {
25146
25726
  const packetSymbolSize = this.#symbolSizeForPacket(ownerPacketId);
25147
25727
  if (ownerPacketId === void 0) {
25148
25728
  throw new Error("[MmtpRouter] a recovered FEC block needs its owner packetId to read its fec.blockProfile");
@@ -25159,7 +25739,7 @@ var MmtpRouter = class {
25159
25739
  ownerPacketId,
25160
25740
  blockProfile
25161
25741
  });
25162
- return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete);
25742
+ return this.#routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted);
25163
25743
  }
25164
25744
  /**
25165
25745
  * A recovered source packet of a protected flow is admitted on its own
@@ -25171,9 +25751,24 @@ var MmtpRouter = class {
25171
25751
  * recovered packets is refused is unrecovered (7.2.7): its caller leaves it
25172
25752
  * open, so it reaches its FEC deadline and fails by class. Always true
25173
25753
  * outside a protected flow.
25174
- */
25175
- #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete) {
25176
- const receiver = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25754
+ *
25755
+ * On a flow the user agent admitted, the live decoder's recoveries are kept
25756
+ * without a record: while the flow is protected that decoder takes only
25757
+ * symbols the user agent or a record verified (a flow-less multicast
25758
+ * datagram of a listed track is dropped or set aside in #processMMTP), so
25759
+ * what it recovers is as verified as they are. An `assisted` (scratch-decoder) recovery mixes in
25760
+ * unlisted MoQ repair and still needs a record.
25761
+ * ponytail: per-flow flag; per-block provenance only if a protected flow
25762
+ * ever mixes verified and unverified live inputs.
25763
+ * ponytail: a worker frame computed before the protection reset can still
25764
+ * arrive after it (frames carry no epoch); its inputs are MoQ copies read
25765
+ * and played before protection, so it admits nothing new. Drop pre-reset
25766
+ * frames by epoch only if pre-protection input ever matters.
25767
+ */
25768
+ #routeVerifiedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, complete, assisted = false) {
25769
+ const entry = ownerPacketId == null ? void 0 : this.#provenanceFor(ownerPacketId, catalogTrackName);
25770
+ const receiver = entry === void 0 || entry === "ambiguous" ? entry : entry.receiver;
25771
+ const userAgent = !assisted && entry !== void 0 && entry !== "ambiguous" && entry.protected === true && entry.userAgent === true;
25177
25772
  let recovered = true;
25178
25773
  if (receiver) {
25179
25774
  const parsed = this.#stream(ownerPacketId, catalogTrackName).duplicates.parsedSymbols;
@@ -25185,6 +25780,8 @@ var MmtpRouter = class {
25185
25780
  let admitted = false;
25186
25781
  if (receiver === "ambiguous") {
25187
25782
  this.#ambiguousRecoveredDrops++;
25783
+ } else if (userAgent) {
25784
+ admitted = true;
25188
25785
  } else {
25189
25786
  const wire = new Uint8Array(sym.rawSymbol.byteLength + 4);
25190
25787
  wire.set(sym.rawSymbol);
@@ -25212,22 +25809,22 @@ var MmtpRouter = class {
25212
25809
  catalogTrackName === void 0 || !byName ? byName?.values() : byName.has(catalogTrackName) ? [byName.get(catalogTrackName)] : []
25213
25810
  );
25214
25811
  if (flows.size > 1) return "ambiguous";
25215
- for (const bound of flows) return this.#provenance.get(bound)?.receiver ?? "ambiguous";
25812
+ for (const bound of flows) return this.#provenance.get(bound) ?? "ambiguous";
25216
25813
  let match;
25217
- for (const { receiver, ingress } of this.#provenance.values()) {
25218
- if (ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
25814
+ for (const entry of this.#provenance.values()) {
25815
+ if (entry.ingress.tracks.some((t) => t.packetId === packetId && (catalogTrackName === void 0 || t.name === catalogTrackName))) {
25219
25816
  if (match) return "ambiguous";
25220
- match = receiver;
25817
+ match = entry;
25221
25818
  }
25222
25819
  }
25223
25820
  return match;
25224
25821
  }
25225
- #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName) {
25822
+ #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25226
25823
  if (ownerPacketId == null) return;
25227
25824
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25228
- const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(
25229
- this.#mediaStreamKey(ownerPacketId, catalogTrackName)
25230
- );
25825
+ const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
25826
+ const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25827
+ const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25231
25828
  if (ssIdSeqOffset !== void 0) {
25232
25829
  result = {
25233
25830
  ...result,
@@ -25235,12 +25832,13 @@ var MmtpRouter = class {
25235
25832
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25236
25833
  if (wire === sym.packetSequenceNumber) return sym;
25237
25834
  this.#restampedRecoveredSeqs++;
25835
+ this.#restampedStreams.add(streamKey);
25238
25836
  return { ...sym, packetSequenceNumber: wire };
25239
25837
  })
25240
25838
  };
25241
25839
  }
25242
- this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
25243
- const deliveryProvenance = this.#fecManager?.blockDeliveryProvenance?.(
25840
+ if (!overlay) this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
25841
+ const deliveryProvenance = overlay?.deliveryProvenance ?? this.#fecManager?.blockDeliveryProvenance?.(
25244
25842
  blockId,
25245
25843
  ownerPacketId,
25246
25844
  catalogTrackName
@@ -25318,7 +25916,7 @@ var MmtpRouter = class {
25318
25916
  rapFlag: sym.fragment.rapFlag,
25319
25917
  recovered: true,
25320
25918
  deliveryProvenance,
25321
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
25919
+ ...fecSbnOf(sym.index),
25322
25920
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
25323
25921
  });
25324
25922
  continue;
@@ -25430,7 +26028,7 @@ var MmtpRouter = class {
25430
26028
  rapFlag: assembled.rapFlag,
25431
26029
  recovered: true,
25432
26030
  ...assembled.deliveryProvenance === void 0 ? {} : { deliveryProvenance: assembled.deliveryProvenance },
25433
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
26031
+ ...fecSbnOf(sym.index),
25434
26032
  ...recoveredCatalogTrackName === void 0 ? {} : { catalogTrackName: recoveredCatalogTrackName }
25435
26033
  });
25436
26034
  continue;
@@ -25567,7 +26165,7 @@ var MmtpRouter = class {
25567
26165
  rapFlag: sym.fragment.rapFlag,
25568
26166
  recovered: true,
25569
26167
  deliveryProvenance,
25570
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
26168
+ ...fecSbnOf(sym.index),
25571
26169
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
25572
26170
  });
25573
26171
  } else {
@@ -25646,7 +26244,7 @@ var MmtpRouter = class {
25646
26244
  rapFlag: assembled.rapFlag,
25647
26245
  recovered: true,
25648
26246
  deliveryProvenance,
25649
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
26247
+ ...fecSbnOf(sym.index),
25650
26248
  ...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName }
25651
26249
  });
25652
26250
  }
@@ -25681,7 +26279,7 @@ var MmtpRouter = class {
25681
26279
  ...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
25682
26280
  recovered: true,
25683
26281
  deliveryProvenance,
25684
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
26282
+ ...fecSbnOf(sym.index),
25685
26283
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
25686
26284
  });
25687
26285
  continue;
@@ -25998,11 +26596,8 @@ function providerErrorOf(error) {
25998
26596
  return Object.freeze(typeof code === "string" ? { name, message, code } : { name, message });
25999
26597
  }
26000
26598
  var MAX_TRACKED_DROPPED_PIDS = 16;
26001
- function getMulticastGlobals() {
26002
- return globalThis;
26003
- }
26004
26599
  function hasMulticastGateway() {
26005
- return typeof getMulticastGlobals().multicast?.subscribe === "function";
26600
+ return typeof resolveMulticastGateway()?.subscribe === "function";
26006
26601
  }
26007
26602
  function senderOf(event) {
26008
26603
  const sourceAddress = event.sourceAddress;
@@ -26094,6 +26689,9 @@ var MulticastTransport = class {
26094
26689
  // listener closure, so an in-place admission change (setContext) applies
26095
26690
  // from the next packet on without rejoining.
26096
26691
  #context = void 0;
26692
+ #deliveredContext = void 0;
26693
+ /** The live join's gateway, when it is the user agent's MulticastReceiver provider. */
26694
+ #userAgentGateway = void 0;
26097
26695
  // The live join's provider-side filter. An in-place widening must reach it,
26098
26696
  // or the provider never delivers the new ID; null without a live join.
26099
26697
  #providerFilter = null;
@@ -26130,7 +26728,7 @@ var MulticastTransport = class {
26130
26728
  this.#dataPlaneLive = false;
26131
26729
  this.#sender = void 0;
26132
26730
  if (this.#disposed) return this.#declined(generation, "disposed");
26133
- const gateway = this.#gateway ?? getMulticastGlobals().multicast;
26731
+ const gateway = this.#gateway ?? resolveMulticastGateway();
26134
26732
  if (typeof gateway?.subscribe !== "function") {
26135
26733
  console.warn("[MulticastTransport] Multicast API not available");
26136
26734
  return this.#declined(generation, "unavailable");
@@ -26146,15 +26744,16 @@ var MulticastTransport = class {
26146
26744
  console.log(
26147
26745
  `[MulticastTransport] window.multicast.subscribe(${config.sourceAddress ?? config.senderAddress ?? "*"}, ${config.groupAddress}, ${config.port})`
26148
26746
  );
26747
+ const userAgent = isUserAgentVerifiedGateway(gateway);
26149
26748
  const subscribeConfig = toWindowMulticastSubscribeConfig({
26150
26749
  source: config.sourceAddress,
26151
26750
  senderAddress: config.senderAddress,
26152
26751
  group: config.groupAddress,
26153
26752
  port: config.port,
26154
- mode: config.mode,
26155
- amtRelay: config.amtRelay,
26156
- amtRelayPort: config.amtRelayPort,
26157
- amtDiscovery: config.amtDiscovery,
26753
+ mode: userAgent ? "native" : config.mode,
26754
+ amtRelay: userAgent ? void 0 : config.amtRelay,
26755
+ amtRelayPort: userAgent ? void 0 : config.amtRelayPort,
26756
+ amtDiscovery: userAgent ? void 0 : config.amtDiscovery,
26158
26757
  packetIds: config.packetIds,
26159
26758
  tracks: config.tracks,
26160
26759
  atsc3: config.atsc3
@@ -26200,7 +26799,8 @@ var MulticastTransport = class {
26200
26799
  acknowledgedUpdate: 0,
26201
26800
  inFlight: 0
26202
26801
  };
26203
- this.#context = context;
26802
+ this.#userAgentGateway = userAgent ? gateway : void 0;
26803
+ this.#setContext(context);
26204
26804
  if (packetIds) {
26205
26805
  for (const packetId of packetIds) {
26206
26806
  if (!this.#acceptedBytesByPacketId.has(packetId)) {
@@ -26208,7 +26808,7 @@ var MulticastTransport = class {
26208
26808
  }
26209
26809
  }
26210
26810
  }
26211
- this.#resolvedTransport = subscribeConfig.origin === "amt" ? "amt" : "native";
26811
+ this.#resolvedTransport = userAgent ? "user-agent" : subscribeConfig.origin === "amt" ? "amt" : "native";
26212
26812
  const delivery = subscription.delivery ?? "datagram";
26213
26813
  if (delivery !== "datagram" && delivery !== "ordered-relay") {
26214
26814
  throw new Error(
@@ -26289,7 +26889,7 @@ var MulticastTransport = class {
26289
26889
  }
26290
26890
  if (!this.#acceptPacket(packet, sourceAddress)) return false;
26291
26891
  const liveness = !this.#isRecordCandidate(packet);
26292
- this.#onPacket(packet, this.#context);
26892
+ this.#onPacket(packet, this.#deliveredContext);
26293
26893
  return liveness;
26294
26894
  }
26295
26895
  #isRecordCandidate(packet) {
@@ -26500,8 +27100,10 @@ var MulticastTransport = class {
26500
27100
  return this.#subscription !== null;
26501
27101
  }
26502
27102
  /**
26503
- * What the live join delivers with every accepted packet (the join's
26504
- * `context`, or the last setContext), or undefined without a live join.
27103
+ * The live join's context as the caller gave it (the join's `context`, or
27104
+ * the last setContext), or undefined without a live join. On a native
27105
+ * provider each accepted packet is delivered with a marked copy of it
27106
+ * (`userAgentVerifiedIngress`); this getter never carries the mark.
26505
27107
  */
26506
27108
  get context() {
26507
27109
  return this.#context;
@@ -26525,7 +27127,16 @@ var MulticastTransport = class {
26525
27127
  "[MulticastTransport] setContext needs a live join; join() carries the first context"
26526
27128
  );
26527
27129
  }
27130
+ this.#setContext(context);
27131
+ }
27132
+ /**
27133
+ * The join's context, and what the subscription delivers with each packet:
27134
+ * marked admitted by the user agent when the gateway is its native
27135
+ * MulticastReceiver provider. feedPacket never marks.
27136
+ */
27137
+ #setContext(context) {
26528
27138
  this.#context = context;
27139
+ this.#deliveredContext = this.#userAgentGateway !== void 0 && typeof context === "object" && context !== null ? userAgentVerifiedIngress(context, this.#userAgentGateway) : context;
26529
27140
  }
26530
27141
  /**
26531
27142
  * Replace the live join's packet-ID allow-list in place, without rejoining.
@@ -26657,6 +27268,8 @@ var MulticastTransport = class {
26657
27268
  this.#matchedSource = void 0;
26658
27269
  this.#providerFilter = null;
26659
27270
  this.#context = void 0;
27271
+ this.#deliveredContext = void 0;
27272
+ this.#userAgentGateway = void 0;
26660
27273
  this.#withdrawnPacketIds.clear();
26661
27274
  }
26662
27275
  /** The ingress filter; an accepted packet is counted here, once. */
@@ -26860,7 +27473,7 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
26860
27473
  repairsSkippedRangeNoAnchor: 0,
26861
27474
  repairsSkippedGroupCannotComplete: 0,
26862
27475
  repairsSkippedMootRap: 0,
26863
- repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0 },
27476
+ repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0, "overlay-recovered": 0 },
26864
27477
  lastRepairSkip: null,
26865
27478
  keyframeRepairFetchesSent: 0,
26866
27479
  rangeRepairFetchesSent: 0,
@@ -27193,9 +27806,7 @@ function repairTrackNamesBySource(tracks) {
27193
27806
  }
27194
27807
  function boundRepairNames(binding) {
27195
27808
  if (binding?.status !== "ok") return [];
27196
- const names = binding.layers.map((layer) => layer.repairTrackName);
27197
- if (binding.overlay) names.push(binding.overlay.repairTrackName);
27198
- return names;
27809
+ return binding.layers.map((layer) => layer.repairTrackName);
27199
27810
  }
27200
27811
  var MulticastManager = class {
27201
27812
  #fec;
@@ -27209,6 +27820,10 @@ var MulticastManager = class {
27209
27820
  /** Live joinRepairEndpoints() releases → the repair owners they registered,
27210
27821
  * re-applied after configureMultiTrack() clears catalog-derived owners. */
27211
27822
  #repairReleases = /* @__PURE__ */ new Map();
27823
+ /** Each source's catalog keyframe overlay, from its `ok` binding. */
27824
+ #overlayBindings = /* @__PURE__ */ new Map();
27825
+ /** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
27826
+ #overlayInstalls = /* @__PURE__ */ new Map();
27212
27827
  #reportedFecDiagnosticsFailure = false;
27213
27828
  // ── Stream-global FEC timing reduction ──
27214
27829
  //
@@ -27449,6 +28064,63 @@ var MulticastManager = class {
27449
28064
  this.#configureTrackMapper(packetId, fecConfig);
27450
28065
  this.#router.setPacketIdType(packetId, trackType);
27451
28066
  }
28067
+ /**
28068
+ * Decode the keyframe overlay of `sourcePacketId` (draft-ramadan-mmt-fec-02
28069
+ * 7.4) with `decoder`, a decoder of the same kind `setFecDecoder` takes.
28070
+ * Opt-in: nothing else installs one, so a receiver that never calls this
28071
+ * holds no overlay state and drops overlay repair. K_overlay, T, scheme
28072
+ * and block profile come from the source's `ok` catalog binding, which
28073
+ * must declare an overlay (`configureMultiTrack` first). The overlay is
28074
+ * decoded on its own FEC state, apart from the base: no repair FETCH, no
28075
+ * repair lifecycle event and no base statistic for its blocks. A second
28076
+ * install on the same source replaces the first; the returned disposer
28077
+ * removes only the install it created. Call it on releasing the overlay
28078
+ * join.
28079
+ */
28080
+ setKeyframeOverlayDecoder(sourcePacketId, decoder, catalogTrackName) {
28081
+ const binding = this.#overlayBindings.get(sourcePacketId);
28082
+ if (!binding || catalogTrackName !== void 0 && binding.sourceCatalogTrackName !== catalogTrackName) {
28083
+ throw new Error(
28084
+ `[MulticastManager] packetId=${sourcePacketId} has no catalog keyframe overlay to decode`
28085
+ );
28086
+ }
28087
+ this.#overlayInstalls.get(sourcePacketId)?.dispose();
28088
+ let fec;
28089
+ let uninstall;
28090
+ const attach = () => {
28091
+ fec = new FECManager();
28092
+ fec.setFecDecoder(binding.repairPacketId, decoder, binding.k, {
28093
+ blockProfile: binding.blockProfile,
28094
+ k: binding.k,
28095
+ symbolSize: binding.symbolSize,
28096
+ trackKind: "video"
28097
+ }, binding.repairTrackName);
28098
+ uninstall = this.#router.installKeyframeOverlay(
28099
+ binding.repairPacketId,
28100
+ fec,
28101
+ binding.k,
28102
+ binding.repairTrackName
28103
+ );
28104
+ };
28105
+ const detach = () => {
28106
+ uninstall();
28107
+ fec.releasePacket(binding.repairPacketId);
28108
+ };
28109
+ attach();
28110
+ const install = {
28111
+ reattach: () => {
28112
+ detach();
28113
+ attach();
28114
+ },
28115
+ dispose: () => {
28116
+ if (this.#overlayInstalls.get(sourcePacketId) !== install) return;
28117
+ this.#overlayInstalls.delete(sourcePacketId);
28118
+ detach();
28119
+ }
28120
+ };
28121
+ this.#overlayInstalls.set(sourcePacketId, install);
28122
+ return install.dispose;
28123
+ }
27452
28124
  /**
27453
28125
  * Register a Worker-backed FEC decoder for a specific packetId (track).
27454
28126
  * Worker-backed tracks decode asynchronously — recovered frames arrive via
@@ -27950,8 +28622,17 @@ var MulticastManager = class {
27950
28622
  * repair roles and owners are registered here; in-band owners stay the
27951
28623
  * caller's. Joining is best effort: a group that fails to join is logged
27952
28624
  * and leaves the media join as it was.
27953
- */
27954
- joinRepairEndpoints(endpoints, provider) {
28625
+ *
28626
+ * `options.gateway` names the gateway a subscribe callback reads through.
28627
+ * When it is the browser's own MulticastReceiver provider, the callback's
28628
+ * packets are fed as admitted by the user agent
28629
+ * (`userAgentVerifiedIngress`); nothing the callback passes can mark them.
28630
+ * The mark rests on this statement, not on the data path: every packet the
28631
+ * callback delivers must come from a subscription opened on
28632
+ * `options.gateway`. A callback that also merges another source (a MoQ or
28633
+ * WebSocket repair feed) must not name a native provider here.
28634
+ */
28635
+ joinRepairEndpoints(endpoints, provider, options) {
27955
28636
  const releases = [];
27956
28637
  let released = false;
27957
28638
  const safely = (each) => {
@@ -27961,7 +28642,11 @@ var MulticastManager = class {
27961
28642
  console.warn("[MulticastManager] repair group release failed", error);
27962
28643
  }
27963
28644
  };
27964
- const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners);
28645
+ const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
28646
+ ...owner,
28647
+ ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
28648
+ repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
28649
+ }));
27965
28650
  this.#applyRepairOwners(repairOwners);
27966
28651
  for (const endpoint of endpoints) {
27967
28652
  const ingress = {
@@ -27969,17 +28654,19 @@ var MulticastManager = class {
27969
28654
  ...endpoint.flow === void 0 ? {} : { flow: endpoint.flow }
27970
28655
  };
27971
28656
  let sender;
27972
- const onPacket = (packet, sourceAddress) => {
28657
+ const feed = (packet, sourceAddress, ingressToFeed) => {
27973
28658
  if (released) return;
27974
28659
  if (sourceAddress !== void 0 && !sameDatagramSource(sourceAddress, this.#transport.sender ?? (sender ??= sourceAddress))) return;
27975
- this.#router.feedPacket(packet, "multicast", ingress);
28660
+ this.#router.feedPacket(packet, "multicast", ingressToFeed);
27976
28661
  };
27977
28662
  const fail = (error) => console.warn(`[MulticastManager] repair group ${endpoint.group}:${endpoint.port} not joined`, error);
27978
28663
  if (typeof provider === "function") {
28664
+ const marked = userAgentVerifiedIngress(ingress, options?.gateway);
28665
+ const onPacket = (packet, sourceAddress) => feed(packet, sourceAddress, marked);
27979
28666
  Promise.resolve().then(() => provider(endpoint, onPacket)).then((release2) => released ? safely(release2) : releases.push(release2), fail);
27980
28667
  continue;
27981
28668
  }
27982
- const transport = new MulticastTransport((packet) => onPacket(packet), {
28669
+ const transport = new MulticastTransport((packet, delivered) => feed(packet, void 0, delivered ?? ingress), {
27983
28670
  gateway: provider,
27984
28671
  lockedTo: () => this.#transport.sender
27985
28672
  });
@@ -27995,7 +28682,7 @@ var MulticastManager = class {
27995
28682
  tracks: endpoint.tracks.map(({ name, packetId }) => ({ name, packetId })),
27996
28683
  // A protected repair group's records ride in band on its port (7.2.3).
27997
28684
  ...endpoint.flow === void 0 ? {} : { protected: true }
27998
- }).then((joined) => {
28685
+ }, ingress).then((joined) => {
27999
28686
  if (!joined && !released) fail(transport.lastJoinOutcome);
28000
28687
  });
28001
28688
  }
@@ -28009,9 +28696,17 @@ var MulticastManager = class {
28009
28696
  return release;
28010
28697
  }
28011
28698
  #applyRepairOwners(owners) {
28012
- for (const { packetId, ownerPacketId } of owners) {
28699
+ for (const { packetId, ownerPacketId, instance, ownerTrackName, repairTrackName } of owners) {
28013
28700
  this.#router.setPacketIdRole(packetId, "repair");
28014
- this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
28701
+ if (instance === "keyframe") {
28702
+ this.#router.setKeyframeOverlayLeg(packetId, {
28703
+ sourcePacketId: ownerPacketId,
28704
+ ...ownerTrackName === void 0 ? {} : { sourceCatalogTrackName: ownerTrackName },
28705
+ ...repairTrackName === void 0 ? {} : { repairTrackName }
28706
+ });
28707
+ } else {
28708
+ this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
28709
+ }
28015
28710
  }
28016
28711
  }
28017
28712
  // ---------------------------------------------------------------------------
@@ -28275,8 +28970,8 @@ var MulticastManager = class {
28275
28970
  this.#fec.setMulticastSymbolDelivery(delivery);
28276
28971
  }
28277
28972
  /** Process a repair packet from MoQ (reliable QUIC). */
28278
- processRepairFromMoQ(packet, ownerPacketId, catalogTrackName) {
28279
- this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
28973
+ processRepairFromMoQ(packet, ownerPacketId, catalogTrackName, repairTrackName) {
28974
+ this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName, repairTrackName);
28280
28975
  }
28281
28976
  /** Route a Worker-recovered FEC block through the MMTP parser + dedup pipeline. */
28282
28977
  routeRecoveredBlock(blockId, data, packetId, catalogTrackName) {
@@ -28328,7 +29023,10 @@ var MulticastManager = class {
28328
29023
  const ownedRepairNames = /* @__PURE__ */ new Set();
28329
29024
  const claimed = /* @__PURE__ */ new Map();
28330
29025
  for (const binding of bindings.values()) {
28331
- for (const name of boundRepairNames(binding)) claimed.set(name, (claimed.get(name) ?? 0) + 1);
29026
+ const overlay = binding.status === "ok" ? binding.overlay?.repairTrackName : void 0;
29027
+ for (const name of [...boundRepairNames(binding), ...overlay === void 0 ? [] : [overlay]]) {
29028
+ claimed.set(name, (claimed.get(name) ?? 0) + 1);
29029
+ }
28332
29030
  }
28333
29031
  for (const [name, count] of claimed) if (count === 1) ownedRepairNames.add(name);
28334
29032
  const repairOwnerGroup = /* @__PURE__ */ new Map();
@@ -28431,6 +29129,7 @@ var MulticastManager = class {
28431
29129
  const catalogTracksByName = new Map(
28432
29130
  catalog.tracks.map((track) => [track.name, track])
28433
29131
  );
29132
+ const overlayBindings = /* @__PURE__ */ new Map();
28434
29133
  for (let i = 0; i < catalog.tracks.length; i++) {
28435
29134
  const track = catalog.tracks[i];
28436
29135
  const role = resolvedRoles[i];
@@ -28459,6 +29158,29 @@ var MulticastManager = class {
28459
29158
  this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
28460
29159
  }
28461
29160
  }
29161
+ const overlayName = binding.overlay?.repairTrackName;
29162
+ const overlayPacketId = overlayName === void 0 ? void 0 : catalogTracksByName.get(overlayName)?.packetId;
29163
+ if (overlayName !== void 0 && ownedRepairNames.has(overlayName)) {
29164
+ this.#router.setKeyframeOverlayLeg(overlayPacketId, {
29165
+ sourcePacketId: track.packetId,
29166
+ sourceCatalogTrackName: track.name,
29167
+ repairTrackName: overlayName
29168
+ });
29169
+ }
29170
+ if (overlayName !== void 0 && overlayPacketId !== void 0 && ownedRepairNames.has(overlayName)) {
29171
+ const geometry2 = binding.overlay.geometry;
29172
+ overlayBindings.set(track.packetId, {
29173
+ repairPacketId: overlayPacketId,
29174
+ repairTrackName: overlayName,
29175
+ sourceCatalogTrackName: track.name,
29176
+ k: binding.overlay.sourceSymbols,
29177
+ symbolSize: fecCfg.symbolSize,
29178
+ blockProfile: parseFecBlockProfile(
29179
+ geometry2.blockProfile,
29180
+ `tracks[${JSON.stringify(track.name)}].fec keyframe overlay blockProfile`
29181
+ )
29182
+ });
29183
+ }
28462
29184
  }
28463
29185
  const wasmDecoder = fecDecoders.get(i);
28464
29186
  if (track.packetId !== void 0 && wasmDecoder !== void 0) {
@@ -28492,6 +29214,16 @@ var MulticastManager = class {
28492
29214
  }
28493
29215
  }
28494
29216
  }
29217
+ for (const [sourcePacketId, install] of [...this.#overlayInstalls]) {
29218
+ const before = this.#overlayBindings.get(sourcePacketId);
29219
+ const after = overlayBindings.get(sourcePacketId);
29220
+ const same = before !== void 0 && after !== void 0 && Object.keys(after).every((key) => after[key] === before[key]);
29221
+ if (!same) install.dispose();
29222
+ else if (this.#router.hasKeyframeOverlay(after.repairPacketId)) this.#router.resetKeyframeOverlays(sourcePacketId);
29223
+ else install.reattach();
29224
+ }
29225
+ this.#overlayBindings.clear();
29226
+ for (const [sourcePacketId, binding] of overlayBindings) this.#overlayBindings.set(sourcePacketId, binding);
28495
29227
  }
28496
29228
  /**
28497
29229
  * Why `configureMultiTrack` consumes the named catalog track without FEC
@@ -28663,6 +29395,10 @@ var MulticastManager = class {
28663
29395
  addRapCompleteObserver(observer) {
28664
29396
  return this.#router.addFecRapObserver(observer);
28665
29397
  }
29398
+ /** Observe each base block whose RAP the keyframe overlay recovered (fec-multicast-00 §6.6). */
29399
+ addKeyframeOverlayRecoveredObserver(observer) {
29400
+ return this.#router.addKeyframeOverlayRecoveredObserver(observer);
29401
+ }
28666
29402
  /** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
28667
29403
  epochFor(packetId) {
28668
29404
  return this.#fec.epochFor(packetId);
@@ -31754,12 +32490,27 @@ function wireRepairFetch(mc, opts) {
31754
32490
  dispatch();
31755
32491
  });
31756
32492
  });
32493
+ const removeOverlayObserver = mc.addKeyframeOverlayRecoveredObserver?.((packetId, catalogTrackName, sbn) => {
32494
+ queueMicrotask(() => {
32495
+ if (disposed) return;
32496
+ for (const entry of [...entries.values()]) {
32497
+ if (entry.range || entry.priority !== 0 || entry.packetId !== packetId || entry.sbn !== sbn) continue;
32498
+ if (catalogTrackName !== void 0 && entry.catalogTrackName !== catalogTrackName) continue;
32499
+ stop(entry, "overlay-recovered");
32500
+ retire(entry);
32501
+ releaseHold(entry);
32502
+ clearEntry(entry);
32503
+ }
32504
+ dispatch();
32505
+ });
32506
+ });
31757
32507
  return () => {
31758
32508
  disposed = true;
31759
32509
  mc.setUnicastRepair?.(false);
31760
32510
  removeResetObserver();
31761
32511
  removeLifecycleObserver();
31762
32512
  removeRapObserver?.();
32513
+ removeOverlayObserver?.();
31763
32514
  for (const entry of [...entries.values()]) {
31764
32515
  clearEntry(entry);
31765
32516
  if (entry.running) releaseHold(entry);
@@ -31793,6 +32544,7 @@ export {
31793
32544
  HoldLastTimeline,
31794
32545
  JOIN_DEFER_WARN_MS,
31795
32546
  KEYFRAME_LOST_TIMEOUT_SLACK,
32547
+ KEYFRAME_OVERLAY_EXTENSION_TYPE,
31796
32548
  LOCHeaderExtensionId,
31797
32549
  MAX_ENCODING_SYMBOL_ID,
31798
32550
  MAX_RAPTORQ_SOURCE_SYMBOLS,
@@ -31902,6 +32654,7 @@ export {
31902
32654
  isTrackTimingConfig,
31903
32655
  joinedRepairSymbolCount,
31904
32656
  keyframeIntervalMediaTicks,
32657
+ keyframeOverlaySsId,
31905
32658
  mergeOverrideIntoCmsfTracks,
31906
32659
  mergeOverrideIntoHangRenditions,
31907
32660
  mfuReassemblyTimeoutMs,