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