@blockcast/mmt-base 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.
@@ -0,0 +1,446 @@
1
+ // FEC catalog binding validator (draft-ramadan-moq-fec-catalog-00, as posted
2
+ // 2026-11-02, moqcast-draft 5f49172).
3
+ //
4
+ // The catalog-00 shape only: a track's `fec` is an array of instance objects,
5
+ // and a repair or symbol track binds to its instance by `source` + `instanceId`
6
+ // (or by being named in `repairTrack` / `symbolTrack`). The draft-01 object
7
+ // shape (`fec: {...}`, `depends`) is not accepted: an object `fec` is a type
8
+ // violation, and a repair track that only `depends` on its source is an orphan.
9
+ //
10
+ // Validation fails closed per instance (fec-core-00 10.3): a violation stops
11
+ // FEC for that instance and no other. Three outcomes, each reported with the
12
+ // catalog path that caused it:
13
+ // ok - FEC may start for the instance.
14
+ // unavailable - the receiver forgoes FEC for it (fec-core-00 10.3 rule 1,
15
+ // 4.5); not a validation failure.
16
+ // violation - the configuration is invalid for this receiver.
17
+ // An orphan repair or symbol track (fec-catalog-00 5 rule 6) is reported and
18
+ // stops nothing. `enhancementRepair` anywhere rejects the catalog (rule 4).
19
+ //
20
+ // The Rust twin is `mmt_fec::fec_catalog`; both run vectors/fec-catalog-00/.
21
+
22
+ export type FecCatalogOutcome = "ok" | "unavailable" | "violation";
23
+
24
+ export type FecCatalogCode =
25
+ // Unavailable (fec-core-00 10.3 rule 1; fec-catalog-00 3.1).
26
+ | "fec_scheme_absent"
27
+ | "fec_scheme_unimplemented"
28
+ | "fec_block_profile_absent"
29
+ | "fec_block_profile_unimplemented"
30
+ | "fec_block_profile_packaging"
31
+ // Violations.
32
+ | "fec_member" // fec-catalog-00 5 rule 1; fec-core-00 10.3 rule 11
33
+ | "fec_naming" // fec-catalog-00 5 rule 2
34
+ | "fec_resolution" // fec-catalog-00 5 rule 3
35
+ | "fec_enhancement_repair" // fec-catalog-00 5 rule 4, 3.4
36
+ | "fec_orphan" // fec-catalog-00 5 rule 6 (report only)
37
+ | "fec_layers" // fec-core-00 10.3 rule 3
38
+ | "fec_layer0" // fec-core-00 10.3 rule 4
39
+ | "fec_bounds" // fec-core-00 10.3 rule 5; block-codes-00 7; mmt-02 7.1
40
+ | "fec_instance_id" // fec-core-00 10.3 rule 7
41
+ | "fec_symbol_tracks" // fec-core-00 10.3 rule 7
42
+ | "fec_track_geometry" // fec-core-00 10.3 rule 8
43
+ | "fec_priority" // fec-core-00 10.3 rule 10
44
+ | "fec_overlay" // mmt-02 7.4.1
45
+ | "fec_overlay_unsupported"; // mmt-02 7.4.4
46
+
47
+ export interface FecCatalogIssue {
48
+ readonly code: FecCatalogCode;
49
+ /** JSON Pointer into the catalog. */
50
+ readonly path: string;
51
+ }
52
+
53
+ export interface FecCatalogInstance {
54
+ /** JSON Pointer of the instance object, `/tracks/<i>/fec/<j>`. */
55
+ readonly path: string;
56
+ readonly track: string | null;
57
+ /** Resolved Instance ID; null when `instanceId` is malformed. */
58
+ readonly instanceId: number | null;
59
+ readonly outcome: FecCatalogOutcome;
60
+ readonly issues: readonly FecCatalogIssue[];
61
+ /** Index into `tracks` of the source track declaring the instance. */
62
+ readonly trackIndex: number;
63
+ /** Track index of the repair track `repairTrack` names, or null. */
64
+ readonly repairTrack: number | null;
65
+ /** Track indices of the other repair tracks bound by `source` + `instanceId`. */
66
+ readonly boundRepairTracks: readonly number[];
67
+ /** Track index of the instance's symbol track (named or bound), or null. */
68
+ readonly symbolTrack: number | null;
69
+ }
70
+
71
+ export interface FecCatalogReport {
72
+ readonly instances: readonly FecCatalogInstance[];
73
+ /** Every report, instance-attributed or not, in discovery order. */
74
+ readonly issues: readonly FecCatalogIssue[];
75
+ }
76
+
77
+ export interface FecCatalogReceiver {
78
+ /** Schemes the receiver implements. Default: reed-solomon, raptorq. */
79
+ readonly schemes?: readonly string[];
80
+ /** Block profiles the receiver implements. Default: moq-object-v1, iso-ssbg1-v1. */
81
+ readonly blockProfiles?: readonly string[];
82
+ /** Whether the receiver implements the mmt-02 7.4 keyframe overlay. Default true. */
83
+ readonly overlays?: boolean;
84
+ }
85
+
86
+ const DEFAULT_SCHEMES = ["reed-solomon", "raptorq"];
87
+ const DEFAULT_PROFILES = ["moq-object-v1", "iso-ssbg1-v1"];
88
+ const RS_MAX_BLOCK = 255; // block-codes-00 7
89
+ const RAPTORQ_MAX_K = 56403; // block-codes-00 7
90
+ const MAX_24 = 0xffffff; // fec-core-00 10.3 rule 5
91
+ const ISO_MIN_T = 14; // mmt-02 7.1
92
+ const GEOMETRY_KEYS = [
93
+ "scheme",
94
+ "blockProfile",
95
+ "mmtpMode",
96
+ "timescale",
97
+ "symbolSize",
98
+ "interleaveDepthMs",
99
+ "reorderToleranceMs",
100
+ "sourceSymbols",
101
+ "fec",
102
+ ];
103
+
104
+ type Json = unknown;
105
+ type Known =
106
+ | "tracks" | "fec" | "name" | "namespace" | "packaging" | "source" | "instanceId"
107
+ | "repairLayer" | "repairSymbols" | "priority" | "scheme" | "blockProfile"
108
+ | "sourceSymbols" | "symbolSize" | "repairTrack" | "symbolTrack"
109
+ | "interleaveDepthMs" | "reorderToleranceMs" | "label" | "scope";
110
+ type Obj = Record<string, Json> & Partial<Record<Known, Json>>;
111
+
112
+ const isObj = (v: Json): v is Obj => typeof v === "object" && v !== null && !Array.isArray(v);
113
+ const has = (o: Obj, k: string) => Object.prototype.hasOwnProperty.call(o, k);
114
+ const isInt = (v: Json): v is number => typeof v === "number" && Number.isInteger(v);
115
+ const intIn = (v: Json, lo: number, hi = Number.MAX_SAFE_INTEGER): v is number =>
116
+ isInt(v) && v >= lo && v <= hi;
117
+
118
+ // moq-object-v1 admits any packaging whose objects carry media as whole
119
+ // objects (fec-core-00 15.3); iso-ssbg1-v1 admits mmtp (mmt-02 7.1).
120
+ const profileAdmits = (profile: string, packaging: Json) => profile !== "iso-ssbg1-v1" || packaging === "mmtp";
121
+
122
+ interface Inst {
123
+ path: string;
124
+ ti: number;
125
+ track: Obj;
126
+ obj: Obj | null;
127
+ id: number | null;
128
+ unavailable: boolean;
129
+ overlay: boolean;
130
+ issues: FecCatalogIssue[];
131
+ layer0: number | null; // track index of the resolved repairTrack
132
+ symbolNamed: number | null;
133
+ attached: number[]; // unnamed fec-repair/fec-source tracks bound by source
134
+ }
135
+
136
+ export function validateFecCatalog(catalog: Json, receiver: FecCatalogReceiver = {}): FecCatalogReport {
137
+ const schemes = receiver.schemes ?? DEFAULT_SCHEMES;
138
+ const profiles = receiver.blockProfiles ?? DEFAULT_PROFILES;
139
+ const overlays = receiver.overlays ?? true;
140
+ const all: FecCatalogIssue[] = [];
141
+ const report = (code: FecCatalogCode, path: string, inst?: Inst) => {
142
+ const issue = { code, path };
143
+ all.push(issue);
144
+ inst?.issues.push(issue);
145
+ };
146
+
147
+ const tracks: Json[] = isObj(catalog) && Array.isArray(catalog.tracks) ? catalog.tracks : [];
148
+ if (!isObj(catalog) || !Array.isArray(catalog.tracks)) report("fec_member", "/tracks");
149
+
150
+ // Rule 4: enhancementRepair at track level or inside an instance object.
151
+ let rejectAll = false;
152
+ tracks.forEach((t, ti) => {
153
+ if (!isObj(t)) return;
154
+ if (has(t, "enhancementRepair")) {
155
+ rejectAll = true;
156
+ report("fec_enhancement_repair", `/tracks/${ti}/enhancementRepair`);
157
+ }
158
+ if (Array.isArray(t.fec))
159
+ t.fec.forEach((o, ii) => {
160
+ if (isObj(o) && has(o, "enhancementRepair")) {
161
+ rejectAll = true;
162
+ report("fec_enhancement_repair", `/tracks/${ti}/fec/${ii}/enhancementRepair`);
163
+ }
164
+ });
165
+ });
166
+
167
+ // Track lookup by (namespace, name): a name resolves in the source track's namespace.
168
+ const key = (ns: Json, name: string) => JSON.stringify([ns ?? null, name]);
169
+ const byName = new Map<string, number>();
170
+ tracks.forEach((t, ti) => {
171
+ if (isObj(t) && typeof t.name === "string") {
172
+ const k = key(t.namespace, t.name);
173
+ if (!byName.has(k)) byName.set(k, ti);
174
+ }
175
+ });
176
+ const trackAt = (ti: number) => tracks[ti] as Obj;
177
+
178
+ // Collect instances.
179
+ const insts: Inst[] = [];
180
+ tracks.forEach((t, ti) => {
181
+ // A repair or symbol track's own `fec` is geometry (core rule 8), not instances.
182
+ if (!isObj(t) || !has(t, "fec") || t.packaging === "fec-repair" || t.packaging === "fec-source") return;
183
+ if (!Array.isArray(t.fec)) {
184
+ report("fec_member", `/tracks/${ti}/fec`);
185
+ return;
186
+ }
187
+ t.fec.forEach((o, ii) => {
188
+ const path = `/tracks/${ti}/fec/${ii}`;
189
+ const inst: Inst = {
190
+ path,
191
+ ti,
192
+ track: t,
193
+ obj: isObj(o) ? o : null,
194
+ id: null,
195
+ unavailable: false,
196
+ overlay: false,
197
+ issues: [],
198
+ layer0: null,
199
+ symbolNamed: null,
200
+ attached: [],
201
+ };
202
+ insts.push(inst);
203
+ if (!inst.obj) return report("fec_member", path, inst);
204
+ const o2 = inst.obj;
205
+ if (!has(o2, "instanceId")) inst.id = 0;
206
+ else if (intIn(o2.instanceId, 0, 255)) inst.id = o2.instanceId;
207
+ else report("fec_member", `${path}/instanceId`, inst);
208
+ inst.overlay = has(o2, "scope");
209
+ });
210
+ });
211
+ const declaresFec = insts.length > 0;
212
+
213
+ // Availability (fec-core-00 10.3 rule 1). Scheme first, then profile.
214
+ for (const inst of insts) {
215
+ const o = inst.obj;
216
+ if (!o) continue;
217
+ const avail = (field: "scheme" | "blockProfile", impl: readonly string[]) => {
218
+ const p = `${inst.path}/${field}`;
219
+ const absent = field === "scheme" ? "fec_scheme_absent" : "fec_block_profile_absent";
220
+ const unimpl = field === "scheme" ? "fec_scheme_unimplemented" : "fec_block_profile_unimplemented";
221
+ if (!has(o, field)) return report(absent, p, inst), false;
222
+ if (typeof o[field] !== "string") return report("fec_member", p, inst), true;
223
+ if (!impl.includes(o[field] as string)) return report(unimpl, p, inst), false;
224
+ return true;
225
+ };
226
+ if (!avail("scheme", schemes) || !avail("blockProfile", profiles)) inst.unavailable = true;
227
+ else if (typeof o.blockProfile === "string" && !profileAdmits(o.blockProfile, inst.track.packaging)) {
228
+ report("fec_block_profile_packaging", `${inst.path}/blockProfile`, inst);
229
+ inst.unavailable = true;
230
+ }
231
+ }
232
+ const live = (i: Inst) => !i.unavailable;
233
+
234
+ // Rule 1 on instance members, and core rule 11.
235
+ for (const inst of insts.filter(live)) {
236
+ const o = inst.obj;
237
+ if (!o) continue;
238
+ const p = inst.path;
239
+ for (const m of ["sourceSymbols", "repairSymbols", "symbolSize"])
240
+ if (!intIn(o[m], 1)) report("fec_member", `${p}/${m}`, inst);
241
+ if (typeof o.repairTrack !== "string") report("fec_member", `${p}/repairTrack`, inst);
242
+ if (has(o, "symbolTrack") && typeof o.symbolTrack !== "string") report("fec_member", `${p}/symbolTrack`, inst);
243
+ if (has(o, "interleaveDepthMs")) {
244
+ const d = o.interleaveDepthMs;
245
+ if (!intIn(d, 0) || (d === 0 && o.blockProfile !== "iso-ssbg1-v1"))
246
+ report("fec_member", `${p}/interleaveDepthMs`, inst);
247
+ }
248
+ if (has(o, "reorderToleranceMs")) {
249
+ const r = o.reorderToleranceMs;
250
+ if (typeof r !== "number" || !Number.isFinite(r) || r < 0) report("fec_member", `${p}/reorderToleranceMs`, inst);
251
+ }
252
+ if (has(o, "label") && typeof o.label !== "string") report("fec_member", `${p}/label`, inst);
253
+ if (inst.overlay && o.scope !== "keyframe") report("fec_member", `${p}/scope`, inst);
254
+ }
255
+
256
+ // Core rule 7: unique Instance IDs within a track.
257
+ const idGroups = new Map<string, Inst[]>();
258
+ for (const inst of insts)
259
+ if (inst.id !== null) {
260
+ const k = `${inst.ti}:${inst.id}`;
261
+ idGroups.set(k, [...(idGroups.get(k) ?? []), inst]);
262
+ }
263
+ for (const group of idGroups.values())
264
+ if (group.length > 1)
265
+ for (const inst of group.filter(live))
266
+ report("fec_instance_id", inst.obj && has(inst.obj, "instanceId") ? `${inst.path}/instanceId` : inst.path, inst);
267
+
268
+ // Rule 2: repairTrack / symbolTrack name a track of the right packaging, once
269
+ // per catalog. A name used twice binds no layer 0 or symbol track for either
270
+ // namer; the track is still claimed by the first, so it is not re-attached.
271
+ const nameUses = new Map<string, { inst: Inst; field: "repairTrack" | "symbolTrack" }[]>();
272
+ for (const inst of insts)
273
+ for (const field of ["repairTrack", "symbolTrack"] as const)
274
+ if (inst.obj && typeof inst.obj[field] === "string") {
275
+ const k = key(inst.track.namespace, inst.obj[field] as string);
276
+ nameUses.set(k, [...(nameUses.get(k) ?? []), { inst, field }]);
277
+ }
278
+ const namedBy = new Map<number, Inst>();
279
+ for (const [k, uses] of nameUses) {
280
+ const tj = byName.get(k);
281
+ for (const { inst, field } of uses) {
282
+ const path = `${inst.path}/${field}`;
283
+ if (tj === undefined || trackAt(tj).packaging !== (field === "repairTrack" ? "fec-repair" : "fec-source")) {
284
+ if (live(inst)) report("fec_naming", path, inst);
285
+ } else if (uses.length > 1) {
286
+ if (live(inst)) report("fec_naming", path, inst);
287
+ if (!namedBy.has(tj)) namedBy.set(tj, inst);
288
+ } else {
289
+ namedBy.set(tj, inst);
290
+ if (field === "repairTrack") inst.layer0 = tj;
291
+ else inst.symbolNamed = tj;
292
+ }
293
+ }
294
+ }
295
+
296
+ // Repair and symbol tracks: resolve each to its instance.
297
+ const owner = new Map<number, Inst>();
298
+ if (declaresFec)
299
+ tracks.forEach((t, tj) => {
300
+ if (!isObj(t) || (t.packaging !== "fec-repair" && t.packaging !== "fec-source")) return;
301
+ const tp = `/tracks/${tj}`;
302
+ const named = namedBy.get(tj);
303
+ if (named) {
304
+ owner.set(tj, named);
305
+ if (!live(named)) return;
306
+ // Rule 3: a named track's source / instanceId agree with its namer.
307
+ if (has(t, "source") && t.source !== named.track.name) report("fec_resolution", `${tp}/source`, named);
308
+ if (has(t, "instanceId") && t.instanceId !== named.id) report("fec_resolution", `${tp}/instanceId`, named);
309
+ return;
310
+ }
311
+ if (!has(t, "source")) return report("fec_orphan", tp); // rule 6, stops nothing
312
+ const id = has(t, "instanceId") ? t.instanceId : 0;
313
+ const src = typeof t.source === "string" ? byName.get(key(t.namespace, t.source)) : undefined;
314
+ const target = src === undefined ? undefined : insts.find((i) => i.ti === src && i.id === id);
315
+ if (!target) return report("fec_naming", has(t, "instanceId") && src !== undefined ? `${tp}/instanceId` : `${tp}/source`);
316
+ owner.set(tj, target);
317
+ target.attached.push(tj);
318
+ });
319
+
320
+ // Track fields (rule 1) and no geometry (core rule 8), charged to the owner.
321
+ for (const [tj, inst] of owner) {
322
+ if (!live(inst)) continue;
323
+ const t = trackAt(tj);
324
+ const tp = `/tracks/${tj}`;
325
+ const symbol = t.packaging === "fec-source";
326
+ if (has(t, "source") && typeof t.source !== "string") report("fec_member", `${tp}/source`, inst);
327
+ if (has(t, "instanceId") && !intIn(t.instanceId, 0, 255)) report("fec_member", `${tp}/instanceId`, inst);
328
+ for (const m of ["repairLayer", "repairSymbols"]) {
329
+ if (!has(t, m)) continue;
330
+ if (symbol || !intIn(t[m], m === "repairLayer" ? 0 : 1)) report("fec_member", `${tp}/${m}`, inst);
331
+ }
332
+ if (has(t, "priority") && !intIn(t.priority, symbol ? 0 : 192, 255)) report("fec_member", `${tp}/priority`, inst);
333
+ for (const k of Object.keys(t))
334
+ if (GEOMETRY_KEYS.includes(k) || k.startsWith("groupDuration"))
335
+ report("fec_track_geometry", `${tp}/${k.replace(/~/g, "~0").replace(/\//g, "~1")}`, inst); // RFC 6901
336
+ }
337
+
338
+ // Per instance: layers, symbol tracks, priorities, scheme bounds.
339
+ for (const inst of insts.filter(live)) {
340
+ const o = inst.obj;
341
+ if (!o) continue;
342
+ const repairs = inst.attached.filter((tj) => trackAt(tj).packaging === "fec-repair");
343
+ const symbols = inst.attached.filter((tj) => trackAt(tj).packaging === "fec-source");
344
+ if (symbols.length + (inst.symbolNamed !== null ? 1 : 0) > 1)
345
+ for (const tj of symbols) report("fec_symbol_tracks", `/tracks/${tj}`, inst);
346
+
347
+ const l0 = inst.layer0 === null ? null : trackAt(inst.layer0);
348
+ const l0p = `/tracks/${inst.layer0}`;
349
+ if (inst.overlay) {
350
+ // mmt-02 7.4.1: an overlay has one repair track and no repairLayer.
351
+ if (l0 && has(l0, "repairLayer")) report("fec_overlay", `${l0p}/repairLayer`, inst);
352
+ for (const tj of repairs)
353
+ report("fec_overlay", has(trackAt(tj), "repairLayer") ? `/tracks/${tj}/repairLayer` : `/tracks/${tj}`, inst);
354
+ } else {
355
+ if (l0 && has(l0, "repairLayer") && l0.repairLayer !== 0) report("fec_layers", `${l0p}/repairLayer`, inst);
356
+ if (l0 && has(l0, "repairSymbols") && intIn(o.repairSymbols, 1) && l0.repairSymbols !== o.repairSymbols)
357
+ report("fec_layer0", `${l0p}/repairSymbols`, inst);
358
+ const layers: { tj: number; layer: number }[] = [];
359
+ for (const tj of repairs) {
360
+ const t = trackAt(tj);
361
+ if (!has(t, "repairLayer")) report("fec_member", `/tracks/${tj}/repairLayer`, inst);
362
+ else if (t.repairLayer === 0) report("fec_layers", `/tracks/${tj}/repairLayer`, inst);
363
+ else if (intIn(t.repairLayer, 1)) layers.push({ tj, layer: t.repairLayer });
364
+ if (!has(t, "repairSymbols")) report("fec_member", `/tracks/${tj}/repairSymbols`, inst);
365
+ }
366
+ // Core rule 3: layers 1..n contiguous, no duplicates.
367
+ const counts = new Map<number, number>();
368
+ for (const { layer } of layers) counts.set(layer, (counts.get(layer) ?? 0) + 1);
369
+ let missing = 1;
370
+ while (counts.has(missing)) missing++;
371
+ for (const { tj, layer } of layers)
372
+ if (counts.get(layer)! > 1 || layer > missing) report("fec_layers", `/tracks/${tj}/repairLayer`, inst);
373
+ // Core rule 10: priority non-decreasing with layer; layer 0 defaults to 240.
374
+ const chain = [
375
+ { layer: 0, tj: inst.layer0, pri: l0 && intIn(l0.priority, 192, 255) ? l0.priority : l0 && has(l0, "priority") ? null : 240 },
376
+ ...layers.map(({ tj, layer }) => {
377
+ const pr = trackAt(tj).priority;
378
+ return { layer, tj, pri: intIn(pr, 192, 255) ? pr : null };
379
+ }),
380
+ ].sort((a, b) => a.layer - b.layer);
381
+ let prev: number | null = null;
382
+ for (const c of chain) {
383
+ if (c.pri === null) continue;
384
+ if (prev !== null && c.pri < prev) report("fec_priority", `/tracks/${c.tj}/priority`, inst);
385
+ prev = Math.max(prev ?? c.pri, c.pri);
386
+ }
387
+ }
388
+
389
+ // Core rule 5 and the scheme / profile bounds.
390
+ const K = o.sourceSymbols;
391
+ const T = o.symbolSize;
392
+ const P0 = o.repairSymbols;
393
+ if (intIn(K, 1) && intIn(P0, 1)) {
394
+ let P = P0;
395
+ if (!inst.overlay)
396
+ for (const tj of repairs) {
397
+ const t = trackAt(tj);
398
+ if (intIn(t.repairLayer, 1) && intIn(t.repairSymbols, 1)) P += t.repairSymbols;
399
+ }
400
+ if (P > MAX_24 || K + P - 1 > MAX_24) report("fec_bounds", inst.path, inst);
401
+ else if (o.scheme === "reed-solomon" && K + P > RS_MAX_BLOCK) report("fec_bounds", inst.path, inst);
402
+ }
403
+ if (o.scheme === "raptorq" && intIn(K, 1) && K > RAPTORQ_MAX_K) report("fec_bounds", `${inst.path}/sourceSymbols`, inst);
404
+ if (intIn(T, 1)) {
405
+ if (o.scheme === "raptorq" && T % 8 !== 0) report("fec_bounds", `${inst.path}/symbolSize`, inst);
406
+ else if (o.blockProfile === "iso-ssbg1-v1" && T < ISO_MIN_T) report("fec_bounds", `${inst.path}/symbolSize`, inst);
407
+ }
408
+ }
409
+
410
+ // mmt-02 7.4.1 / 7.4.4: overlays, per source track.
411
+ const overlayTracks = new Set(insts.filter((i) => live(i) && i.overlay).map((i) => i.ti));
412
+ for (const ti of overlayTracks) {
413
+ const ofTrack = insts.filter((i) => i.ti === ti && live(i));
414
+ const ovs = ofTrack.filter((i) => i.overlay);
415
+ if (!overlays) {
416
+ for (const ov of ovs) report("fec_overlay_unsupported", `${ov.path}/scope`, ov);
417
+ for (const i of ofTrack) i.issues.length === 0 && i.issues.push({ code: "fec_overlay_unsupported", path: `${ovs[0].path}/scope` });
418
+ continue;
419
+ }
420
+ if (ovs.length > 1) for (const ov of ovs) report("fec_overlay", `${ov.path}/scope`, ov);
421
+ for (const ov of ovs)
422
+ if (ov.obj?.blockProfile !== "iso-ssbg1-v1") report("fec_overlay", `${ov.path}/blockProfile`, ov);
423
+ const cause = ovs.flatMap((ov) => ov.issues).find((x) => x.code === "fec_overlay");
424
+ if (cause) for (const ov of ovs) if (ov.issues.length === 0) ov.issues.push(cause);
425
+ }
426
+
427
+ // Rule 4 rejects every instance; each carries the cause so a consumer reading
428
+ // `instances[].issues` sees why.
429
+ const rule4 = all.find((x) => x.code === "fec_enhancement_repair");
430
+ if (rejectAll && rule4) for (const i of insts) if (i.issues.length === 0) i.issues.push(rule4);
431
+
432
+ return {
433
+ instances: insts.map((i) => ({
434
+ path: i.path,
435
+ track: typeof i.track.name === "string" ? i.track.name : null,
436
+ instanceId: i.id,
437
+ outcome: rejectAll || (!i.unavailable && i.issues.length > 0) ? "violation" : i.unavailable ? "unavailable" : "ok",
438
+ issues: i.issues,
439
+ trackIndex: i.ti,
440
+ repairTrack: i.layer0,
441
+ boundRepairTracks: i.attached.filter((tj) => trackAt(tj).packaging === "fec-repair"),
442
+ symbolTrack: i.symbolNamed ?? i.attached.find((tj) => trackAt(tj).packaging === "fec-source") ?? null,
443
+ })),
444
+ issues: all,
445
+ };
446
+ }
package/src/index.ts CHANGED
@@ -4,5 +4,6 @@ export * from './ntp-short.js'
4
4
  export * from './signalling.js'
5
5
  export * from './serial-number.js'
6
6
  export * from './fec-block-profile.js'
7
- export * from './catalog-fec-source.js'
8
7
  export * from './multicast-catalog.js'
8
+ export * from './fec-catalog.js'
9
+ export * from './fec-bindings.js'
@@ -1,88 +0,0 @@
1
- /** The catalog media roles that carry hang renditions. */
2
- export type CatalogFecRole = 'video' | 'audio';
3
- /** One media source's `fec` block, as `locateCatalogFecSource` found it. */
4
- export type CatalogFecSource = {
5
- readonly dialect: 'hang';
6
- readonly role: CatalogFecRole;
7
- /** The rendition object. */
8
- readonly entry: Record<string, unknown>;
9
- /** The rendition's `fec` value, unvalidated (may be undefined). */
10
- readonly fec: unknown;
11
- /** JSON path of that `fec` block. */
12
- readonly fecPath: string;
13
- } | {
14
- readonly dialect: 'msf';
15
- /** Index of the entry in `tracks[]`. */
16
- readonly index: number;
17
- /** The `tracks[]` entry (an empty object when the entry is not an object). */
18
- readonly entry: Record<string, unknown>;
19
- /** The entry's `fec` value, unvalidated (may be undefined). */
20
- readonly fec: unknown;
21
- /** JSON path of that `fec` block. */
22
- readonly fecPath: string;
23
- };
24
- /**
25
- * Locate the catalog entry that declares one media source's `fec` block, in
26
- * either catalog dialect.
27
- *
28
- * A hang `video`/`audio` rendition named `source` wins, in `roles` order, over
29
- * a `tracks[]` entry of the same name: when a catalog carries rendition blocks
30
- * beside a `tracks[]` switching-set index (the unified ABR ladder catalog),
31
- * the rendition blocks are authoritative, exactly as they are for hang's own
32
- * catalog decode. The precedence is keyed on the rendition EXISTING as an
33
- * object, not on it carrying `fec`: a rendition without `fec` locates with
34
- * `fec: undefined` and never falls through to `tracks[]`.
35
- *
36
- * Otherwise the first `tracks[]` entry named `source` locates. Returns
37
- * undefined when neither dialect names it, or when `catalog` is not an object.
38
- *
39
- * @param roles the hang roles to search, in order (both by default). A typed
40
- * track reference passes its own role only.
41
- */
42
- export declare function locateCatalogFecSource(catalog: unknown, source: string, roles?: readonly CatalogFecRole[]): CatalogFecSource | undefined;
43
- /**
44
- * Whether an `iso-ssbg1-v1` block's optional `interleaveDepthMs` span is
45
- * usable: absent, or a non-negative safe integer. Zero or absence supplies no
46
- * elapsed deadline and still permits in-band FEC. True for every block that
47
- * names another profile (the span is not read) and for a non-object.
48
- */
49
- export declare function isoBlockSpanUsable(fec: unknown): boolean;
50
- /**
51
- * Whether one catalog `fec` block leaves its FEC available
52
- * (draft-ramadan-moq-fec §5.1): the decision the container's
53
- * `catalogFecUnavailable` explains with a typed reason.
54
- *
55
- * False when the block's `algorithm` is a token this receiver does not
56
- * implement (`fecAlgorithmUnimplemented`); when it declares an FEC instance
57
- * (any `algorithm` but `"none"`) and names no implemented `blockProfile` (the retired `k-padded-v1`
58
- * included), or carries an `iso-ssbg1-v1` span it cannot use; and when a block
59
- * declaring no FEC instance carries a profile token that is not implemented.
60
- * True for a non-object and for every other block. It reads only the profile
61
- * and the span: malformed geometry (`sourceSymbols`, `symbolSize`) is
62
- * `catalogFecDeclaresUnconfigurableInstance`'s.
63
- *
64
- * A receiver applies no FEC to a source whose block is unavailable and joins
65
- * none of its repair tracks, without failing the session.
66
- */
67
- export declare function catalogFecAvailable(fec: unknown): boolean;
68
- /**
69
- * Whether `algorithm` is a token this receiver does not implement
70
- * (draft-ramadan-moq-fec-01 4): any string but `none`, `raptorq` and
71
- * `reed-solomon`. Such a track is consumed without FEC and its repair tracks
72
- * are not joined; the catalog is not in error. An absent algorithm is not a
73
- * token (the hang dialect omits it) and stays implemented.
74
- */
75
- export declare function fecAlgorithmUnimplemented(algorithm: unknown): boolean;
76
- /** Smallest `symbolSize` under `iso-ssbg1-v1` (draft-ramadan-moq-fec-01 5.1, 6.3.4 rule 13). */
77
- export declare const ISO_SSBG1_MIN_SYMBOL_SIZE = 14;
78
- /**
79
- * Whether a source's `fec` block declares an FEC instance (any `algorithm`
80
- * but `"none"`) whose `sourceSymbols` or `symbolSize` is not a positive
81
- * integer, or whose `iso-ssbg1-v1` `symbolSize` is below
82
- * `ISO_SSBG1_MIN_SYMBOL_SIZE` (rule 13: such a symbol carries no protected
83
- * MMTP packet). Geometry a decoder cannot configure is a malformed catalog, not
84
- * "no FEC": the container's per-track resolver throws `track_fec` for exactly
85
- * this block, whatever its profile or span.
86
- */
87
- export declare function catalogFecDeclaresUnconfigurableInstance(fec: unknown): boolean;
88
- //# sourceMappingURL=catalog-fec-source.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"catalog-fec-source.d.ts","sourceRoot":"","sources":["../src/catalog-fec-source.ts"],"names":[],"mappings":"AAuBA,0DAA0D;AAC1D,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,CAAA;AAE9C,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GACxB;IACE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACvC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,GACD;IACE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAA;IACvB,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACvC,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAUL;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,SAAS,cAAc,EAAc,GAC3C,gBAAgB,GAAG,SAAS,CA4B9B;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAKxD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAQzD;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAErE;AAMD,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAE3C;;;;;;;;GAQG;AACH,wBAAgB,wCAAwC,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAK9E"}