@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.
- package/dist/fec-bindings.d.ts +103 -0
- package/dist/fec-bindings.d.ts.map +1 -0
- package/dist/fec-bindings.js +210 -0
- package/dist/fec-bindings.js.map +1 -0
- package/dist/fec-catalog.d.ts +41 -0
- package/dist/fec-catalog.d.ts.map +1 -0
- package/dist/fec-catalog.js +410 -0
- package/dist/fec-catalog.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/fec-bindings.ts +281 -0
- package/src/fec-catalog.ts +446 -0
- package/src/index.ts +2 -1
- package/dist/catalog-fec-source.d.ts +0 -88
- package/dist/catalog-fec-source.d.ts.map +0 -1
- package/dist/catalog-fec-source.js +0 -154
- package/dist/catalog-fec-source.js.map +0 -1
- package/src/catalog-fec-source.ts +0 -183
|
@@ -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"}
|