@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,281 @@
1
+ /**
2
+ * Runtime FEC bindings (BLO-50171): what each source track's FEC resolves to
3
+ * for this library's receiver, read from `validateFecCatalog`'s per-instance
4
+ * result and never re-derived.
5
+ *
6
+ * Every runtime reader (the container's `resolveCatalogTrackFec`,
7
+ * `resolveRepairLayers`, `MulticastManager.configureMultiTrack`, transport's
8
+ * `catalogRepairTracks`) goes through `resolveFecBindings`, so they cannot
9
+ * disagree with the validator or with each other about which tracks have FEC
10
+ * and which repair tracks belong to them.
11
+ *
12
+ * The draft-ramadan-moq-fec-catalog-00 array is the only shape. Both catalog
13
+ * dialects bind through it: an MSF `tracks[]` entry directly, and a hang
14
+ * `video`/`audio` rendition (`fec` the same array) with its `repair.renditions`
15
+ * entries (`source` + `instanceId`), mapped onto a `tracks[]` view so the same
16
+ * validator runs. A hang rendition is authoritative over a `tracks[]` entry of
17
+ * the same name.
18
+ *
19
+ * Fail closed per instance: a source whose base instance is not `ok` resolves
20
+ * to `unavailable` with the validator's code and path, and costs no other
21
+ * track anything. A draft-01 object `fec` is `fec_member` at that track's
22
+ * `fec`: the track plays without FEC.
23
+ */
24
+ import { validateFecCatalog, type FecCatalogCode, type FecCatalogReceiver } from './fec-catalog.js'
25
+
26
+ /** The receiver this runtime is: it decodes `iso-ssbg1-v1` only. */
27
+ export const RUNTIME_FEC_RECEIVER: FecCatalogReceiver = Object.freeze({
28
+ schemes: Object.freeze(['raptorq', 'reed-solomon']),
29
+ blockProfiles: Object.freeze(['iso-ssbg1-v1']),
30
+ overlays: true,
31
+ })
32
+
33
+ /** Repair track priority when the track states none (MoQT scale, 192..255). */
34
+ export const DEFAULT_FEC_REPAIR_PRIORITY = 240
35
+
36
+ /** One `ok` instance's geometry. */
37
+ export interface FecInstanceGeometry {
38
+ readonly instanceId: number
39
+ readonly scheme: 'raptorq' | 'reed-solomon'
40
+ /** `iso-ssbg1-v1` under `RUNTIME_FEC_RECEIVER`. */
41
+ readonly blockProfile: string
42
+ /** K. */
43
+ readonly sourceSymbols: number
44
+ /** P_0 for a base instance; the overlay's own P for an overlay. */
45
+ readonly repairSymbols: number
46
+ /** T. */
47
+ readonly symbolSize: number
48
+ readonly interleaveDepthMs?: number
49
+ readonly reorderToleranceMs?: number
50
+ readonly repairTrack: string
51
+ readonly symbolTrack?: string
52
+ }
53
+
54
+ export interface FecLayerBinding {
55
+ readonly layer: number
56
+ readonly repairTrackName: string
57
+ readonly repairSymbols: number
58
+ /** O_i = sum(P_j, j < i). */
59
+ readonly rsIdOffset: number
60
+ readonly priority: number
61
+ }
62
+
63
+ export interface FecOverlayBinding {
64
+ readonly repairTrackName: string
65
+ readonly scope: 'keyframe'
66
+ readonly sourceSymbols: number
67
+ readonly repairSymbols: number
68
+ readonly priority: number
69
+ readonly geometry: FecInstanceGeometry
70
+ }
71
+
72
+ export interface FecBindingUnavailable {
73
+ /** The validator's code, or `fec_base_instances` for a track with no base
74
+ * instance or more than one `ok` one (the MMTP wire carries one base Source FEC Payload
75
+ * ID, mmt-02 7.4.1). */
76
+ readonly code: FecCatalogCode | 'fec_base_instances'
77
+ /** JSON Pointer into the catalog (a hang path starts `/video` or `/audio`). */
78
+ readonly path: string
79
+ readonly outcome: 'unavailable' | 'violation'
80
+ }
81
+
82
+ export type FecSourceBinding =
83
+ | {
84
+ readonly status: 'ok'
85
+ readonly track: string
86
+ /** JSON Pointer of the source track (`/tracks/<i>`, `/video/renditions/<name>`). */
87
+ readonly pointer: string
88
+ readonly base: FecInstanceGeometry
89
+ /** Base layers in order, layer 0 first. */
90
+ readonly layers: readonly FecLayerBinding[]
91
+ /** Sum of P over the base layers: the wire RSB_length. */
92
+ readonly totalBaseRepairSymbols: number
93
+ readonly overlay?: FecOverlayBinding
94
+ /** Why a declared overlay is not used, when one is not. */
95
+ readonly overlayUnavailable?: FecBindingUnavailable
96
+ }
97
+ | {
98
+ readonly status: 'unavailable'
99
+ readonly track: string
100
+ readonly pointer: string
101
+ readonly reason: FecBindingUnavailable
102
+ }
103
+
104
+ type Obj = Record<string, unknown>
105
+ const recordOf = (v: unknown): Obj | undefined =>
106
+ typeof v === 'object' && v !== null && !Array.isArray(v) ? (v as Obj) : undefined
107
+ const escape = (s: string) => s.replace(/~/g, '~0').replace(/\//g, '~1')
108
+
109
+ /**
110
+ * The `tracks[]` view of a catalog in either dialect, with each view track's
111
+ * JSON Pointer in the original catalog.
112
+ */
113
+ export function fecCatalogTracks(catalog: unknown): { tracks: unknown[]; pointers: string[] } {
114
+ const root = recordOf(catalog) ?? {}
115
+ const tracks: unknown[] = []
116
+ const pointers: string[] = []
117
+ const hangNames = new Set<string>()
118
+ for (const role of ['video', 'audio'] as const) {
119
+ for (const [name, r] of Object.entries(recordOf(recordOf(root[role])?.['renditions']) ?? {})) {
120
+ const entry = recordOf(r)
121
+ if (!entry) continue
122
+ hangNames.add(name)
123
+ const kind = recordOf(entry['container'])?.['kind']
124
+ // A hang rendition carries no `packaging`; its container kind is it.
125
+ // `mmtp-fec` is hang's kind for MMTP that carries FEC (the FFmpeg
126
+ // publisher's `moq_publish_set_video_packaging`): MMTP packaging.
127
+ const view: Obj = { name, packaging: kind === undefined || kind === 'mmtp-fec' ? 'mmtp' : kind }
128
+ if (entry['fec'] !== undefined) view['fec'] = entry['fec']
129
+ tracks.push(view)
130
+ pointers.push(`/${role}/renditions/${escape(name)}`)
131
+ }
132
+ }
133
+ const hangRenditions = tracks.length
134
+ for (const [name, r] of Object.entries(recordOf(recordOf(root['repair'])?.['renditions']) ?? {})) {
135
+ hangNames.add(name)
136
+ tracks.push({ packaging: 'fec-repair', ...recordOf(r), name })
137
+ pointers.push(`/repair/renditions/${escape(name)}`)
138
+ }
139
+ const raw = Array.isArray(root['tracks']) ? root['tracks'] : []
140
+ const named = new Set(hangNames)
141
+ raw.forEach((t, i) => {
142
+ const name = recordOf(t)?.['name']
143
+ if (typeof name === 'string') named.add(name)
144
+ if (typeof name === 'string' && hangNames.has(name)) return
145
+ tracks.push(t)
146
+ pointers.push(`/tracks/${i}`)
147
+ })
148
+ // hang layer 0 needs no `repair.renditions` entry: the rendition's instance
149
+ // names it, and a hang Root (hang's toHang) carries no repair block. Such a
150
+ // name stands for a repair track of that instance.
151
+ tracks.slice(0, hangRenditions).forEach((t, ti) => {
152
+ const view = t as Obj
153
+ if (!Array.isArray(view['fec'])) return
154
+ view['fec'].forEach((o, ii) => {
155
+ const repairTrack = recordOf(o)?.['repairTrack']
156
+ if (typeof repairTrack !== 'string' || named.has(repairTrack)) return
157
+ named.add(repairTrack)
158
+ const instanceId = recordOf(o)?.['instanceId']
159
+ tracks.push({ name: repairTrack, packaging: 'fec-repair', source: view['name'], ...(instanceId === undefined ? {} : { instanceId }) })
160
+ pointers.push(`${pointers[ti]}/fec/${ii}/repairTrack`)
161
+ })
162
+ })
163
+ return { tracks, pointers }
164
+ }
165
+
166
+ /**
167
+ * Every source track's runtime FEC binding, by track name. A track with no
168
+ * `fec`, or an empty `fec` array, declares no FEC and has no entry.
169
+ */
170
+ export function resolveFecBindings(
171
+ catalog: unknown,
172
+ receiver: FecCatalogReceiver = RUNTIME_FEC_RECEIVER,
173
+ ): ReadonlyMap<string, FecSourceBinding> {
174
+ const { tracks, pointers } = fecCatalogTracks(catalog)
175
+ const report = validateFecCatalog({ tracks }, receiver)
176
+ const at = (path: string) => {
177
+ const m = /^\/tracks\/(\d+)(.*)$/.exec(path)
178
+ return m ? pointers[Number(m[1])] + m[2] : path
179
+ }
180
+ const trackAt = (i: number) => recordOf(tracks[i]) ?? {}
181
+ const nameAt = (i: number | null) => (i === null ? undefined : (trackAt(i)['name'] as string))
182
+ const why = (i: { outcome: string; issues: readonly { code: FecCatalogCode; path: string }[]; path: string }) =>
183
+ Object.freeze({
184
+ code: i.issues[0]?.code ?? 'fec_member',
185
+ path: at(i.issues[0]?.path ?? i.path),
186
+ outcome: i.outcome === 'unavailable' ? ('unavailable' as const) : ('violation' as const),
187
+ })
188
+
189
+ const out = new Map<string, FecSourceBinding>()
190
+ tracks.forEach((t, ti) => {
191
+ const track = recordOf(t)
192
+ const name = track?.['name']
193
+ if (!track || typeof name !== 'string' || out.has(name)) return
194
+ if (track['packaging'] === 'fec-repair' || track['packaging'] === 'fec-source') return
195
+ const fec = track['fec']
196
+ if (fec === undefined) return
197
+ if (!Array.isArray(fec)) {
198
+ const reason = { code: 'fec_member' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const }
199
+ out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason: Object.freeze(reason) }))
200
+ return
201
+ }
202
+ if (fec.length === 0) return
203
+ const insts = report.instances.filter((i) => i.trackIndex === ti)
204
+ const isOverlay = (i: (typeof insts)[number]) => Object.hasOwn(recordOf(fec[Number(i.path.split('/').pop())]) ?? {}, 'scope')
205
+ const bases = insts.filter((i) => !isOverlay(i))
206
+ const okBases = bases.filter((i) => i.outcome === 'ok')
207
+ if (okBases.length !== 1) {
208
+ const reason =
209
+ // No base instance at all (overlay-only `fec`) is a count of 0, not
210
+ // the overlay's own issue.
211
+ okBases.length > 1 || bases.length === 0
212
+ ? Object.freeze({ code: 'fec_base_instances' as const, path: at(`/tracks/${ti}/fec`), outcome: 'violation' as const })
213
+ : why(bases[0] ?? insts[0]!)
214
+ out.set(name, Object.freeze({ status: 'unavailable', track: name, pointer: pointers[ti]!, reason }))
215
+ return
216
+ }
217
+ const b = okBases[0]!
218
+ const base = geometry(recordOf(fec[Number(b.path.split('/').pop())])!, b.instanceId!, nameAt(b.symbolTrack))
219
+ const priorityOf = (i: number | null) => {
220
+ const p = i === null ? undefined : trackAt(i)['priority']
221
+ return typeof p === 'number' ? p : DEFAULT_FEC_REPAIR_PRIORITY
222
+ }
223
+ const layers: FecLayerBinding[] = [
224
+ { layer: 0, repairTrackName: base.repairTrack, repairSymbols: base.repairSymbols, rsIdOffset: 0, priority: priorityOf(b.repairTrack) },
225
+ ]
226
+ const more = b.boundRepairTracks
227
+ .map((tj) => ({ tj, layer: trackAt(tj)['repairLayer'] as number }))
228
+ .sort((x, y) => x.layer - y.layer)
229
+ let offset = base.repairSymbols
230
+ for (const { tj, layer } of more) {
231
+ const repairSymbols = trackAt(tj)['repairSymbols'] as number
232
+ layers.push({ layer, repairTrackName: nameAt(tj)!, repairSymbols, rsIdOffset: offset, priority: priorityOf(tj) })
233
+ offset += repairSymbols
234
+ }
235
+ let overlay: FecOverlayBinding | undefined
236
+ let overlayUnavailable: FecBindingUnavailable | undefined
237
+ const ov = insts.find(isOverlay)
238
+ if (ov?.outcome === 'ok') {
239
+ const g = geometry(recordOf(fec[Number(ov.path.split('/').pop())])!, ov.instanceId!, nameAt(ov.symbolTrack))
240
+ overlay = Object.freeze({
241
+ repairTrackName: g.repairTrack,
242
+ scope: 'keyframe' as const,
243
+ sourceSymbols: g.sourceSymbols,
244
+ repairSymbols: g.repairSymbols,
245
+ priority: priorityOf(ov.repairTrack),
246
+ geometry: g,
247
+ })
248
+ } else if (ov) overlayUnavailable = why(ov)
249
+ out.set(
250
+ name,
251
+ Object.freeze({
252
+ status: 'ok' as const,
253
+ track: name,
254
+ pointer: pointers[ti]!,
255
+ base,
256
+ layers: Object.freeze(layers.map((l) => Object.freeze(l))),
257
+ totalBaseRepairSymbols: offset,
258
+ ...(overlay ? { overlay } : {}),
259
+ ...(overlayUnavailable ? { overlayUnavailable } : {}),
260
+ }),
261
+ )
262
+ })
263
+ return out
264
+ }
265
+
266
+ // The validator passed this instance for this receiver, so every member read
267
+ // here is present and in range.
268
+ function geometry(o: Obj, instanceId: number, symbolTrack: string | undefined): FecInstanceGeometry {
269
+ return Object.freeze({
270
+ instanceId,
271
+ scheme: o['scheme'] as 'raptorq' | 'reed-solomon',
272
+ blockProfile: o['blockProfile'] as string,
273
+ sourceSymbols: o['sourceSymbols'] as number,
274
+ repairSymbols: o['repairSymbols'] as number,
275
+ symbolSize: o['symbolSize'] as number,
276
+ ...(typeof o['interleaveDepthMs'] === 'number' ? { interleaveDepthMs: o['interleaveDepthMs'] } : {}),
277
+ ...(typeof o['reorderToleranceMs'] === 'number' ? { reorderToleranceMs: o['reorderToleranceMs'] } : {}),
278
+ repairTrack: o['repairTrack'] as string,
279
+ ...(symbolTrack !== undefined ? { symbolTrack } : {}),
280
+ })
281
+ }