@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
@@ -5,83 +5,55 @@
5
5
  // one shared implementation — players keep only wiring, not MMT/catalog
6
6
  // interpretation. Structural track shape ({ name, type }) keeps this module
7
7
  // player-agnostic.
8
+ //
9
+ // FEC comes only from `resolveFecBindings` (mmt-base), which reads
10
+ // `validateFecCatalog`'s per-instance result over the
11
+ // draft-ramadan-moq-fec-catalog-00 array (BLO-50171). Nothing here reads a
12
+ // `fec` block or a repair track itself, and nothing throws on catalog input:
13
+ // a source whose base instance is not `ok` has no FEC and a typed reason, and
14
+ // no other track is affected.
8
15
 
9
16
  import {
10
- catalogFecAvailable,
11
- catalogFecDeclaresUnconfigurableInstance,
12
- fecAlgorithmUnimplemented,
13
- ISO_SSBG1_MIN_SYMBOL_SIZE,
14
17
  FEC_BLOCK_PROFILES,
15
18
  isFecBlockProfile,
16
- locateCatalogFecSource,
17
19
  parseFecBlockProfile,
18
- type CatalogFecRole,
19
- type CatalogFecSource,
20
+ resolveFecBindings,
21
+ type FecBindingUnavailable,
20
22
  type FecBlockProfile,
23
+ type FecSourceBinding,
21
24
  } from "@blockcast/mmt-base";
22
25
  import { catalogFecInterleaveMs, type FecInterleaveCatalog } from "@blockcast/mmt-fec";
23
26
  import { resolveGroupDurationMs } from "./carp-catalog.js";
24
- import { catalogDeclaresFec, isoBlockSpanUsable } from "./fec-state.js";
25
- import {
26
- assembleRepairLayers,
27
- assertRepairTrackReachable,
28
- describeRepairTrack,
29
- rejectRepairFieldsOnMediaTrack,
30
- rejectRepairTrackGeometry,
31
- repairReachability,
32
- dependsTarget,
33
- readFecBlockProfile,
34
- RepairContractError,
35
- type RepairLayerParams,
36
- type RepairLayersResolution,
37
- type RepairOverlayParams,
38
- type RepairSourceGeometry,
39
- type RepairTrackDescriptor,
40
- throwRepairContractError,
41
- validateRepairTrackFields,
27
+ import type {
28
+ RepairLayerParams,
29
+ RepairLayersResolution,
30
+ RepairOverlayParams,
42
31
  } from "./repair-layers.js";
43
32
  import type { RepairTarget } from "./repair-fetch.js";
44
33
 
45
34
  export { catalogFecInterleaveMs, type FecInterleaveCatalog };
46
35
  export { FEC_BLOCK_PROFILES, isFecBlockProfile, parseFecBlockProfile, type FecBlockProfile };
47
- // The shared catalog FEC source rules (BLO-40851): one copy, in the leaf
48
- // package transport and every player also read.
36
+ // Re-exported here, not from index.ts: index's bundle keeps `./*.js` external,
37
+ // so a direct mmt-base re-export there leaves a dangling `./mmtp.js` import.
49
38
  export {
50
- catalogFecAvailable,
51
- catalogFecDeclaresUnconfigurableInstance,
52
- fecAlgorithmUnimplemented,
53
- locateCatalogFecSource,
54
- type CatalogFecRole,
55
- type CatalogFecSource,
56
- };
39
+ validateFecCatalog,
40
+ type FecCatalogCode,
41
+ type FecCatalogInstance,
42
+ type FecCatalogIssue,
43
+ type FecCatalogOutcome,
44
+ type FecCatalogReceiver,
45
+ type FecCatalogReport,
46
+ } from "@blockcast/mmt-base";
57
47
  export {
58
- assembleRepairLayers,
59
- assertRepairTrackReachable,
60
- repairReachability,
61
- type AssembleRepairLayersOptions,
62
- type RepairReachability,
63
- dependsTarget,
64
- describeRepairTrack,
65
- readFecBlockProfile,
66
- rejectRepairFieldsOnMediaTrack,
67
- rejectRepairTrackGeometry,
68
- REPAIR_TRACK_GEOMETRY_KEYS,
69
- REPAIR_TRACK_ONLY_KEYS,
70
- validateRepairTrackFields,
71
48
  DEFAULT_REPAIR_PRIORITY,
72
49
  MAX_ENCODING_SYMBOL_ID,
73
50
  MAX_RAPTORQ_SOURCE_SYMBOLS,
74
51
  MAX_REPAIR_SYMBOLS,
75
52
  REPAIR_PRIORITY_MAX,
76
53
  REPAIR_PRIORITY_MIN,
77
- RepairContractError,
78
- type RepairContractCode,
79
- type RepairContractFail,
80
54
  type RepairLayerParams,
81
55
  type RepairLayersResolution,
82
56
  type RepairOverlayParams,
83
- type RepairSourceGeometry,
84
- type RepairTrackDescriptor,
85
57
  } from "./repair-layers.js";
86
58
 
87
59
  /**
@@ -94,40 +66,13 @@ export type CatalogTrackRef = {
94
66
  type: "video" | "audio" | (string & {});
95
67
  };
96
68
 
97
- type FecCatalog = FecInterleaveCatalog & {
98
- algorithm?: unknown;
99
- /** FEC block profile token (BLO-35597): required on a declared FEC
100
- * instance, read through `readFecBlockProfile`. */
101
- blockProfile?: unknown;
102
- sourceSymbols?: number;
103
- repairSymbols?: number;
104
- symbolSize?: number;
105
- /** Signaled jitter/reorder allowance in ms (added to the block span). */
106
- reorderToleranceMs?: number;
107
- /** Layer 0 repair track. Further layers and overlays describe themselves
108
- * (draft-ramadan-moq-fec §6.3): MSF `tracks[]` entries with
109
- * `packaging: "fec-repair"`, or hang `repair.renditions` entries, that
110
- * `depend` on this rendition. */
111
- repairTrack?: string;
112
- };
113
69
 
114
- /** One self-describing repair rendition of the hang catalog's `repair` block
115
- * (draft-ramadan-moq-fec §6.3). Mirrors the MSF fec-repair track fields. */
116
- export type RepairRenditionCatalog = {
117
- depends?: unknown;
118
- repairLayer?: unknown;
119
- repairSymbols?: unknown;
120
- priority?: unknown;
121
- scope?: unknown;
122
- sourceSymbols?: unknown;
123
- };
124
-
125
- /** The container fields the FEC and group-duration readers consult. */
70
+ /** The container fields the group-duration reader consults. */
126
71
  type ContainerCatalog = { kind?: unknown; timescale?: unknown };
127
72
 
128
73
  type RenditionCatalog = KeyframeCadenceCatalog & {
129
74
  container?: ContainerCatalog;
130
- fec?: FecCatalog;
75
+ fec?: unknown;
131
76
  framerate?: unknown;
132
77
  sampleRate?: unknown;
133
78
  samplerate?: unknown;
@@ -136,8 +81,8 @@ type RenditionCatalog = KeyframeCadenceCatalog & {
136
81
  type CatalogWithRenditions = {
137
82
  video?: { renditions?: Record<string, RenditionCatalog> };
138
83
  audio?: { renditions?: Record<string, RenditionCatalog> };
139
- /** hang-native repair renditions: every repair track beyond layer 0. */
140
- repair?: { renditions?: Record<string, RepairRenditionCatalog> };
84
+ /** hang-native repair renditions, bound by `source` + `instanceId`. */
85
+ repair?: { renditions?: Record<string, unknown> };
141
86
  };
142
87
 
143
88
  /** An MSF/CMSF catalog: `tracks[]` entries, each self-describing. */
@@ -280,65 +225,32 @@ export function catalogFecMediaUnitsPerSecond(
280
225
  const sampleRate = positiveNumber(endpointSampleRate ?? rendition?.sampleRate ?? rendition?.timescale);
281
226
  return sampleRate === undefined ? undefined : sampleRate / 1024;
282
227
  }
283
-
284
228
  /**
285
- * Why a track's catalog `fec` block resolved to no FEC although it is not a
286
- * plain no-FEC declaration (BLO-35597, draft-ramadan-moq-fec §5.1):
287
- *
288
- * - `track_fec_block_profile_unimplemented`: its `blockProfile` is a token
289
- * this library does not implement. Only `iso-ssbg1-v1` is implemented; the
290
- * retired `k-padded-v1` lands here.
291
- * - `track_fec_block_profile`: it declares an FEC instance but names no
292
- * `blockProfile`, or the value is not a non-empty string.
293
- * - `track_fec_block_span`: retained for compatibility with older consumers;
294
- * absence no longer disables in-band FEC.
295
- * - `track_fec` (on `interleaveDepthMs`): it names `iso-ssbg1-v1` with a
296
- * declared span that is not a non-negative safe integer in milliseconds.
297
- *
298
- * The profile selects the block math and is never defaulted or inferred, and
299
- * the span is never borrowed from another track. Per §5.1 a receiver consumes
300
- * such a track's source without FEC and does not join its repair tracks, so it
301
- * carries no FEC geometry and no repair track; it is the only track affected,
302
- * and neither the session nor any other track's FEC fails on its account.
229
+ * Why a track that declares FEC resolved to no FEC: its base instance is not
230
+ * `ok` for this receiver (`resolveFecBindings`). `code` and `path` are the
231
+ * validator's (`validateFecCatalog`, draft-ramadan-moq-fec-catalog-00 5,
232
+ * fec-core-00 10.3); `outcome` tells an instance this receiver forgoes
233
+ * (`unavailable`, e.g. a block profile it does not decode) from an invalid
234
+ * one (`violation`). A draft-01 object `fec` is `fec_member` at that `fec`.
235
+ * Per fec-core-00 10.3 the track's source is consumed without FEC and none of
236
+ * its repair tracks is joined; it is the only track affected.
303
237
  */
304
- export type TrackFecUnavailable = {
305
- /** The repair-contract code a catalog validator reports for the same field
306
- * (`track_fec_block_span` excepted: an omitted span is valid shape). */
307
- readonly code:
308
- | "track_fec_block_profile"
309
- | "track_fec_block_profile_unimplemented"
310
- | "track_fec_block_span"
311
- | "track_fec"
312
- /** draft-ramadan-moq-fec-01 4: an `algorithm` this receiver does not
313
- * implement. No validator reports it: such a catalog is valid. */
314
- | "track_fec_algorithm_unimplemented";
315
- /** JSON path of the offending field: `blockProfile`, or `interleaveDepthMs`. */
316
- readonly path: string;
317
- /** What is wrong, worded as the `RepairContractError` a validator throws. */
318
- readonly message: string;
319
- };
238
+ export type TrackFecUnavailable = FecBindingUnavailable;
320
239
 
321
240
  export type TrackFecParams = {
322
241
  fecK: number;
323
- /** The FEC scheme the track's decoder runs (draft-ramadan-moq-fec-01 4):
324
- * present exactly when the track has FEC. An absent catalog `algorithm`
325
- * (the hang dialect) is RaptorQ; a token this receiver does not implement
326
- * leaves the track without FEC (`track_fec_algorithm_unimplemented`). */
242
+ /** The base instance's `scheme`: present exactly when the track has FEC. */
327
243
  fecAlgorithm?: "raptorq" | "reed-solomon";
328
- /** The catalog `fec.blockProfile` (BLO-35597, ISO/IEC 23008-1 Annex C).
329
- * Present exactly when the track has FEC (`fecK > 0`) and absent on no
330
- * FEC. The only profile is `iso-ssbg1-v1` (ssbg_mode1, K' = SSB_length,
331
- * consecutive SS_IDs); it is never defaulted. A declared instance that
332
- * names no implemented profile has no FEC; `fecUnavailable` says why. */
244
+ /** The base instance's `blockProfile`, present exactly when the track has
245
+ * FEC. This runtime decodes `iso-ssbg1-v1` only (ssbg_mode1, K' =
246
+ * SSB_length, consecutive SS_IDs); it is never defaulted. */
333
247
  fecBlockProfile?: FecBlockProfile;
334
- /** Present exactly when the track's `fec` block names no implemented
335
- * `blockProfile`, or names `iso-ssbg1-v1` without a usable block span
336
- * (see `TrackFecUnavailable`). The track then resolves to
337
- * no FEC (`fecK` 0, no `fecBlockProfile`, no repair track) and this is
338
- * the reason a caller surfaces. Absent on every other track, with FEC or
339
- * without. */
248
+ /** Present exactly when the track declares FEC and its base instance is
249
+ * not `ok` (see `TrackFecUnavailable`). The track then resolves to no FEC
250
+ * (`fecK` 0, no `fecBlockProfile`, no repair track) and this is the
251
+ * reason a caller surfaces. */
340
252
  fecUnavailable?: TrackFecUnavailable;
341
- /** P_0: repair symbols per block on layer 0 (the track-level repairSymbols). */
253
+ /** P_0: the base instance's `repairSymbols` (layer 0). */
342
254
  fecRepairCount: number;
343
255
  /** Total repair symbols per block across the BASE layers: the wire
344
256
  * RSB_length on every base layer packet. Equals fecRepairCount on a
@@ -401,156 +313,59 @@ export function joinedRepairSymbolCount(
401
313
  return total;
402
314
  }
403
315
 
404
- function recordOf(value: unknown): Record<string, unknown> | undefined {
405
- return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
316
+ /** The binding of one source, scoped to a typed ref's role. */
317
+ function bindingOf(catalog: unknown, source: string | CatalogTrackRef): FecSourceBinding | undefined {
318
+ if (!catalog) return undefined;
319
+ const name = typeof source === "string" ? source : source.name;
320
+ const binding = resolveFecBindings(catalog).get(name);
321
+ if (binding === undefined || typeof source === "string") return binding;
322
+ const role = binding.pointer.split("/")[1];
323
+ // A typed ref resolves only within its own role; a `tracks[]` entry has none.
324
+ return role === "tracks" || role === source.type ? binding : undefined;
406
325
  }
407
326
 
408
- /** The MSF `tracks[]` view of a catalog, or undefined for the hang dialect. */
409
- function msfTracks(catalog: RepairLayerCatalog): { tracks: Record<string, unknown>[] } | undefined {
410
- const raw = (catalog as CatalogWithTracks).tracks;
411
- if (!Array.isArray(raw)) return undefined;
412
- return { tracks: raw.map((t) => recordOf(t) ?? {}) };
327
+ /**
328
+ * Why `source` has no FEC although it declares some, or undefined (it has FEC,
329
+ * or declares none).
330
+ */
331
+ export function catalogTrackFecUnavailable(
332
+ catalog: RepairLayerCatalog | undefined,
333
+ source: string | CatalogTrackRef,
334
+ ): TrackFecUnavailable | undefined {
335
+ const binding = bindingOf(catalog, source);
336
+ return binding?.status === "unavailable" ? binding.reason : undefined;
413
337
  }
414
338
 
415
339
  /**
416
- * Resolve every repair track of one source track: base layers with derived
417
- * RS_ID offsets and keyframe overlays (draft-ramadan-moq-fec §6.3), from either
418
- * catalog dialect.
419
- *
420
- * - MSF `tracks[]`: the source is the entry named `source`; repair tracks are
421
- * `packaging: "fec-repair"` entries. Layer 0 is the source's
422
- * `fec.repairTrack`; layers 1..n and overlays `depend` on the source.
423
- * - hang: the source is a `video`/`audio` rendition; repair tracks beyond
424
- * layer 0 are `repair.renditions` entries that `depend` on it.
425
- *
426
- * Returns undefined when the source carries no FEC repair track and nothing
427
- * depends on it, and when the source's FEC is unavailable
428
- * (`catalogFecUnavailable`: no implemented `blockProfile`, the retired
429
- * `k-padded-v1` included, or an `iso-ssbg1-v1` span it cannot use). Per
430
- * draft-ramadan-moq-fec §5.1 a receiver joins none of that source's repair
431
- * tracks, so it gets no layers; the catalog's repair contract is still
432
- * enforced for it as for every source. Throws `RepairContractError` (code +
433
- * JSON path) on any violation, including a repair rendition whose `depends`
434
- * names a rendition without FEC, so a player fails closed before joining any
435
- * repair track.
340
+ * Every repair track of one source track: base layers with derived RS_ID
341
+ * offsets and its keyframe overlay, from either catalog dialect, as
342
+ * `resolveFecBindings` binds them. Undefined when the source declares no FEC
343
+ * or its base instance is not `ok` (`catalogTrackFecUnavailable` says why): a
344
+ * receiver then joins none of its repair tracks. Never throws.
436
345
  */
437
346
  export function resolveRepairLayers(
438
347
  catalog: RepairLayerCatalog | undefined,
439
348
  source: string | CatalogTrackRef,
440
349
  ): RepairLayersResolution | undefined {
441
- if (!catalog) return undefined;
442
- const sourceName = typeof source === "string" ? source : source.name;
443
-
444
- // hang dialect first. When a catalog carries hang-native rendition blocks
445
- // beside a tracks[] switching-set index (the unified ABR ladder catalog),
446
- // the rendition blocks are authoritative, exactly as they are for hang's own
447
- // catalog decode. A typed ref resolves only within its own role.
448
- // `locateCatalogFecSource` is that precedence rule, shared with transport's
449
- // endpoint selection and the players' repair inventories.
450
- const hang = catalog as CatalogWithRenditions;
451
- const roles: CatalogFecRole[] =
452
- typeof source === "string"
453
- ? ["video", "audio"]
454
- : source.type === "audio"
455
- ? ["audio"]
456
- : source.type === "video"
457
- ? ["video"]
458
- : ["video", "audio"];
459
- const located = locateCatalogFecSource(catalog, sourceName, roles);
460
- if (located?.dialect === "hang") {
461
- const renditionPath = `$.${located.role}.renditions[${JSON.stringify(sourceName)}]`;
462
- const rendition = located.entry;
463
- rejectRepairFieldsOnMediaTrack(renditionPath, rendition, throwRepairContractError);
464
- const repairRenditions = recordOf(hang.repair?.renditions) ?? {};
465
- const descriptors: RepairTrackDescriptor[] = Object.entries(repairRenditions).map(([name, entry]) => {
466
- const path = `$.repair.renditions[${JSON.stringify(name)}]`;
467
- const fields = recordOf(entry);
468
- if (fields === undefined) {
469
- throwRepairContractError("track_repair_layer", path, "a repair rendition must be an object");
470
- }
471
- rejectRepairTrackGeometry(path, fields, throwRepairContractError);
472
- return describeRepairTrack(name, path, fields);
473
- });
474
- const fecOfRendition = (entry: unknown): Record<string, unknown> | undefined => {
475
- const fec = recordOf(recordOf(entry)?.["fec"]);
476
- return fec !== undefined && fec["algorithm"] !== "none" ? fec : undefined;
477
- };
478
- const mediaRenditions = [
479
- ...Object.values(recordOf(hang.video?.renditions) ?? {}),
480
- ...Object.values(recordOf(hang.audio?.renditions) ?? {}),
481
- ];
482
- const reachability = repairReachability(mediaRenditions.map((entry) => recordOf(entry)?.["fec"]));
483
- for (const d of descriptors) {
484
- validateRepairTrackFields(d, throwRepairContractError);
485
- assertRepairTrackReachable(d, reachability, throwRepairContractError);
486
- const target = dependsTarget(d);
487
- if (target === undefined) continue;
488
- const targetFec =
489
- fecOfRendition(hang.video?.renditions?.[target]) ?? fecOfRendition(hang.audio?.renditions?.[target]);
490
- if (targetFec === undefined) {
491
- throwRepairContractError(
492
- "track_repair_depends",
493
- `${d.path}.depends`,
494
- `${JSON.stringify(target)} is not a video or audio rendition with FEC`,
495
- );
496
- }
497
- }
498
- const layers = assembleRepairLayers(
499
- geometryOf(sourceName, `${renditionPath}.fec`, recordOf(rendition["fec"])),
500
- descriptors,
501
- throwRepairContractError,
502
- );
503
- return catalogFecUnavailable(rendition["fec"], `${renditionPath}.fec`) === undefined ? layers : undefined;
504
- }
505
-
506
- const msf = msfTracks(catalog);
507
- if (!msf || located?.dialect !== "msf") return undefined;
508
- const msfIndex = located.index;
509
- const track = msf.tracks[msfIndex]!;
510
- const fec = recordOf(track["fec"]);
511
- const packagingOf = (t: Record<string, unknown>): unknown => t["packaging"];
512
- const fecOfTrack = (t: Record<string, unknown> | undefined): Record<string, unknown> | undefined => {
513
- const trackFec = recordOf(t?.["fec"]);
514
- return trackFec !== undefined && trackFec["algorithm"] !== "none" ? trackFec : undefined;
350
+ const binding = bindingOf(catalog, source);
351
+ if (binding?.status !== "ok") return undefined;
352
+ const overlays: RepairOverlayParams[] = binding.overlay
353
+ ? [
354
+ {
355
+ repairTrackName: binding.overlay.repairTrackName,
356
+ scope: "keyframe",
357
+ sourceSymbols: binding.overlay.sourceSymbols,
358
+ repairSymbols: binding.overlay.repairSymbols,
359
+ priority: binding.overlay.priority,
360
+ },
361
+ ]
362
+ : [];
363
+ return {
364
+ sourceTrackName: binding.track,
365
+ baseLayers: binding.layers as readonly RepairLayerParams[],
366
+ overlays,
367
+ totalBaseRepairSymbols: binding.totalBaseRepairSymbols,
515
368
  };
516
- // Rule 8 on the resolver path too: a repair track carries no block geometry
517
- // and a media track carries no repair-track fields. Without this a repair
518
- // track with its own `fec` would count as a FEC source below and name
519
- // itself as layer 0, so rule 9 would never see it.
520
- const descriptors: RepairTrackDescriptor[] = [];
521
- const mediaTracks: Record<string, unknown>[] = [];
522
- msf.tracks.forEach((t, i) => {
523
- const path = `$.tracks[${i}]`;
524
- if (packagingOf(t) === "fec-repair") {
525
- rejectRepairTrackGeometry(path, t, throwRepairContractError);
526
- if (typeof t["name"] === "string") descriptors.push(describeRepairTrack(t["name"], path, t));
527
- return;
528
- }
529
- rejectRepairFieldsOnMediaTrack(path, t, throwRepairContractError);
530
- mediaTracks.push(t);
531
- });
532
- const reachability = repairReachability(mediaTracks.map((t) => t["fec"]));
533
- for (const d of descriptors) {
534
- validateRepairTrackFields(d, throwRepairContractError);
535
- assertRepairTrackReachable(d, reachability, throwRepairContractError);
536
- const target = dependsTarget(d);
537
- if (target === undefined) continue;
538
- const targetTrack = msf.tracks.find((t) => t["name"] === target);
539
- if (targetTrack === undefined || packagingOf(targetTrack) !== "mmtp" || fecOfTrack(targetTrack) === undefined) {
540
- throwRepairContractError(
541
- "track_repair_depends",
542
- `${d.path}.depends`,
543
- `${JSON.stringify(target)} is not an mmtp track with FEC`,
544
- );
545
- }
546
- }
547
- const layers = assembleRepairLayers(
548
- geometryOf(sourceName, `$.tracks[${msfIndex}].fec`, fec),
549
- descriptors,
550
- throwRepairContractError,
551
- { layerZeroMustBeDeclared: true },
552
- );
553
- return catalogFecUnavailable(fec, `$.tracks[${msfIndex}].fec`) === undefined ? layers : undefined;
554
369
  }
555
370
 
556
371
  /** A MoQ Transport priority (0..255, lower first), or null. */
@@ -559,59 +374,32 @@ function moqtPriority(value: unknown): number | null {
559
374
  }
560
375
 
561
376
  /**
562
- * The on-demand repair target of one source track, read from the catalog:
563
- * its base layer 0 repair track and the two catalog priorities
564
- * draft-ramadan-moq-fec §11.4.5 derives the FETCH's SUBSCRIBER_PRIORITY from
565
- * (see `repairFetchSubscriberPriority`). This is what a player's
566
- * `ResolveRepairTarget` returns once it knows which track owns the block.
377
+ * The on-demand repair target of one source track: its base layer 0 repair
378
+ * track and the two catalog priorities draft-ramadan-moq-fec §11.4.5 derives
379
+ * the FETCH's SUBSCRIBER_PRIORITY from (see `repairFetchSubscriberPriority`).
567
380
  *
568
- * - `repairPriority`: layer 0's catalog `priority`, or 240 when it declares
569
- * none; that value is defined by draft-ramadan-moq-fec §10, not chosen
570
- * here (`resolveRepairLayers` applies it).
571
- * - `sourcePriority`: the source track's MSF `tracks[]` entry `priority`
572
- * (MoQ Transport scale). A catalog with no MSF entry for the track (a hang-only catalog,
573
- * whose renditions carry no MoQ Transport priority) yields null, and the
574
- * keyframe repair FETCH is then sent without the parameter.
381
+ * - `repairPriority`: layer 0's catalog `priority`, or 240 when it declares none.
382
+ * - `sourcePriority`: the source's MSF `tracks[]` entry `priority` (MoQ
383
+ * Transport scale), or null for a hang rendition, which carries none.
575
384
  *
576
- * Returns null without a positive elapsed span, or when the track has no repair track, including when its FEC is
577
- * unavailable (`catalogFecUnavailable`): draft-ramadan-moq-fec §5.1 forbids
578
- * joining that track's repair tracks, so there is nothing to FETCH.
579
- *
580
- * @throws RepairContractError exactly as `resolveRepairLayers` does.
385
+ * Null when the source has no `ok` base instance, or no positive
386
+ * `interleaveDepthMs`: unicast repair requires a known elapsed FEC deadline
387
+ * (§11.4).
581
388
  */
582
389
  export function catalogRepairTarget(
583
390
  catalog: RepairLayerCatalog | undefined,
584
391
  source: string | CatalogTrackRef,
585
392
  ): RepairTarget | null {
586
- const repair = resolveRepairLayers(catalog, source);
587
- const layerZero = repair?.baseLayers[0];
588
- if (!catalog || !layerZero) return null;
589
- const sourceName = typeof source === "string" ? source : source.name;
590
- const msf = msfTracks(catalog);
591
- const entry = msf?.tracks.find((t) => t["name"] === sourceName);
592
- const hang = catalog as CatalogWithRenditions;
593
- const roles: Array<"video" | "audio"> = typeof source === "string" ? ["video", "audio"]
594
- : source.type === "video" ? ["video"] : source.type === "audio" ? ["audio"] : ["video", "audio"];
595
- const rendition = roles.map((role) => hang[role]?.renditions?.[sourceName]).find((r) => recordOf(r) !== undefined);
596
- const fec = rendition === undefined ? recordOf(entry?.["fec"]) : recordOf(rendition.fec);
597
- // Unicast repair requires a known elapsed FEC deadline (§11.4).
598
- if (!fec || !(catalogFecInterleaveMs(fec) ?? 0)) return null;
599
- const sourcePriority =
600
- entry === undefined ? null : moqtPriority(entry["priority"]);
393
+ const binding = bindingOf(catalog, source);
394
+ if (binding?.status !== "ok" || !(binding.base.interleaveDepthMs ?? 0)) return null;
395
+ const layerZero = binding.layers[0]!;
396
+ const m = /^\/tracks\/(\d+)$/.exec(binding.pointer);
397
+ const tracks = (catalog as CatalogWithTracks).tracks;
398
+ const entry = m && Array.isArray(tracks) ? tracks[Number(m[1])] : undefined;
399
+ const sourcePriority = entry ? moqtPriority((entry as Record<string, unknown>)["priority"]) : null;
601
400
  return { repairTrack: layerZero.repairTrackName, sourcePriority, repairPriority: layerZero.priority };
602
401
  }
603
402
 
604
- function geometryOf(name: string, path: string, fec: Record<string, unknown> | undefined): RepairSourceGeometry {
605
- return {
606
- name,
607
- path,
608
- algorithm: fec?.["algorithm"],
609
- sourceSymbols: fec?.["sourceSymbols"],
610
- repairSymbols: fec?.["repairSymbols"],
611
- repairTrack: fec?.["repairTrack"],
612
- };
613
- }
614
-
615
403
  function numericField(value: unknown): number | undefined {
616
404
  const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
617
405
  return Number.isFinite(n) && n > 0 ? n : undefined;
@@ -626,167 +414,36 @@ function nonNegativeField(value: unknown): number | undefined {
626
414
  }
627
415
 
628
416
  /**
629
- * The track-scoped reading of `readFecBlockProfile` (BLO-35597): the same one
630
- * rule every catalog reader applies (a declared FEC instance names the
631
- * implemented profile; a block declaring none may omit it, but a token it
632
- * carries must be implemented), with its two failures,
633
- * `track_fec_block_profile_unimplemented` and `track_fec_block_profile`,
634
- * returned as the track's reason instead of thrown. A profile problem costs
635
- * only its own track: it never aborts resolving the catalog's other tracks, and
636
- * the token is still never defaulted or inferred.
637
- */
638
- function blockProfileUnavailable(
639
- value: unknown,
640
- path: string,
641
- declaresFecInstance: boolean,
642
- ): TrackFecUnavailable | undefined {
643
- try {
644
- readFecBlockProfile(value, path, declaresFecInstance, throwRepairContractError);
645
- return undefined;
646
- } catch (error) {
647
- if (
648
- !(error instanceof RepairContractError) ||
649
- (error.code !== "track_fec_block_profile" && error.code !== "track_fec_block_profile_unimplemented")
650
- ) {
651
- throw error;
652
- }
653
- return Object.freeze({ code: error.code, path: error.path, message: error.message });
654
- }
655
- }
656
-
657
- /** Invalid declared spans disable the track; absence/zero still permits in-band FEC. */
658
- function isoSpanUnavailable(
659
- fec: Record<string, unknown>,
660
- path: string,
661
- declaresFecInstance: boolean,
662
- ): TrackFecUnavailable | undefined {
663
- if (!declaresFecInstance || isoBlockSpanUsable(fec)) return undefined;
664
-
665
- return Object.freeze({
666
- code: "track_fec",
667
- path,
668
- message:
669
- `[repair-layers:track_fec] ${path}: must be a non-negative safe integer in milliseconds`,
670
- });
671
- }
672
-
673
- /**
674
- * Whether one catalog `fec` block leaves its track without FEC although it is
675
- * not a plain no-FEC declaration (draft-ramadan-moq-fec §5.1), and why: the
676
- * one track-scoped availability rule both catalog dialects apply. The hang
677
- * resolver (`resolveCatalogTrackFec`, `resolveRepairLayers`) and the MSF
678
- * `tracks[]` ingest (`MulticastManager.configureMultiTrack`) read it, so they
679
- * cannot disagree about which tracks keep FEC.
680
- *
681
- * Returns the reason (`TrackFecUnavailable`) when the block names no
682
- * implemented `blockProfile` (the retired `k-padded-v1` included; on
683
- * `algorithm: "none"` only a present token is read) or, for a declared
684
- * `iso-ssbg1-v1` instance, carries no usable block span. Returns undefined for
685
- * a non-object and for every block whose FEC is available. It reads only the
686
- * profile and the span: malformed geometry (`sourceSymbols`, `symbolSize`, ...)
687
- * is the caller's to reject, and takes precedence. Never throws on catalog
688
- * input: whether FEC is available is `catalogFecAvailable` (mmt-base), and a
689
- * test pins that every unavailable block gets a reason here.
690
- *
691
- * @param fec the track's catalog `fec` value.
692
- * @param fecPath JSON path of that `fec` block; the reason's `path` extends it.
693
- */
694
- export function catalogFecUnavailable(fec: unknown, fecPath: string): TrackFecUnavailable | undefined {
695
- // The decision is mmt-base's `catalogFecAvailable`, which transport reads
696
- // too; this function only explains a negative one.
697
- if (catalogFecAvailable(fec)) return undefined;
698
- const block = recordOf(fec)!;
699
- if (fecAlgorithmUnimplemented(block["algorithm"])) {
700
- return Object.freeze({
701
- code: "track_fec_algorithm_unimplemented",
702
- path: `${fecPath}.algorithm`,
703
- message: `algorithm ${JSON.stringify(block["algorithm"])} is not implemented: the track plays without FEC`,
704
- });
705
- }
706
- const declaresFecInstance = block["algorithm"] !== "none";
707
- const reason =
708
- blockProfileUnavailable(block["blockProfile"], `${fecPath}.blockProfile`, declaresFecInstance) ??
709
- isoSpanUnavailable(block, `${fecPath}.interleaveDepthMs`, declaresFecInstance);
710
- if (reason === undefined) {
711
- throw new Error(`catalogFecUnavailable: ${fecPath} is unavailable but no reason applies (rule drift)`);
712
- }
713
- return reason;
714
- }
715
-
716
- /**
717
- * Per-track FEC geometry, or `NO_FEC` when the rendition declares none (no
718
- * `fec`, or `algorithm: "none"`) or its FEC is unavailable
719
- * (`catalogFecUnavailable`: no implemented `blockProfile`, or an
720
- * `iso-ssbg1-v1` span it cannot use), which carries `fecUnavailable`.
721
- *
722
- * @throws RepairContractError from `resolveRepairLayers` when the rendition
723
- * declares a `fec` block (any algorithm, `none` included) and the catalog's
724
- * repair contract is malformed, and `track_fec` when its `fec` declares an FEC
725
- * instance whose `sourceSymbols` or `symbolSize` is not a positive integer;
726
- * see `resolveCatalogTrackFec`. An absent or unimplemented `blockProfile`, or
727
- * an invalid `iso-ssbg1-v1` span,
728
- * never throws: it is this track's `fecUnavailable`.
417
+ * Per-track FEC geometry from the source's binding: `NO_FEC` when it declares
418
+ * none, `NO_FEC` plus `fecUnavailable` when its base instance is not `ok`.
729
419
  */
730
420
  function fecParams(
731
421
  catalog: CatalogWithRenditions | undefined,
732
422
  track: CatalogTrackRef | undefined,
733
- rendition: RenditionCatalog | undefined,
734
423
  fps?: number,
735
424
  sampleRate?: number,
736
425
  ): TrackFecParams {
737
426
  const mediaRate = sampleRate === undefined ? { fps } : { fps, sampleRate };
738
- const fec = rendition?.fec;
739
- // Every caller passes a track with its rendition.
740
- if (!fec || !track) return { ...NO_FEC, ...mediaRate };
741
- // The repair contract is enforced for every declared `fec` block, including
742
- // one that declares no FEC instance (a repair layer may not depend on it).
743
- const repair = resolveRepairLayers(catalog, track);
744
- const fecPath = `$.${track.type}.renditions[${JSON.stringify(track.name)}].fec`;
745
- // One predicate for "this track has FEC", shared with resolveFecState: K
746
- // and T are positive integers, the algorithm is not `none`, and the block
747
- // profile is known.
748
- if (!catalogDeclaresFec(fec)) {
749
- if (catalogFecDeclaresUnconfigurableInstance(fec)) {
750
- const kValid = Number.isSafeInteger(fec.sourceSymbols) && (fec.sourceSymbols ?? 0) > 0;
751
- // Any algorithm but `none` declares an FEC instance, so geometry a
752
- // decoder cannot configure is a malformed catalog, not "no FEC".
753
- // Returning NO_FEC would silently drop the track's repair
754
- // subscription (repairTrackName, repairLayers, repairOverlays).
755
- throwRepairContractError(
756
- "track_fec",
757
- `${fecPath}.${kValid ? "symbolSize" : "sourceSymbols"}`,
758
- kValid && Number.isSafeInteger(fec.symbolSize) && (fec.symbolSize ?? 0) > 0
759
- ? `iso-ssbg1-v1 symbolSize must be at least ${ISO_SSBG1_MIN_SYMBOL_SIZE}`
760
- : "must be a positive integer",
761
- );
762
- }
763
- // What remains is `algorithm: "none"` (which carries no profile; a token
764
- // it does carry must still be implemented) or a declared instance whose
765
- // FEC is unavailable (§5.1). Neither has FEC; the reason says why, for
766
- // this track alone.
767
- const fecUnavailable = catalogFecUnavailable(fec, fecPath);
768
- return fecUnavailable === undefined ? { ...NO_FEC, ...mediaRate } : { ...NO_FEC, ...mediaRate, fecUnavailable };
769
- }
770
- // The predicate held, so the profile is implemented: this read cannot fail.
771
- const fecBlockProfile = readFecBlockProfile(fec.blockProfile, `${fecPath}.blockProfile`, true, throwRepairContractError);
427
+ const binding = track ? bindingOf(catalog, track) : undefined;
428
+ if (binding === undefined) return { ...NO_FEC, ...mediaRate };
429
+ if (binding.status === "unavailable") return { ...NO_FEC, ...mediaRate, fecUnavailable: binding.reason };
430
+ const { base } = binding;
431
+ const repair = resolveRepairLayers(catalog, track!)!;
772
432
  return {
773
- fecK: fec.sourceSymbols ?? 0,
774
- fecAlgorithm: fec.algorithm === "reed-solomon" ? "reed-solomon" : "raptorq",
775
- fecBlockProfile,
776
- fecRepairCount: fec.repairSymbols ?? 0,
777
- // The wire RSB_length: every base layer's packets carry the base total (§7.1).
778
- fecTotalRepairCount: repair?.totalBaseRepairSymbols ?? fec.repairSymbols ?? 0,
779
- fecSymbolSize: fec.symbolSize ?? 0,
433
+ fecK: base.sourceSymbols,
434
+ fecAlgorithm: base.scheme,
435
+ fecBlockProfile: parseFecBlockProfile(base.blockProfile, `${binding.pointer}/fec/blockProfile`),
436
+ fecRepairCount: base.repairSymbols,
437
+ // The wire RSB_length: every base layer's packets carry the base total.
438
+ fecTotalRepairCount: binding.totalBaseRepairSymbols,
439
+ fecSymbolSize: base.symbolSize,
780
440
  // Zero/absence leaves the elapsed deadline unknown; geometry stays in-band.
781
- fecInterleaveMs: catalogFecInterleaveMs(fec) ?? 0,
782
- // Additive, never a multiplier on the span: jitter belongs to the network
783
- // path and the span belongs to the encoder, so scaling one by the other
784
- // hands one network different slack per track. `numericField` rejects 0,
785
- // so it cannot be reused here — a declared 0 is a legitimate value.
786
- fecReorderToleranceMs: nonNegativeField(fec.reorderToleranceMs),
787
- repairTrackName: fec.repairTrack,
788
- repairLayers: repair?.baseLayers,
789
- repairOverlays: repair?.overlays,
441
+ fecInterleaveMs: catalogFecInterleaveMs(base) ?? 0,
442
+ // Additive to the span, never a multiplier; a declared 0 stays 0.
443
+ fecReorderToleranceMs: nonNegativeField(base.reorderToleranceMs),
444
+ repairTrackName: base.repairTrack,
445
+ repairLayers: repair.baseLayers,
446
+ repairOverlays: repair.overlays,
790
447
  ...mediaRate,
791
448
  };
792
449
  }
@@ -801,28 +458,9 @@ function fecParams(
801
458
  * interleave and repair layers, never the first rendition's. A track with no
802
459
  * rendition of its type resolves to no FEC (`fecK` 0) and no media rate.
803
460
  *
804
- * The block profile is track-scoped (BLO-35597, draft-ramadan-moq-fec §5.1).
805
- * `iso-ssbg1-v1` is the only implemented profile. A rendition whose `fec`
806
- * declares an FEC instance but names no `blockProfile`
807
- * (`track_fec_block_profile`), or whose `fec` (a `none` block included) names
808
- * any other token (`track_fec_block_profile_unimplemented`; the retired
809
- * `k-padded-v1` is one), resolves to no FEC: `fecK` 0, no `fecBlockProfile`,
810
- * no repair track, layers or overlays, and `fecUnavailable` carrying the
811
- * reason at the field's JSON path for the caller to surface. Every other track
812
- * resolves as usual. The profile is never defaulted or inferred;
813
- * `algorithm: "none"` may omit it.
814
- *
815
- * @throws RepairContractError `track_fec` when a rendition's `fec` declares an
816
- * FEC instance (any `algorithm` but `none`) whose `sourceSymbols` or
817
- * `symbolSize` is not a positive integer, and when a rendition that declares a
818
- * `fec` block (any algorithm, `none` included) carries a repair contract that
819
- * violates draft-ramadan-moq-fec 6.3 (a gap or
820
- * duplicate in the layer chain, a bad `depends` target, an unreachable repair
821
- * track, a bound violation). Because the hang dialect validates every `repair.renditions`
822
- * entry of the catalog, resolving the video track fails on a malformed audio
823
- * repair rendition too. This is deliberate: a player must fail closed before it
824
- * joins any repair track. Callers that used to treat this function as total
825
- * must catch it and surface the catalog error.
461
+ * FEC is per track and per instance (fec-core-00 10.3): a track whose base
462
+ * instance is not `ok` resolves to no FEC with `fecUnavailable` saying why,
463
+ * and every other track resolves as usual. Never throws on catalog input.
826
464
  */
827
465
  export function resolveCatalogTrackFec(
828
466
  catalog: CatalogWithRenditions | undefined,
@@ -847,11 +485,10 @@ export function resolveCatalogTrackFec(
847
485
  const catalogFps = numericField(video?.framerate);
848
486
  const sampleRate = numericField(audio?.sampleRate ?? audio?.samplerate);
849
487
  const audioFps = sampleRate ? sampleRate / 1024 : undefined;
850
- const videoParams = fecParams(catalog, videoTrackName ? { name: videoTrackName, type: "video" } : undefined, video, catalogFps);
488
+ const videoParams = fecParams(catalog, videoTrackName ? { name: videoTrackName, type: "video" } : undefined, catalogFps);
851
489
  const audioParams = fecParams(
852
490
  catalog,
853
491
  audioTrackName ? { name: audioTrackName, type: "audio" } : undefined,
854
- audio,
855
492
  audioFps,
856
493
  sampleRate,
857
494
  );
@@ -862,12 +499,12 @@ export function resolveCatalogTrackFec(
862
499
  const rendition = audioRenditions?.[track.name];
863
500
  if (!rendition) return { ...NO_FEC };
864
501
  const rate = numericField(rendition.sampleRate ?? rendition.samplerate);
865
- return fecParams(catalog, { name: track.name, type: "audio" }, rendition, rate ? rate / 1024 : undefined, rate);
502
+ return fecParams(catalog, { name: track.name, type: "audio" }, rate ? rate / 1024 : undefined, rate);
866
503
  }
867
504
  if (track.name === videoTrackName) return videoParams;
868
505
  const rendition = videoRenditions?.[track.name];
869
506
  if (!rendition) return { ...NO_FEC };
870
- return fecParams(catalog, { name: track.name, type: "video" }, rendition, numericField(rendition.framerate));
507
+ return fecParams(catalog, { name: track.name, type: "video" }, numericField(rendition.framerate));
871
508
  };
872
509
 
873
510
  return {