@blockcast/mmt-container 0.5.0-main.dd44e35d4e29 → 0.5.0-main.e4c61672f9de

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.
Files changed (44) hide show
  1. package/dist/browser-runtime.js +356 -29
  2. package/dist/browser-runtime.js.map +2 -2
  3. package/dist/fec-manager.d.ts.map +1 -1
  4. package/dist/fec-manager.js +20 -0
  5. package/dist/fec-manager.js.map +2 -2
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +2 -2
  9. package/dist/mmtp-container-parser.d.ts +10 -0
  10. package/dist/mmtp-container-parser.d.ts.map +1 -1
  11. package/dist/mmtp-container-parser.js +31 -1
  12. package/dist/mmtp-container-parser.js.map +2 -2
  13. package/dist/mmtp-container-parser.no-modules.js +31 -1
  14. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  15. package/dist/mmtp-decode-pipeline.js +31 -1
  16. package/dist/mmtp-decode-pipeline.js.map +2 -2
  17. package/dist/mmtp-router.d.ts +26 -1
  18. package/dist/mmtp-router.d.ts.map +1 -1
  19. package/dist/mmtp-router.js +232 -9
  20. package/dist/mmtp-router.js.map +2 -2
  21. package/dist/multicast-manager.d.ts +30 -2
  22. package/dist/multicast-manager.d.ts.map +1 -1
  23. package/dist/multicast-manager.js +298 -10
  24. package/dist/multicast-manager.js.map +2 -2
  25. package/dist/no-modules.js +31 -1
  26. package/dist/no-modules.js.map +2 -2
  27. package/dist/repair-fetch.d.ts +41 -10
  28. package/dist/repair-fetch.d.ts.map +1 -1
  29. package/dist/repair-fetch.js +58 -19
  30. package/dist/repair-fetch.js.map +2 -2
  31. package/dist/wasm-mmtp-container-parser.js +31 -1
  32. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  33. package/package.json +7 -6
  34. package/src/fec-manager.ts +26 -0
  35. package/src/index.ts +1 -0
  36. package/src/mmtp-container-parser.ts +48 -0
  37. package/src/mmtp-router.ts +286 -11
  38. package/src/multicast-manager.ts +85 -1
  39. package/src/repair-fetch.ts +131 -31
  40. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
  41. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
  42. package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
  43. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
  44. package/test-vectors/repair-policy-v1.json +113 -0
@@ -13555,6 +13555,11 @@ var FECManager = class _FECManager {
13555
13555
  // Worker-recovered blocks a recovered packet of which admission refused,
13556
13556
  // until the decode's final symbol (7.2.7: the block is unrecovered).
13557
13557
  this.#refusedRecoveries = /* @__PURE__ */ new Set();
13558
+ // Worker-recovered blocks: the next ESI the decode's ordered emit owes. The
13559
+ // Worker emits every ESI 0..SSB_length-1 in order and drops only a symbol it
13560
+ // refuses (a malformed ssbg_mode1 symbol), so a skipped ESI is a dropped
13561
+ // recovered packet and the decode recovered nothing.
13562
+ this.#workerRecoveredNextEsi = /* @__PURE__ */ new Map();
13558
13563
  // How the multicast leg delivers symbols (see MulticastSymbolDelivery). The
13559
13564
  // provider declares it per transport join; absence keeps datagram semantics.
13560
13565
  this.#multicastSymbolDelivery = "datagram";
@@ -13764,6 +13769,7 @@ var FECManager = class _FECManager {
13764
13769
  #repairDeadlineAtMs;
13765
13770
  #workerRecoveredBlocks;
13766
13771
  #refusedRecoveries;
13772
+ #workerRecoveredNextEsi;
13767
13773
  #multicastSymbolDelivery;
13768
13774
  #orderedFrontiers;
13769
13775
  #orderedPendingBlocks;
@@ -13904,6 +13910,7 @@ var FECManager = class _FECManager {
13904
13910
  this.#cleanlyCompletedBlocks.delete(record.key);
13905
13911
  this.#workerRecoveredBlocks.delete(record.key);
13906
13912
  this.#refusedRecoveries.delete(record.key);
13913
+ this.#workerRecoveredNextEsi.delete(record.key);
13907
13914
  this.#failedBlocks.delete(record.key);
13908
13915
  this.#resolvedBlockClasses.delete(record.key);
13909
13916
  this.#repairedBlocks.delete(record.key);
@@ -15059,6 +15066,7 @@ var FECManager = class _FECManager {
15059
15066
  resetSet(this.#repairRequestedBlocks);
15060
15067
  resetSet(this.#workerRecoveredBlocks);
15061
15068
  resetSet(this.#refusedRecoveries);
15069
+ resetMap(this.#workerRecoveredNextEsi);
15062
15070
  resetSet(this.#startupAlignedPackets);
15063
15071
  resetSet(this.#startupPartialBlocks);
15064
15072
  resetSet(this.#startupRepairReadyPackets);
@@ -15186,6 +15194,7 @@ var FECManager = class _FECManager {
15186
15194
  dropSet(this.#repairRequestedBlocks);
15187
15195
  dropSet(this.#workerRecoveredBlocks);
15188
15196
  dropSet(this.#refusedRecoveries);
15197
+ dropMap(this.#workerRecoveredNextEsi);
15189
15198
  dropSet(this.#startupAlignedPackets);
15190
15199
  dropSet(this.#startupPartialBlocks);
15191
15200
  dropSet(this.#startupRepairReadyPackets);
@@ -15973,6 +15982,16 @@ var FECManager = class _FECManager {
15973
15982
  if (!this.#claimDecoderBlock(client, meta.sbn, recoveredCatalogTrackName)) return;
15974
15983
  const blockKey = this.#blockKey(meta.sbn, packetId, recoveredCatalogTrackName);
15975
15984
  const cleanlyCompleted = this.#cleanlyCompletedBlocks.has(blockKey);
15985
+ if (esi !== (this.#workerRecoveredNextEsi.get(blockKey) ?? 0)) {
15986
+ this.#refusedRecoveries.add(blockKey);
15987
+ if (this.#refusedRecoveries.size > 1e3) dropOldest(this.#refusedRecoveries, 500);
15988
+ }
15989
+ if (esi >= meta.ssbLength - 1) {
15990
+ this.#workerRecoveredNextEsi.delete(blockKey);
15991
+ } else {
15992
+ this.#workerRecoveredNextEsi.set(blockKey, esi + 1);
15993
+ if (this.#workerRecoveredNextEsi.size > 1e3) dropOldest(this.#workerRecoveredNextEsi, 500);
15994
+ }
15976
15995
  if (!cleanlyCompleted) {
15977
15996
  const symbolKey = `${blockKey}:${esi}`;
15978
15997
  if (!this.#seenSymbols.has(symbolKey)) {
@@ -16782,6 +16801,7 @@ var FECManager = class _FECManager {
16782
16801
  this.#repairRequestedBlocks.clear();
16783
16802
  this.#workerRecoveredBlocks.clear();
16784
16803
  this.#refusedRecoveries.clear();
16804
+ this.#workerRecoveredNextEsi.clear();
16785
16805
  this.#syncDecoderBlockOwners.clear();
16786
16806
  this.#startupAlignedPackets.clear();
16787
16807
  this.#startupPartialBlocks.clear();
@@ -17635,6 +17655,9 @@ function authoredGap(before, after) {
17635
17655
  if (!a || !b || a.mediaTimescale !== b.mediaTimescale) return false;
17636
17656
  return b.decodeTime > a.decodeTime + BigInt(a.durationTicks);
17637
17657
  }
17658
+ function ssIdAfter(a, b) {
17659
+ return (a - b | 0) > 0;
17660
+ }
17638
17661
  var FecReleaseSequencer = class {
17639
17662
  #streams = /* @__PURE__ */ new Map();
17640
17663
  #gate;
@@ -17696,11 +17719,17 @@ var FecReleaseSequencer = class {
17696
17719
  stream.last = null;
17697
17720
  stream.lastSbn = void 0;
17698
17721
  stream.discontinuity = false;
17722
+ stream.rapSsId = void 0;
17699
17723
  }
17700
17724
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
17701
17725
  this.#recordStat("droppedStale");
17702
17726
  return;
17703
17727
  }
17728
+ const ssId = meta.fecSbn;
17729
+ if (meta.trackType === "video" && ssId !== void 0 && this.#gate.isRap?.(data, meta) === true) {
17730
+ if (stream.rapSsId === void 0 || ssIdAfter(ssId, stream.rapSsId)) stream.rapSsId = ssId;
17731
+ this.#gate.onRap?.(stream.packetId, stream.catalogTrackName, ssId);
17732
+ }
17704
17733
  if (stream.last === null) {
17705
17734
  if (meta.trackType === "video" && !meta.keyframe) {
17706
17735
  this.#recordStat("droppedBeforeStartupKeyframe");
@@ -17736,6 +17765,22 @@ var FecReleaseSequencer = class {
17736
17765
  }
17737
17766
  return earliest;
17738
17767
  }
17768
+ /**
17769
+ * Whether a complete RAP above SS_ID `ssId` entered the gate of a stream of
17770
+ * `packetId` (draft-ramadan-moq-fec 11.4.2: repair of a block below it is
17771
+ * moot). Its SS_ID is the one its frame completed in; a frame's packets
17772
+ * hold consecutive SS_IDs in authored order, so a complete RAP with a
17773
+ * packet above `ssId` has every packet of it at or above the first SS_ID
17774
+ * any loss at or below `ssId` could have taken.
17775
+ */
17776
+ rapAbove(packetId, catalogTrackName, ssId) {
17777
+ for (const stream of this.#streams.values()) {
17778
+ if (stream.packetId !== packetId) continue;
17779
+ if (catalogTrackName !== void 0 && stream.catalogTrackName !== catalogTrackName) continue;
17780
+ if (stream.rapSsId !== void 0 && ssIdAfter(stream.rapSsId, ssId)) return true;
17781
+ }
17782
+ return false;
17783
+ }
17739
17784
  /** Re-evaluate held gaps after FEC state changed. */
17740
17785
  poke(streamKey) {
17741
17786
  if (streamKey !== void 0) {
@@ -17826,7 +17871,8 @@ var FecReleaseSequencer = class {
17826
17871
  buf: [],
17827
17872
  last: null,
17828
17873
  lastSbn: void 0,
17829
- discontinuity: false
17874
+ discontinuity: false,
17875
+ rapSsId: void 0
17830
17876
  };
17831
17877
  this.#streams.set(streamKey, stream);
17832
17878
  }
@@ -18189,6 +18235,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18189
18235
  fecReleaseEarliestHeldAtMs(packetId, catalogTrackName) {
18190
18236
  return this.#fecSequencer?.earliestHeldArrivalMs(packetId, catalogTrackName);
18191
18237
  }
18238
+ /** See FecReleaseSequencer.rapAbove; false when no stream is FEC-gated. */
18239
+ fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
18240
+ return this.#fecSequencer?.rapAbove(packetId, catalogTrackName, ssId) ?? false;
18241
+ }
18192
18242
  /** Re-evaluate held gaps after FEC block state changed. */
18193
18243
  pokeFecRelease(packetId, catalogTrackName) {
18194
18244
  if (!this.#fecSequencer) return;
@@ -19459,6 +19509,10 @@ var NO_BLOCK_SPAN_HORIZON = Object.freeze({
19459
19509
  reason: "no-block-span"
19460
19510
  });
19461
19511
  var DIRECT_RECOVERED_VIDEO_QUEUE_LIMIT = 512;
19512
+ var ASSIST_BLOCK_LIMIT = 64;
19513
+ var ASSIST_FAILURE_LEDGER_LIMIT = 4 * ASSIST_BLOCK_LIMIT;
19514
+ var ASSIST_FAILED_DECODES_MAX = 2;
19515
+ var assistKey = (ownerPacketId, catalogTrackName, blockStart) => `${ownerPacketId}/${catalogTrackName ?? ""}/${blockStart}`;
19462
19516
  var DIRECT_RECOVERED_AUDIO_QUEUE_LIMIT = 512;
19463
19517
  function sourceSymbolEvidence(packet, header, rapFlag, packetSequenceNumber) {
19464
19518
  const identity = MmtpContainerParser.sourcePacketIdentity(packet, header);
@@ -19483,6 +19537,12 @@ var MmtpRouter = class {
19483
19537
  #provenance = /* @__PURE__ */ new Map();
19484
19538
  #onFlowProtected = null;
19485
19539
  #unadmittedListedDrops = 0;
19540
+ #unlistedRepairSetAside = 0;
19541
+ #assistBlocks = /* @__PURE__ */ new Map();
19542
+ #assistFailures = /* @__PURE__ */ new Map();
19543
+ #scratchDecoderFactory = null;
19544
+ #unlistedAssistedRecoveries = 0;
19545
+ #unlistedAssistedRefusals = 0;
19486
19546
  /** The ingress a held datagram arrived with; the receiver returns the same bytes on release. */
19487
19547
  #heldIngress = /* @__PURE__ */ new WeakMap();
19488
19548
  #unicastRepair = false;
@@ -19575,6 +19635,7 @@ var MmtpRouter = class {
19575
19635
  #onBlockFailed = null;
19576
19636
  /** Typed observer of every gap decision the release gate makes. */
19577
19637
  #onFrameGap;
19638
+ #fecRapObservers = /* @__PURE__ */ new Set();
19578
19639
  // Pokes requested while a packet is being processed are deferred to its
19579
19640
  // end: a synchronous recovery completes its block BEFORE routing the
19580
19641
  // recovered frames, and releasing a held gap in between would strand them.
@@ -20198,14 +20259,13 @@ var MmtpRouter = class {
20198
20259
  updateProvenanceFlowIngress(ingress) {
20199
20260
  const entry = this.#provenance.get(ingress.flow);
20200
20261
  if (!entry) return;
20262
+ const added = entry.protected ? ingress.tracks.filter(({ name, packetId }) => name && !entry.ingress.tracks.some((t) => t.name === name && t.packetId === packetId)) : [];
20263
+ const governed = added.map(({ packetId }) => [...this.#repairOwnersByPacketId.get(packetId) ?? [packetId]].some((owner) => this.#governs(owner)));
20201
20264
  this.#provenance.set(ingress.flow, { ...entry, ingress });
20202
- if (entry.protected) {
20203
- for (const { name, packetId } of ingress.tracks) {
20204
- if (name && !entry.ingress.tracks.some((t) => t.name === name && t.packetId === packetId)) {
20205
- this.#fecManager?.resetTrack?.(packetId, name);
20206
- }
20207
- }
20208
- }
20265
+ added.forEach(({ name, packetId }, i) => {
20266
+ if (governed[i]) for (const block of this.#assistBlocks.values()) block.pooled.clear();
20267
+ else this.#fecManager?.resetTrack?.(packetId, name);
20268
+ });
20209
20269
  }
20210
20270
  /**
20211
20271
  * Called once per protection of a flow: at the admission that makes this
@@ -20226,11 +20286,154 @@ var MmtpRouter = class {
20226
20286
  if (!entry) return;
20227
20287
  entry.receiver.release();
20228
20288
  entry.protected = false;
20289
+ this.#assistBlocks.clear();
20290
+ this.#assistFailures.clear();
20229
20291
  }
20230
20292
  /** MoQ packets of a track a protected flow lists, dropped on its unicast path: duplicate, overflow, or listed by more than one flow. */
20231
20293
  get unadmittedListedDropCount() {
20232
20294
  return this.#unadmittedListedDrops;
20233
20295
  }
20296
+ /** MoQ repair symbols of an unlisted track kept out of a governed FEC instance (7.2.7 step 2). */
20297
+ get unlistedRepairSetAsideCount() {
20298
+ return this.#unlistedRepairSetAside;
20299
+ }
20300
+ /** Governed blocks recovered with the help of unlisted repair (7.2.7 step 2). */
20301
+ get unlistedAssistedRecoveryCount() {
20302
+ return this.#unlistedAssistedRecoveries;
20303
+ }
20304
+ /** Unlisted-assisted decodes refused because two already failed for the block (7.2.7 step 2). */
20305
+ get unlistedAssistedRefusalCount() {
20306
+ return this.#unlistedAssistedRefusals;
20307
+ }
20308
+ /** Unlisted repair symbols pooled now, across every governed block (7.2.7 step 2). */
20309
+ get unlistedPoolSymbolCount() {
20310
+ let n = 0;
20311
+ for (const block of this.#assistBlocks.values()) n += block.pooled.size;
20312
+ return n;
20313
+ }
20314
+ /**
20315
+ * Wire the decoder an unlisted-assisted decode runs in: a fresh, configured
20316
+ * decoder of the owner's track, used once and discarded, never the live one.
20317
+ * Without it unlisted repair on a governed instance is set aside and dropped.
20318
+ */
20319
+ setScratchFecDecoderFactory(factory) {
20320
+ this.#scratchDecoderFactory = factory;
20321
+ if (!factory) {
20322
+ this.#assistBlocks.clear();
20323
+ this.#assistFailures.clear();
20324
+ }
20325
+ }
20326
+ /** Whether a protected flow lists `ownerPacketId`: its FEC instance is governed (7.2.7). */
20327
+ #governs(ownerPacketId) {
20328
+ for (const entry of this.#provenance.values()) {
20329
+ if (entry.protected && ingressHasPacketId(entry.ingress, ownerPacketId)) return true;
20330
+ }
20331
+ return false;
20332
+ }
20333
+ /**
20334
+ * A MoQ repair symbol no protected flow lists, owned by a source track one
20335
+ * does: its FEC instance is governed (7.2.7 step 2) and takes only admitted
20336
+ * symbols, so the symbol is set aside: pooled for an unlisted-assisted
20337
+ * decode of a block admitted repair already delimits, else dropped. It
20338
+ * never states a block, so the live locator and decoder are unchanged.
20339
+ * Un-listing needs no hook: an unprotected or released flow lists nothing.
20340
+ */
20341
+ #setAsideUnlistedRepair(ownerPacketId, catalogTrackName, repair) {
20342
+ if (!this.#governs(ownerPacketId)) return false;
20343
+ this.#unlistedRepairSetAside++;
20344
+ const key = assistKey(ownerPacketId, catalogTrackName, repair.ssStart);
20345
+ const block = this.#assistBlocks.get(key);
20346
+ if (block !== void 0 && block.ssbLength === repair.ssbLength && !block.repairs.has(repair.rsId) && block.pooled.size < block.ssbLength && repair.symbolData.byteLength === this.#symbolSizeForPacket(ownerPacketId) && this.#fecManager?.admitRepairBlockLength?.(ownerPacketId, repair.ssbLength, repair.rsbLength, repair.rsId) !== false) {
20347
+ block.pooled.set(repair.rsId, repair.symbolData);
20348
+ this.#tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, repair.ssStart);
20349
+ }
20350
+ return true;
20351
+ }
20352
+ /**
20353
+ * An admitted repair placed a governed block (its SSB_length delimits it):
20354
+ * open the block's record, which the symbols its live decoder takes join.
20355
+ */
20356
+ #openAssistBlock(ownerPacketId, catalogTrackName, blockStart, ssbLength) {
20357
+ const key = assistKey(ownerPacketId, catalogTrackName, blockStart);
20358
+ if (!this.#scratchDecoderFactory || this.#assistBlocks.has(key) || !this.#governs(ownerPacketId)) return;
20359
+ this.#assistBlocks.set(key, { ssbLength, sources: /* @__PURE__ */ new Map(), repairs: /* @__PURE__ */ new Map(), pooled: /* @__PURE__ */ new Map() });
20360
+ for (const oldest of this.#assistBlocks.keys()) {
20361
+ if (this.#assistBlocks.size <= ASSIST_BLOCK_LIMIT) break;
20362
+ this.#assistBlocks.delete(oldest);
20363
+ }
20364
+ }
20365
+ /** A symbol fed to an open governed block's live decoder that did not recover it. */
20366
+ #noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, symbol, data) {
20367
+ const key = assistKey(ownerPacketId, catalogTrackName, blockStart);
20368
+ const block = this.#assistBlocks.get(key);
20369
+ if (block === void 0) return;
20370
+ if ("rsId" in symbol) {
20371
+ block.repairs.set(symbol.rsId, data);
20372
+ block.pooled.delete(symbol.rsId);
20373
+ } else {
20374
+ block.sources.set(symbol.esi, data);
20375
+ }
20376
+ this.#tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, blockStart);
20377
+ }
20378
+ /**
20379
+ * 7.2.7 step 2, phase two: a governed block its admitted symbols cannot
20380
+ * decode (the live decoder already had them all) is decoded in a scratch
20381
+ * decoder from its admitted symbols plus pooled unlisted repair, only as
20382
+ * many as close the shortfall. What it recovers is released only through
20383
+ * admission (#routeRecoveredBlock), so the live decoder is never touched and
20384
+ * a refused recovery leaves the block to fail at its FEC deadline. At most
20385
+ * two such decodes may fail per block; a third is refused.
20386
+ */
20387
+ #tryAssistedDecode(key, block, ownerPacketId, catalogTrackName, blockStart) {
20388
+ const fec = this.#fecManager;
20389
+ const admitted = block.sources.size + block.repairs.size;
20390
+ if (!fec || block.pooled.size === 0) return;
20391
+ if (admitted >= block.ssbLength || admitted + block.pooled.size < block.ssbLength) return;
20392
+ if (fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName)) {
20393
+ this.#assistBlocks.delete(key);
20394
+ return;
20395
+ }
20396
+ if ((this.#assistFailures.get(key) ?? 0) >= ASSIST_FAILED_DECODES_MAX) {
20397
+ this.#unlistedAssistedRefusals++;
20398
+ return;
20399
+ }
20400
+ const decoder = this.#scratchDecoderFactory?.(ownerPacketId, catalogTrackName);
20401
+ if (!decoder) return;
20402
+ let recovered = null;
20403
+ const fed = [];
20404
+ try {
20405
+ for (const [esi, data] of block.sources) decoder.add_source(serial32Add(blockStart, esi), data, 0);
20406
+ for (const [rsId, data] of block.repairs) {
20407
+ recovered ??= decoder.add_repair(blockStart, block.ssbLength, rsId, data, 0);
20408
+ }
20409
+ for (const [rsId, data] of block.pooled) {
20410
+ if (recovered || fed.length >= block.ssbLength - admitted) break;
20411
+ fed.push(rsId);
20412
+ recovered = decoder.add_repair(blockStart, block.ssbLength, rsId, data, 0);
20413
+ }
20414
+ } catch {
20415
+ recovered = null;
20416
+ } finally {
20417
+ decoder.free?.();
20418
+ }
20419
+ if (recovered && this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
20420
+ fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
20421
+ fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
20422
+ fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
20423
+ })) {
20424
+ this.#unlistedAssistedRecoveries++;
20425
+ this.#assistBlocks.delete(key);
20426
+ return;
20427
+ }
20428
+ for (const rsId of fed) block.pooled.delete(rsId);
20429
+ const failed = (this.#assistFailures.get(key) ?? 0) + 1;
20430
+ this.#assistFailures.delete(key);
20431
+ this.#assistFailures.set(key, failed);
20432
+ for (const oldest of this.#assistFailures.keys()) {
20433
+ if (this.#assistFailures.size <= ASSIST_FAILURE_LEDGER_LIMIT) break;
20434
+ this.#assistFailures.delete(oldest);
20435
+ }
20436
+ }
20234
20437
  /**
20235
20438
  * The admission that makes this router protected under `flow` (7.2.7):
20236
20439
  * its FEC instances discard what they hold of the flow's listed tracks so
@@ -20406,8 +20609,9 @@ var MmtpRouter = class {
20406
20609
  );
20407
20610
  }
20408
20611
  if (ownerPacketId != null && !admitted) {
20409
- const admit = this.#admitUnicast(ownerPacketId, packet, () => this.#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName, true));
20612
+ const admit = this.#admitUnicast(repairPacketId, packet, () => this.#feedRepairPacket(packet, ownerPacketIdHint, catalogTrackName, repairTrackName, true));
20410
20613
  if (admit === "held" || admit === "dropped") return;
20614
+ if (admit === "unlisted" && this.#fecManager && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackName, parsed)) return;
20411
20615
  }
20412
20616
  if (ownerPacketId != null && this.#fecManager) {
20413
20617
  this.#moqRepairPidCounts[repairPacketId] = (this.#moqRepairPidCounts[repairPacketId] || 0) + 1;
@@ -20527,6 +20731,7 @@ var MmtpRouter = class {
20527
20731
  const placement = fec.placeIsoRepair(ownerPacketId, catalogTrackName, repair.ssStart, repair.ssbLength);
20528
20732
  if (placement.status !== "placed") return;
20529
20733
  const blockStart = placement.block.start;
20734
+ this.#openAssistBlock(ownerPacketId, catalogTrackName, blockStart, repair.ssbLength);
20530
20735
  if (placement.span !== void 0) {
20531
20736
  this.#replayIsoSources(ownerPacketId, catalogTrackName, placement.span.block.start, placement.span.placed);
20532
20737
  }
@@ -20557,6 +20762,8 @@ var MmtpRouter = class {
20557
20762
  fec.countRecovery(ownerPacketId, blockStart, catalogTrackName);
20558
20763
  fec.markBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
20559
20764
  });
20765
+ } else if (!recovered) {
20766
+ this.#noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, { rsId: repair.rsId }, repair.symbolData);
20560
20767
  }
20561
20768
  }
20562
20769
  /**
@@ -20596,6 +20803,7 @@ var MmtpRouter = class {
20596
20803
  if (payload.oversized) continue;
20597
20804
  const alreadyCompleted = fec.isBlockCompleted(blockStart, ownerPacketId, catalogTrackName);
20598
20805
  const recovered = fec.feedSourceSymbol(ownerPacketId, ssId, payload.symbolData, catalogTrackName);
20806
+ if (!recovered) this.#noteAssistAdmitted(ownerPacketId, catalogTrackName, blockStart, { esi }, payload.symbolData);
20599
20807
  if (!recovered || alreadyCompleted) continue;
20600
20808
  this.#routeRecoveredBlock(blockStart, recovered, ownerPacketId, catalogTrackName, () => {
20601
20809
  fec.cancelBlockTimeout(blockStart, ownerPacketId, catalogTrackName);
@@ -20666,6 +20874,9 @@ var MmtpRouter = class {
20666
20874
  }
20667
20875
  }
20668
20876
  this.#parser.resetTrack(packetId, catalogTrackName);
20877
+ const assistPrefix = assistKey(packetId, catalogTrackName, 0).slice(0, -1);
20878
+ for (const key of this.#assistBlocks.keys()) if (key.startsWith(assistPrefix)) this.#assistBlocks.delete(key);
20879
+ for (const key of this.#assistFailures.keys()) if (key.startsWith(assistPrefix)) this.#assistFailures.delete(key);
20669
20880
  const streamKey = this.#mediaStreamKey(packetId, catalogTrackName);
20670
20881
  this.#trackSynced.delete(streamKey);
20671
20882
  this.#droppedPreSyncVideo.delete(streamKey);
@@ -21083,6 +21294,24 @@ var MmtpRouter = class {
21083
21294
  fecReleaseEarliestHeldAtMs(packetId, catalogTrackName) {
21084
21295
  return this.#parser.fecReleaseEarliestHeldAtMs(packetId, catalogTrackName);
21085
21296
  }
21297
+ /**
21298
+ * Whether a complete RAP above SS_ID `ssId` entered this stream's FEC
21299
+ * release gate (see FecReleaseSequencer.rapAbove).
21300
+ */
21301
+ fecReleaseRapAbove(packetId, catalogTrackName, ssId) {
21302
+ return this.#parser.fecReleaseRapAbove(packetId, catalogTrackName, ssId);
21303
+ }
21304
+ /**
21305
+ * Observe each complete RAP entering an FEC release gate, with the SS_ID
21306
+ * its frame completed in. Called synchronously from the frame push: an
21307
+ * observer that drives FEC state must defer that work.
21308
+ */
21309
+ addFecRapObserver(observer) {
21310
+ this.#fecRapObservers.add(observer);
21311
+ return () => {
21312
+ this.#fecRapObservers.delete(observer);
21313
+ };
21314
+ }
21086
21315
  #requestFecPoke(packetId, catalogTrackName) {
21087
21316
  if (this.#disposed) return;
21088
21317
  if (this.#fecPokeDepth > 0) {
@@ -21128,6 +21357,10 @@ var MmtpRouter = class {
21128
21357
  this.#onFrameGap?.(decision);
21129
21358
  return decision;
21130
21359
  },
21360
+ isRap: (data, meta) => this.#isRapForGap(data, meta),
21361
+ onRap: (packetId, catalogTrackName, ssId) => {
21362
+ for (const observer of this.#fecRapObservers) observer(packetId, catalogTrackName, ssId);
21363
+ },
21131
21364
  onReleased: (data, meta) => {
21132
21365
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
21133
21366
  const timing = meta.authoredTiming;
@@ -21326,6 +21559,7 @@ var MmtpRouter = class {
21326
21559
  const ownerPacketId = this.#resolveRepairOwnerPacketId(info.packetId, ingress);
21327
21560
  if (ownerPacketId != null && this.#fecManager) {
21328
21561
  if (hasAmbiguousCatalogTrackName(ingress, ownerPacketId)) return;
21562
+ if (source === "moq" && !manifestAdmitted && this.#setAsideUnlistedRepair(ownerPacketId, catalogTrackNameForPacket(ingress, ownerPacketId), repair)) return;
21329
21563
  this.#repairPidCounts[info.packetId] = (this.#repairPidCounts[info.packetId] || 0) + 1;
21330
21564
  const count = this.#repairPidCounts[info.packetId];
21331
21565
  if (count <= 2 || count % 500 === 0) {
@@ -21493,6 +21727,15 @@ var MmtpRouter = class {
21493
21727
  sourceFec.symbolData,
21494
21728
  catalogTrackName
21495
21729
  );
21730
+ if (!recovered) {
21731
+ this.#noteAssistAdmitted(
21732
+ info.packetId,
21733
+ catalogTrackName,
21734
+ sourceFec.sbn,
21735
+ { esi: serial32Distance(sourceFec.sbn, sourceFec.ssId) },
21736
+ sourceFec.symbolData
21737
+ );
21738
+ }
21496
21739
  if (recovered && !alreadyCompleted) {
21497
21740
  const fec = this.#fecManager;
21498
21741
  const sbn = sourceFec.sbn;
@@ -26566,6 +26809,8 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
26566
26809
  repairsSkippedPresentationDelayUnavailable: 0,
26567
26810
  repairsSkippedRangeNoAnchor: 0,
26568
26811
  repairsSkippedGroupCannotComplete: 0,
26812
+ repairsSkippedMootRap: 0,
26813
+ repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0 },
26569
26814
  lastRepairSkip: null,
26570
26815
  keyframeRepairFetchesSent: 0,
26571
26816
  rangeRepairFetchesSent: 0,
@@ -26652,7 +26897,17 @@ var REPAIR_SKIP_REASONS = /* @__PURE__ */ new Set([
26652
26897
  "rtt-exceeds-budget",
26653
26898
  "presentation-delay-unavailable",
26654
26899
  "range-no-anchor",
26655
- "group-cannot-complete"
26900
+ "group-cannot-complete",
26901
+ "moot-rap"
26902
+ ]);
26903
+ var REPAIR_STOP_REASONS = /* @__PURE__ */ new Set([
26904
+ "preempted",
26905
+ "reprioritized",
26906
+ "expired",
26907
+ "resolved",
26908
+ "reset",
26909
+ "disposed",
26910
+ "moot"
26656
26911
  ]);
26657
26912
  var REPAIR_DEADLINE_BOUNDS = /* @__PURE__ */ new Set(["wait", "presentation", "keyframe", "retention"]);
26658
26913
  var GAP_REASONS = {
@@ -26711,6 +26966,8 @@ function isRepairLifecycleEvent(value) {
26711
26966
  ]) && isRepairBlockIdentity(block) && (value["priority"] === 0 || value["priority"] === 1 || value["priority"] === 2) && isFiniteNumber(value["rttMs"]) && value["rttMs"] > 0 && isFiniteNumber(value["budgetMs"]) && value["budgetMs"] > value["rttMs"] && isNullableNonNegative(value["delayMs"]) && isNullableNonNegative(value["blockAgeMs"]) && value["delayMs"] === null === (value["blockAgeMs"] === null) && REPAIR_DEADLINE_BOUNDS.has(value["bound"]);
26712
26967
  case "repair-skipped":
26713
26968
  return hasExactKeys(value, ["version", "type", "block", "reason", "rttMs", "budgetMs"]) && isRepairBlockIdentity(block) && REPAIR_SKIP_REASONS.has(value["reason"]) && isNullableNonNegative(value["rttMs"]) && isFiniteNumber(value["budgetMs"]);
26969
+ case "repair-stopped":
26970
+ return hasExactKeys(value, ["version", "type", "block", "reason"]) && isRepairBlockIdentity(block) && REPAIR_STOP_REASONS.has(value["reason"]);
26714
26971
  case "repair-priority": {
26715
26972
  const blockClass = value["blockClass"];
26716
26973
  const action = value["action"];
@@ -28120,6 +28377,14 @@ var MulticastManager = class {
28120
28377
  }
28121
28378
  }
28122
28379
  }
28380
+ const factory = typeof wasmDecoderSource === "function" ? wasmDecoderSource : null;
28381
+ const fecTracks = fecTrackIndexes.map((i) => catalog.tracks[i]);
28382
+ this.#router.setScratchFecDecoderFactory(factory && ((ownerPacketId, catalogTrackName) => {
28383
+ const track = fecTracks.find((t) => t.packetId === ownerPacketId && t.name === catalogTrackName);
28384
+ if (!track) return null;
28385
+ const decoder = factory(track);
28386
+ return [...fecDecoders.values()].includes(decoder) ? null : decoder;
28387
+ }));
28123
28388
  this.#switchingSets.clear();
28124
28389
  this.#repairPacketIds.clear();
28125
28390
  this.#router.clearRepairPacketIdOwners();
@@ -28293,6 +28558,7 @@ var MulticastManager = class {
28293
28558
  else if (reason === "rtt-exceeds-budget") stats.repairsSkippedRttExceedsBudget++;
28294
28559
  else if (reason === "range-no-anchor") stats.repairsSkippedRangeNoAnchor++;
28295
28560
  else if (reason === "group-cannot-complete") stats.repairsSkippedGroupCannotComplete++;
28561
+ else if (reason === "moot-rap") stats.repairsSkippedMootRap++;
28296
28562
  else stats.repairsSkippedPresentationDelayUnavailable++;
28297
28563
  stats.lastRepairSkip = {
28298
28564
  reason,
@@ -28353,6 +28619,27 @@ var MulticastManager = class {
28353
28619
  block: repairBlockIdentity(packetId, sbn, catalogTrackName)
28354
28620
  });
28355
28621
  }
28622
+ /** Record a running repair FETCH the scheduler stopped, and why. */
28623
+ noteRepairStopped(reason, sbn, packetId, catalogTrackName) {
28624
+ this.#statsFor(packetId, catalogTrackName).repairStops[reason]++;
28625
+ this.#emitRepairLifecycle({
28626
+ version: 2,
28627
+ type: "repair-stopped",
28628
+ block: repairBlockIdentity(packetId, sbn, catalogTrackName),
28629
+ reason
28630
+ });
28631
+ }
28632
+ /**
28633
+ * Whether a complete RAP above SS_ID `ssId` reached the stream's in-order
28634
+ * release gate (draft-ramadan-moq-fec 11.4.2: repair below it is moot).
28635
+ */
28636
+ rapCompleteAbove(packetId, catalogTrackName, ssId) {
28637
+ return this.#router.fecReleaseRapAbove(packetId, catalogTrackName, ssId);
28638
+ }
28639
+ /** Observe each complete RAP reaching a release gate (synchronous: defer FEC work). */
28640
+ addRapCompleteObserver(observer) {
28641
+ return this.#router.addFecRapObserver(observer);
28642
+ }
28356
28643
  /** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
28357
28644
  epochFor(packetId) {
28358
28645
  return this.#fec.epochFor(packetId);
@@ -28368,6 +28655,7 @@ var MulticastManager = class {
28368
28655
  if (stats.epoch !== this.#fec.epochFor(stats.packetId)) continue;
28369
28656
  out[key] = {
28370
28657
  ...stats,
28658
+ repairStops: { ...stats.repairStops },
28371
28659
  lastRepairSkip: stats.lastRepairSkip === null ? null : { ...stats.lastRepairSkip }
28372
28660
  };
28373
28661
  }
@@ -30731,6 +31019,34 @@ function wireRepairFetch(mc, opts) {
30731
31019
  run.stopped = reason;
30732
31020
  run.controller.abort();
30733
31021
  if (reason === "preempted") releaseHold(entry);
31022
+ mc.noteRepairStopped?.(reason, entry.sbn, entry.packetId, entry.catalogTrackName);
31023
+ };
31024
+ const lastSsId = (entry) => {
31025
+ if (entry.range) return entry.sbn + entry.range.holeLength - 1;
31026
+ const length = symbolCounts(entry.sbn, entry.packetId, entry.catalogTrackName)?.authoredLength;
31027
+ return length === void 0 ? void 0 : entry.sbn + length - 1;
31028
+ };
31029
+ const moot = (entry) => {
31030
+ if (entry.priority !== 0) return false;
31031
+ const last = lastSsId(entry);
31032
+ if (last === void 0) return false;
31033
+ try {
31034
+ return mc.rapCompleteAbove?.(entry.packetId, entry.catalogTrackName, last) === true;
31035
+ } catch {
31036
+ return false;
31037
+ }
31038
+ };
31039
+ const skip = (entry, reason, rttMs, budgetMs) => {
31040
+ retire(entry);
31041
+ releaseHold(entry);
31042
+ clearEntry(entry);
31043
+ mc.noteRepairSkipped(
31044
+ reason,
31045
+ { rttMs, budgetMs, priority: entry.priority },
31046
+ entry.sbn,
31047
+ entry.packetId,
31048
+ entry.catalogTrackName
31049
+ );
30734
31050
  };
30735
31051
  const readKeyframeBudget = (packetId, catalogTrackName) => {
30736
31052
  const reader = opts.keyframeRepairBudgetMs;
@@ -30755,31 +31071,22 @@ function wireRepairFetch(mc, opts) {
30755
31071
  const rttMs = readRtt();
30756
31072
  const reason = !entry.range && !groupCanComplete(entry.sbn, entry.packetId, entry.catalogTrackName) ? "group-cannot-complete" : rttMs === null ? "rtt-unmeasured" : rttMs < budgetMs ? null : "rtt-exceeds-budget";
30757
31073
  if (reason === null) return rttMs;
30758
- retire(entry);
30759
- releaseHold(entry);
30760
- clearEntry(entry);
30761
- mc.noteRepairSkipped(
30762
- reason,
30763
- { rttMs, budgetMs, priority: entry.priority },
30764
- entry.sbn,
30765
- entry.packetId,
30766
- entry.catalogTrackName
30767
- );
31074
+ skip(entry, reason, rttMs, budgetMs);
30768
31075
  return null;
30769
31076
  };
30770
31077
  const retire = (entry) => {
30771
31078
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
30772
31079
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
30773
31080
  };
30774
- const urgentOutstanding = () => {
31081
+ const outstandingBelow = (priority) => {
30775
31082
  for (const entry of entries.values()) {
30776
- if (entry.priority < 2) return true;
31083
+ if (entry.priority < priority) return true;
30777
31084
  }
30778
31085
  return false;
30779
31086
  };
30780
- const preemptDeltas = () => {
31087
+ const preemptBestEffort = () => {
30781
31088
  for (const entry of entries.values()) {
30782
- if (entry.priority !== 2 || !entry.running) continue;
31089
+ if (entry.priority === 0 || !entry.running) continue;
30783
31090
  stop(entry, "preempted");
30784
31091
  note("repair-preempted", entry);
30785
31092
  }
@@ -30799,8 +31106,13 @@ function wireRepairFetch(mc, opts) {
30799
31106
  };
30800
31107
  const start = (entry, counted = true) => {
30801
31108
  const now = performance.now();
31109
+ if (moot(entry)) {
31110
+ skip(entry, "moot-rap", readRtt(), entry.deadlineAt - now);
31111
+ return;
31112
+ }
30802
31113
  const rttMs = admit(entry, now);
30803
31114
  if (rttMs === null) return;
31115
+ if (entry.priority === 0) preemptBestEffort();
30804
31116
  if (counted) entry.attempts++;
30805
31117
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
30806
31118
  rttMs,
@@ -30844,9 +31156,13 @@ function wireRepairFetch(mc, opts) {
30844
31156
  queued.push(entry);
30845
31157
  }
30846
31158
  for (const entry of queued) {
30847
- if (entry.priority < 2) start(entry);
31159
+ if (entry.priority === 0) start(entry);
31160
+ }
31161
+ if (outstandingBelow(1)) return;
31162
+ for (const entry of queued) {
31163
+ if (entry.priority === 1) start(entry);
30848
31164
  }
30849
- if (urgentOutstanding()) return;
31165
+ if (outstandingBelow(2)) return;
30850
31166
  queued.filter((entry) => entry.priority === 2).sort((a, b) => a.deadlineAt - b.deadlineAt || a.order - b.order).forEach((entry) => start(entry));
30851
31167
  };
30852
31168
  const symbolCounts = (sbn, packetId, catalogTrackName) => {
@@ -30974,7 +31290,6 @@ function wireRepairFetch(mc, opts) {
30974
31290
  stop(entry2, "reprioritized");
30975
31291
  start(entry2, false);
30976
31292
  }
30977
- preemptDeltas();
30978
31293
  dispatch();
30979
31294
  return;
30980
31295
  }
@@ -31106,7 +31421,6 @@ function wireRepairFetch(mc, opts) {
31106
31421
  };
31107
31422
  entries.set(key, entry);
31108
31423
  noteBudget(entry, deadline.extensionMs);
31109
- if (entry.priority < 2) preemptDeltas();
31110
31424
  dispatch();
31111
31425
  });
31112
31426
  const rangeFor = (loss, repairTrack) => {
@@ -31182,15 +31496,28 @@ function wireRepairFetch(mc, opts) {
31182
31496
  }
31183
31497
  entries.set(entry.key, entry);
31184
31498
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
31185
- preemptDeltas();
31186
31499
  dispatch();
31187
31500
  });
31188
31501
  }
31502
+ const removeRapObserver = mc.addRapCompleteObserver?.((packetId) => {
31503
+ queueMicrotask(() => {
31504
+ if (disposed) return;
31505
+ for (const entry of [...entries.values()]) {
31506
+ if (entry.packetId !== packetId || !entry.running || !moot(entry)) continue;
31507
+ stop(entry, "moot");
31508
+ retire(entry);
31509
+ releaseHold(entry);
31510
+ clearEntry(entry);
31511
+ }
31512
+ dispatch();
31513
+ });
31514
+ });
31189
31515
  return () => {
31190
31516
  disposed = true;
31191
31517
  mc.setUnicastRepair?.(false);
31192
31518
  removeResetObserver();
31193
31519
  removeLifecycleObserver();
31520
+ removeRapObserver?.();
31194
31521
  for (const entry of [...entries.values()]) {
31195
31522
  clearEntry(entry);
31196
31523
  if (entry.running) releaseHold(entry);