@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.
@@ -22431,6 +22431,14 @@ function isRouterDebugEnabled() {
22431
22431
  function debugRouter(message) {
22432
22432
  if (isRouterDebugEnabled()) console.debug(message);
22433
22433
  }
22434
+ var KEYFRAME_OVERLAY_EXTENSION_TYPE = 35585;
22435
+ function keyframeOverlaySsId(header) {
22436
+ const ext = header.extension;
22437
+ if (ext?.type !== KEYFRAME_OVERLAY_EXTENSION_TYPE || ext.data.length !== 4) return void 0;
22438
+ return new DataView(ext.data.buffer, ext.data.byteOffset, 4).getUint32(0);
22439
+ }
22440
+ var OVERLAY_OFFSET_MPUS = 16;
22441
+ var OVERLAY_MOQ_HORIZON_BLOCKS = 64;
22434
22442
  function assertStreamIdentity(role, stream) {
22435
22443
  if (stream === void 0 || !Number.isInteger(stream.packetId) || stream.packetId <= MMTP_SIGNALING_PACKET_ID || typeof stream.catalogTrackName !== "string" || stream.catalogTrackName.length === 0) {
22436
22444
  throw new Error(
@@ -22647,6 +22655,26 @@ var MmtpRouter = class {
22647
22655
  #packetIdRoles = /* @__PURE__ */ new Map();
22648
22656
  /** Explicit catalog ownership for dedicated repair-track packet IDs. */
22649
22657
  #repairOwnersByPacketId = /* @__PURE__ */ new Map();
22658
+ /** Keyframe-overlay repair PID → the source its FEC instance protects
22659
+ * (draft-ramadan-mmt-fec-02 7.4.1). Never an owner in
22660
+ * #repairOwnersByPacketId: the overlay is decoded as its own instance. */
22661
+ #overlayLegs = /* @__PURE__ */ new Map();
22662
+ /** Keyframe-overlay repair track names, which name a leg too: a catalog
22663
+ * may state no PID for it (learned at subscribe time). */
22664
+ #overlayRepairNames = /* @__PURE__ */ new Map();
22665
+ /** Installed overlay instances, by overlay repair PID (opt-in only). */
22666
+ #overlays = /* @__PURE__ */ new Map();
22667
+ #overlayRecoveredObservers = /* @__PURE__ */ new Set();
22668
+ /** Streams whose recovered sequence the base path ever re-stamped. */
22669
+ #restampedStreams = /* @__PURE__ */ new Set();
22670
+ #overlayStats = {
22671
+ repairDropped: 0,
22672
+ ownerRefused: 0,
22673
+ recovered: 0,
22674
+ restarts: 0,
22675
+ unsequencedDropped: 0,
22676
+ protectedDropped: 0
22677
+ };
22650
22678
  /** Callback fired when a delta-track FEC block completes or fails. */
22651
22679
  #onDeltaBlockCompleteCb = null;
22652
22680
  // Debug counters
@@ -22843,6 +22871,7 @@ var MmtpRouter = class {
22843
22871
  this.#fecManager.onSourceDiscontinuity = (packetId, catalogTrackName) => {
22844
22872
  this.#parser.markFecDiscontinuity(packetId, catalogTrackName);
22845
22873
  this.#requestFecPoke(packetId, catalogTrackName);
22874
+ this.resetKeyframeOverlays(packetId, catalogTrackName);
22846
22875
  };
22847
22876
  this.#fecManager.onBlockComplete = (sbn, packetId, catalogTrackName) => {
22848
22877
  this.#mseAssemblerFor(packetId, catalogTrackName)?.completeBlock(sbn);
@@ -23165,13 +23194,287 @@ var MmtpRouter = class {
23165
23194
  /** Replace catalog-derived repair ownership before a catalog reconfiguration. */
23166
23195
  clearRepairPacketIdOwners() {
23167
23196
  this.#repairOwnersByPacketId.clear();
23197
+ this.#overlayLegs.clear();
23198
+ this.#overlayRepairNames.clear();
23168
23199
  }
23169
- /** Bind one dedicated repair-track PID to its authoritative media PID. */
23200
+ /** Bind one dedicated repair-track PID to its authoritative media PID. A
23201
+ * known keyframe-overlay PID is refused, and counted: its symbols belong to
23202
+ * the overlay instance, never to the source's base decoder (mmt-02 7.4.4). */
23170
23203
  setRepairPacketIdOwner(repairPacketId, ownerPacketId) {
23204
+ if (this.#overlayLegs.has(repairPacketId)) {
23205
+ this.#overlayStats.ownerRefused++;
23206
+ return;
23207
+ }
23171
23208
  const owners = this.#repairOwnersByPacketId.get(repairPacketId) ?? /* @__PURE__ */ new Set();
23172
23209
  owners.add(ownerPacketId);
23173
23210
  this.#repairOwnersByPacketId.set(repairPacketId, owners);
23174
23211
  }
23212
+ /**
23213
+ * Record a keyframe-overlay repair leg (draft-ramadan-mmt-fec-02 7.4): its
23214
+ * PID, the source it protects, and its repair track name. Either one names
23215
+ * the leg, so a catalog that states no PID (learned at subscribe time) is
23216
+ * covered by the name. Overlay repair is then never resolved to a base
23217
+ * owner, on multicast or MoQ. Cleared with the catalog owners
23218
+ * (`clearRepairPacketIdOwners`).
23219
+ */
23220
+ setKeyframeOverlayLeg(repairPacketId, leg) {
23221
+ if (leg.repairTrackName !== void 0) this.#overlayRepairNames.set(leg.repairTrackName, { ...leg });
23222
+ if (repairPacketId === void 0) return;
23223
+ this.#repairOwnersByPacketId.delete(repairPacketId);
23224
+ this.#overlayLegs.set(repairPacketId, { ...leg });
23225
+ }
23226
+ /** The keyframe-overlay leg registered on `repairPacketId`, if any. */
23227
+ keyframeOverlayLeg(repairPacketId) {
23228
+ return this.#overlayLegs.get(repairPacketId);
23229
+ }
23230
+ /**
23231
+ * Overlay counters: repair dropped for want of an installed instance, base
23232
+ * owners refused on an overlay PID, overlay blocks recovered, overlay
23233
+ * restarts, and recovered packets dropped for want of a wire sequence or
23234
+ * for a MoQ symbol on a protected flow (multicast-02 7.2.7).
23235
+ */
23236
+ get keyframeOverlayStats() {
23237
+ return { ...this.#overlayStats };
23238
+ }
23239
+ #isOverlayRepair(repairPacketId, repairTrackName) {
23240
+ return this.#overlayLegs.has(repairPacketId) || repairTrackName !== void 0 && this.#overlayRepairNames.has(repairTrackName);
23241
+ }
23242
+ /**
23243
+ * Install the keyframe overlay decoded on `fec` for a known overlay leg
23244
+ * (draft-ramadan-mmt-fec-02 7.4.4: decoded independently of the base). `fec`
23245
+ * holds the overlay stream under `repairPacketId` and `streamName`. Replaces
23246
+ * an earlier install on the same leg; the returned disposer removes only
23247
+ * this one.
23248
+ */
23249
+ installKeyframeOverlay(repairPacketId, fec, k, streamName) {
23250
+ const leg = this.#overlayLegs.get(repairPacketId);
23251
+ if (!leg) throw new Error(`[MmtpRouter] packetId=${repairPacketId} is no known keyframe-overlay leg`);
23252
+ const inst = {
23253
+ repairPacketId,
23254
+ streamName,
23255
+ leg: { ...leg },
23256
+ fec,
23257
+ k,
23258
+ offsets: /* @__PURE__ */ new Map(),
23259
+ moqBlocks: /* @__PURE__ */ new Set()
23260
+ };
23261
+ this.#overlays.set(repairPacketId, inst);
23262
+ return () => {
23263
+ if (this.#overlays.get(repairPacketId) === inst) this.#overlays.delete(repairPacketId);
23264
+ };
23265
+ }
23266
+ /**
23267
+ * Observe each base block whose RAP MFU fragments a keyframe-overlay
23268
+ * recovery supplied, with the keyframe's FT=0 and FT=1 held
23269
+ * (fec-multicast-00 §6.6). The block is named by the recovered packets'
23270
+ * restored wire sequence through the base SS_ID offset, never by an overlay
23271
+ * coordinate. Called synchronously from the recovered-block route.
23272
+ */
23273
+ addKeyframeOverlayRecoveredObserver(observer) {
23274
+ this.#overlayRecoveredObservers.add(observer);
23275
+ return () => {
23276
+ this.#overlayRecoveredObservers.delete(observer);
23277
+ };
23278
+ }
23279
+ /** Whether an overlay is installed on `repairPacketId`. */
23280
+ hasKeyframeOverlay(repairPacketId) {
23281
+ return this.#overlays.has(repairPacketId);
23282
+ }
23283
+ /**
23284
+ * Start a new epoch of every overlay protecting `sourcePacketId` (its
23285
+ * source restarted): empty overlay stream, no coverage, no offsets.
23286
+ */
23287
+ resetKeyframeOverlays(sourcePacketId, catalogTrackName) {
23288
+ for (const inst of this.#overlaysForSource(sourcePacketId, catalogTrackName)) this.#resetOverlay(inst);
23289
+ }
23290
+ #resetOverlay(inst) {
23291
+ inst.fec.resetTrack(inst.repairPacketId, inst.streamName);
23292
+ inst.last = void 0;
23293
+ inst.offsets.clear();
23294
+ inst.moqBlocks.clear();
23295
+ }
23296
+ #overlaysForSource(sourcePacketId, catalogTrackName) {
23297
+ return [...this.#overlays.values()].filter(({ leg }) => leg.sourcePacketId === sourcePacketId && (catalogTrackName === void 0 || leg.sourceCatalogTrackName === void 0 || leg.sourceCatalogTrackName === catalogTrackName));
23298
+ }
23299
+ /**
23300
+ * The overlay instance a repair packet belongs to, by the track it arrived
23301
+ * on (7.4.2: its PID or its repair track name); null for overlay repair
23302
+ * with no instance installed; undefined for repair of no overlay.
23303
+ */
23304
+ #overlayRepairTarget(repairPacketId, repairTrackName) {
23305
+ const inst = this.#overlays.get(repairPacketId) ?? (repairTrackName === void 0 ? void 0 : [...this.#overlays.values()].find(({ leg }) => leg.repairTrackName === repairTrackName));
23306
+ if (inst) return inst;
23307
+ return this.#isOverlayRepair(repairPacketId, repairTrackName) ? null : void 0;
23308
+ }
23309
+ /** Overlay repair goes to its own instance, never to a base owner, locator
23310
+ * or decoder; with none installed it is dropped and counted. */
23311
+ #routeOverlayRepair(inst, repairPacketId, packet, header, source) {
23312
+ const repair = inst ? parseMoqRepairFrameWithHeader(packet, header) : null;
23313
+ if (!inst || !repair) {
23314
+ this.#overlayStats.repairDropped++;
23315
+ debugRouter(`[MmtpRouter] Dropping keyframe-overlay repair pid=${repairPacketId}: no overlay instance`);
23316
+ return;
23317
+ }
23318
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23319
+ if (!fec.admitRepairBlockLength(pid, repair.ssbLength, repair.rsbLength, repair.rsId)) return;
23320
+ if (!fec.admitRepairSymbolSize(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData.byteLength)) return;
23321
+ const placement = fec.placeIsoRepair(pid, name, repair.ssStart, repair.ssbLength);
23322
+ if (placement.status !== "placed") return;
23323
+ const blockStart = placement.block.start;
23324
+ if (placement.span !== void 0) this.#replayOverlaySources(inst, placement.span.block.start, placement.span.placed);
23325
+ this.#replayOverlaySources(inst, blockStart, placement.placed);
23326
+ if (source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
23327
+ const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
23328
+ const recovered = fec.feedRepairSymbol(pid, repair.ssStart, repair.ssbLength, repair.rsId, repair.symbolData, name);
23329
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
23330
+ }
23331
+ #replayOverlaySources(inst, blockStart, placed) {
23332
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23333
+ for (const { ssId, payload } of placed) {
23334
+ if (payload.source === "moq") this.#noteOverlayMoqBlock(inst, blockStart);
23335
+ if (payload.oversized) continue;
23336
+ const alreadyCompleted = fec.isBlockCompleted(blockStart, pid, name);
23337
+ const recovered = fec.feedSourceSymbol(pid, ssId, payload.symbolData, name);
23338
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, blockStart, recovered);
23339
+ }
23340
+ }
23341
+ /**
23342
+ * A source packet that carries a 0x8B01 overlay SS_ID is also a symbol of
23343
+ * every overlay installed for its source, at that SS_ID, with the bytes the
23344
+ * base protects: the packet minus its 4-byte SS_ID trailer, extension
23345
+ * included (7.1, 7.4.3).
23346
+ */
23347
+ #feedOverlaySource(packetId, catalogTrackName, packet, header, sequenceNumber, rapFlag, source) {
23348
+ if (this.#overlays.size === 0) return;
23349
+ const ssId = keyframeOverlaySsId(header);
23350
+ if (ssId === void 0) return;
23351
+ for (const inst of this.#overlaysForSource(packetId, catalogTrackName)) {
23352
+ this.#noteOverlaySsId(inst, ssId, sequenceNumber);
23353
+ const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
23354
+ if (identity !== null) {
23355
+ inst.offsets.delete(identity.mpuSequenceNumber);
23356
+ inst.offsets.set(identity.mpuSequenceNumber, ssId - sequenceNumber >>> 0);
23357
+ if (inst.offsets.size > OVERLAY_OFFSET_MPUS) inst.offsets.delete(inst.offsets.keys().next().value);
23358
+ }
23359
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23360
+ const symbolData = packet.subarray(0, packet.length - 4);
23361
+ const oversized = !fec.isoSourceFits(pid, symbolData.length);
23362
+ const coords = fec.locateIsoSource(pid, name, ssId);
23363
+ if (coords === null) {
23364
+ fec.holdIsoSource(pid, name, ssId, {
23365
+ symbolData,
23366
+ source,
23367
+ oversized,
23368
+ evidence: sourceSymbolEvidence(packet, header, rapFlag, sequenceNumber)
23369
+ });
23370
+ continue;
23371
+ }
23372
+ if (source === "moq") this.#noteOverlayMoqBlock(inst, coords.sbn);
23373
+ if (oversized) continue;
23374
+ const alreadyCompleted = fec.isBlockCompleted(coords.sbn, pid, name);
23375
+ const recovered = fec.feedSourceSymbol(pid, ssId, symbolData, name);
23376
+ if (recovered && !alreadyCompleted) this.#routeOverlayRecovered(inst, coords.sbn, recovered);
23377
+ }
23378
+ }
23379
+ #noteOverlayMoqBlock(inst, blockStart) {
23380
+ inst.moqBlocks.add(blockStart);
23381
+ const newest = inst.last?.ssId;
23382
+ if (newest === void 0) return;
23383
+ for (const start of inst.moqBlocks) {
23384
+ if ((newest - start | 0) > OVERLAY_MOQ_HORIZON_BLOCKS * inst.k) inst.moqBlocks.delete(start);
23385
+ }
23386
+ }
23387
+ /**
23388
+ * An overlay SS_ID inconsistent with the source's packet_sequence_number
23389
+ * advance since the last one is an overlay restart (an FFmpeg muxer start,
23390
+ * a bridge swap that keeps psn continuous): it moved the other way, or
23391
+ * further than the psn advance plus K_overlay. Reordering keeps the two
23392
+ * consistent and is no restart.
23393
+ */
23394
+ #noteOverlaySsId(inst, ssId, psn) {
23395
+ const last = inst.last;
23396
+ if (last !== void 0) {
23397
+ const dSs = ssId - last.ssId | 0;
23398
+ const dPsn = psn - last.psn | 0;
23399
+ const consistent = dSs === 0 ? dPsn === 0 : Math.sign(dSs) === Math.sign(dPsn) && Math.abs(dSs) <= Math.abs(dPsn) + inst.k;
23400
+ if (!consistent) {
23401
+ this.#overlayStats.restarts++;
23402
+ this.#resetOverlay(inst);
23403
+ } else if (dPsn < 0) {
23404
+ return;
23405
+ }
23406
+ }
23407
+ inst.last = { ssId, psn };
23408
+ }
23409
+ /**
23410
+ * Route an overlay-recovered block's packets as source media of the
23411
+ * overlay's source (7.2: an overlay coordinate yields no base SS_ID). No
23412
+ * base-SS_ID derivation runs: no release-gate anchor, no parse ledger, no
23413
+ * base re-stamp, no block advance. Each packet's wire sequence is restored
23414
+ * from its keyframe's own offset; without one, a stream the base never
23415
+ * re-stamped keeps the header's sequence and any other drops the packet.
23416
+ */
23417
+ #routeOverlayRecovered(inst, blockStart, recovered) {
23418
+ const { fec, repairPacketId: pid, streamName: name } = inst;
23419
+ fec.markBlockCompleted(blockStart, pid, name);
23420
+ const sourcePacketId = inst.leg.sourcePacketId;
23421
+ const sourceName = inst.leg.sourceCatalogTrackName;
23422
+ if (this.#classifyPacketId(sourcePacketId) === null) return;
23423
+ const receiver = this.#provenanceFor(sourcePacketId, sourceName);
23424
+ if (receiver !== void 0 && inst.moqBlocks.has(blockStart)) {
23425
+ this.#overlayStats.protectedDropped++;
23426
+ return;
23427
+ }
23428
+ const blockProfile = fec.blockProfileForPacket(pid);
23429
+ const symbolSize = fec.symbolSizeForPacket(pid);
23430
+ if (blockProfile === void 0 || symbolSize === void 0 || !(symbolSize > 0)) return;
23431
+ const result = processFecBlock(recovered, { symbolSize, ownerPacketId: sourcePacketId, blockProfile });
23432
+ const streamKey = this.#mediaStreamKey(sourcePacketId, sourceName);
23433
+ const restamped = this.#restampedStreams.has(streamKey);
23434
+ const restored = /* @__PURE__ */ new Set();
23435
+ const symbols = result.symbols.flatMap((sym) => {
23436
+ const offset = inst.offsets.get(sym.fragment.mpuSequenceNumber);
23437
+ if (offset !== void 0) {
23438
+ const packetSequenceNumber = serial32Add(blockStart, sym.index) - offset >>> 0;
23439
+ restored.add(packetSequenceNumber);
23440
+ return [{ ...sym, packetSequenceNumber }];
23441
+ }
23442
+ if (!restamped) return [sym];
23443
+ this.#overlayStats.unsequencedDropped++;
23444
+ return [];
23445
+ });
23446
+ this.#overlayStats.recovered++;
23447
+ this.#routeProcessedFecSymbols(blockStart, { ...result, symbols }, sourcePacketId, sourceName, {
23448
+ deliveryProvenance: inst.moqBlocks.has(blockStart) ? "mixed" : "multicast"
23449
+ });
23450
+ this.#noteOverlayRecoveredRaps(sourcePacketId, sourceName, streamKey, symbols.filter((sym) => restored.has(sym.packetSequenceNumber)));
23451
+ }
23452
+ /** fec-multicast-00 §6.6: report each base block whose RAP MFU fragments
23453
+ * the overlay restored, when the keyframe's FT=0 and FT=1 are held too. A
23454
+ * packet whose wire sequence was not restored names no base block. */
23455
+ #noteOverlayRecoveredRaps(packetId, catalogTrackName, streamKey, symbols) {
23456
+ const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(streamKey);
23457
+ if (ssIdSeqOffset === void 0 || this.#overlayRecoveredObservers.size === 0) return;
23458
+ const withMeta = (type) => new Set(symbols.filter((sym) => sym.fragmentType === type).map((sym) => sym.fragment.mpuSequenceNumber));
23459
+ const ft0 = withMeta(0);
23460
+ const ft1 = withMeta(1);
23461
+ const rapMfu = (fi) => new Set(symbols.filter((sym) => sym.fragmentType === 2 && sym.fragment.rapFlag && sym.fragment.fragmentationIndicator === fi).map((sym) => sym.fragment.mpuSequenceNumber));
23462
+ const single = rapMfu(0);
23463
+ const first = rapMfu(1);
23464
+ const last = rapMfu(3);
23465
+ const blocks = /* @__PURE__ */ new Set();
23466
+ for (const sym of symbols) {
23467
+ const mpu = sym.fragment.mpuSequenceNumber;
23468
+ if (sym.fragmentType !== 2 || !sym.fragment.rapFlag || !ft0.has(mpu) || !ft1.has(mpu)) continue;
23469
+ if (!single.has(mpu) && !(first.has(mpu) && last.has(mpu))) continue;
23470
+ const ssId = sym.packetSequenceNumber + ssIdSeqOffset >>> 0;
23471
+ const sbn = this.#fecManager?.locateIsoSource?.(packetId, catalogTrackName, ssId)?.sbn;
23472
+ if (sbn !== void 0) blocks.add(sbn);
23473
+ }
23474
+ for (const sbn of blocks) {
23475
+ for (const observer of this.#overlayRecoveredObservers) observer(packetId, catalogTrackName, sbn);
23476
+ }
23477
+ }
23175
23478
  /**
23176
23479
  * Register callback fired when a delta-track FEC block completes or fails.
23177
23480
  *
@@ -23595,6 +23898,11 @@ var MmtpRouter = class {
23595
23898
  return;
23596
23899
  }
23597
23900
  const repairPacketId = header.packetId;
23901
+ const overlay = this.#overlayRepairTarget(repairPacketId, repairTrackName);
23902
+ if (overlay !== void 0) {
23903
+ this.#routeOverlayRepair(overlay, repairPacketId, packet, header, "moq");
23904
+ return;
23905
+ }
23598
23906
  const hintedOwner = ownerPacketIdHint != null && this.#classifyPacketId(ownerPacketIdHint) !== null ? ownerPacketIdHint : null;
23599
23907
  const ownerPacketId = hintedOwner ?? (this.#classifyPacketId(repairPacketId) !== null ? repairPacketId : null);
23600
23908
  if (ownerPacketId === null) {
@@ -23884,6 +24192,8 @@ var MmtpRouter = class {
23884
24192
  }
23885
24193
  }
23886
24194
  this.#parser.resetTrack(packetId, catalogTrackName);
24195
+ this.resetKeyframeOverlays(packetId, catalogTrackName);
24196
+ this.#restampedStreams.delete(this.#mediaStreamKey(packetId, catalogTrackName));
23887
24197
  const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
23888
24198
  for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
23889
24199
  for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
@@ -23970,6 +24280,17 @@ var MmtpRouter = class {
23970
24280
  this.#packetIdRoles.delete(packetId);
23971
24281
  this.#catalogTrackNamesByPacketId.delete(packetId);
23972
24282
  this.#repairOwnersByPacketId.delete(packetId);
24283
+ this.#overlayLegs.delete(packetId);
24284
+ for (const [repairPacketId, leg] of [...this.#overlayLegs]) {
24285
+ if (leg.sourcePacketId === packetId) this.#overlayLegs.delete(repairPacketId);
24286
+ }
24287
+ for (const [name, leg] of [...this.#overlayRepairNames]) {
24288
+ if (leg.sourcePacketId === packetId) this.#overlayRepairNames.delete(name);
24289
+ }
24290
+ this.#overlays.delete(packetId);
24291
+ for (const [repairPacketId, inst] of [...this.#overlays]) {
24292
+ if (inst.leg.sourcePacketId === packetId) this.#overlays.delete(repairPacketId);
24293
+ }
23973
24294
  delete this.#pidCounts[packetId];
23974
24295
  delete this.#repairPidCounts[packetId];
23975
24296
  delete this.#moqRepairPidCounts[packetId];
@@ -24208,6 +24529,9 @@ var MmtpRouter = class {
24208
24529
  this.#parserDiagnostics.clear();
24209
24530
  this.#catalogTrackNamesByPacketId.clear();
24210
24531
  this.#repairOwnersByPacketId.clear();
24532
+ this.#overlayLegs.clear();
24533
+ this.#overlayRepairNames.clear();
24534
+ this.#overlays.clear();
24211
24535
  for (const { receiver } of this.#provenance.values()) receiver.release();
24212
24536
  this.#provenance.clear();
24213
24537
  this.#provenanceStreamFlow.clear();
@@ -24563,6 +24887,11 @@ var MmtpRouter = class {
24563
24887
  }
24564
24888
  }
24565
24889
  if (info.isRepair) {
24890
+ const overlay = this.#overlayRepairTarget(info.packetId, catalogTrackNameForPacket(ingress, info.packetId));
24891
+ if (overlay !== void 0) {
24892
+ this.#routeOverlayRepair(overlay, info.packetId, packet, parsed.header, source);
24893
+ return;
24894
+ }
24566
24895
  const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
24567
24896
  if (ownerPacketId != null && this.#fecManager && hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
24568
24897
  const repair = this.#readRepairObject(
@@ -24769,6 +25098,17 @@ var MmtpRouter = class {
24769
25098
  });
24770
25099
  }
24771
25100
  }
25101
+ if (!overlapCopy && this.#classifyPacketId(info.packetId) !== null) {
25102
+ this.#feedOverlaySource(
25103
+ info.packetId,
25104
+ catalogTrackName,
25105
+ packet,
25106
+ parsed.header,
25107
+ info.sequenceNumber,
25108
+ info.rapFlag,
25109
+ source
25110
+ );
25111
+ }
24772
25112
  }
24773
25113
  }
24774
25114
  /**
@@ -25222,12 +25562,12 @@ var MmtpRouter = class {
25222
25562
  }
25223
25563
  return match;
25224
25564
  }
25225
- #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName) {
25565
+ #routeProcessedFecSymbols(blockId, result, ownerPacketId, catalogTrackName, overlay) {
25226
25566
  if (ownerPacketId == null) return;
25227
25567
  if (this.#classifyPacketId(ownerPacketId) === null) return;
25228
- const ssIdSeqOffset = this.#ssIdSeqOffsetByStream.get(
25229
- this.#mediaStreamKey(ownerPacketId, catalogTrackName)
25230
- );
25568
+ const fecSbnOf = (index) => overlay ? {} : { fecSbn: this.#fecAnchorForRecovered(blockId, index) };
25569
+ const streamKey = this.#mediaStreamKey(ownerPacketId, catalogTrackName);
25570
+ const ssIdSeqOffset = overlay ? void 0 : this.#ssIdSeqOffsetByStream.get(streamKey);
25231
25571
  if (ssIdSeqOffset !== void 0) {
25232
25572
  result = {
25233
25573
  ...result,
@@ -25235,12 +25575,13 @@ var MmtpRouter = class {
25235
25575
  const wire = this.#fecAnchorForRecovered(blockId, sym.index) - ssIdSeqOffset >>> 0;
25236
25576
  if (wire === sym.packetSequenceNumber) return sym;
25237
25577
  this.#restampedRecoveredSeqs++;
25578
+ this.#restampedStreams.add(streamKey);
25238
25579
  return { ...sym, packetSequenceNumber: wire };
25239
25580
  })
25240
25581
  };
25241
25582
  }
25242
- this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
25243
- const deliveryProvenance = this.#fecManager?.blockDeliveryProvenance?.(
25583
+ if (!overlay) this.#stream(ownerPacketId, catalogTrackName).duplicates.advance(blockId);
25584
+ const deliveryProvenance = overlay?.deliveryProvenance ?? this.#fecManager?.blockDeliveryProvenance?.(
25244
25585
  blockId,
25245
25586
  ownerPacketId,
25246
25587
  catalogTrackName
@@ -25318,7 +25659,7 @@ var MmtpRouter = class {
25318
25659
  rapFlag: sym.fragment.rapFlag,
25319
25660
  recovered: true,
25320
25661
  deliveryProvenance,
25321
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
25662
+ ...fecSbnOf(sym.index),
25322
25663
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
25323
25664
  });
25324
25665
  continue;
@@ -25430,7 +25771,7 @@ var MmtpRouter = class {
25430
25771
  rapFlag: assembled.rapFlag,
25431
25772
  recovered: true,
25432
25773
  ...assembled.deliveryProvenance === void 0 ? {} : { deliveryProvenance: assembled.deliveryProvenance },
25433
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
25774
+ ...fecSbnOf(sym.index),
25434
25775
  ...recoveredCatalogTrackName === void 0 ? {} : { catalogTrackName: recoveredCatalogTrackName }
25435
25776
  });
25436
25777
  continue;
@@ -25567,7 +25908,7 @@ var MmtpRouter = class {
25567
25908
  rapFlag: sym.fragment.rapFlag,
25568
25909
  recovered: true,
25569
25910
  deliveryProvenance,
25570
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
25911
+ ...fecSbnOf(sym.index),
25571
25912
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
25572
25913
  });
25573
25914
  } else {
@@ -25646,7 +25987,7 @@ var MmtpRouter = class {
25646
25987
  rapFlag: assembled.rapFlag,
25647
25988
  recovered: true,
25648
25989
  deliveryProvenance,
25649
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
25990
+ ...fecSbnOf(sym.index),
25650
25991
  ...assembled.catalogTrackName === void 0 ? {} : { catalogTrackName: assembled.catalogTrackName }
25651
25992
  });
25652
25993
  }
@@ -25681,7 +26022,7 @@ var MmtpRouter = class {
25681
26022
  ...sym.fragment.sampleNumber === void 0 ? {} : { sampleNumber: sym.fragment.sampleNumber },
25682
26023
  recovered: true,
25683
26024
  deliveryProvenance,
25684
- fecSbn: this.#fecAnchorForRecovered(blockId, sym.index),
26025
+ ...fecSbnOf(sym.index),
25685
26026
  ...catalogTrackName === void 0 ? {} : { catalogTrackName }
25686
26027
  });
25687
26028
  continue;
@@ -26860,7 +27201,7 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
26860
27201
  repairsSkippedRangeNoAnchor: 0,
26861
27202
  repairsSkippedGroupCannotComplete: 0,
26862
27203
  repairsSkippedMootRap: 0,
26863
- repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0 },
27204
+ repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0, "overlay-recovered": 0 },
26864
27205
  lastRepairSkip: null,
26865
27206
  keyframeRepairFetchesSent: 0,
26866
27207
  rangeRepairFetchesSent: 0,
@@ -27193,9 +27534,7 @@ function repairTrackNamesBySource(tracks) {
27193
27534
  }
27194
27535
  function boundRepairNames(binding) {
27195
27536
  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;
27537
+ return binding.layers.map((layer) => layer.repairTrackName);
27199
27538
  }
27200
27539
  var MulticastManager = class {
27201
27540
  #fec;
@@ -27209,6 +27548,10 @@ var MulticastManager = class {
27209
27548
  /** Live joinRepairEndpoints() releases → the repair owners they registered,
27210
27549
  * re-applied after configureMultiTrack() clears catalog-derived owners. */
27211
27550
  #repairReleases = /* @__PURE__ */ new Map();
27551
+ /** Each source's catalog keyframe overlay, from its `ok` binding. */
27552
+ #overlayBindings = /* @__PURE__ */ new Map();
27553
+ /** Installed overlay decoders (`setKeyframeOverlayDecoder`), by source. */
27554
+ #overlayInstalls = /* @__PURE__ */ new Map();
27212
27555
  #reportedFecDiagnosticsFailure = false;
27213
27556
  // ── Stream-global FEC timing reduction ──
27214
27557
  //
@@ -27449,6 +27792,63 @@ var MulticastManager = class {
27449
27792
  this.#configureTrackMapper(packetId, fecConfig);
27450
27793
  this.#router.setPacketIdType(packetId, trackType);
27451
27794
  }
27795
+ /**
27796
+ * Decode the keyframe overlay of `sourcePacketId` (draft-ramadan-mmt-fec-02
27797
+ * 7.4) with `decoder`, a decoder of the same kind `setFecDecoder` takes.
27798
+ * Opt-in: nothing else installs one, so a receiver that never calls this
27799
+ * holds no overlay state and drops overlay repair. K_overlay, T, scheme
27800
+ * and block profile come from the source's `ok` catalog binding, which
27801
+ * must declare an overlay (`configureMultiTrack` first). The overlay is
27802
+ * decoded on its own FEC state, apart from the base: no repair FETCH, no
27803
+ * repair lifecycle event and no base statistic for its blocks. A second
27804
+ * install on the same source replaces the first; the returned disposer
27805
+ * removes only the install it created. Call it on releasing the overlay
27806
+ * join.
27807
+ */
27808
+ setKeyframeOverlayDecoder(sourcePacketId, decoder, catalogTrackName) {
27809
+ const binding = this.#overlayBindings.get(sourcePacketId);
27810
+ if (!binding || catalogTrackName !== void 0 && binding.sourceCatalogTrackName !== catalogTrackName) {
27811
+ throw new Error(
27812
+ `[MulticastManager] packetId=${sourcePacketId} has no catalog keyframe overlay to decode`
27813
+ );
27814
+ }
27815
+ this.#overlayInstalls.get(sourcePacketId)?.dispose();
27816
+ let fec;
27817
+ let uninstall;
27818
+ const attach = () => {
27819
+ fec = new FECManager();
27820
+ fec.setFecDecoder(binding.repairPacketId, decoder, binding.k, {
27821
+ blockProfile: binding.blockProfile,
27822
+ k: binding.k,
27823
+ symbolSize: binding.symbolSize,
27824
+ trackKind: "video"
27825
+ }, binding.repairTrackName);
27826
+ uninstall = this.#router.installKeyframeOverlay(
27827
+ binding.repairPacketId,
27828
+ fec,
27829
+ binding.k,
27830
+ binding.repairTrackName
27831
+ );
27832
+ };
27833
+ const detach = () => {
27834
+ uninstall();
27835
+ fec.releasePacket(binding.repairPacketId);
27836
+ };
27837
+ attach();
27838
+ const install = {
27839
+ reattach: () => {
27840
+ detach();
27841
+ attach();
27842
+ },
27843
+ dispose: () => {
27844
+ if (this.#overlayInstalls.get(sourcePacketId) !== install) return;
27845
+ this.#overlayInstalls.delete(sourcePacketId);
27846
+ detach();
27847
+ }
27848
+ };
27849
+ this.#overlayInstalls.set(sourcePacketId, install);
27850
+ return install.dispose;
27851
+ }
27452
27852
  /**
27453
27853
  * Register a Worker-backed FEC decoder for a specific packetId (track).
27454
27854
  * Worker-backed tracks decode asynchronously — recovered frames arrive via
@@ -27961,7 +28361,11 @@ var MulticastManager = class {
27961
28361
  console.warn("[MulticastManager] repair group release failed", error);
27962
28362
  }
27963
28363
  };
27964
- const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners);
28364
+ const repairOwners = endpoints.flatMap((endpoint) => endpoint.repairOwners.map((owner) => owner.instance !== "keyframe" ? owner : {
28365
+ ...owner,
28366
+ ownerTrackName: endpoint.tracks.find((t) => t.packetId === owner.ownerPacketId)?.name,
28367
+ repairTrackName: endpoint.tracks.find((t) => t.packetId === owner.packetId)?.name
28368
+ }));
27965
28369
  this.#applyRepairOwners(repairOwners);
27966
28370
  for (const endpoint of endpoints) {
27967
28371
  const ingress = {
@@ -28009,9 +28413,17 @@ var MulticastManager = class {
28009
28413
  return release;
28010
28414
  }
28011
28415
  #applyRepairOwners(owners) {
28012
- for (const { packetId, ownerPacketId } of owners) {
28416
+ for (const { packetId, ownerPacketId, instance, ownerTrackName, repairTrackName } of owners) {
28013
28417
  this.#router.setPacketIdRole(packetId, "repair");
28014
- this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
28418
+ if (instance === "keyframe") {
28419
+ this.#router.setKeyframeOverlayLeg(packetId, {
28420
+ sourcePacketId: ownerPacketId,
28421
+ ...ownerTrackName === void 0 ? {} : { sourceCatalogTrackName: ownerTrackName },
28422
+ ...repairTrackName === void 0 ? {} : { repairTrackName }
28423
+ });
28424
+ } else {
28425
+ this.#router.setRepairPacketIdOwner(packetId, ownerPacketId);
28426
+ }
28015
28427
  }
28016
28428
  }
28017
28429
  // ---------------------------------------------------------------------------
@@ -28275,8 +28687,8 @@ var MulticastManager = class {
28275
28687
  this.#fec.setMulticastSymbolDelivery(delivery);
28276
28688
  }
28277
28689
  /** Process a repair packet from MoQ (reliable QUIC). */
28278
- processRepairFromMoQ(packet, ownerPacketId, catalogTrackName) {
28279
- this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName);
28690
+ processRepairFromMoQ(packet, ownerPacketId, catalogTrackName, repairTrackName) {
28691
+ this.#router.feedRepairPacket(packet, ownerPacketId, catalogTrackName, repairTrackName);
28280
28692
  }
28281
28693
  /** Route a Worker-recovered FEC block through the MMTP parser + dedup pipeline. */
28282
28694
  routeRecoveredBlock(blockId, data, packetId, catalogTrackName) {
@@ -28328,7 +28740,10 @@ var MulticastManager = class {
28328
28740
  const ownedRepairNames = /* @__PURE__ */ new Set();
28329
28741
  const claimed = /* @__PURE__ */ new Map();
28330
28742
  for (const binding of bindings.values()) {
28331
- for (const name of boundRepairNames(binding)) claimed.set(name, (claimed.get(name) ?? 0) + 1);
28743
+ const overlay = binding.status === "ok" ? binding.overlay?.repairTrackName : void 0;
28744
+ for (const name of [...boundRepairNames(binding), ...overlay === void 0 ? [] : [overlay]]) {
28745
+ claimed.set(name, (claimed.get(name) ?? 0) + 1);
28746
+ }
28332
28747
  }
28333
28748
  for (const [name, count] of claimed) if (count === 1) ownedRepairNames.add(name);
28334
28749
  const repairOwnerGroup = /* @__PURE__ */ new Map();
@@ -28431,6 +28846,7 @@ var MulticastManager = class {
28431
28846
  const catalogTracksByName = new Map(
28432
28847
  catalog.tracks.map((track) => [track.name, track])
28433
28848
  );
28849
+ const overlayBindings = /* @__PURE__ */ new Map();
28434
28850
  for (let i = 0; i < catalog.tracks.length; i++) {
28435
28851
  const track = catalog.tracks[i];
28436
28852
  const role = resolvedRoles[i];
@@ -28459,6 +28875,29 @@ var MulticastManager = class {
28459
28875
  this.#router.setRepairPacketIdOwner(repairTrack.packetId, track.packetId);
28460
28876
  }
28461
28877
  }
28878
+ const overlayName = binding.overlay?.repairTrackName;
28879
+ const overlayPacketId = overlayName === void 0 ? void 0 : catalogTracksByName.get(overlayName)?.packetId;
28880
+ if (overlayName !== void 0 && ownedRepairNames.has(overlayName)) {
28881
+ this.#router.setKeyframeOverlayLeg(overlayPacketId, {
28882
+ sourcePacketId: track.packetId,
28883
+ sourceCatalogTrackName: track.name,
28884
+ repairTrackName: overlayName
28885
+ });
28886
+ }
28887
+ if (overlayName !== void 0 && overlayPacketId !== void 0 && ownedRepairNames.has(overlayName)) {
28888
+ const geometry2 = binding.overlay.geometry;
28889
+ overlayBindings.set(track.packetId, {
28890
+ repairPacketId: overlayPacketId,
28891
+ repairTrackName: overlayName,
28892
+ sourceCatalogTrackName: track.name,
28893
+ k: binding.overlay.sourceSymbols,
28894
+ symbolSize: fecCfg.symbolSize,
28895
+ blockProfile: parseFecBlockProfile(
28896
+ geometry2.blockProfile,
28897
+ `tracks[${JSON.stringify(track.name)}].fec keyframe overlay blockProfile`
28898
+ )
28899
+ });
28900
+ }
28462
28901
  }
28463
28902
  const wasmDecoder = fecDecoders.get(i);
28464
28903
  if (track.packetId !== void 0 && wasmDecoder !== void 0) {
@@ -28492,6 +28931,16 @@ var MulticastManager = class {
28492
28931
  }
28493
28932
  }
28494
28933
  }
28934
+ for (const [sourcePacketId, install] of [...this.#overlayInstalls]) {
28935
+ const before = this.#overlayBindings.get(sourcePacketId);
28936
+ const after = overlayBindings.get(sourcePacketId);
28937
+ const same = before !== void 0 && after !== void 0 && Object.keys(after).every((key) => after[key] === before[key]);
28938
+ if (!same) install.dispose();
28939
+ else if (this.#router.hasKeyframeOverlay(after.repairPacketId)) this.#router.resetKeyframeOverlays(sourcePacketId);
28940
+ else install.reattach();
28941
+ }
28942
+ this.#overlayBindings.clear();
28943
+ for (const [sourcePacketId, binding] of overlayBindings) this.#overlayBindings.set(sourcePacketId, binding);
28495
28944
  }
28496
28945
  /**
28497
28946
  * Why `configureMultiTrack` consumes the named catalog track without FEC
@@ -28663,6 +29112,10 @@ var MulticastManager = class {
28663
29112
  addRapCompleteObserver(observer) {
28664
29113
  return this.#router.addFecRapObserver(observer);
28665
29114
  }
29115
+ /** Observe each base block whose RAP the keyframe overlay recovered (fec-multicast-00 §6.6). */
29116
+ addKeyframeOverlayRecoveredObserver(observer) {
29117
+ return this.#router.addKeyframeOverlayRecoveredObserver(observer);
29118
+ }
28666
29119
  /** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
28667
29120
  epochFor(packetId) {
28668
29121
  return this.#fec.epochFor(packetId);
@@ -31754,12 +32207,27 @@ function wireRepairFetch(mc, opts) {
31754
32207
  dispatch();
31755
32208
  });
31756
32209
  });
32210
+ const removeOverlayObserver = mc.addKeyframeOverlayRecoveredObserver?.((packetId, catalogTrackName, sbn) => {
32211
+ queueMicrotask(() => {
32212
+ if (disposed) return;
32213
+ for (const entry of [...entries.values()]) {
32214
+ if (entry.range || entry.priority !== 0 || entry.packetId !== packetId || entry.sbn !== sbn) continue;
32215
+ if (catalogTrackName !== void 0 && entry.catalogTrackName !== catalogTrackName) continue;
32216
+ stop(entry, "overlay-recovered");
32217
+ retire(entry);
32218
+ releaseHold(entry);
32219
+ clearEntry(entry);
32220
+ }
32221
+ dispatch();
32222
+ });
32223
+ });
31757
32224
  return () => {
31758
32225
  disposed = true;
31759
32226
  mc.setUnicastRepair?.(false);
31760
32227
  removeResetObserver();
31761
32228
  removeLifecycleObserver();
31762
32229
  removeRapObserver?.();
32230
+ removeOverlayObserver?.();
31763
32231
  for (const entry of [...entries.values()]) {
31764
32232
  clearEntry(entry);
31765
32233
  if (entry.running) releaseHold(entry);
@@ -31793,6 +32261,7 @@ export {
31793
32261
  HoldLastTimeline,
31794
32262
  JOIN_DEFER_WARN_MS,
31795
32263
  KEYFRAME_LOST_TIMEOUT_SLACK,
32264
+ KEYFRAME_OVERLAY_EXTENSION_TYPE,
31796
32265
  LOCHeaderExtensionId,
31797
32266
  MAX_ENCODING_SYMBOL_ID,
31798
32267
  MAX_RAPTORQ_SOURCE_SYMBOLS,
@@ -31902,6 +32371,7 @@ export {
31902
32371
  isTrackTimingConfig,
31903
32372
  joinedRepairSymbolCount,
31904
32373
  keyframeIntervalMediaTicks,
32374
+ keyframeOverlaySsId,
31905
32375
  mergeOverrideIntoCmsfTracks,
31906
32376
  mergeOverrideIntoHangRenditions,
31907
32377
  mfuReassemblyTimeoutMs,