@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
@@ -1,26 +1,11 @@
1
- // Layered and overlay FEC repair contract (draft-ramadan-moq-fec §6.3).
1
+ // Resolved repair layer and overlay shapes (draft-ramadan-moq-fec-catalog-00,
2
+ // fec-core-00): the output of `resolveRepairLayers`. Every base layer comes
3
+ // from one instance's partition and differs only in P_i; layer i owns RS_IDs
4
+ // [O_i, O_i + P_i) with O_i = sum(P_j, j < i), and ESI = K + RS_ID. A keyframe
5
+ // overlay is a second instance with its own K, P and RS_ID space.
2
6
  //
3
- // One source track may carry several repair tracks. Every base layer is
4
- // generated from the same source partition (RaptorQ, K, T, interleaveDepthMs,
5
- // reorderToleranceMs) and differs from its siblings only in its repair symbol
6
- // count P_i. Layer i owns the Repair Symbol IDs [O_i, O_i + P_i) with
7
- // O_i = sum(P_j, j < i), and ESI = K + RS_ID, so any subset of layers combines
8
- // into one decode. A keyframe overlay is a second, independent FEC instance
9
- // covering only random access point fragments, with its own K and P and its
10
- // own RS_ID space starting at 0.
11
- //
12
- // Repair tracks describe themselves (`depends`, `repairLayer`, `scope`); names
13
- // carry no meaning. This module holds the one rule set both catalog dialects
14
- // (MSF `tracks[]` and hang `repair.renditions`) resolve through, so the CMSF
15
- // validator and the player resolver cannot drift apart.
16
- //
17
- // The one dialect difference is where layer 0 lives. In MSF, `fec.repairTrack`
18
- // must name a `fec-repair` track in `tracks[]`, so a dangling pointer fails
19
- // closed (`layerZeroMustBeDeclared`). In hang, layer 0 is named only by the
20
- // rendition's `fec.repairTrack` and needs no `repair.renditions` entry; an
21
- // entry there is optional and, when present, must agree with the source.
22
-
23
- import { isFecBlockProfile, parseFecBlockProfile, type FecBlockProfile } from "@blockcast/mmt-base";
7
+ // Binding (which repair tracks belong to which instance) is
8
+ // `resolveFecBindings` in mmt-base; nothing here reads a catalog.
24
9
 
25
10
  /** Lowest MoQT-scale priority a repair track may declare. */
26
11
  export const REPAIR_PRIORITY_MIN = 192;
@@ -37,124 +22,6 @@ export const MAX_ENCODING_SYMBOL_ID = 0xffffff;
37
22
  /** Reed-Solomon block bound: K + P <= 255 (draft-ramadan-moq-fec-01 4.5, 6.3.4 rule 12). */
38
23
  export const MAX_REED_SOLOMON_BLOCK_SYMBOLS = 255;
39
24
 
40
- export type RepairContractCode =
41
- | "track_fec"
42
- | "track_repair_track"
43
- | "track_repair_depends"
44
- | "track_repair_layer"
45
- | "track_repair_priority"
46
- | "track_repair_overlay"
47
- | "track_repair_geometry"
48
- | "track_fec_bounds"
49
- | "track_fec_block_profile"
50
- | "track_fec_block_profile_unimplemented";
51
-
52
- /** Reports one contract violation and never returns. */
53
- export type RepairContractFail = (code: RepairContractCode, path: string, message: string) => never;
54
-
55
- export class RepairContractError extends Error {
56
- constructor(
57
- public readonly code: RepairContractCode,
58
- public readonly path: string,
59
- message: string,
60
- ) {
61
- super(`[repair-layers:${code}] ${path}: ${message}`);
62
- this.name = "RepairContractError";
63
- }
64
- }
65
-
66
- export const throwRepairContractError: RepairContractFail = (code, path, message) => {
67
- throw new RepairContractError(code, path, message);
68
- };
69
-
70
- /**
71
- * Read one catalog `fec.blockProfile` (BLO-35597, ISO/IEC 23008-1 Annex C):
72
- * the one rule both catalog dialects, the CMSF validator and the per-track
73
- * resolver apply.
74
- *
75
- * draft-ramadan-moq-fec §5.1 defines exactly one profile, `iso-ssbg1-v1`, and
76
- * it is the only one this library implements. A `fec` block that declares an
77
- * FEC instance (any `algorithm` other than `"none"`) MUST name it; a block
78
- * declaring no FEC instance may omit the token (returns undefined), but a token
79
- * it does carry must be implemented. Failures are reported at `path`:
80
- *
81
- * - `track_fec_block_profile_unimplemented`: a non-empty string that names no
82
- * implemented profile. The retired Blockcast-private `k-padded-v1` is one
83
- * such token; a near-miss such as `"iso-ssbg1"` is another, never
84
- * normalized.
85
- * - `track_fec_block_profile`: no token on a declared FEC instance, or a value
86
- * that is not a non-empty string.
87
- *
88
- * The token is never defaulted and never inferred from the wire. A receiver
89
- * treats either failure as "FEC unavailable" for that track alone (it consumes
90
- * the source without FEC and joins none of its repair tracks); a conformance
91
- * validator rejects the catalog.
92
- *
93
- * @param path JSON path of the `blockProfile` field, for the error.
94
- */
95
- export function readFecBlockProfile(
96
- value: unknown,
97
- path: string,
98
- declaresFecInstance: true,
99
- fail: RepairContractFail,
100
- ): FecBlockProfile;
101
- export function readFecBlockProfile(
102
- value: unknown,
103
- path: string,
104
- declaresFecInstance: boolean,
105
- fail: RepairContractFail,
106
- ): FecBlockProfile | undefined;
107
- export function readFecBlockProfile(
108
- value: unknown,
109
- path: string,
110
- declaresFecInstance: boolean,
111
- fail: RepairContractFail,
112
- ): FecBlockProfile | undefined {
113
- if (value === undefined && !declaresFecInstance) return undefined;
114
- if (isFecBlockProfile(value)) return value;
115
- if (typeof value === "string" && value.length > 0) {
116
- return fail(
117
- "track_fec_block_profile_unimplemented",
118
- path,
119
- `${JSON.stringify(value)} is not an implemented FEC block profile; only "iso-ssbg1-v1" is implemented`,
120
- );
121
- }
122
- try {
123
- return parseFecBlockProfile(value, "blockProfile");
124
- } catch (error) {
125
- return fail("track_fec_block_profile", path, (error as Error).message);
126
- }
127
- }
128
-
129
- /** The self-describing fields of one repair track entry, in either dialect. */
130
- export type RepairTrackDescriptor = {
131
- /** Track (MSF) or rendition (hang) name. */
132
- name: string;
133
- /** JSON path of the entry, for error reporting. */
134
- path: string;
135
- depends?: unknown;
136
- repairLayer?: unknown;
137
- repairSymbols?: unknown;
138
- priority?: unknown;
139
- scope?: unknown;
140
- sourceSymbols?: unknown;
141
- };
142
-
143
- /** The source track's `fec` object as the layer contract reads it. */
144
- export type RepairSourceGeometry = {
145
- /** Source track or rendition name. */
146
- name: string;
147
- /** JSON path of the `fec` object, for error reporting. */
148
- path: string;
149
- algorithm?: unknown;
150
- /** K. */
151
- sourceSymbols?: unknown;
152
- /** P_0. */
153
- repairSymbols?: unknown;
154
- /** Layer 0 track name. */
155
- repairTrack?: unknown;
156
- };
157
-
158
25
  /** A resolved base repair layer with its derived RS_ID range. */
159
26
  export type RepairLayerParams = {
160
27
  /** Layer index; 0 is the track named by the source's `fec.repairTrack`. */
@@ -191,383 +58,3 @@ export type RepairLayersResolution = {
191
58
  totalBaseRepairSymbols: number;
192
59
  };
193
60
 
194
- const REPAIR_FIELDS = ["depends", "repairLayer", "repairSymbols", "priority", "scope", "sourceSymbols"] as const;
195
-
196
- /** Track-level block geometry keys a repair track must not carry (§6.3 rule 8): geometry is the source's. */
197
- export const REPAIR_TRACK_GEOMETRY_KEYS = [
198
- "mmtpMode",
199
- "timescale",
200
- "groupDurationMs",
201
- "groupDurationTicks",
202
- "keyframeIntervalMs",
203
- "keyframeIntervalTicks",
204
- "fec",
205
- "symbolSize",
206
- "interleaveDepthMs",
207
- ] as const;
208
-
209
- /** Self-describing repair fields that only a repair track or repair rendition may carry. */
210
- export const REPAIR_TRACK_ONLY_KEYS = ["repairLayer", "repairSymbols", "sourceSymbols", "scope"] as const;
211
-
212
- /** Rule 8: a repair track (MSF) or repair rendition (hang) carries no block geometry. */
213
- export function rejectRepairTrackGeometry(path: string, fields: Record<string, unknown>, fail: RepairContractFail): void {
214
- for (const key of REPAIR_TRACK_GEOMETRY_KEYS) {
215
- if (fields[key] !== undefined) {
216
- fail(
217
- "track_repair_geometry",
218
- `${path}.${key}`,
219
- "a repair track carries no block geometry; it is shared with the source track's fec object",
220
- );
221
- }
222
- }
223
- }
224
-
225
- /** Which repair tracks a catalog can reach, for rule 9 (no orphan repair track). */
226
- export type RepairReachability = {
227
- /** True once any track or rendition carries `fec` with an algorithm other than `none`. */
228
- declaresFecSource: boolean;
229
- /** Every non-empty `fec.repairTrack` a FEC source names: the layer-0 tracks. */
230
- layerZeroNames: ReadonlySet<string>;
231
- };
232
-
233
- /**
234
- * Derive rule 9's inputs from the raw `fec` objects of every media track (MSF,
235
- * packaging other than `fec-repair`) or media rendition (hang) in a catalog.
236
- * Callers must not pass repair tracks: rule 8 forbids `fec` on them, and a
237
- * repair track that carried one would otherwise name itself as its own
238
- * layer 0 and slip past rule 9. One derivation for the validator and both
239
- * resolver dialects, so an empty-string pointer or an `algorithm: "none"`
240
- * source is read the same way everywhere.
241
- */
242
- export function repairReachability(fecObjects: Iterable<unknown>): RepairReachability {
243
- let declaresFecSource = false;
244
- const layerZeroNames = new Set<string>();
245
- for (const candidate of fecObjects) {
246
- if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) continue;
247
- const fec = candidate as Record<string, unknown>;
248
- if (fec["algorithm"] === "none") continue;
249
- declaresFecSource = true;
250
- const repairTrack = fec["repairTrack"];
251
- if (typeof repairTrack === "string" && repairTrack.length > 0) layerZeroNames.add(repairTrack);
252
- }
253
- return { declaresFecSource, layerZeroNames };
254
- }
255
-
256
- /**
257
- * Rule 9: once a catalog declares a FEC source, a repair track that declares no
258
- * `depends` must be some source's layer 0. A catalog with no FEC source may
259
- * still carry a naming-era repair track that nothing joins.
260
- */
261
- export function assertRepairTrackReachable(
262
- descriptor: RepairTrackDescriptor,
263
- reachability: RepairReachability,
264
- fail: RepairContractFail,
265
- ): void {
266
- if (dependsTarget(descriptor) !== undefined) return;
267
- if (reachability.declaresFecSource && !reachability.layerZeroNames.has(descriptor.name)) {
268
- fail(
269
- "track_repair_depends",
270
- `${descriptor.path}.depends`,
271
- "no source names this repair track in fec.repairTrack and it declares no depends",
272
- );
273
- }
274
- }
275
-
276
- /** A media track (MSF) or media rendition (hang) carries no repair-track fields. */
277
- export function rejectRepairFieldsOnMediaTrack(path: string, fields: Record<string, unknown>, fail: RepairContractFail): void {
278
- for (const key of REPAIR_TRACK_ONLY_KEYS) {
279
- if (fields[key] !== undefined) {
280
- fail("track_repair_layer", `${path}.${key}`, `${key} is a repair track field`);
281
- }
282
- }
283
- }
284
-
285
- /** Pick the self-describing repair fields out of a raw track or rendition record. */
286
- export function describeRepairTrack(name: string, path: string, fields: Record<string, unknown>): RepairTrackDescriptor {
287
- const descriptor: RepairTrackDescriptor = { name, path };
288
- for (const field of REPAIR_FIELDS) {
289
- if (field in fields) descriptor[field] = fields[field];
290
- }
291
- return descriptor;
292
- }
293
-
294
- function isSafeInteger(value: unknown): value is number {
295
- return typeof value === "number" && Number.isSafeInteger(value);
296
- }
297
-
298
- function positiveInteger(value: unknown): value is number {
299
- return isSafeInteger(value) && value > 0;
300
- }
301
-
302
- /** The single source a repair track names in `depends`, once its shape has been validated. */
303
- export function dependsTarget(descriptor: RepairTrackDescriptor): string | undefined {
304
- const depends = descriptor.depends;
305
- return Array.isArray(depends) && depends.length === 1 && typeof depends[0] === "string" && depends[0].length > 0
306
- ? depends[0]
307
- : undefined;
308
- }
309
-
310
- /**
311
- * Validate the shape of one repair track's self-describing fields, independent
312
- * of any source. Cross-track rules (layer 0 agreement, contiguity, priority
313
- * order, bounds) live in `assembleRepairLayers`.
314
- */
315
- export function validateRepairTrackFields(descriptor: RepairTrackDescriptor, fail: RepairContractFail): void {
316
- const { path } = descriptor;
317
- if (descriptor.depends !== undefined && dependsTarget(descriptor) === undefined) {
318
- fail("track_repair_depends", `${path}.depends`, "must name exactly one source track");
319
- }
320
- if (descriptor.repairLayer !== undefined && (!isSafeInteger(descriptor.repairLayer) || descriptor.repairLayer < 0)) {
321
- fail("track_repair_layer", `${path}.repairLayer`, "must be a non-negative integer");
322
- }
323
- if (
324
- descriptor.repairSymbols !== undefined &&
325
- (!positiveInteger(descriptor.repairSymbols) || descriptor.repairSymbols > MAX_REPAIR_SYMBOLS)
326
- ) {
327
- fail("track_repair_layer", `${path}.repairSymbols`, `must be a positive integer at most ${MAX_REPAIR_SYMBOLS}`);
328
- }
329
- if (
330
- descriptor.priority !== undefined &&
331
- (!isSafeInteger(descriptor.priority) ||
332
- descriptor.priority < REPAIR_PRIORITY_MIN ||
333
- descriptor.priority > REPAIR_PRIORITY_MAX)
334
- ) {
335
- fail(
336
- "track_repair_priority",
337
- `${path}.priority`,
338
- `must be an integer from ${REPAIR_PRIORITY_MIN} to ${REPAIR_PRIORITY_MAX} (MoQT scale, lower first)`,
339
- );
340
- }
341
- if (descriptor.scope !== undefined) {
342
- if (descriptor.scope !== "keyframe") {
343
- fail("track_repair_overlay", `${path}.scope`, 'must be "keyframe"');
344
- }
345
- if (descriptor.repairLayer !== undefined) {
346
- fail("track_repair_overlay", `${path}.repairLayer`, "an overlay is a separate FEC instance and carries no repairLayer");
347
- }
348
- for (const field of ["depends", "sourceSymbols", "repairSymbols", "priority"] as const) {
349
- if (descriptor[field] === undefined) {
350
- fail("track_repair_overlay", `${path}.${field}`, `an overlay must declare ${field}`);
351
- }
352
- }
353
- }
354
- if (descriptor.sourceSymbols !== undefined) {
355
- if (descriptor.scope === undefined) {
356
- fail("track_repair_overlay", `${path}.sourceSymbols`, "only an overlay (scope: keyframe) declares its own sourceSymbols");
357
- }
358
- if (!positiveInteger(descriptor.sourceSymbols) || descriptor.sourceSymbols > MAX_RAPTORQ_SOURCE_SYMBOLS) {
359
- fail(
360
- "track_fec_bounds",
361
- `${path}.sourceSymbols`,
362
- `must be a positive integer at most ${MAX_RAPTORQ_SOURCE_SYMBOLS} (RFC 6330 source block limit)`,
363
- );
364
- }
365
- }
366
- if (isSafeInteger(descriptor.repairLayer) && descriptor.repairLayer >= 1) {
367
- for (const field of ["depends", "repairSymbols", "priority"] as const) {
368
- if (descriptor[field] === undefined) {
369
- fail("track_repair_layer", `${path}.${field}`, `repair layer ${descriptor.repairLayer} must declare ${field}`);
370
- }
371
- }
372
- }
373
- }
374
-
375
- /**
376
- * Resolve the base layers and overlays of one source track and derive each
377
- * base layer's RS_ID offset. Fails closed on every §6.3 rule violation through
378
- * `fail`, which receives the code and the JSON path of the offending field.
379
- *
380
- * `descriptors` are every repair track (or repair rendition) in the catalog,
381
- * already shape-checked by `validateRepairTrackFields`. Returns undefined when
382
- * the source names no layer 0 and nothing depends on it.
383
- */
384
- export type AssembleRepairLayersOptions = {
385
- /**
386
- * MSF: `fec.repairTrack` must name one of `descriptors` (a `fec-repair`
387
- * track), so a dangling pointer fails closed with `track_repair_track`.
388
- * hang: layer 0 lives outside `repair.renditions`, so leave this unset.
389
- */
390
- layerZeroMustBeDeclared?: boolean;
391
- };
392
-
393
- export function assembleRepairLayers(
394
- source: RepairSourceGeometry,
395
- descriptors: readonly RepairTrackDescriptor[],
396
- fail: RepairContractFail = throwRepairContractError,
397
- options: AssembleRepairLayersOptions = {},
398
- ): RepairLayersResolution | undefined {
399
- const layerZeroName =
400
- typeof source.repairTrack === "string" && source.repairTrack.length > 0 ? source.repairTrack : undefined;
401
- const layerZero = layerZeroName === undefined ? undefined : descriptors.find((d) => d.name === layerZeroName);
402
- if (layerZeroName !== undefined && layerZero === undefined && options.layerZeroMustBeDeclared) {
403
- fail("track_repair_track", `${source.path}.repairTrack`, `unknown FEC repair track ${JSON.stringify(layerZeroName)}`);
404
- }
405
- const dependents = descriptors.filter((d) => d.name !== layerZeroName && dependsTarget(d) === source.name);
406
- const k = positiveInteger(source.sourceSymbols) ? source.sourceSymbols : 0;
407
- // RFC 6330 bounds every RaptorQ source block, with or without a repair track.
408
- if (source.algorithm === "raptorq" && k > MAX_RAPTORQ_SOURCE_SYMBOLS) {
409
- fail(
410
- "track_fec_bounds",
411
- `${source.path}.sourceSymbols`,
412
- `must be at most ${MAX_RAPTORQ_SOURCE_SYMBOLS} (RFC 6330 source block limit)`,
413
- );
414
- }
415
- if (layerZeroName === undefined) {
416
- if (dependents.length === 0) return undefined;
417
- fail(
418
- "track_repair_layer",
419
- `${dependents[0]!.path}.depends`,
420
- `repair layers of ${JSON.stringify(source.name)} require a layer 0 (fec.repairTrack) on the source`,
421
- );
422
- }
423
-
424
- const layered = dependents.length > 0;
425
- let layerZeroSymbols = 0;
426
- if (positiveInteger(source.repairSymbols)) {
427
- layerZeroSymbols = source.repairSymbols;
428
- } else if (layered) {
429
- fail("track_fec", `${source.path}.repairSymbols`, "layered or overlay repair needs a positive integer repairSymbols on layer 0");
430
- }
431
- if (layered && !positiveInteger(source.sourceSymbols)) {
432
- fail("track_fec", `${source.path}.sourceSymbols`, "layered or overlay repair needs a positive integer sourceSymbols on the source");
433
- }
434
- // Rule 7: the two specified schemes. Reed-Solomon layers decode because its
435
- // Cauchy matrix row for RS_ID is the same whatever the block's total P (fec-01 4.4).
436
- const reedSolomon = source.algorithm === "reed-solomon";
437
- if (layered && source.algorithm !== "raptorq" && !reedSolomon) {
438
- fail(
439
- "track_fec",
440
- `${source.path}.algorithm`,
441
- "repair layers and overlays are defined only for raptorq and reed-solomon (draft-ramadan-moq-fec-01 6.3.4 rule 7)",
442
- );
443
- }
444
-
445
- let layerZeroPriority = DEFAULT_REPAIR_PRIORITY;
446
- if (layerZero !== undefined) {
447
- if (layerZero.depends !== undefined && dependsTarget(layerZero) !== source.name) {
448
- fail(
449
- "track_repair_depends",
450
- `${layerZero.path}.depends`,
451
- `layer 0 of ${JSON.stringify(source.name)} must depend on that source`,
452
- );
453
- }
454
- if (layerZero.repairLayer !== undefined && layerZero.repairLayer !== 0) {
455
- fail("track_repair_layer", `${layerZero.path}.repairLayer`, "the track named by fec.repairTrack is layer 0");
456
- }
457
- if (layerZero.scope !== undefined) {
458
- fail("track_repair_overlay", `${layerZero.path}.scope`, "the track named by fec.repairTrack is base layer 0, not an overlay");
459
- }
460
- if (layerZero.repairSymbols !== undefined && layerZero.repairSymbols !== layerZeroSymbols) {
461
- fail(
462
- "track_repair_layer",
463
- `${layerZero.path}.repairSymbols`,
464
- `must agree with the source's fec.repairSymbols (${layerZeroSymbols})`,
465
- );
466
- }
467
- if (typeof layerZero.priority === "number") layerZeroPriority = layerZero.priority;
468
- }
469
-
470
- const baseLayers: RepairLayerParams[] = [
471
- { layer: 0, repairTrackName: layerZeroName!, repairSymbols: layerZeroSymbols, rsIdOffset: 0, priority: layerZeroPriority },
472
- ];
473
- // Clarification 7 fixes the wire: a RAP packet carries exactly two Source
474
- // FEC Payload IDs, base first and overlay last, and the receiver strips a
475
- // fixed trailer length. A second overlay would need a third trailer that
476
- // the draft does not define, so one overlay per source is the contract.
477
- const overlays: RepairOverlayParams[] = [];
478
- for (const d of dependents) {
479
- if (d.scope === undefined && d.repairLayer === undefined) {
480
- fail(
481
- "track_repair_layer",
482
- `${d.path}.repairLayer`,
483
- `a repair track that depends on ${JSON.stringify(source.name)} must declare repairLayer (1..n) or scope: "keyframe"`,
484
- );
485
- }
486
- }
487
- const layerTracks = dependents
488
- .filter((d) => d.scope === undefined)
489
- .sort((a, b) => (a.repairLayer as number) - (b.repairLayer as number));
490
- for (const d of dependents) {
491
- if (d.scope !== "keyframe") continue;
492
- if (overlays.length === 1) {
493
- fail(
494
- "track_repair_overlay",
495
- `${d.path}.scope`,
496
- `${JSON.stringify(source.name)} already has keyframe overlay ${JSON.stringify(overlays[0]!.repairTrackName)}: a RAP packet carries exactly two Source FEC Payload IDs (base, then overlay), so one overlay per source`,
497
- );
498
- }
499
- const sourceSymbols = d.sourceSymbols as number;
500
- const repairSymbols = d.repairSymbols as number;
501
- if (sourceSymbols + repairSymbols - 1 > MAX_ENCODING_SYMBOL_ID) {
502
- fail(
503
- "track_fec_bounds",
504
- `${d.path}.repairSymbols`,
505
- `overlay sourceSymbols + repairSymbols - 1 exceeds the 24-bit ESI space (${MAX_ENCODING_SYMBOL_ID})`,
506
- );
507
- }
508
- if (reedSolomon && sourceSymbols + repairSymbols > MAX_REED_SOLOMON_BLOCK_SYMBOLS) {
509
- fail(
510
- "track_fec_bounds",
511
- `${d.path}.repairSymbols`,
512
- `reed-solomon overlay sourceSymbols + repairSymbols exceeds ${MAX_REED_SOLOMON_BLOCK_SYMBOLS} (draft-ramadan-moq-fec-01 6.3.4 rule 12)`,
513
- );
514
- }
515
- overlays.push({ repairTrackName: d.name, scope: "keyframe", sourceSymbols, repairSymbols, priority: d.priority as number });
516
- }
517
-
518
- let offset = layerZeroSymbols;
519
- let previousPriority = layerZeroPriority;
520
- layerTracks.forEach((d, i) => {
521
- const expected = i + 1;
522
- if (d.repairLayer === 0) {
523
- fail("track_repair_layer", `${d.path}.repairLayer`, `layer 0 of ${JSON.stringify(source.name)} is ${JSON.stringify(layerZeroName)}, named by fec.repairTrack`);
524
- }
525
- if (d.repairLayer !== expected) {
526
- fail(
527
- "track_repair_layer",
528
- `${d.path}.repairLayer`,
529
- i > 0 && d.repairLayer === layerTracks[i - 1]!.repairLayer
530
- ? `repair layer ${d.repairLayer} is declared twice for ${JSON.stringify(source.name)}`
531
- : `repair layers must be contiguous from 1: expected ${expected}, found ${d.repairLayer}`,
532
- );
533
- }
534
- const priority = d.priority as number;
535
- if (priority < previousPriority) {
536
- fail(
537
- "track_repair_priority",
538
- `${d.path}.priority`,
539
- `must be at least ${previousPriority}, the priority of layer ${expected - 1}: priority is monotonic non-decreasing with layer index`,
540
- );
541
- }
542
- previousPriority = priority;
543
- const repairSymbols = d.repairSymbols as number;
544
- baseLayers.push({ layer: expected, repairTrackName: d.name, repairSymbols, rsIdOffset: offset, priority });
545
- offset += repairSymbols;
546
- });
547
-
548
- const totalBaseRepairSymbols = offset;
549
- if (totalBaseRepairSymbols > MAX_REPAIR_SYMBOLS) {
550
- fail(
551
- "track_fec_bounds",
552
- `${layerTracks.length === 0 ? source.path : layerTracks[layerTracks.length - 1]!.path}.repairSymbols`,
553
- `total repair symbols across base layers exceeds the 24-bit RS_ID space (${MAX_REPAIR_SYMBOLS})`,
554
- );
555
- }
556
- if (k + totalBaseRepairSymbols - 1 > MAX_ENCODING_SYMBOL_ID) {
557
- fail(
558
- "track_fec_bounds",
559
- `${source.path}.sourceSymbols`,
560
- `sourceSymbols + total base repair symbols - 1 exceeds the 24-bit ESI space (${MAX_ENCODING_SYMBOL_ID})`,
561
- );
562
- }
563
-
564
- if (reedSolomon && k + totalBaseRepairSymbols > MAX_REED_SOLOMON_BLOCK_SYMBOLS) {
565
- fail(
566
- "track_fec_bounds",
567
- `${source.path}.sourceSymbols`,
568
- `reed-solomon sourceSymbols + total base repair symbols exceeds ${MAX_REED_SOLOMON_BLOCK_SYMBOLS} (draft-ramadan-moq-fec-01 6.3.4 rule 12)`,
569
- );
570
- }
571
-
572
- return { sourceTrackName: source.name, baseLayers, overlays, totalBaseRepairSymbols };
573
- }