@blockcast/mmt-container 0.5.0 → 1.0.0-main.3cb7359ea316

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 (136) hide show
  1. package/README.md +21 -16
  2. package/dist/browser-runtime.js +1024 -1078
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/carp-catalog.d.ts +33 -87
  5. package/dist/carp-catalog.d.ts.map +1 -1
  6. package/dist/carp-catalog.js +12 -434
  7. package/dist/carp-catalog.js.map +4 -4
  8. package/dist/catalog-fec-params.d.ts +48 -161
  9. package/dist/catalog-fec-params.d.ts.map +1 -1
  10. package/dist/catalog-fec-params.js +567 -592
  11. package/dist/catalog-fec-params.js.map +4 -4
  12. package/dist/catalog-network-source.js +6 -2
  13. package/dist/catalog-network-source.js.map +3 -3
  14. package/dist/delivery-catalog.js +12 -4
  15. package/dist/delivery-catalog.js.map +4 -4
  16. package/dist/delta-fallback-config.d.ts +21 -26
  17. package/dist/delta-fallback-config.d.ts.map +1 -1
  18. package/dist/delta-fallback-config.js +582 -24
  19. package/dist/delta-fallback-config.js.map +4 -4
  20. package/dist/fec-manager.js +6 -2
  21. package/dist/fec-manager.js.map +3 -3
  22. package/dist/fec-state.d.ts +5 -43
  23. package/dist/fec-state.d.ts.map +1 -1
  24. package/dist/fec-state.js +529 -34
  25. package/dist/fec-state.js.map +4 -4
  26. package/dist/fec-worker-setup.js +6 -2
  27. package/dist/fec-worker-setup.js.map +3 -3
  28. package/dist/held-layer-admission.js +6 -2
  29. package/dist/held-layer-admission.js.map +3 -3
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +5 -39
  33. package/dist/index.js.map +2 -2
  34. package/dist/iso-block-locator.js +6 -2
  35. package/dist/iso-block-locator.js.map +3 -3
  36. package/dist/mfu-reassembler.js +6 -2
  37. package/dist/mfu-reassembler.js.map +3 -3
  38. package/dist/mmtp-container-parser.js +6 -2
  39. package/dist/mmtp-container-parser.js.map +3 -3
  40. package/dist/mmtp-container-parser.no-modules.js +551 -452
  41. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  42. package/dist/mmtp-decode-pipeline.js +6 -2
  43. package/dist/mmtp-decode-pipeline.js.map +3 -3
  44. package/dist/mmtp-fec-session.d.ts +7 -6
  45. package/dist/mmtp-fec-session.d.ts.map +1 -1
  46. package/dist/mmtp-fec-session.js +13 -9
  47. package/dist/mmtp-fec-session.js.map +3 -3
  48. package/dist/mmtp-router.d.ts +7 -16
  49. package/dist/mmtp-router.d.ts.map +1 -1
  50. package/dist/mmtp-router.js +19 -29
  51. package/dist/mmtp-router.js.map +3 -3
  52. package/dist/multicast-auth-catalog.js +7 -3
  53. package/dist/multicast-auth-catalog.js.map +3 -3
  54. package/dist/multicast-input.js +7 -3
  55. package/dist/multicast-input.js.map +3 -3
  56. package/dist/multicast-manager.d.ts +29 -32
  57. package/dist/multicast-manager.d.ts.map +1 -1
  58. package/dist/multicast-manager.js +682 -253
  59. package/dist/multicast-manager.js.map +4 -4
  60. package/dist/multicast-transport.d.ts +23 -2
  61. package/dist/multicast-transport.d.ts.map +1 -1
  62. package/dist/multicast-transport.js +57 -16
  63. package/dist/multicast-transport.js.map +4 -4
  64. package/dist/multicast-unwrap.js +6 -2
  65. package/dist/multicast-unwrap.js.map +3 -3
  66. package/dist/no-modules.js +551 -452
  67. package/dist/no-modules.js.map +4 -4
  68. package/dist/phase14-catalog-override.d.ts +10 -5
  69. package/dist/phase14-catalog-override.d.ts.map +1 -1
  70. package/dist/phase14-catalog-override.js +35 -11
  71. package/dist/phase14-catalog-override.js.map +2 -2
  72. package/dist/repair-fetch.d.ts +3 -1
  73. package/dist/repair-fetch.d.ts.map +1 -1
  74. package/dist/repair-fetch.js +16 -3
  75. package/dist/repair-fetch.js.map +3 -3
  76. package/dist/repair-layers.d.ts +0 -123
  77. package/dist/repair-layers.d.ts.map +1 -1
  78. package/dist/repair-layers.js +1 -349
  79. package/dist/repair-layers.js.map +3 -3
  80. package/dist/signaling.js +6 -2
  81. package/dist/signaling.js.map +3 -3
  82. package/dist/timestamp-codec.js +6 -2
  83. package/dist/timestamp-codec.js.map +3 -3
  84. package/dist/wasm-mfu-reassembler.js +6 -2
  85. package/dist/wasm-mfu-reassembler.js.map +3 -3
  86. package/dist/wasm-mmtp-container-parser.js +6 -2
  87. package/dist/wasm-mmtp-container-parser.js.map +3 -3
  88. package/package.json +9 -5
  89. package/schemas/catalog.schema.json +53 -58
  90. package/src/carp-catalog.ts +39 -229
  91. package/src/catalog-fec-params.ts +130 -493
  92. package/src/delta-fallback-config.ts +63 -50
  93. package/src/fec-state.ts +7 -62
  94. package/src/index.ts +10 -30
  95. package/src/mmtp-fec-session.ts +11 -10
  96. package/src/mmtp-router.ts +14 -29
  97. package/src/multicast-manager.ts +89 -121
  98. package/src/multicast-transport.ts +65 -12
  99. package/src/phase14-catalog-override.ts +67 -16
  100. package/src/repair-fetch.ts +27 -3
  101. package/src/repair-layers.ts +7 -520
  102. package/test-vectors/catalog/README.md +39 -67
  103. package/test-vectors/catalog/fec-01-runtime/README.md +15 -0
  104. package/test-vectors/catalog/{negative/fec-block-profile-retired.json → fec-01-runtime/fec-iso-ssbg1.json} +1 -1
  105. package/test-vectors/catalog/fec-01-runtime/fec-layered-repair.json +106 -0
  106. package/test-vectors/catalog/{negative/fec-block-profile-unknown.json → fec-01-runtime/fec-multicast.json} +2 -1
  107. package/test-vectors/catalog/fec-01-runtime/multicast-native-ssm-no-network-source.json +44 -0
  108. package/test-vectors/catalog/negative/fec-block-profile-missing.json +24 -11
  109. package/test-vectors/catalog/negative/fec-iso-symbol-size-below-floor.json +13 -8
  110. package/test-vectors/catalog/negative/fec-reed-solomon-iso-symbol-size-13.json +13 -8
  111. package/test-vectors/catalog/negative/fec-repair-layer-geometry.json +20 -10
  112. package/test-vectors/catalog/negative/fec-repair-layer-missing-symbols.json +20 -10
  113. package/test-vectors/catalog/negative/fec-repair-overlay-source-symbols-zero.json +25 -17
  114. package/test-vectors/catalog/negative/fec-repair-symbols-zero.json +13 -8
  115. package/test-vectors/catalog/negative/fec-source-symbols-zero.json +13 -8
  116. package/test-vectors/catalog/negative/fec-symbol-size-zero.json +13 -8
  117. package/test-vectors/catalog/negative/raptorq-source-symbols-over-max.json +18 -9
  118. package/test-vectors/catalog/negative/raptorq-unaligned-symbol.json +13 -8
  119. package/test-vectors/catalog/positive/fec-algorithm-unimplemented.json +14 -9
  120. package/test-vectors/catalog/positive/fec-iso-ssbg1.json +25 -12
  121. package/test-vectors/catalog/positive/fec-layered-repair.json +79 -35
  122. package/test-vectors/catalog/positive/fec-multicast.json +26 -13
  123. package/test-vectors/catalog/positive/fec-reed-solomon-layered.json +29 -18
  124. package/test-vectors/catalog/positive/multicast-draft01-4-1-example.json +28 -20
  125. package/test-vectors/catalog/positive/multicast-draft01-7-2-6-example.json +14 -10
  126. package/test-vectors/catalog/positive/multicast-native-ssm-no-network-source.json +22 -12
  127. package/test-vectors/catalog/section-codes.json +13 -22
  128. package/test-vectors/catalog-timing.json +2 -2
  129. package/test-vectors/catalog-validation.json +30 -243
  130. package/test-vectors/catalog/negative/fec-enhancement-repair-removed.json +0 -28
  131. package/test-vectors/catalog/negative/fec-reed-solomon-k-plus-p-over-255.json +0 -39
  132. package/test-vectors/catalog/negative/fec-reed-solomon-overlay-over-255.json +0 -40
  133. package/test-vectors/catalog/negative/fec-repair-multiple-depends.json +0 -32
  134. package/test-vectors/catalog/negative/fec-repair-overlay-with-layer.json +0 -34
  135. package/test-vectors/catalog/negative/fec-repair-priority-out-of-band.json +0 -24
  136. package/test-vectors/catalog/negative/fec-repair-source-symbols-without-scope.json +0 -33
@@ -14,6 +14,10 @@
14
14
  * switching set without asking the publisher to change its catalog.
15
15
  * - Tracks present only in the override are appended as-is.
16
16
  * - Tracks present only in the MoQ catalog are preserved unchanged.
17
+ * - `fec` is the draft-ramadan-moq-fec-catalog-00 instance array; a repair
18
+ * track is `packaging: "fec-repair"` with `source` + `instanceId`, and each
19
+ * of base and delta names its own (one Track Name in two `repairTrack`
20
+ * members is no FEC on either, catalog-00 5 rule 2).
17
21
  *
18
22
  * This module is deliberately player-agnostic. It exposes:
19
23
  *
@@ -30,7 +34,7 @@
30
34
  * runtime unless the test harness opts in.
31
35
  */
32
36
 
33
- import type { FecAlgorithm, FecCatalogConfig } from "./carp-catalog.js";
37
+ import type { FecCatalogConfig } from "./carp-catalog.js";
34
38
 
35
39
  // ---------------------------------------------------------------------------
36
40
  // Override fixture shape (IETF draft-ietf-moq-catalogformat-01)
@@ -48,7 +52,10 @@ export interface Phase14OverrideTrack {
48
52
  name: string;
49
53
  /** CMSF packaging kind. Optional per track — when absent, the merge
50
54
  * derives it from the override's top-level `streamingFormat`. */
51
- packaging?: "mmtp" | "cmaf" | "loc" | "timeline";
55
+ packaging?: "mmtp" | "cmaf" | "loc" | "timeline" | "fec-repair";
56
+ /** A `fec-repair` track's source track and instance (catalog-00 3.2). */
57
+ source?: string;
58
+ instanceId?: number;
52
59
  /** Required when packaging is `mmtp`; the current publisher emits MFU mode. */
53
60
  mmtpMode?: "mfu";
54
61
  /** Required when packaging is `mmtp`; copied into synthesized CMSF tracks. */
@@ -63,7 +70,7 @@ export interface Phase14OverrideTrack {
63
70
  role?: "video" | "audio" | "timeline";
64
71
  altGroup?: number;
65
72
  framerate?: number;
66
- fec?: Partial<FecCatalogConfig> & { algorithm: FecAlgorithm };
73
+ fec?: FecCatalogConfig;
67
74
  }
68
75
 
69
76
  /**
@@ -150,6 +157,8 @@ export function mergeOverrideIntoHangRenditions<T extends Record<string, any>>(
150
157
  if (!override) return catalog;
151
158
 
152
159
  for (const track of override.tracks) {
160
+ // Repair tracks are placed after the media merge, below.
161
+ if (track.packaging === "fec-repair") continue;
153
162
  // Default role inference: names without an explicit role field are
154
163
  // treated as video unless the name suffix says audio.
155
164
  const role: "video" | "audio" =
@@ -188,9 +197,41 @@ export function mergeOverrideIntoHangRenditions<T extends Record<string, any>>(
188
197
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
198
  (catalog as any)[bagKey] = bag;
190
199
  }
200
+
201
+ // A repair track is a hang `repair.renditions` entry, bound by `source` +
202
+ // `instanceId`, never a media rendition. It is added only when the
203
+ // source's effective `fec` names it: a publisher `fec` that won the merge
204
+ // may bind a different repair track, and attaching this one would cost
205
+ // that instance its FEC (rule 3).
206
+ for (const track of override.tracks) {
207
+ if (track.packaging !== "fec-repair") continue;
208
+ const source =
209
+ catalog["video"]?.renditions?.[track.source as string] ??
210
+ catalog["audio"]?.renditions?.[track.source as string];
211
+ if (!namesRepairTrack(source?.fec, track.name, track.instanceId)) continue;
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
+ const repair: { renditions?: Record<string, any> } = catalog["repair"] ?? {};
214
+ repair.renditions ??= {};
215
+ const { name, packaging: _p, mmtpMode: _m, timescale: _t, groupDurationMs: _g, altGroup: _a, ...entry } = track;
216
+ repair.renditions[name] ??= entry;
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ (catalog as any)["repair"] = repair;
219
+ }
191
220
  return catalog;
192
221
  }
193
222
 
223
+ /** Whether `fec` holds the instance `instanceId` (default 0) whose `repairTrack` is `name`. */
224
+ function namesRepairTrack(fec: unknown, name: string, instanceId: unknown): boolean {
225
+ const id = instanceId ?? 0;
226
+ return (
227
+ Array.isArray(fec) &&
228
+ fec.some(
229
+ (i) =>
230
+ typeof i === "object" && i !== null && (i.instanceId ?? 0) === id && i.repairTrack === name,
231
+ )
232
+ );
233
+ }
234
+
194
235
  // ---------------------------------------------------------------------------
195
236
  // CMSF tracks merge (moqtail-private)
196
237
  // ---------------------------------------------------------------------------
@@ -248,7 +289,12 @@ export function mergeOverrideIntoCmsfTracks(
248
289
  (t) => t.role === "audio" && typeof t.codec === "string" && (t.codec as string).length > 0,
249
290
  );
250
291
 
251
- for (const ot of override.tracks) {
292
+ // Media first, so a repair track can check its source's effective `fec`.
293
+ const ordered = [
294
+ ...override.tracks.filter((t) => t.packaging !== "fec-repair"),
295
+ ...override.tracks.filter((t) => t.packaging === "fec-repair"),
296
+ ];
297
+ for (const ot of ordered) {
252
298
  const existing = byName.get(ot.name);
253
299
  if (existing !== undefined) {
254
300
  // Preserve publisher-emitted fec + framerate — those describe what
@@ -290,13 +336,18 @@ export function mergeOverrideIntoCmsfTracks(
290
336
  `[phase14-catalog-override] track "${ot.name}" has no packaging — supply per-track or set top-level streamingFormat`,
291
337
  );
292
338
  }
293
- // Role inference: repair tracks (ending in /repair) stay role-less
294
- // per CMSF convention; others default to video.
295
- const role = ot.role ?? (ot.name.endsWith("/repair") ? undefined : "video");
339
+ // Role inference: repair tracks stay role-less per CMSF convention;
340
+ // others default to video. Decided by packaging, never by name.
341
+ const isRepair = packaging === "fec-repair";
342
+ // Same rule as the hang path: only a repair track its source names.
343
+ if (isRepair && !namesRepairTrack(byName.get(ot.source as string)?.fec, ot.name, ot.instanceId)) {
344
+ continue;
345
+ }
346
+ const role = ot.role ?? (isRepair ? undefined : "video");
296
347
  const inheritedCodec =
297
348
  role === "audio" ? existingAudioTrack?.codec : existingVideoTrack?.codec;
298
349
  const codec = ot.codec ?? inheritedCodec;
299
- if (packaging !== "timeline" && (typeof codec !== "string" || codec.length === 0)) {
350
+ if (packaging !== "timeline" && !isRepair && (typeof codec !== "string" || codec.length === 0)) {
300
351
  throw new Error(
301
352
  `[phase14-catalog-override] track "${ot.name}" has no codec (packaging='${packaging}') — supply per-track or ensure the publisher catalog has a ${role ?? "video"} track to inherit from`,
302
353
  );
@@ -317,14 +368,14 @@ export function mergeOverrideIntoCmsfTracks(
317
368
  `[phase14-catalog-override] track "${ot.name}" has packaging='mmtp' but no valid groupDurationMs`,
318
369
  );
319
370
  }
320
- const entry: CmsfTrackJson = {
321
- name: ot.name,
322
- packaging,
323
- codec: codec as string,
324
- };
325
- if (mmtpMode !== undefined) entry.mmtpMode = mmtpMode;
326
- if (ot.timescale !== undefined) entry.timescale = ot.timescale;
327
- if (ot.groupDurationMs !== undefined) entry.groupDurationMs = ot.groupDurationMs;
371
+ const entry: CmsfTrackJson = { name: ot.name, packaging };
372
+ if (codec !== undefined && !isRepair) entry.codec = codec as string;
373
+ if (ot.source !== undefined) entry["source"] = ot.source;
374
+ if (ot.instanceId !== undefined) entry["instanceId"] = ot.instanceId;
375
+ // Block geometry on a repair track costs its instance FEC (rule 1).
376
+ if (mmtpMode !== undefined && !isRepair) entry.mmtpMode = mmtpMode;
377
+ if (ot.timescale !== undefined && !isRepair) entry.timescale = ot.timescale;
378
+ if (ot.groupDurationMs !== undefined && !isRepair) entry.groupDurationMs = ot.groupDurationMs;
328
379
  if (role !== undefined) entry.role = role;
329
380
  if (ot.framerate !== undefined) entry.framerate = ot.framerate;
330
381
  if (ot.fec !== undefined) entry.fec = ot.fec;
@@ -29,7 +29,9 @@
29
29
  * below a RAP the receiver already holds complete is not requested at
30
30
  * all (`moot-rap`), and a P0 FETCH still running when such a RAP
31
31
  * arrives is cancelled (`repair-stopped`, `moot`): the receiver resumes
32
- * from that RAP (draft-ramadan-moq-fec §11.4.2).
32
+ * from that RAP (draft-ramadan-moq-fec §11.4.2). Only a repair that
33
+ * could not land by the block's playout deadline (it relies on the
34
+ * keyframe budget's extension) is moot: one that can stalls nothing.
33
35
  * - P1 (audio) dispatches, uncapped, while no P0 is queued or in flight;
34
36
  * P2 (delta) only while no P0/P1 is, earliest absolute fail deadline
35
37
  * first (§11.4.5 item 3).
@@ -575,6 +577,8 @@ type StopReason = RepairStopReason
575
577
 
576
578
  interface FetchRun {
577
579
  readonly controller: AbortController
580
+ /** When this FETCH went out: it lands around `sentAt + rtt`. */
581
+ readonly sentAt: number
578
582
  stopped: StopReason | null
579
583
  }
580
584
 
@@ -596,6 +600,11 @@ interface RepairEntry {
596
600
  * later: a block reclassified from delta can gain the keyframe budget.
597
601
  */
598
602
  deadlineAt: number
603
+ /**
604
+ * `deadlineAt` without the keyframe budget's extension: the block's playout
605
+ * deadline. A FETCH landing by it costs no stall, so it is never moot.
606
+ */
607
+ playoutDeadlineAt: number
599
608
  /** Inputs `deadlineAt` was computed from; they move with it. */
600
609
  anchorAt: number | null
601
610
  delayMs: number | null
@@ -697,9 +706,18 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
697
706
  return length === undefined ? undefined : entry.sbn + length - 1
698
707
  }
699
708
 
700
- /** A keyframe request whose whole block lies below a RAP already held complete. */
709
+ /**
710
+ * A keyframe request whose whole block lies below a RAP already held
711
+ * complete, and whose repair can only land on the keyframe budget's
712
+ * extension. One that lands by the playout deadline stalls nothing, and
713
+ * skipping it only throws away the block's frames (BLO-51635).
714
+ */
701
715
  const moot = (entry: RepairEntry): boolean => {
702
716
  if (entry.priority !== 0) return false
717
+ const rttMs = readRtt()
718
+ // A running FETCH lands relative to when it was sent, not to now.
719
+ const sentAt = entry.running?.sentAt ?? performance.now()
720
+ if (rttMs !== null && sentAt + rttMs < entry.playoutDeadlineAt) return false
703
721
  const last = lastSsId(entry)
704
722
  if (last === undefined) return false
705
723
  try {
@@ -832,7 +850,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
832
850
  bound: entry.bound,
833
851
  ...(entry.range ? { range: true as const } : {}),
834
852
  })
835
- const run: FetchRun = { controller: new AbortController(), stopped: null }
853
+ const run: FetchRun = { controller: new AbortController(), sentAt: now, stopped: null }
836
854
  entry.running = run
837
855
  // Hold the block open to the presentation deadline while this FETCH can
838
856
  // still land: the multicast span elapsing must not abort it.
@@ -1053,6 +1071,10 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
1053
1071
  )
1054
1072
  if (promotedDeadline !== null && promotedDeadline.deadlineAt > entry.deadlineAt) {
1055
1073
  entry.deadlineAt = promotedDeadline.deadlineAt
1074
+ entry.playoutDeadlineAt = Math.max(
1075
+ entry.playoutDeadlineAt,
1076
+ promotedDeadline.deadlineAt - promotedDeadline.extensionMs,
1077
+ )
1056
1078
  entry.anchorAt = promotedDeadline.anchorAt
1057
1079
  entry.delayMs = promotedDeadline.delayMs
1058
1080
  entry.bound = promotedDeadline.bound
@@ -1231,6 +1253,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
1231
1253
  priority,
1232
1254
  subscriberPriority: wire.subscriberPriority,
1233
1255
  deadlineAt: deadline.deadlineAt,
1256
+ playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
1234
1257
  anchorAt: deadline.anchorAt,
1235
1258
  delayMs: deadline.delayMs,
1236
1259
  bound: deadline.bound,
@@ -1313,6 +1336,7 @@ export function wireRepairFetch(mc: RepairFetchHost, opts: WireRepairFetchOption
1313
1336
  priority,
1314
1337
  subscriberPriority: wire.subscriberPriority,
1315
1338
  deadlineAt: deadline.deadlineAt,
1339
+ playoutDeadlineAt: deadline.deadlineAt - deadline.extensionMs,
1316
1340
  anchorAt: deadline.anchorAt,
1317
1341
  delayMs: deadline.delayMs,
1318
1342
  bound: deadline.bound,