@blockcast/mmt-container 2.0.1 → 2.1.0-main.5b10b5454faf

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.
@@ -16314,6 +16314,14 @@ function isRouterDebugEnabled() {
16314
16314
  function debugRouter(message) {
16315
16315
  if (isRouterDebugEnabled()) console.debug(message);
16316
16316
  }
16317
+ var KEYFRAME_OVERLAY_EXTENSION_TYPE = 35585;
16318
+ function keyframeOverlaySsId(header) {
16319
+ const ext = header.extension;
16320
+ if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
16321
+ return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
16322
+ }
16323
+ var OVERLAY_OFFSET_MPUS = 16;
16324
+ var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
16317
16325
  function assertStreamIdentity(role, stream) {
16318
16326
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
16319
16327
  throw new Error(
@@ -16530,6 +16538,26 @@ var MmtpRouter = class {
16530
16538
  #packetIdRoles = /* @__PURE__ */ new Map();
16531
16539
  /** Explicit catalog ownership for dedicated repair-track packet IDs. */
16532
16540
  #repairOwnersByPacketId = /* @__PURE__ */ new Map();
16541
+ /** Keyframe-overlay repair PID → the source its FEC instance protects
16542
+ * (draft-ramadan-mmt-fec-02 7.4.1). Never an owner in
16543
+ * #repairOwnersByPacketId: the overlay is decoded as its own instance. */
16544
+ #overlayLegs = /* @__PURE__ */ new Map();
16545
+ /** Keyframe-overlay repair track names, which name a leg too: a catalog
16546
+ * may state no PID for it (learned at subscribe time). */
16547
+ #overlayRepairNames = /* @__PURE__ */ new Map();
16548
+ /** Installed overlay instances, by overlay repair PID (opt-in only). */
16549
+ #overlays = /* @__PURE__ */ new Map();
16550
+ #overlayRecoveredObservers = /* @__PURE__ */ new Set();
16551
+ /** Streams whose recovered sequence the base path ever re-stamped. */
16552
+ #restampedStreams = /* @__PURE__ */ new Set();
16553
+ #overlayStats = {
16554
+ repairDropped: 0,
16555
+ ownerRefused: 0,
16556
+ recovered: 0,
16557
+ restarts: 0,
16558
+ unsequencedDropped: 0,
16559
+ protectedDropped: 0
16560
+ };
16533
16561
  /** Callback fired when a delta-track FEC block completes or fails. */
16534
16562
  #onDeltaBlockCompleteCb = null;
16535
16563
  // Debug counters
@@ -16726,6 +16754,7 @@ var MmtpRouter = class {
16726
16754
  this.#fecManager.onSourceDiscontinuity = (packetId, catalogTrackName) => {
16727
16755
  this.#parser.markFecDiscontinuity(packetId, catalogTrackName);
16728
16756
  this.#requestFecPoke(packetId, catalogTrackName);
16757
+ this.resetKeyframeOverlays(packetId, catalogTrackName);
16729
16758
  };
16730
16759
  this.#fecManager.onBlockComplete = (sbn, packetId, catalogTrackName) => {
16731
16760
  this.#mseAssemblerFor(packetId, catalogTrackName)?.completeBlock(sbn);
@@ -17048,13 +17077,287 @@ var MmtpRouter = class {
17048
17077
  /** Replace catalog-derived repair ownership before a catalog reconfiguration. */
17049
17078
  clearRepairPacketIdOwners() {
17050
17079
  this.#repairOwnersByPacketId.clear();
17080
+ this.#overlayLegs.clear();
17081
+ this.#overlayRepairNames.clear();
17051
17082
  }
17052
- /** Bind one dedicated repair-track PID to its authoritative media PID. */
17083
+ /** Bind one dedicated repair-track PID to its authoritative media PID. A
17084
+ * known keyframe-overlay PID is refused, and counted: its symbols belong to
17085
+ * the overlay instance, never to the source's base decoder (mmt-02 7.4.4). */
17053
17086
  setRepairPacketIdOwner(repairPacketId, ownerPacketId) {
17087
+ if (this.#overlayLegs.has(repairPacketId)) {
17088
+ this.#overlayStats.ownerRefused++;
17089
+ return;
17090
+ }
17054
17091
  const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? /* @__PURE__ */ new Set();
17055
17092
  owners.add(ownerPacketId);
17056
17093
  this.#repairOwnersByPacketId.set(repairPacketId, owners);
17057
17094
  }
17095
+ /**
17096
+ * Record a keyframe-overlay repair leg (draft-ramadan-mmt-fec-02 7.4): its
17097
+ * PID, the source it protects, and its repair track name. Either one names
17098
+ * the leg, so a catalog that states no PID (learned at subscribe time) is
17099
+ * covered by the name. Overlay repair is then never resolved to a base
17100
+ * owner, on multicast or MoQ. Cleared with the catalog owners
17101
+ * (`clearRepairPacketIdOwners`).
17102
+ */
17103
+ setKeyframeOverlayLeg(repairPacketId, leg) {
17104
+ if (leg.repairTrackName !== void 0) this.#overlayRepairNames.set(leg.repairTrackName, { ...leg });
17105
+ if (repairPacketId === void 0) return;
17106
+ this.#repairOwnersByPacketId.delete(repairPacketId);
17107
+ this.#overlayLegs.set(repairPacketId, { ...leg });
17108
+ }
17109
+ /** The keyframe-overlay leg registered on `repairPacketId`, if any. */
17110
+ keyframeOverlayLeg(repairPacketId) {
17111
+ return this.#overlayLegs.get(repairPacketId);
17112
+ }
17113
+ /**
17114
+ * Overlay counters: repair dropped for want of an installed instance, base
17115
+ * owners refused on an overlay PID, overlay blocks recovered, overlay
17116
+ * restarts, and recovered packets dropped for want of a wire sequence or
17117
+ * for a MoQ symbol on a protected flow (multicast-02 7.2.7).
17118
+ */
17119
+ get keyframeOverlayStats() {
17120
+ return { ...this.#overlayStats };
17121
+ }
17122
+ #isOverlayRepair(repairPacketId, repairTrackName) {
17123
+ return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
17124
+ }
17125
+ /**
17126
+ * Install the keyframe overlay decoded on `fec` for a known overlay leg
17127
+ * (draft-ramadan-mmt-fec-02 7.4.4: decoded independently of the base). `fec`
17128
+ * holds the overlay stream under `repairPacketId` and `streamName`. Replaces
17129
+ * an earlier install on the same leg; the returned disposer removes only
17130
+ * this one.
17131
+ */
17132
+ installKeyframeOverlay(repairPacketId, fec, k, streamName) {
17133
+ const leg = this.#overlayLegs.get(repairPacketId);
17134
+ if (!leg) throw new Error(`[MmtpRouter] packetId=${repairPacketId} is no known keyframe-overlay leg`);
17135
+ const inst = {
17136
+ repairPacketId,
17137
+ streamName,
17138
+ leg: { ...leg },
17139
+ fec,
17140
+ k,
17141
+ offsets: /* @__PURE__ */ new Map(),
17142
+ moqBlocks: /* @__PURE__ */ new Set()
17143
+ };
17144
+ this.#overlays.set(repairPacketId, inst);
17145
+ return () => {
17146
+ if (this.#overlays.get(repairPacketId) === inst) this.#overlays.delete(repairPacketId);
17147
+ };
17148
+ }
17149
+ /**
17150
+ * Observe each base block whose RAP MFU fragments a keyframe-overlay
17151
+ * recovery supplied, with the keyframe's FT=0 and FT=1 held
17152
+ * (fec-multicast-00 §6.6). The block is named by the recovered packets'
17153
+ * restored wire sequence through the base SS_ID offset, never by an overlay
17154
+ * coordinate. Called synchronously from the recovered-block route.
17155
+ */
17156
+ addKeyframeOverlayRecoveredObserver(observer) {
17157
+ this.#overlayRecoveredObservers.add(observer);
17158
+ return () => {
17159
+ this.#overlayRecoveredObservers.delete(observer);
17160
+ };
17161
+ }
17162
+ /** Whether an overlay is installed on `repairPacketId`. */
17163
+ hasKeyframeOverlay(repairPacketId) {
17164
+ return this.#overlays.has(repairPacketId);
17165
+ }
17166
+ /**
17167
+ * Start a new epoch of every overlay protecting `sourcePacketId` (its
17168
+ * source restarted): empty overlay stream, no coverage, no offsets.
17169
+ */
17170
+ resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
17171
+ for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
17172
+ }
17173
+ #resetOverlay(inst) {
17174
+ inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
17175
+ inst.last = void 0;
17176
+ inst.offsets.clear();
17177
+ inst.moqBlocks.clear();
17178
+ }
17179
+ #overlaysForSource(sourcePacketId, catalogTrackName) {
17180
+ return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
17181
+ }
17182
+ /**
17183
+ * The overlay instance a repair packet belongs to, by the track it arrived
17184
+ * on (7.4.2: its PID or its repair track name); null for overlay repair
17185
+ * with no instance installed; undefined for repair of no overlay.
17186
+ */
17187
+ #overlayRepairTarget(repairPacketId, repairTrackName) {
17188
+ const inst = this.#overlays.get(repairPacketId) ?? (repairTrackName === void 0 ? void 0 : [...this.#overlays.values()].find(({ leg }) => leg.repairTrackName === repairTrackName));
17189
+ if (inst) return inst;
17190
+ return this.#isOverlayRepair(repairPacketId, repairTrackName) ? null : void 0;
17191
+ }
17192
+ /** Overlay repair goes to its own instance, never to a base owner, locator
17193
+ * or decoder; with none installed it is dropped and counted. */
17194
+ #routeOverlayRepair(inst, repairPacketId, packet, header, source) {
17195
+ const repair = inst ? parseMoqRepairFrameWithHeader(packet, header) : null;
17196
+ if (!inst || !repair) {
17197
+ this.#overlayStats.repairDropped++;
17198
+ debugRouter(`[MmtpRouter] Dropping keyframe-overlay repair pid=${repairPacketId}: no overlay instance`);
17199
+ return;
17200
+ }
17201
+ const { fec, repairPacketId: pid, streamName: name } = inst;
17202
+ if (!fec.admitRepairBlockLength(pid, repair.ssbLength, repair.rsbLength, repair.rsId)) return;
17203
+ if (!fec.admitRepairSymbolSize(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData.byteLength)) return;
17204
+ const placement = fec.placeIsoRepair(pid, name, repair.ssStart, repair.ssbLength);
17205
+ if (placement.status !== "placed") return;
17206
+ const blockStart = placement.block.start;
17207
+ if (placement.span !== void 0) this.#replayOverlaySources(inst, placement.span.block.start, placement.span.placed);
17208
+ this.#replayOverlaySources(inst, blockStart, placement.placed);
17209
+ if (source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
17210
+ const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
17211
+ const recovered = fec.feedRepairSymbol(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData, name);
17212
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
17213
+ }
17214
+ #replayOverlaySources(inst, blockStart, placed) {
17215
+ const { fec, repairPacketId: pid, streamName: name } = inst;
17216
+ for (const { ssId, payload } of placed) {
17217
+ if (payload.source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
17218
+ if (payload.oversized) continue;
17219
+ const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
17220
+ const recovered = fec.feedSourceSymbol(pid, ssId, payload.symbolData, name);
17221
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
17222
+ }
17223
+ }
17224
+ /**
17225
+ * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
17226
+ * every overlay installed for its source, at that SS_ID, with the bytes the
17227
+ * base protects: the packet minus its 4-byte SS_ID trailer, extension
17228
+ * included (7.1, 7.4.3).
17229
+ */
17230
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
17231
+ if (this.#overlays.size === 0) return;
17232
+ const ssId = keyframeOverlaySsId(header);
17233
+ if (ssId === void 0) return;
17234
+ for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
17235
+ this.#noteOverlaySsId(inst, ssId, sequenceNumber);
17236
+ const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
17237
+ if (identity !== null) {
17238
+ inst.offsets.delete(identity.mpuSequenceNumber);
17239
+ inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
17240
+ if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
17241
+ }
17242
+ const { fec, repairPacketId: pid, streamName: name } = inst;
17243
+ const symbolData = packet.subarray(0, packet.length - 4);
17244
+ const oversized = !fec.isoSourceFits(pid, symbolData.length);
17245
+ const coords = fec.locateIsoSource(pid, name, ssId);
17246
+ if (coords === null) {
17247
+ fec.holdIsoSource(pid, name, ssId, {
17248
+ symbolData,
17249
+ source,
17250
+ oversized,
17251
+ evidence: sourceSymbolEvidence(packet, header, rapFlag, sequenceNumber)
17252
+ });
17253
+ continue;
17254
+ }
17255
+ if (source === "moq") this.#noteOverlayMoqBlock(inst, coords.sbn);
17256
+ if (oversized) continue;
17257
+ const alreadyCompleted = fec.isBlockCompleted(coords.sbn, pid, name);
17258
+ const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
17259
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
17260
+ }
17261
+ }
17262
+ #noteOverlayMoqBlock(inst, blockStart) {
17263
+ inst.moqBlocks.add(blockStart);
17264
+ const newest = inst.last?.ssId;
17265
+ if (newest === void 0) return;
17266
+ for (const start of inst.moqBlocks) {
17267
+ if ((newest - start | 0) > OVERLAY_MOQ_HORIZON_BLOCKS * inst.k) inst.moqBlocks.delete(start);
17268
+ }
17269
+ }
17270
+ /**
17271
+ * An overlay SS_ID inconsistent with the source's packet_sequence_number
17272
+ * advance since the last one is an overlay restart (an FFmpeg muxer start,
17273
+ * a bridge swap that keeps psn continuous): it moved the other way, or
17274
+ * further than the psn advance plus K_overlay. Reordering keeps the two
17275
+ * consistent and is no restart.
17276
+ */
17277
+ #noteOverlaySsId(inst, ssId, psn) {
17278
+ const last = inst.last;
17279
+ if (last !== void 0) {
17280
+ const dSs = ssId - last.ssId | 0;
17281
+ const dPsn = psn - last.psn | 0;
17282
+ const consistent = dSs === 0 ? dPsn === 0 : Math.sign(dSs) === Math.sign(dPsn) && Math.abs(dSs) <= Math.abs(dPsn) + inst.k;
17283
+ if (!consistent) {
17284
+ this.#overlayStats.restarts++;
17285
+ this.#resetOverlay(inst);
17286
+ } else if (dPsn < 0) {
17287
+ return;
17288
+ }
17289
+ }
17290
+ inst.last = { ssId, psn };
17291
+ }
17292
+ /**
17293
+ * Route an overlay-recovered block's packets as source media of the
17294
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
17295
+ * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
17296
+ * base re-stamp, no block advance. Each packet's wire sequence is restored
17297
+ * from its keyframe's own offset; without one, a stream the base never
17298
+ * re-stamped keeps the header's sequence and any other drops the packet.
17299
+ */
17300
+ #routeOverlayRecovered(inst, blockStart, recovered) {
17301
+ const { fec, repairPacketId: pid, streamName: name } = inst;
17302
+ fec.markBlockCompleted(blockStart, pid, name);
17303
+ const sourcePacketId = inst.leg.sourcePacketId;
17304
+ const sourceName = inst.leg.sourceCatalogTrackName;
17305
+ if (this.#classifyPacketId(sourcePacketId) === null) return;
17306
+ const receiver = this.#provenanceFor(sourcePacketId, sourceName);
17307
+ if (receiver !== void 0 && inst.moqBlocks.has(blockStart)) {
17308
+ this.#overlayStats.protectedDropped++;
17309
+ return;
17310
+ }
17311
+ const blockProfile = fec.blockProfileForPacket(pid);
17312
+ const symbolSize = fec.symbolSizeForPacket(pid);
17313
+ if (blockProfile === void 0 || symbolSize === void 0 || !(symbolSize > 0)) return;
17314
+ const result = processFecBlock(recovered, { symbolSize, ownerPacketId: sourcePacketId, blockProfile });
17315
+ const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
17316
+ const restamped = this.#restampedStreams.has(streamKey);
17317
+ const restored = /* @__PURE__ */ new Set();
17318
+ const symbols = result.symbols.flatMap((sym) => {
17319
+ const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
17320
+ if (offset !== void 0) {
17321
+ const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
17322
+ restored.add(packetSequenceNumber);
17323
+ return [{ ...sym, packetSequenceNumber }];
17324
+ }
17325
+ if (!restamped) return [sym];
17326
+ this.#overlayStats.unsequencedDropped++;
17327
+ return [];
17328
+ });
17329
+ this.#overlayStats.recovered++;
17330
+ this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
17331
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
17332
+ });
17333
+ this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
17334
+ }
17335
+ /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
17336
+ * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
17337
+ * packet whose wire sequence was not restored names no base block. */
17338
+ #noteOverlayRecoveredRaps(packetId, catalogTrackName, streamKey, symbols) {
17339
+ const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(streamKey);
17340
+ if (ssIdSeqOffset === void 0 || this.#overlayRecoveredObservers.size === 0) return;
17341
+ const withMeta = (type) => new Set(symbols.filter((sym) => sym.fragmentType === type).map((sym) => sym.fragment.mpuSequenceNumber));
17342
+ const ft0 = withMeta(0);
17343
+ const ft1 = withMeta(1);
17344
+ const rapMfu = (fi) => new Set(symbols.filter((sym) => sym.fragmentType === 2 && sym.fragment.rapFlag && sym.fragment.fragmentationIndicator === fi).map((sym) => sym.fragment.mpuSequenceNumber));
17345
+ const single = rapMfu(0);
17346
+ const first = rapMfu(1);
17347
+ const last = rapMfu(3);
17348
+ const blocks = /* @__PURE__ */ new Set();
17349
+ for (const sym of symbols) {
17350
+ const mpu = sym.fragment.mpuSequenceNumber;
17351
+ if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
17352
+ if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
17353
+ const ssId = sym.packetSequenceNumber + ssIdSeqOffset >>> 0;
17354
+ const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
17355
+ if (sbn !== void 0) blocks.add(sbn);
17356
+ }
17357
+ for (const sbn of blocks) {
17358
+ for (const observer of this.#overlayRecoveredObservers) observer(packetId, catalogTrackName, sbn);
17359
+ }
17360
+ }
17058
17361
  /**
17059
17362
  * Register callback fired when a delta-track FEC block completes or fails.
17060
17363
  *
@@ -17478,6 +17781,11 @@ var MmtpRouter = class {
17478
17781
  return;
17479
17782
  }
17480
17783
  const repairPacketId = header.packetId;
17784
+ const overlay = this.#overlayRepairTarget(repairPacketId, repairTrackName);
17785
+ if (overlay !== void 0) {
17786
+ this.#routeOverlayRepair(overlay, repairPacketId, packet, header, "moq");
17787
+ return;
17788
+ }
17481
17789
  const hintedOwner = ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null ? ownerPacketIdHint : null;
17482
17790
  const ownerPacketId = hintedOwner ?? (this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
17483
17791
  if (ownerPacketId === null) {
@@ -17767,6 +18075,8 @@ var MmtpRouter = class {
17767
18075
  }
17768
18076
  }
17769
18077
  this.#parser.resetTrack(packetId, catalogTrackName);
18078
+ this.resetKeyframeOverlays(packetId, catalogTrackName);
18079
+ this.#restampedStreams.delete(this.#mediaStreamKey(packetId, catalogTrackName));
17770
18080
  const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
17771
18081
  for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
17772
18082
  for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
@@ -17853,6 +18163,17 @@ var MmtpRouter = class {
17853
18163
  this.#packetIdRoles.delete(packetId);
17854
18164
  this.#catalogTrackNamesByPacketId.delete(packetId);
17855
18165
  this.#repairOwnersByPacketId.delete(packetId);
18166
+ this.#overlayLegs.delete(packetId);
18167
+ for (const [repairPacketId, leg] of [...this.#overlayLegs]) {
18168
+ if (leg.sourcePacketId === packetId) this.#overlayLegs.delete(repairPacketId);
18169
+ }
18170
+ for (const [name, leg] of [...this.#overlayRepairNames]) {
18171
+ if (leg.sourcePacketId === packetId) this.#overlayRepairNames.delete(name);
18172
+ }
18173
+ this.#overlays.delete(packetId);
18174
+ for (const [repairPacketId, inst] of [...this.#overlays]) {
18175
+ if (inst.leg.sourcePacketId === packetId) this.#overlays.delete(repairPacketId);
18176
+ }
17856
18177
  delete this.#pidCounts[packetId];
17857
18178
  delete this.#repairPidCounts[packetId];
17858
18179
  delete this.#moqRepairPidCounts[packetId];
@@ -18091,6 +18412,9 @@ var MmtpRouter = class {
18091
18412
  this.#parserDiagnostics.clear();
18092
18413
  this.#catalogTrackNamesByPacketId.clear();
18093
18414
  this.#repairOwnersByPacketId.clear();
18415
+ this.#overlayLegs.clear();
18416
+ this.#overlayRepairNames.clear();
18417
+ this.#overlays.clear();
18094
18418
  for (const { receiver } of this.#provenance.values()) receiver.release();
18095
18419
  this.#provenance.clear();
18096
18420
  this.#provenanceStreamFlow.clear();
@@ -18446,6 +18770,11 @@ var MmtpRouter = class {
18446
18770
  }
18447
18771
  }
18448
18772
  if (info.isRepair) {
18773
+ const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
18774
+ if (overlay !== void 0) {
18775
+ this.#routeOverlayRepair(overlay, info.packetId, packet, parsed.header, source);
18776
+ return;
18777
+ }
18449
18778
  const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
18450
18779
  if (ownerPacketId != null && this.#fecManager && hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
18451
18780
  const repair = this.#readRepairObject(
@@ -18652,6 +18981,17 @@ var MmtpRouter = class {
18652
18981
  });
18653
18982
  }
18654
18983
  }
18984
+ if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
18985
+ this.#feedOverlaySource(
18986
+ info.packetId,
18987
+ catalogTrackName,
18988
+ packet,
18989
+ parsed.header,
18990
+ info.sequenceNumber,
18991
+ info.rapFlag,
18992
+ source
18993
+ );
18994
+ }
18655
18995
  }
18656
18996
  }
18657
18997
  /**
@@ -19105,12 +19445,12 @@ var MmtpRouter = class {
19105
19445
  }
19106
19446
  return match;
19107
19447
  }
19108
- #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName) {
19448
+ #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
19109
19449
  if (ownerPacketId == null) return;
19110
19450
  if (this.#classifyPacketId(ownerPacketId) === null) return;
19111
- const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(
19112
- this.#mediaStreamKey(ownerPacketId, catalogTrackName)
19113
- );
19451
+ const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
19452
+ const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
19453
+ const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
19114
19454
  if (ssIdSeqOffset !== void 0) {
19115
19455
  result = {
19116
19456
  ...result,
@@ -19118,12 +19458,13 @@ var MmtpRouter = class {
19118
19458
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
19119
19459
  if (wire === sym.packetSequenceNumber) return sym;
19120
19460
  this.#restampedRecoveredSeqs++;
19461
+ this.#restampedStreams.add(streamKey);
19121
19462
  return { ...sym, packetSequenceNumber: wire };
19122
19463
  })
19123
19464
  };
19124
19465
  }
19125
- this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
19126
- const deliveryProvenance = this.#fecManager?.blockDeliveryProvenance?.(
19466
+ if (!overlay) this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
19467
+ const deliveryProvenance = overlay?.deliveryProvenance ?? this.#fecManager?.blockDeliveryProvenance?.(
19127
19468
  blockId,
19128
19469
  ownerPacketId,
19129
19470
  catalogTrackName
@@ -19201,7 +19542,7 @@ var MmtpRouter = class {
19201
19542
  rapFlag: sym.fragment.rapFlag,
19202
19543
  recovered: true,
19203
19544
  deliveryProvenance,
19204
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
19545
+ ...fecSbnOf(sym.index),
19205
19546
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
19206
19547
  });
19207
19548
  continue;
@@ -19313,7 +19654,7 @@ var MmtpRouter = class {
19313
19654
  rapFlag: assembled.rapFlag,
19314
19655
  recovered: true,
19315
19656
  ...assembled.deliveryProvenance === void 0 ? {} : { deliveryProvenance: assembled.deliveryProvenance },
19316
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
19657
+ ...fecSbnOf(sym.index),
19317
19658
  ...recoveredCatalogTrackName === void 0 ? {} : { catalogTrackName: recoveredCatalogTrackName }
19318
19659
  });
19319
19660
  continue;
@@ -19450,7 +19791,7 @@ var MmtpRouter = class {
19450
19791
  rapFlag: sym.fragment.rapFlag,
19451
19792
  recovered: true,
19452
19793
  deliveryProvenance,
19453
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
19794
+ ...fecSbnOf(sym.index),
19454
19795
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
19455
19796
  });
19456
19797
  } else {
@@ -19529,7 +19870,7 @@ var MmtpRouter = class {
19529
19870
  rapFlag: assembled.rapFlag,
19530
19871
  recovered: true,
19531
19872
  deliveryProvenance,
19532
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
19873
+ ...fecSbnOf(sym.index),
19533
19874
  ...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName }
19534
19875
  });
19535
19876
  }
@@ -19564,7 +19905,7 @@ var MmtpRouter = class {
19564
19905
  ...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
19565
19906
  recovered: true,
19566
19907
  deliveryProvenance,
19567
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
19908
+ ...fecSbnOf(sym.index),
19568
19909
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
19569
19910
  });
19570
19911
  continue;
@@ -21896,7 +22237,7 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
21896
22237
  repairsSkippedRangeNoAnchor: 0,
21897
22238
  repairsSkippedGroupCannotComplete: 0,
21898
22239
  repairsSkippedMootRap: 0,
21899
- repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0 },
22240
+ repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0, "overlay-recovered": 0 },
21900
22241
  lastRepairSkip: null,
21901
22242
  keyframeRepairFetchesSent: 0,
21902
22243
  rangeRepairFetchesSent: 0,
@@ -22229,9 +22570,7 @@ function repairTrackNamesBySource(tracks) {
22229
22570
  }
22230
22571
  function boundRepairNames(binding) {
22231
22572
  if (binding?.status !== "ok") return [];
22232
- const names = binding.layers.map((layer) => layer.repairTrackName);
22233
- if (binding.overlay) names.push(binding.overlay.repairTrackName);
22234
- return names;
22573
+ return binding.layers.map((layer) => layer.repairTrackName);
22235
22574
  }
22236
22575
  var MulticastManager = class {
22237
22576
  #fec;
@@ -22245,6 +22584,10 @@ var MulticastManager = class {
22245
22584
  /** Live joinRepairEndpoints() releases → the repair owners they registered,
22246
22585
  * re-applied after configureMultiTrack() clears catalog-derived owners. */
22247
22586
  #repairReleases = /* @__PURE__ */ new Map();
22587
+ /** Each source's catalog keyframe overlay, from its `ok` binding. */
22588
+ #overlayBindings = /* @__PURE__ */ new Map();
22589
+ /** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
22590
+ #overlayInstalls = /* @__PURE__ */ new Map();
22248
22591
  #reportedFecDiagnosticsFailure = false;
22249
22592
  // ── Stream-global FEC timing reduction ──
22250
22593
  //
@@ -22485,6 +22828,63 @@ var MulticastManager = class {
22485
22828
  this.#configureTrackMapper(packetId, fecConfig);
22486
22829
  this.#router.setPacketIdType(packetId, trackType);
22487
22830
  }
22831
+ /**
22832
+ * Decode the keyframe overlay of `sourcePacketId` (draft-ramadan-mmt-fec-02
22833
+ * 7.4) with `decoder`, a decoder of the same kind `setFecDecoder` takes.
22834
+ * Opt-in: nothing else installs one, so a receiver that never calls this
22835
+ * holds no overlay state and drops overlay repair. K_overlay, T, scheme
22836
+ * and block profile come from the source's `ok` catalog binding, which
22837
+ * must declare an overlay (`configureMultiTrack` first). The overlay is
22838
+ * decoded on its own FEC state, apart from the base: no repair FETCH, no
22839
+ * repair lifecycle event and no base statistic for its blocks. A second
22840
+ * install on the same source replaces the first; the returned disposer
22841
+ * removes only the install it created. Call it on releasing the overlay
22842
+ * join.
22843
+ */
22844
+ setKeyframeOverlayDecoder(sourcePacketId, decoder, catalogTrackName) {
22845
+ const binding = this.#overlayBindings.get(sourcePacketId);
22846
+ if (!binding || catalogTrackName !== void 0 && binding.sourceCatalogTrackName !== catalogTrackName) {
22847
+ throw new Error(
22848
+ `[MulticastManager] packetId=${sourcePacketId} has no catalog keyframe overlay to decode`
22849
+ );
22850
+ }
22851
+ this.#overlayInstalls.get(sourcePacketId)?.dispose();
22852
+ let fec;
22853
+ let uninstall;
22854
+ const attach = () => {
22855
+ fec = new FECManager();
22856
+ fec.setFecDecoder(binding.repairPacketId, decoder, binding.k, {
22857
+ blockProfile: binding.blockProfile,
22858
+ k: binding.k,
22859
+ symbolSize: binding.symbolSize,
22860
+ trackKind: "video"
22861
+ }, binding.repairTrackName);
22862
+ uninstall = this.#router.installKeyframeOverlay(
22863
+ binding.repairPacketId,
22864
+ fec,
22865
+ binding.k,
22866
+ binding.repairTrackName
22867
+ );
22868
+ };
22869
+ const detach = () => {
22870
+ uninstall();
22871
+ fec.releasePacket(binding.repairPacketId);
22872
+ };
22873
+ attach();
22874
+ const install = {
22875
+ reattach: () => {
22876
+ detach();
22877
+ attach();
22878
+ },
22879
+ dispose: () => {
22880
+ if (this.#overlayInstalls.get(sourcePacketId) !== install) return;
22881
+ this.#overlayInstalls.delete(sourcePacketId);
22882
+ detach();
22883
+ }
22884
+ };
22885
+ this.#overlayInstalls.set(sourcePacketId, install);
22886
+ return install.dispose;
22887
+ }
22488
22888
  /**
22489
22889
  * Register a Worker-backed FEC decoder for a specific packetId (track).
22490
22890
  * Worker-backed tracks decode asynchronously — recovered frames arrive via
@@ -22997,7 +23397,11 @@ var MulticastManager = class {
22997
23397
  console.warn("[MulticastManager] repair group release failed", error);
22998
23398
  }
22999
23399
  };
23000
- const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners);
23400
+ const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
23401
+ ...owner,
23402
+ ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
23403
+ repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
23404
+ }));
23001
23405
  this.#applyRepairOwners(repairOwners);
23002
23406
  for (const endpoint of endpoints) {
23003
23407
  const ingress = {
@@ -23045,9 +23449,17 @@ var MulticastManager = class {
23045
23449
  return release;
23046
23450
  }
23047
23451
  #applyRepairOwners(owners) {
23048
- for (const { packetId, ownerPacketId } of owners) {
23452
+ for (const { packetId, ownerPacketId, instance, ownerTrackName, repairTrackName } of owners) {
23049
23453
  this.#router.setPacketIdRole(packetId, "repair");
23050
- this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
23454
+ if (instance === "keyframe") {
23455
+ this.#router.setKeyframeOverlayLeg(packetId, {
23456
+ sourcePacketId: ownerPacketId,
23457
+ ...ownerTrackName === void 0 ? {} : { sourceCatalogTrackName: ownerTrackName },
23458
+ ...repairTrackName === void 0 ? {} : { repairTrackName }
23459
+ });
23460
+ } else {
23461
+ this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
23462
+ }
23051
23463
  }
23052
23464
  }
23053
23465
  // ---------------------------------------------------------------------------
@@ -23311,8 +23723,8 @@ var MulticastManager = class {
23311
23723
  this.#fec.setMulticastSymbolDelivery(delivery);
23312
23724
  }
23313
23725
  /** Process a repair packet from MoQ (reliable QUIC). */
23314
- processRepairFromMoQ(packet, ownerPacketId, catalogTrackName) {
23315
- this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
23726
+ processRepairFromMoQ(packet, ownerPacketId, catalogTrackName, repairTrackName) {
23727
+ this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName, repairTrackName);
23316
23728
  }
23317
23729
  /** Route a Worker-recovered FEC block through the MMTP parser + dedup pipeline. */
23318
23730
  routeRecoveredBlock(blockId, data, packetId, catalogTrackName) {
@@ -23364,7 +23776,10 @@ var MulticastManager = class {
23364
23776
  const ownedRepairNames = /* @__PURE__ */ new Set();
23365
23777
  const claimed = /* @__PURE__ */ new Map();
23366
23778
  for (const binding of bindings.values()) {
23367
- for (const name of boundRepairNames(binding)) claimed.set(name, (claimed.get(name) ?? 0) + 1);
23779
+ const overlay = binding.status === "ok" ? binding.overlay?.repairTrackName : void 0;
23780
+ for (const name of [...boundRepairNames(binding), ...overlay === void 0 ? [] : [overlay]]) {
23781
+ claimed.set(name, (claimed.get(name) ?? 0) + 1);
23782
+ }
23368
23783
  }
23369
23784
  for (const [name, count] of claimed) if (count === 1) ownedRepairNames.add(name);
23370
23785
  const repairOwnerGroup = /* @__PURE__ */ new Map();
@@ -23467,6 +23882,7 @@ var MulticastManager = class {
23467
23882
  const catalogTracksByName = new Map(
23468
23883
  catalog.tracks.map((track) => [track.name, track])
23469
23884
  );
23885
+ const overlayBindings = /* @__PURE__ */ new Map();
23470
23886
  for (let i = 0; i < catalog.tracks.length; i++) {
23471
23887
  const track = catalog.tracks[i];
23472
23888
  const role = resolvedRoles[i];
@@ -23495,6 +23911,29 @@ var MulticastManager = class {
23495
23911
  this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
23496
23912
  }
23497
23913
  }
23914
+ const overlayName = binding.overlay?.repairTrackName;
23915
+ const overlayPacketId = overlayName === void 0 ? void 0 : catalogTracksByName.get(overlayName)?.packetId;
23916
+ if (overlayName !== void 0 && ownedRepairNames.has(overlayName)) {
23917
+ this.#router.setKeyframeOverlayLeg(overlayPacketId, {
23918
+ sourcePacketId: track.packetId,
23919
+ sourceCatalogTrackName: track.name,
23920
+ repairTrackName: overlayName
23921
+ });
23922
+ }
23923
+ if (overlayName !== void 0 && overlayPacketId !== void 0 && ownedRepairNames.has(overlayName)) {
23924
+ const geometry2 = binding.overlay.geometry;
23925
+ overlayBindings.set(track.packetId, {
23926
+ repairPacketId: overlayPacketId,
23927
+ repairTrackName: overlayName,
23928
+ sourceCatalogTrackName: track.name,
23929
+ k: binding.overlay.sourceSymbols,
23930
+ symbolSize: fecCfg.symbolSize,
23931
+ blockProfile: parseFecBlockProfile(
23932
+ geometry2.blockProfile,
23933
+ `tracks[${JSON.stringify(track.name)}].fec keyframe overlay blockProfile`
23934
+ )
23935
+ });
23936
+ }
23498
23937
  }
23499
23938
  const wasmDecoder = fecDecoders.get(i);
23500
23939
  if (track.packetId !== void 0 && wasmDecoder !== void 0) {
@@ -23528,6 +23967,16 @@ var MulticastManager = class {
23528
23967
  }
23529
23968
  }
23530
23969
  }
23970
+ for (const [sourcePacketId, install] of [...this.#overlayInstalls]) {
23971
+ const before = this.#overlayBindings.get(sourcePacketId);
23972
+ const after = overlayBindings.get(sourcePacketId);
23973
+ const same = before !== void 0 && after !== void 0 && Object.keys(after).every((key) => after[key] === before[key]);
23974
+ if (!same) install.dispose();
23975
+ else if (this.#router.hasKeyframeOverlay(after.repairPacketId)) this.#router.resetKeyframeOverlays(sourcePacketId);
23976
+ else install.reattach();
23977
+ }
23978
+ this.#overlayBindings.clear();
23979
+ for (const [sourcePacketId, binding] of overlayBindings) this.#overlayBindings.set(sourcePacketId, binding);
23531
23980
  }
23532
23981
  /**
23533
23982
  * Why `configureMultiTrack` consumes the named catalog track without FEC
@@ -23699,6 +24148,10 @@ var MulticastManager = class {
23699
24148
  addRapCompleteObserver(observer) {
23700
24149
  return this.#router.addFecRapObserver(observer);
23701
24150
  }
24151
+ /** Observe each base block whose RAP the keyframe overlay recovered (fec-multicast-00 §6.6). */
24152
+ addKeyframeOverlayRecoveredObserver(observer) {
24153
+ return this.#router.addKeyframeOverlayRecoveredObserver(observer);
24154
+ }
23702
24155
  /** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
23703
24156
  epochFor(packetId) {
23704
24157
  return this.#fec.epochFor(packetId);