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