@blockcast/mmt-container 0.5.0-main.ed11aec88d91 → 0.5.0

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 (43) hide show
  1. package/dist/browser-runtime.js +150 -21
  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.map +2 -2
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +2 -2
  8. package/dist/mmtp-container-parser.d.ts +10 -0
  9. package/dist/mmtp-container-parser.d.ts.map +1 -1
  10. package/dist/mmtp-container-parser.js +31 -1
  11. package/dist/mmtp-container-parser.js.map +2 -2
  12. package/dist/mmtp-container-parser.no-modules.js +31 -1
  13. package/dist/mmtp-container-parser.no-modules.js.map +2 -2
  14. package/dist/mmtp-decode-pipeline.js +31 -1
  15. package/dist/mmtp-decode-pipeline.js.map +2 -2
  16. package/dist/mmtp-router.d.ts +11 -0
  17. package/dist/mmtp-router.d.ts.map +1 -1
  18. package/dist/mmtp-router.js +54 -1
  19. package/dist/mmtp-router.js.map +2 -2
  20. package/dist/multicast-manager.d.ts +25 -1
  21. package/dist/multicast-manager.d.ts.map +1 -1
  22. package/dist/multicast-manager.js +92 -2
  23. package/dist/multicast-manager.js.map +2 -2
  24. package/dist/no-modules.js +31 -1
  25. package/dist/no-modules.js.map +2 -2
  26. package/dist/repair-fetch.d.ts +41 -10
  27. package/dist/repair-fetch.d.ts.map +1 -1
  28. package/dist/repair-fetch.js +58 -19
  29. package/dist/repair-fetch.js.map +2 -2
  30. package/dist/wasm-mmtp-container-parser.js +31 -1
  31. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  32. package/package.json +7 -6
  33. package/src/fec-manager.ts +3 -0
  34. package/src/index.ts +1 -0
  35. package/src/mmtp-container-parser.ts +48 -0
  36. package/src/mmtp-router.ts +27 -0
  37. package/src/multicast-manager.ts +68 -0
  38. package/src/repair-fetch.ts +131 -31
  39. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
  40. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
  41. package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
  42. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
  43. package/test-vectors/repair-policy-v1.json +113 -0
@@ -17655,6 +17655,9 @@ function authoredGap(before, after) {
17655
17655
  if (!a || !b || a.mediaTimescale !== b.mediaTimescale) return false;
17656
17656
  return b.decodeTime > a.decodeTime + BigInt(a.durationTicks);
17657
17657
  }
17658
+ function ssIdAfter(a, b) {
17659
+ return (a - b | 0) > 0;
17660
+ }
17658
17661
  var FecReleaseSequencer = class {
17659
17662
  #streams = /* @__PURE__ */ new Map();
17660
17663
  #gate;
@@ -17716,11 +17719,17 @@ var FecReleaseSequencer = class {
17716
17719
  stream.last = null;
17717
17720
  stream.lastSbn = void 0;
17718
17721
  stream.discontinuity = false;
17722
+ stream.rapSsId = void 0;
17719
17723
  }
17720
17724
  if (stream.last !== null && compareFrameOrder(frame, stream.last) <= 0) {
17721
17725
  this.#recordStat("droppedStale");
17722
17726
  return;
17723
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
+ }
17724
17733
  if (stream.last === null) {
17725
17734
  if (meta.trackType === "video" && !meta.keyframe) {
17726
17735
  this.#recordStat("droppedBeforeStartupKeyframe");
@@ -17756,6 +17765,22 @@ var FecReleaseSequencer = class {
17756
17765
  }
17757
17766
  return earliest;
17758
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
+ }
17759
17784
  /** Re-evaluate held gaps after FEC state changed. */
17760
17785
  poke(streamKey) {
17761
17786
  if (streamKey !== void 0) {
@@ -17846,7 +17871,8 @@ var FecReleaseSequencer = class {
17846
17871
  buf: [],
17847
17872
  last: null,
17848
17873
  lastSbn: void 0,
17849
- discontinuity: false
17874
+ discontinuity: false,
17875
+ rapSsId: void 0
17850
17876
  };
17851
17877
  this.#streams.set(streamKey, stream);
17852
17878
  }
@@ -18209,6 +18235,10 @@ var MmtpContainerParser = class _MmtpContainerParser {
18209
18235
  fecReleaseEarliestHeldAtMs(packetId, catalogTrackName) {
18210
18236
  return this.#fecSequencer?.earliestHeldArrivalMs(packetId, catalogTrackName);
18211
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
+ }
18212
18242
  /** Re-evaluate held gaps after FEC block state changed. */
18213
18243
  pokeFecRelease(packetId, catalogTrackName) {
18214
18244
  if (!this.#fecSequencer) return;
@@ -19605,6 +19635,7 @@ var MmtpRouter = class {
19605
19635
  #onBlockFailed = null;
19606
19636
  /** Typed observer of every gap decision the release gate makes. */
19607
19637
  #onFrameGap;
19638
+ #fecRapObservers = /* @__PURE__ */ new Set();
19608
19639
  // Pokes requested while a packet is being processed are deferred to its
19609
19640
  // end: a synchronous recovery completes its block BEFORE routing the
19610
19641
  // recovered frames, and releasing a held gap in between would strand them.
@@ -21263,6 +21294,24 @@ var MmtpRouter = class {
21263
21294
  fecReleaseEarliestHeldAtMs(packetId, catalogTrackName) {
21264
21295
  return this.#parser.fecReleaseEarliestHeldAtMs(packetId, catalogTrackName);
21265
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
+ }
21266
21315
  #requestFecPoke(packetId, catalogTrackName) {
21267
21316
  if (this.#disposed) return;
21268
21317
  if (this.#fecPokeDepth > 0) {
@@ -21308,6 +21357,10 @@ var MmtpRouter = class {
21308
21357
  this.#onFrameGap?.(decision);
21309
21358
  return decision;
21310
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
+ },
21311
21364
  onReleased: (data, meta) => {
21312
21365
  if (meta.trackType !== "video" || meta.mpuSequenceNumber === void 0) return;
21313
21366
  const timing = meta.authoredTiming;
@@ -26756,6 +26809,8 @@ function newBlockPolicyStats(packetId, catalogTrackName, epoch) {
26756
26809
  repairsSkippedPresentationDelayUnavailable: 0,
26757
26810
  repairsSkippedRangeNoAnchor: 0,
26758
26811
  repairsSkippedGroupCannotComplete: 0,
26812
+ repairsSkippedMootRap: 0,
26813
+ repairStops: { preempted: 0, reprioritized: 0, expired: 0, resolved: 0, reset: 0, disposed: 0, moot: 0 },
26759
26814
  lastRepairSkip: null,
26760
26815
  keyframeRepairFetchesSent: 0,
26761
26816
  rangeRepairFetchesSent: 0,
@@ -26842,7 +26897,17 @@ var REPAIR_SKIP_REASONS = /* @__PURE__ */ new Set([
26842
26897
  "rtt-exceeds-budget",
26843
26898
  "presentation-delay-unavailable",
26844
26899
  "range-no-anchor",
26845
- "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"
26846
26911
  ]);
26847
26912
  var REPAIR_DEADLINE_BOUNDS = /* @__PURE__ */ new Set(["wait", "presentation", "keyframe", "retention"]);
26848
26913
  var GAP_REASONS = {
@@ -26901,6 +26966,8 @@ function isRepairLifecycleEvent(value) {
26901
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"]);
26902
26967
  case "repair-skipped":
26903
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"]);
26904
26971
  case "repair-priority": {
26905
26972
  const blockClass = value["blockClass"];
26906
26973
  const action = value["action"];
@@ -28491,6 +28558,7 @@ var MulticastManager = class {
28491
28558
  else if (reason === "rtt-exceeds-budget") stats.repairsSkippedRttExceedsBudget++;
28492
28559
  else if (reason === "range-no-anchor") stats.repairsSkippedRangeNoAnchor++;
28493
28560
  else if (reason === "group-cannot-complete") stats.repairsSkippedGroupCannotComplete++;
28561
+ else if (reason === "moot-rap") stats.repairsSkippedMootRap++;
28494
28562
  else stats.repairsSkippedPresentationDelayUnavailable++;
28495
28563
  stats.lastRepairSkip = {
28496
28564
  reason,
@@ -28551,6 +28619,27 @@ var MulticastManager = class {
28551
28619
  block: repairBlockIdentity(packetId, sbn, catalogTrackName)
28552
28620
  });
28553
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
+ }
28554
28643
  /** Catalog/reset epoch of a packet's streams; gap decisions carry it. */
28555
28644
  epochFor(packetId) {
28556
28645
  return this.#fec.epochFor(packetId);
@@ -28566,6 +28655,7 @@ var MulticastManager = class {
28566
28655
  if (stats.epoch !== this.#fec.epochFor(stats.packetId)) continue;
28567
28656
  out[key] = {
28568
28657
  ...stats,
28658
+ repairStops: { ...stats.repairStops },
28569
28659
  lastRepairSkip: stats.lastRepairSkip === null ? null : { ...stats.lastRepairSkip }
28570
28660
  };
28571
28661
  }
@@ -30929,6 +31019,34 @@ function wireRepairFetch(mc, opts) {
30929
31019
  run.stopped = reason;
30930
31020
  run.controller.abort();
30931
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
+ );
30932
31050
  };
30933
31051
  const readKeyframeBudget = (packetId, catalogTrackName) => {
30934
31052
  const reader = opts.keyframeRepairBudgetMs;
@@ -30953,31 +31071,22 @@ function wireRepairFetch(mc, opts) {
30953
31071
  const rttMs = readRtt();
30954
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";
30955
31073
  if (reason === null) return rttMs;
30956
- retire(entry);
30957
- releaseHold(entry);
30958
- clearEntry(entry);
30959
- mc.noteRepairSkipped(
30960
- reason,
30961
- { rttMs, budgetMs, priority: entry.priority },
30962
- entry.sbn,
30963
- entry.packetId,
30964
- entry.catalogTrackName
30965
- );
31074
+ skip(entry, reason, rttMs, budgetMs);
30966
31075
  return null;
30967
31076
  };
30968
31077
  const retire = (entry) => {
30969
31078
  if (entries.get(entry.key) === entry) entries.delete(entry.key);
30970
31079
  if (entry.expiry !== void 0) clearTimeout(entry.expiry);
30971
31080
  };
30972
- const urgentOutstanding = () => {
31081
+ const outstandingBelow = (priority) => {
30973
31082
  for (const entry of entries.values()) {
30974
- if (entry.priority < 2) return true;
31083
+ if (entry.priority < priority) return true;
30975
31084
  }
30976
31085
  return false;
30977
31086
  };
30978
- const preemptDeltas = () => {
31087
+ const preemptBestEffort = () => {
30979
31088
  for (const entry of entries.values()) {
30980
- if (entry.priority !== 2 || !entry.running) continue;
31089
+ if (entry.priority === 0 || !entry.running) continue;
30981
31090
  stop(entry, "preempted");
30982
31091
  note("repair-preempted", entry);
30983
31092
  }
@@ -30997,8 +31106,13 @@ function wireRepairFetch(mc, opts) {
30997
31106
  };
30998
31107
  const start = (entry, counted = true) => {
30999
31108
  const now = performance.now();
31109
+ if (moot(entry)) {
31110
+ skip(entry, "moot-rap", readRtt(), entry.deadlineAt - now);
31111
+ return;
31112
+ }
31000
31113
  const rttMs = admit(entry, now);
31001
31114
  if (rttMs === null) return;
31115
+ if (entry.priority === 0) preemptBestEffort();
31002
31116
  if (counted) entry.attempts++;
31003
31117
  mc.noteRepairFetchSent?.(entry.priority, entry.sbn, entry.packetId, entry.catalogTrackName, {
31004
31118
  rttMs,
@@ -31042,9 +31156,13 @@ function wireRepairFetch(mc, opts) {
31042
31156
  queued.push(entry);
31043
31157
  }
31044
31158
  for (const entry of queued) {
31045
- 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);
31046
31164
  }
31047
- if (urgentOutstanding()) return;
31165
+ if (outstandingBelow(2)) return;
31048
31166
  queued.filter((entry) => entry.priority === 2).sort((a, b) => a.deadlineAt - b.deadlineAt || a.order - b.order).forEach((entry) => start(entry));
31049
31167
  };
31050
31168
  const symbolCounts = (sbn, packetId, catalogTrackName) => {
@@ -31172,7 +31290,6 @@ function wireRepairFetch(mc, opts) {
31172
31290
  stop(entry2, "reprioritized");
31173
31291
  start(entry2, false);
31174
31292
  }
31175
- preemptDeltas();
31176
31293
  dispatch();
31177
31294
  return;
31178
31295
  }
@@ -31304,7 +31421,6 @@ function wireRepairFetch(mc, opts) {
31304
31421
  };
31305
31422
  entries.set(key, entry);
31306
31423
  noteBudget(entry, deadline.extensionMs);
31307
- if (entry.priority < 2) preemptDeltas();
31308
31424
  dispatch();
31309
31425
  });
31310
31426
  const rangeFor = (loss, repairTrack) => {
@@ -31380,15 +31496,28 @@ function wireRepairFetch(mc, opts) {
31380
31496
  }
31381
31497
  entries.set(entry.key, entry);
31382
31498
  entry.expiry = setTimeout(dispatch, Math.max(0, entry.deadlineAt - performance.now()) + 1);
31383
- preemptDeltas();
31384
31499
  dispatch();
31385
31500
  });
31386
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
+ });
31387
31515
  return () => {
31388
31516
  disposed = true;
31389
31517
  mc.setUnicastRepair?.(false);
31390
31518
  removeResetObserver();
31391
31519
  removeLifecycleObserver();
31520
+ removeRapObserver?.();
31392
31521
  for (const entry of [...entries.values()]) {
31393
31522
  clearEntry(entry);
31394
31523
  if (entry.running) releaseHold(entry);