@blockcast/mmt-base 0.1.0 → 0.3.0-main.288475b64b3e

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,860 @@
1
+ /**
2
+ * Receiver-side parsers for MMTP signalling message mode.
3
+ *
4
+ * - The MMTP payload header for signalling message mode
5
+ * (ISO/IEC 23008-1:2023 9.3.4.2, Figure 15).
6
+ * - The general signalling message format (10.2, Table 20).
7
+ * - The AL-FEC message (Annex C Table C.3 as replaced by
8
+ * ISO/IEC 23008-1:2023/Amd 1:2025; semantics C.6.3).
9
+ * - The PA message (10.3.2, Table 21) and MPT message (10.3.4, Table 23)
10
+ * with the MP table (10.3.9, Table 29), Identifier_mapping (10.6.3,
11
+ * Table 98), asset_id (10.6.2, Table 97) and MMT_general_location_info
12
+ * (10.6.1, Table 95).
13
+ *
14
+ * Every parser here is total: malformed, truncated, ambiguous or unsupported
15
+ * input yields null, never an exception. Nothing is inferred: a field the
16
+ * receiver cannot place exactly makes the whole message unparseable, because
17
+ * every later field of a sequential syntax would be misread after it.
18
+ *
19
+ * Reserved bits are ignored on receipt. The senders write them, and a
20
+ * receiver that refuses a reserved value cannot accept a conformant revision
21
+ * of the syntax that assigns it.
22
+ */
23
+ import {
24
+ ALFEC_MESSAGE_ID,
25
+ FEC_CODE_RAPTORQ,
26
+ PACKET_TYPE_SIGNALING,
27
+ parseMMTPHeader,
28
+ type MMTPHeader,
29
+ } from './mmtp.js'
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Wire reader
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /** Internal: raised by WireReader, caught at every exported parser. */
36
+ class MalformedSignalling extends Error {}
37
+
38
+ class WireReader {
39
+ readonly #bytes: Uint8Array
40
+ readonly #end: number
41
+ #offset: number
42
+
43
+ constructor(bytes: Uint8Array, start = 0, end = bytes.length) {
44
+ this.#bytes = bytes
45
+ this.#offset = start
46
+ this.#end = end
47
+ }
48
+
49
+ get offset(): number {
50
+ return this.#offset
51
+ }
52
+
53
+ get remaining(): number {
54
+ return this.#end - this.#offset
55
+ }
56
+
57
+ #take(length: number): number {
58
+ if (length < 0 || length > this.remaining) throw new MalformedSignalling('truncated')
59
+ const at = this.#offset
60
+ this.#offset += length
61
+ return at
62
+ }
63
+
64
+ u8(): number {
65
+ return this.#bytes[this.#take(1)]!
66
+ }
67
+
68
+ u16(): number {
69
+ const at = this.#take(2)
70
+ return (this.#bytes[at]! << 8) | this.#bytes[at + 1]!
71
+ }
72
+
73
+ u24(): number {
74
+ const at = this.#take(3)
75
+ return (this.#bytes[at]! << 16) | (this.#bytes[at + 1]! << 8) | this.#bytes[at + 2]!
76
+ }
77
+
78
+ u32(): number {
79
+ const at = this.#take(4)
80
+ return ((this.#bytes[at]! << 24) | (this.#bytes[at + 1]! << 16) |
81
+ (this.#bytes[at + 2]! << 8) | this.#bytes[at + 3]!) >>> 0
82
+ }
83
+
84
+ /** A copy, so a parsed message never aliases a reused packet buffer. */
85
+ bytes(length: number): Uint8Array {
86
+ const at = this.#take(length)
87
+ return this.#bytes.slice(at, at + length)
88
+ }
89
+
90
+ /** Throw unless every byte up to the end bound was consumed. */
91
+ expectEnd(): void {
92
+ if (this.remaining !== 0) throw new MalformedSignalling('trailing bytes')
93
+ }
94
+ }
95
+
96
+ function fail(reason: string): never {
97
+ throw new MalformedSignalling(reason)
98
+ }
99
+
100
+ /** Run a parser, turning only our own malformed-input signal into null. */
101
+ function total<T>(parse: () => T | null): T | null {
102
+ try {
103
+ return parse()
104
+ } catch (error) {
105
+ if (error instanceof MalformedSignalling) return null
106
+ throw error
107
+ }
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // 9.3.4.2 — MMTP payload header for signalling message mode
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * How the signalling payload of an MMTP packet was framed: always
116
+ * `iso-9.3.4.2`, the 2-byte `f_i(2) RES(4) H(1) A(1) | frag_counter(8)`
117
+ * header of ISO/IEC 23008-1:2023 9.3.4.2 ahead of the message(s).
118
+ *
119
+ * The pre-remediation Blockcast form, an AL-FEC message directly after the
120
+ * MMTP header, is not accepted (BLO-35597, Phase L). Its first two bytes,
121
+ * `0x02 0x03`, read as a 9.3.4.2 header claiming three further fragments
122
+ * of a complete message, which the header parser refuses: such a packet
123
+ * parses to null, never to a guess.
124
+ */
125
+ export type SignallingPayloadForm = 'iso-9.3.4.2'
126
+
127
+ export interface SignallingPayload {
128
+ form: SignallingPayloadForm
129
+ /** Each complete signalling message, starting at its message_id. */
130
+ messages: Uint8Array[]
131
+ }
132
+
133
+ /** f_i (Table 19): payload contains one or more complete signalling messages. */
134
+ export const SIGNALLING_FRAGMENTATION_COMPLETE = 0b00
135
+
136
+ /**
137
+ * Split the payload of an MMTP signalling packet into its complete
138
+ * signalling messages.
139
+ *
140
+ * Returns null when the packet is not a well-formed, unprotected (FEC_type 0)
141
+ * signalling packet, or when it carries a fragment (f_i != 00): reassembly
142
+ * across packets is not implemented, and a fragment is not a message.
143
+ */
144
+ export function parseSignallingPayload(mmtpPacket: Uint8Array): SignallingPayload | null {
145
+ let header: MMTPHeader
146
+ try {
147
+ header = parseMMTPHeader(mmtpPacket)
148
+ } catch {
149
+ return null
150
+ }
151
+ if (header.packetType !== PACKET_TYPE_SIGNALING || header.fecType !== 0) return null
152
+ const payload = mmtpPacket.subarray(header.headerLength)
153
+ return total(() => splitSignallingPayload(payload))
154
+ }
155
+
156
+ function splitSignallingPayload(payload: Uint8Array): SignallingPayload {
157
+ if (payload.length < 2) fail('short signalling payload')
158
+ const reader = new WireReader(payload)
159
+ const flags = reader.u8()
160
+ const fragmentation = flags >> 6
161
+ const lengthIs32Bits = ((flags >> 1) & 1) === 1 // H
162
+ const aggregated = (flags & 1) === 1 // A
163
+ const fragmentCounter = reader.u8()
164
+ if (fragmentation !== SIGNALLING_FRAGMENTATION_COMPLETE) fail('fragmented signalling message')
165
+ // frag_counter counts the fragments that follow this one; a complete
166
+ // message has none, and an aggregated payload "shall" carry 0.
167
+ if (fragmentCounter !== 0) fail('frag_counter on a complete message')
168
+
169
+ if (!aggregated) {
170
+ // A=0: exactly one message and no MSG_length field (H is meaningless).
171
+ if (reader.remaining === 0) fail('empty signalling payload')
172
+ return { form: 'iso-9.3.4.2', messages: [payload.subarray(reader.offset)] }
173
+ }
174
+
175
+ const messages: Uint8Array[] = []
176
+ while (reader.remaining > 0) {
177
+ const length = lengthIs32Bits ? reader.u32() : reader.u16()
178
+ if (length === 0) fail('zero MSG_length')
179
+ const start = reader.offset
180
+ reader.bytes(length) // bounds check
181
+ messages.push(payload.subarray(start, start + length))
182
+ }
183
+ if (messages.length === 0) fail('aggregated payload with no message')
184
+ return { form: 'iso-9.3.4.2', messages }
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // 10.2 — general signalling message format
189
+ // ---------------------------------------------------------------------------
190
+
191
+ export const PA_MESSAGE_ID = 0x0000
192
+ export const MPI_MESSAGE_ID_FIRST = 0x0001
193
+ export const MPI_MESSAGE_ID_LAST = 0x0010
194
+ export const MPT_MESSAGE_ID_FIRST = 0x0011
195
+ /** MPT message carrying the complete MP table (Table 101). */
196
+ export const MPT_MESSAGE_ID_COMPLETE = 0x0020
197
+
198
+ /**
199
+ * Whether a message's `length` field is 32 bits (Table 20: PA and MPI
200
+ * messages; Table 101: the 0x7000-0x7FFF range "32-bit length message").
201
+ */
202
+ export function signallingMessageHas32BitLength(messageId: number): boolean {
203
+ return messageId === PA_MESSAGE_ID ||
204
+ (messageId >= MPI_MESSAGE_ID_FIRST && messageId <= MPI_MESSAGE_ID_LAST) ||
205
+ (messageId >= 0x7000 && messageId <= 0x7fff)
206
+ }
207
+
208
+ /**
209
+ * Read the general header (Table 20) of a complete message and check that
210
+ * `length` covers exactly the rest of the message.
211
+ */
212
+ function readMessageHeader(message: Uint8Array): { reader: WireReader; messageId: number; version: number } {
213
+ const reader = new WireReader(message)
214
+ const messageId = reader.u16()
215
+ const version = reader.u8()
216
+ const length = signallingMessageHas32BitLength(messageId) ? reader.u32() : reader.u16()
217
+ // "The value 0 is not valid for this field" (C.6.3, 10.3.2.3, 10.3.4.3).
218
+ if (length === 0 || length !== reader.remaining) fail('message length mismatch')
219
+ return { reader, messageId, version }
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Annex C Table C.3 (Amd 1:2025) — AL-FEC message
224
+ // ---------------------------------------------------------------------------
225
+
226
+ /** fec_coding_structure values (Table C.8 as replaced by Amd 1:2025). */
227
+ export const FEC_CODING_STRUCTURE_NONE = 0b0000
228
+ export const FEC_CODING_STRUCTURE_ONE_STAGE = 0b0001
229
+ export const FEC_CODING_STRUCTURE_TWO_STAGE = 0b0010
230
+ export const FEC_CODING_STRUCTURE_LAYER_AWARE = 0b0011
231
+ export const FEC_CODING_STRUCTURE_ADAPTIVE = 0b0100
232
+
233
+ /** ssbg_mode values (Table C.5). b11 is reserved. */
234
+ export const SSBG_MODE_0 = 0b00
235
+ export const SSBG_MODE_1 = 0b01
236
+ export const SSBG_MODE_2 = 0b10
237
+
238
+ export interface AlFecPrivateField {
239
+ privateFlag: 0 | 1
240
+ data: Uint8Array
241
+ }
242
+
243
+ export interface AlFecRepairFlow {
244
+ /**
245
+ * repair_flow_id (16 bits under Amd 1:2025 for one-stage, two-stage and
246
+ * LA-FEC). Blockcast senders set it to the repair flow's MMTP packet_id.
247
+ */
248
+ repairFlowId: number
249
+ /**
250
+ * fec_code_id_for_repair_flow. Under LA-FEC (0011) one value is signalled
251
+ * per FEC flow and is repeated here on every layer.
252
+ */
253
+ fecCodeId: number
254
+ /**
255
+ * The private_flag/private_field_length/private_field triple, present
256
+ * exactly when the message's private_fec_flag is 1. Shared by every layer
257
+ * under LA-FEC, like fecCodeId.
258
+ */
259
+ privateField?: AlFecPrivateField
260
+ maximumK: number
261
+ maximumP: number
262
+ protectionWindowTime: number
263
+ protectionWindowSize: number
264
+ }
265
+
266
+ export interface AlFecFlow {
267
+ fecFlowId: number
268
+ sourceFlowId: number
269
+ /** Every asset packet_id of the FEC source flow, in signalled order. */
270
+ packetIds: number[]
271
+ fecCodingStructure: number
272
+ ssbgMode: number
273
+ ffsrptsFlag: 0 | 1
274
+ /**
275
+ * The low bit of the coding byte. Amd 1:2025 Table C.3 names it
276
+ * fec_payload_id_mode (Table C.7: 0 = mode 0, Source FEC Payload ID added
277
+ * to each packet; 1 = mode 1, C.7, packets unmodified). The unamended 2023
278
+ * Table C.3 lists the same bit as reserved "1". The parse never depends on
279
+ * it — no later field is conditional on it — so both readings are
280
+ * accepted and the value is reported, not interpreted (BLO-35597 N1).
281
+ */
282
+ fecPayloadIdMode: 0 | 1
283
+ /** length_of_repair_symbol, in bytes. */
284
+ repairSymbolSize: number
285
+ /** Present exactly when ssbg_mode is b10. */
286
+ ssbgMode2?: {
287
+ numRepairSymbolPerPacket: number
288
+ numSymbolElementPerSourceSymbol: number
289
+ }
290
+ /** num_of_sub_block_per_source_block, two-stage (0010) only. */
291
+ numSubBlockPerSourceBlock?: number
292
+ /** One entry for one-stage; N3 parity flows; N4 LA-FEC layers; none for 0000. */
293
+ repairFlows: AlFecRepairFlow[]
294
+ }
295
+
296
+ export interface AlFecMessageBody {
297
+ /** The message's version counter (C.6.3). Any value is accepted. */
298
+ version: number
299
+ fecFlag: 0 | 1
300
+ privateFecFlag: 0 | 1
301
+ /** Empty exactly when fec_flag is 0 (no FEC source flow). */
302
+ flows: AlFecFlow[]
303
+ }
304
+
305
+ export interface AlFecMessage extends AlFecMessageBody {
306
+ payloadForm: SignallingPayloadForm
307
+ }
308
+
309
+ function readPrivateField(reader: WireReader): AlFecPrivateField {
310
+ const head = reader.u8()
311
+ return { privateFlag: (head >> 7) as 0 | 1, data: reader.bytes(head & 0x7f) }
312
+ }
313
+
314
+ function readRepairFlowTail(
315
+ reader: WireReader,
316
+ repairFlowId: number,
317
+ fecCodeId: number,
318
+ privateField: AlFecPrivateField | undefined,
319
+ ): AlFecRepairFlow {
320
+ const maximumK = reader.u24()
321
+ const maximumP = reader.u24()
322
+ const protectionWindowTime = reader.u32()
323
+ const protectionWindowSize = reader.u32()
324
+ return {
325
+ repairFlowId,
326
+ fecCodeId,
327
+ ...(privateField ? { privateField } : {}),
328
+ maximumK,
329
+ maximumP,
330
+ protectionWindowTime,
331
+ protectionWindowSize,
332
+ }
333
+ }
334
+
335
+ function readRepairFlow(reader: WireReader, privateFecFlag: boolean): AlFecRepairFlow {
336
+ const repairFlowId = reader.u16()
337
+ const fecCodeId = reader.u8()
338
+ const privateField = privateFecFlag ? readPrivateField(reader) : undefined
339
+ return readRepairFlowTail(reader, repairFlowId, fecCodeId, privateField)
340
+ }
341
+
342
+ function readFecFlow(reader: WireReader, privateFecFlag: boolean): AlFecFlow {
343
+ const fecFlowId = reader.u8()
344
+ const sourceFlowId = reader.u8()
345
+ const numberOfAssets = reader.u8()
346
+ if (numberOfAssets === 0) fail('FEC flow with no asset')
347
+ const packetIds: number[] = []
348
+ for (let i = 0; i < numberOfAssets; i++) packetIds.push(reader.u16())
349
+
350
+ const coding = reader.u8()
351
+ const fecCodingStructure = coding >> 4
352
+ const ssbgMode = (coding >> 2) & 0b11
353
+ const ffsrptsFlag = ((coding >> 1) & 1) as 0 | 1
354
+ const fecPayloadIdMode = (coding & 1) as 0 | 1
355
+ if (ssbgMode === 0b11) fail('reserved ssbg_mode')
356
+ const repairSymbolSize = reader.u16()
357
+
358
+ const flow: AlFecFlow = {
359
+ fecFlowId,
360
+ sourceFlowId,
361
+ packetIds,
362
+ fecCodingStructure,
363
+ ssbgMode,
364
+ ffsrptsFlag,
365
+ fecPayloadIdMode,
366
+ repairSymbolSize,
367
+ repairFlows: [],
368
+ }
369
+ if (ssbgMode === SSBG_MODE_2) {
370
+ flow.ssbgMode2 = {
371
+ numRepairSymbolPerPacket: reader.u16(),
372
+ numSymbolElementPerSourceSymbol: reader.u16(),
373
+ }
374
+ }
375
+
376
+ switch (fecCodingStructure) {
377
+ case FEC_CODING_STRUCTURE_NONE:
378
+ break
379
+ case FEC_CODING_STRUCTURE_ONE_STAGE:
380
+ flow.repairFlows.push(readRepairFlow(reader, privateFecFlag))
381
+ break
382
+ case FEC_CODING_STRUCTURE_TWO_STAGE: {
383
+ flow.numSubBlockPerSourceBlock = reader.u8()
384
+ const numberOfParityFlows = reader.u8()
385
+ for (let k = 0; k < numberOfParityFlows; k++) {
386
+ flow.repairFlows.push(readRepairFlow(reader, privateFecFlag))
387
+ }
388
+ break
389
+ }
390
+ case FEC_CODING_STRUCTURE_LAYER_AWARE: {
391
+ const numberOfLayers = reader.u8()
392
+ const fecCodeId = reader.u8()
393
+ const privateField = privateFecFlag ? readPrivateField(reader) : undefined
394
+ for (let l = 0; l < numberOfLayers; l++) {
395
+ flow.repairFlows.push(readRepairFlowTail(reader, reader.u16(), fecCodeId, privateField))
396
+ }
397
+ break
398
+ }
399
+ default:
400
+ // 0100 (adaptive FEC, Amd 1:2025): the published table leaves the
401
+ // width of number_of__class unstated, so its flows cannot be placed
402
+ // exactly. 0101-1111 are reserved. Either way every later flow would
403
+ // be misread, so the whole message is refused.
404
+ fail(`unsupported fec_coding_structure ${fecCodingStructure}`)
405
+ }
406
+ return flow
407
+ }
408
+
409
+ /**
410
+ * Parse a complete AL-FEC message (Table C.3, Amd 1:2025), starting at its
411
+ * message_id. Every FEC flow, every asset packet_id and every
412
+ * coding-structure-conditional field is returned.
413
+ */
414
+ export function parseAlFecMessageBody(message: Uint8Array): AlFecMessageBody | null {
415
+ return total(() => {
416
+ const { reader, messageId, version } = readMessageHeader(message)
417
+ if (messageId !== ALFEC_MESSAGE_ID) return null
418
+ const flags = reader.u8()
419
+ const fecFlag = (flags >> 7) as 0 | 1
420
+ const privateFecFlag = ((flags >> 6) & 1) as 0 | 1
421
+ if (fecFlag === 0) {
422
+ // "If every source flows are delivered without AL-FEC protection, all
423
+ // the remaining message is not to be delivered" (C.6.3).
424
+ reader.expectEnd()
425
+ return { version, fecFlag, privateFecFlag, flows: [] }
426
+ }
427
+ const descriptorLength = reader.u16()
428
+ if (descriptorLength === 0 || descriptorLength !== reader.remaining) {
429
+ fail('length_of_fec_flow_descriptor mismatch')
430
+ }
431
+ const numberOfFecFlows = reader.u8()
432
+ if (numberOfFecFlows === 0) fail('fec_flag=1 with no FEC flow')
433
+ const flows: AlFecFlow[] = []
434
+ for (let i = 0; i < numberOfFecFlows; i++) flows.push(readFecFlow(reader, privateFecFlag === 1))
435
+ reader.expectEnd()
436
+ return { version, fecFlag, privateFecFlag, flows }
437
+ })
438
+ }
439
+
440
+ /**
441
+ * Parse the AL-FEC message carried by a full MMTP signalling packet (a
442
+ * 9.3.4.2 payload, see SignallingPayloadForm). Returns null when the packet
443
+ * carries no AL-FEC message, more than one, or a malformed one.
444
+ */
445
+ export function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecMessage | null {
446
+ const packet = parseSignallingPacket(mmtpPacket)
447
+ if (!packet) return null
448
+ const alFec = packet.messages.filter(
449
+ (message): message is Extract<MmtSignallingMessage, { kind: 'al-fec' }> => message.kind === 'al-fec',
450
+ )
451
+ if (alFec.length !== 1) return null
452
+ return { ...alFec[0]!.message, payloadForm: packet.payloadForm }
453
+ }
454
+
455
+ /**
456
+ * Decoder geometry of one FEC flow under the Blockcast one-stage RaptorQ
457
+ * profile: private_field = RaptorQ FEC OTI (RFC 6330 3.3.2-3.3.3, 12 bytes)
458
+ * followed by the interleave depth (1 byte).
459
+ */
460
+ export interface AlFecRaptorQFlowConfig {
461
+ sourcePacketIds: number[]
462
+ repairFlowId: number
463
+ /** Always `SSBG_MODE_1` (`iso-ssbg1-v1`). */
464
+ ssbgMode: number
465
+ /** The coding byte's low bit as signalled (see AlFecFlow.fecPayloadIdMode). */
466
+ fecPayloadIdMode: 0 | 1
467
+ repairSymbolSize: number
468
+ maximumK: number
469
+ maximumP: number
470
+ interleaveDepth: number
471
+ oti: Uint8Array
472
+ protectionWindowTime: number
473
+ protectionWindowSize: number
474
+ }
475
+
476
+ const RAPTORQ_PRIVATE_FIELD_LENGTH = 13
477
+ const RAPTORQ_MAX_SOURCE_SYMBOLS = 56_403
478
+ const REPAIR_SYMBOL_ID_MAX = 0x00ff_ffff
479
+
480
+ function canonicalRaptorQOtiK(oti: Uint8Array, repairSymbolSize: number): number | null {
481
+ if (oti.length !== 12 || oti[5] !== 0) return null
482
+ const transferLength = (oti[0]! * 0x1_0000_0000) +
483
+ (((oti[1]! << 24) | (oti[2]! << 16) | (oti[3]! << 8) | oti[4]!) >>> 0)
484
+ const symbolSize = (oti[6]! << 8) | oti[7]!
485
+ const sourceBlocks = oti[8]!
486
+ const subBlocks = (oti[9]! << 8) | oti[10]!
487
+ const alignment = oti[11]!
488
+ if (!Number.isSafeInteger(transferLength) || transferLength <= 0 ||
489
+ symbolSize !== repairSymbolSize || symbolSize === 0 || symbolSize % 8 !== 0 ||
490
+ sourceBlocks !== 1 || subBlocks !== 1 || alignment !== 8 ||
491
+ transferLength % symbolSize !== 0) {
492
+ return null
493
+ }
494
+ const k = transferLength / symbolSize
495
+ return Number.isInteger(k) && k >= 1 && k <= RAPTORQ_MAX_SOURCE_SYMBOLS ? k : null
496
+ }
497
+
498
+ /**
499
+ * Validate one parsed FEC flow against the Blockcast one-stage RaptorQ
500
+ * profile and return its decoder geometry, or null when the flow is outside
501
+ * the profile. Nothing is inferred: K comes from the OTI and must equal
502
+ * maximum_k_for_repair_flow.
503
+ *
504
+ * The profile is `iso-ssbg1-v1`, whose AL-FEC coding byte is 0x14
505
+ * (draft-ramadan-moq-fec §7.3; ISO/IEC 23008-1 Table C.3 as replaced by
506
+ * Amd 1:2025): one-stage, ssbg_mode1, ffsrpts_flag 0, fec_payload_id_mode 0.
507
+ * 0x15 is accepted too: its low bit is fec_payload_id_mode 1 under Amd
508
+ * 1:2025 but reserved "1" in the unamended 2023 Table C.3, so a 2023 sender
509
+ * of the same profile emits it. No later field depends on the bit, and each
510
+ * source packet's FEC_type says whether it carries a Source FEC Payload ID,
511
+ * so the bit is reported, not interpreted (BLO-35597 N1). Any other coding
512
+ * byte is refused here, though `parseAlFecMessageBody` still reports it:
513
+ * ssbg_mode0 (0x10/0x11, the retired `k-padded-v1` wire) carries no length
514
+ * field; ffsrpts_flag=1 changes the source/repair packet trailer layout.
515
+ */
516
+ export function alFecRaptorQFlowConfig(flow: AlFecFlow): AlFecRaptorQFlowConfig | null {
517
+ if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1) return null
518
+ if (flow.ssbgMode !== SSBG_MODE_1) return null
519
+ if (flow.ffsrptsFlag !== 0) return null
520
+ const repair = flow.repairFlows[0]!
521
+ if (repair.fecCodeId !== FEC_CODE_RAPTORQ) return null
522
+ const privateField = repair.privateField
523
+ if (!privateField || privateField.privateFlag !== 1 ||
524
+ privateField.data.length !== RAPTORQ_PRIVATE_FIELD_LENGTH) {
525
+ return null
526
+ }
527
+ const oti = privateField.data.slice(0, 12)
528
+ const interleaveDepth = privateField.data[12]!
529
+ const otiK = canonicalRaptorQOtiK(oti, flow.repairSymbolSize)
530
+ if (otiK === null || interleaveDepth === 0) return null
531
+ if (repair.maximumK !== otiK || repair.maximumP === 0 ||
532
+ repair.maximumK + repair.maximumP - 1 > REPAIR_SYMBOL_ID_MAX) {
533
+ return null
534
+ }
535
+ return {
536
+ sourcePacketIds: [...flow.packetIds],
537
+ repairFlowId: repair.repairFlowId,
538
+ ssbgMode: flow.ssbgMode,
539
+ fecPayloadIdMode: flow.fecPayloadIdMode,
540
+ repairSymbolSize: flow.repairSymbolSize,
541
+ maximumK: repair.maximumK,
542
+ maximumP: repair.maximumP,
543
+ interleaveDepth,
544
+ oti,
545
+ protectionWindowTime: repair.protectionWindowTime,
546
+ protectionWindowSize: repair.protectionWindowSize,
547
+ }
548
+ }
549
+
550
+ // ---------------------------------------------------------------------------
551
+ // 10.6 — location, asset_id and identifier mapping
552
+ // ---------------------------------------------------------------------------
553
+
554
+ /** location_type values (Table 96) a receiver may meet in an asset location. */
555
+ export type MmtGeneralLocation =
556
+ | { locationType: 0x00; packetId: number }
557
+ | { locationType: 0x01; srcAddress: Uint8Array; dstAddress: Uint8Array; dstPort: number; packetId: number }
558
+ | { locationType: 0x02; srcAddress: Uint8Array; dstAddress: Uint8Array; dstPort: number; packetId: number }
559
+ | { locationType: 0x03; networkId: number; transportStreamId: number; mpeg2Pid: number }
560
+ | { locationType: 0x04; srcAddress: Uint8Array; dstAddress: Uint8Array; dstPort: number; mpeg2Pid: number }
561
+ | { locationType: 0x05; url: Uint8Array }
562
+ | { locationType: 0x06; data: Uint8Array }
563
+
564
+ /**
565
+ * MMT_general_location_info() for an Asset location. "Only the value of
566
+ * location_type between 0x00 and 0x06 shall be used for an Asset location"
567
+ * (10.3.9.3), so any other value is refused rather than skipped: the
568
+ * remaining types have their own lengths, and a reserved one has none.
569
+ */
570
+ function readAssetLocation(reader: WireReader): MmtGeneralLocation {
571
+ const locationType = reader.u8()
572
+ switch (locationType) {
573
+ case 0x00:
574
+ return { locationType, packetId: reader.u16() }
575
+ case 0x01:
576
+ case 0x02: {
577
+ const addressLength = locationType === 0x01 ? 4 : 16
578
+ const srcAddress = reader.bytes(addressLength)
579
+ const dstAddress = reader.bytes(addressLength)
580
+ const dstPort = reader.u16()
581
+ return { locationType, srcAddress, dstAddress, dstPort, packetId: reader.u16() }
582
+ }
583
+ case 0x03: {
584
+ const networkId = reader.u16()
585
+ const transportStreamId = reader.u16()
586
+ return { locationType, networkId, transportStreamId, mpeg2Pid: reader.u16() & 0x1fff }
587
+ }
588
+ case 0x04: {
589
+ const srcAddress = reader.bytes(16)
590
+ const dstAddress = reader.bytes(16)
591
+ const dstPort = reader.u16()
592
+ return { locationType, srcAddress, dstAddress, dstPort, mpeg2Pid: reader.u16() & 0x1fff }
593
+ }
594
+ case 0x05:
595
+ return { locationType, url: reader.bytes(reader.u8()) }
596
+ case 0x06:
597
+ return { locationType, data: reader.bytes(reader.u16()) }
598
+ default:
599
+ fail(`location_type ${locationType} is not an Asset location`)
600
+ }
601
+ }
602
+
603
+ /** Identifier_mapping() (Table 98). */
604
+ export type MmtIdentifierMapping =
605
+ | { identifierType: 0x00; assetIdScheme: number; assetId: Uint8Array }
606
+ | { identifierType: 0x01; urls: Uint8Array[] }
607
+ | { identifierType: 0x02; regex: Uint8Array }
608
+ | { identifierType: 0x03; representationId: Uint8Array }
609
+ | { identifierType: number; privateIdentifier: Uint8Array }
610
+
611
+ function readIdentifierMapping(reader: WireReader): MmtIdentifierMapping {
612
+ const identifierType = reader.u8()
613
+ switch (identifierType) {
614
+ case 0x00: {
615
+ const assetIdScheme = reader.u32()
616
+ return { identifierType, assetIdScheme, assetId: reader.bytes(reader.u32()) }
617
+ }
618
+ case 0x01: {
619
+ const urls: Uint8Array[] = []
620
+ const urlCount = reader.u16()
621
+ for (let i = 0; i < urlCount; i++) urls.push(reader.bytes(reader.u16()))
622
+ return { identifierType, urls }
623
+ }
624
+ case 0x02:
625
+ return { identifierType, regex: reader.bytes(reader.u16()) }
626
+ case 0x03:
627
+ return { identifierType, representationId: reader.bytes(reader.u16()) }
628
+ default:
629
+ return { identifierType, privateIdentifier: reader.bytes(reader.u16()) }
630
+ }
631
+ }
632
+
633
+ // ---------------------------------------------------------------------------
634
+ // 10.3.9 — MP table; 10.3.2 PA message; 10.3.4 MPT message
635
+ // ---------------------------------------------------------------------------
636
+
637
+ /** table_id range of the MP table (Table 102): subset 0..14, then complete. */
638
+ export const MP_TABLE_ID_SUBSET_0 = 0x11
639
+ export const MP_TABLE_ID_COMPLETE = 0x20
640
+ export const MPI_TABLE_ID_FIRST = 0x01
641
+ export const MPI_TABLE_ID_LAST = 0x10
642
+
643
+ export interface MpAsset {
644
+ identifier: MmtIdentifierMapping
645
+ /** asset_type, a 4CC (for example 'hvc1', 'mp4a'). */
646
+ assetType: string
647
+ assetModificationFlag: boolean
648
+ defaultAssetFlag: boolean
649
+ /** Present exactly when asset_clock_relation_flag is 1. */
650
+ assetClockRelationId?: number
651
+ /** Present exactly when asset_timescale_flag is 1. */
652
+ assetTimescale?: number
653
+ locations: MmtGeneralLocation[]
654
+ descriptors: Uint8Array
655
+ }
656
+
657
+ export interface MpTable {
658
+ tableId: number
659
+ version: number
660
+ /** MP_table_mode (Table 30); b11 is reserved and reported as read. */
661
+ mpTableMode: number
662
+ /** Present exactly for the complete table (0x20) and subset 0 (0x11). */
663
+ packageId?: Uint8Array
664
+ /** Present exactly for the complete table (0x20) and subset 0 (0x11). */
665
+ descriptors?: Uint8Array
666
+ assets: MpAsset[]
667
+ }
668
+
669
+ function readMpTable(reader: WireReader): MpTable {
670
+ const tableId = reader.u8()
671
+ if (tableId < MP_TABLE_ID_SUBSET_0 || tableId > MP_TABLE_ID_COMPLETE) fail('not an MP table')
672
+ const version = reader.u8()
673
+ const length = reader.u16()
674
+ if (length === 0) fail('zero MP table length')
675
+ const table = new WireReader(reader.bytes(length))
676
+
677
+ const mpTableMode = table.u8() & 0b11
678
+ const result: MpTable = { tableId, version, mpTableMode, assets: [] }
679
+ if (tableId === MP_TABLE_ID_COMPLETE || tableId === MP_TABLE_ID_SUBSET_0) {
680
+ result.packageId = table.bytes(table.u8())
681
+ result.descriptors = table.bytes(table.u16())
682
+ }
683
+ const numberOfAssets = table.u8()
684
+ for (let i = 0; i < numberOfAssets; i++) {
685
+ const identifier = readIdentifierMapping(table)
686
+ const assetType = String.fromCharCode(...table.bytes(4))
687
+ const flags = table.u8()
688
+ const asset: MpAsset = {
689
+ identifier,
690
+ assetType,
691
+ assetModificationFlag: ((flags >> 2) & 1) === 1,
692
+ defaultAssetFlag: ((flags >> 1) & 1) === 1,
693
+ locations: [],
694
+ descriptors: new Uint8Array(0),
695
+ }
696
+ if ((flags & 1) === 1) { // asset_clock_relation_flag
697
+ asset.assetClockRelationId = table.u8()
698
+ if ((table.u8() & 1) === 1) asset.assetTimescale = table.u32() // asset_timescale_flag
699
+ }
700
+ const locationCount = table.u8()
701
+ for (let j = 0; j < locationCount; j++) asset.locations.push(readAssetLocation(table))
702
+ asset.descriptors = table.bytes(table.u16())
703
+ result.assets.push(asset)
704
+ }
705
+ table.expectEnd()
706
+ return result
707
+ }
708
+
709
+ /** Parse a complete MP table (Table 29), starting at its table_id. */
710
+ export function parseMpTable(bytes: Uint8Array): MpTable | null {
711
+ return total(() => {
712
+ const reader = new WireReader(bytes)
713
+ const table = readMpTable(reader)
714
+ reader.expectEnd()
715
+ return table
716
+ })
717
+ }
718
+
719
+ /**
720
+ * The packet_ids an Asset is delivered on: every location of type 0x00
721
+ * (same MMTP packet flow), 0x01 (UDP/IPv4) or 0x02 (UDP/IPv6), in
722
+ * signalled order.
723
+ */
724
+ export function mpAssetPacketIds(asset: MpAsset): number[] {
725
+ const packetIds: number[] = []
726
+ for (const location of asset.locations) {
727
+ if (location.locationType === 0x00 || location.locationType === 0x01 || location.locationType === 0x02) {
728
+ packetIds.push(location.packetId)
729
+ }
730
+ }
731
+ return packetIds
732
+ }
733
+
734
+ export interface MmtSignallingTable {
735
+ tableId: number
736
+ version: number
737
+ /** The complete table, starting at its table_id. */
738
+ bytes: Uint8Array
739
+ }
740
+
741
+ export interface PaMessage {
742
+ version: number
743
+ /** Every table of the message payload, in order. */
744
+ tables: MmtSignallingTable[]
745
+ /** The MP tables among `tables`, decoded. */
746
+ mpTables: MpTable[]
747
+ }
748
+
749
+ export interface MptMessage {
750
+ /** 0x0011-0x001F carry subset 0-14, 0x0020 the complete MP table. */
751
+ messageId: number
752
+ version: number
753
+ table: MpTable
754
+ }
755
+
756
+ /**
757
+ * Parse a complete PA message (Table 21), starting at its message_id.
758
+ *
759
+ * Every table in the payload is delimited by its own 16-bit `length` and
760
+ * must match the extension's table_id and table_version. For non-MPI tables
761
+ * table_length is "a copy of the length field" (10.3.2.3) and must match it
762
+ * too; for an MPI table it is derived differently, so it is not compared.
763
+ */
764
+ export function parsePaMessageBody(message: Uint8Array): PaMessage | null {
765
+ return total(() => {
766
+ const { reader, messageId, version } = readMessageHeader(message)
767
+ if (messageId !== PA_MESSAGE_ID) return null
768
+ const numberOfTables = reader.u8()
769
+ const entries: { tableId: number; tableVersion: number; tableLength: number }[] = []
770
+ for (let i = 0; i < numberOfTables; i++) {
771
+ entries.push({ tableId: reader.u8(), tableVersion: reader.u8(), tableLength: reader.u16() })
772
+ }
773
+ const tables: MmtSignallingTable[] = []
774
+ const mpTables: MpTable[] = []
775
+ for (const entry of entries) {
776
+ const start = reader.offset
777
+ const tableId = reader.u8()
778
+ const tableVersion = reader.u8()
779
+ const length = reader.u16()
780
+ if (length === 0) fail('zero table length')
781
+ reader.bytes(length) // bounds check
782
+ const isMpi = tableId >= MPI_TABLE_ID_FIRST && tableId <= MPI_TABLE_ID_LAST
783
+ if (tableId !== entry.tableId || tableVersion !== entry.tableVersion ||
784
+ (!isMpi && length !== entry.tableLength)) {
785
+ fail('PA extension does not describe its table')
786
+ }
787
+ const bytes = message.subarray(start, reader.offset)
788
+ tables.push({ tableId, version: tableVersion, bytes })
789
+ if (tableId >= MP_TABLE_ID_SUBSET_0 && tableId <= MP_TABLE_ID_COMPLETE) {
790
+ const table = parseMpTable(bytes)
791
+ if (!table) fail('malformed MP table in PA message')
792
+ mpTables.push(table)
793
+ }
794
+ }
795
+ reader.expectEnd()
796
+ return { version, tables, mpTables }
797
+ })
798
+ }
799
+
800
+ /** Parse a complete MPT message (Table 23), starting at its message_id. */
801
+ export function parseMptMessageBody(message: Uint8Array): MptMessage | null {
802
+ return total(() => {
803
+ const { reader, messageId, version } = readMessageHeader(message)
804
+ if (messageId < MPT_MESSAGE_ID_FIRST || messageId > MPT_MESSAGE_ID_COMPLETE) return null
805
+ const table = readMpTable(reader)
806
+ reader.expectEnd()
807
+ return { messageId, version, table }
808
+ })
809
+ }
810
+
811
+ // ---------------------------------------------------------------------------
812
+ // Packet-level dispatch (the packet_id 0 signalling handler)
813
+ // ---------------------------------------------------------------------------
814
+
815
+ export type MmtSignallingMessage =
816
+ | { kind: 'al-fec'; message: AlFecMessageBody }
817
+ | { kind: 'pa'; message: PaMessage }
818
+ | { kind: 'mpt'; message: MptMessage }
819
+ /** A message this module does not decode, delimited and length-checked. */
820
+ | { kind: 'other'; messageId: number; version: number; bytes: Uint8Array }
821
+
822
+ export interface MmtSignallingPacket {
823
+ payloadForm: SignallingPayloadForm
824
+ messages: MmtSignallingMessage[]
825
+ }
826
+
827
+ function decodeSignallingMessage(bytes: Uint8Array): MmtSignallingMessage {
828
+ const { messageId, version } = readMessageHeader(bytes)
829
+ if (messageId === ALFEC_MESSAGE_ID) {
830
+ const message = parseAlFecMessageBody(bytes)
831
+ if (!message) fail('malformed AL-FEC message')
832
+ return { kind: 'al-fec', message }
833
+ }
834
+ if (messageId === PA_MESSAGE_ID) {
835
+ const message = parsePaMessageBody(bytes)
836
+ if (!message) fail('malformed PA message')
837
+ return { kind: 'pa', message }
838
+ }
839
+ if (messageId >= MPT_MESSAGE_ID_FIRST && messageId <= MPT_MESSAGE_ID_COMPLETE) {
840
+ const message = parseMptMessageBody(bytes)
841
+ if (!message) fail('malformed MPT message')
842
+ return { kind: 'mpt', message }
843
+ }
844
+ return { kind: 'other', messageId, version, bytes }
845
+ }
846
+
847
+ /**
848
+ * Parse every signalling message of a full MMTP signalling packet (the
849
+ * packet_id 0 flow). Returns null when the packet or any message in it is
850
+ * malformed: one unparseable message leaves the receiver unable to say what
851
+ * the packet signalled. Only the 9.3.4.2 payload form is accepted.
852
+ */
853
+ export function parseSignallingPacket(mmtpPacket: Uint8Array): MmtSignallingPacket | null {
854
+ const payload = parseSignallingPayload(mmtpPacket)
855
+ if (!payload) return null
856
+ return total(() => ({
857
+ payloadForm: payload.form,
858
+ messages: payload.messages.map(decodeSignallingMessage),
859
+ }))
860
+ }