@blockcast/mmt-base 0.1.0 → 0.3.0-main.d9354616fdf2
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
package/src/mmtp.ts
CHANGED
|
@@ -91,6 +91,18 @@ export const PACKET_TYPE_REPAIR = 0x03
|
|
|
91
91
|
// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)
|
|
92
92
|
export const ALFEC_MESSAGE_ID = 0x0203
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING
|
|
96
|
+
* packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`
|
|
97
|
+
* muxer sends that message on this packet_id into the same multicast group as
|
|
98
|
+
* the media, and moqcast-draft (PR #44) makes advertising it for a track a
|
|
99
|
+
* MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.
|
|
100
|
+
*/
|
|
101
|
+
export const MMTP_SIGNALING_PACKET_ID = 0
|
|
102
|
+
|
|
103
|
+
/** Largest value of the 16-bit MMTP `packet_id` header field. */
|
|
104
|
+
export const MMTP_PACKET_ID_MAX = 0xffff
|
|
105
|
+
|
|
94
106
|
// FEC Code IDs
|
|
95
107
|
export const FEC_CODE_XOR = 1
|
|
96
108
|
export const FEC_CODE_RAPTORQ = 2
|
|
@@ -171,6 +183,32 @@ export function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {
|
|
|
171
183
|
}
|
|
172
184
|
}
|
|
173
185
|
|
|
186
|
+
/**
|
|
187
|
+
* The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would
|
|
188
|
+
* throw for it (short buffer, V != 0, truncated packet_counter or header
|
|
189
|
+
* extension).
|
|
190
|
+
*
|
|
191
|
+
* This is the per-packet filter a receiver runs before any other work, so it
|
|
192
|
+
* builds no header object, DataView or BigInt timestamp (BLO-36599). Use
|
|
193
|
+
* parseMMTPHeader when any other header field is needed.
|
|
194
|
+
*/
|
|
195
|
+
export function readMMTPPacketId(buffer: Uint8Array): number | null {
|
|
196
|
+
if (buffer.length < MMTP_HEADER_SIZE) return null
|
|
197
|
+
const flags = buffer[0]!
|
|
198
|
+
if (((flags >> 6) & 0x03) !== 0) return null
|
|
199
|
+
let offset = MMTP_HEADER_SIZE
|
|
200
|
+
if ((flags & 0x20) !== 0) {
|
|
201
|
+
if (offset + 4 > buffer.length) return null
|
|
202
|
+
offset += 4
|
|
203
|
+
}
|
|
204
|
+
if ((flags & 0x02) !== 0) {
|
|
205
|
+
if (offset + 4 > buffer.length) return null
|
|
206
|
+
const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!
|
|
207
|
+
if (offset + 4 + length > buffer.length) return null
|
|
208
|
+
}
|
|
209
|
+
return (buffer[2]! << 8) | buffer[3]!
|
|
210
|
+
}
|
|
211
|
+
|
|
174
212
|
/**
|
|
175
213
|
* Unwrap MMTP packet to extract payload
|
|
176
214
|
*/
|
|
@@ -236,6 +274,30 @@ export function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUD
|
|
|
236
274
|
}
|
|
237
275
|
}
|
|
238
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Why `extractMFUPayload` refused a packet.
|
|
279
|
+
*
|
|
280
|
+
* These identifiers are part of the public contract: callers surface them in
|
|
281
|
+
* diagnostics, so renaming one is a breaking change.
|
|
282
|
+
*/
|
|
283
|
+
export type MFURejectionGate =
|
|
284
|
+
| 'packet-shorter-than-mmtp-plus-mpu-header'
|
|
285
|
+
| 'mmtp-header-unparseable'
|
|
286
|
+
| 'not-an-mpu-packet'
|
|
287
|
+
| 'payload-shorter-than-mpu-header'
|
|
288
|
+
| 'aggregation-flag-set'
|
|
289
|
+
| 'mpu-payload-length-below-header-remainder'
|
|
290
|
+
| 'mpu-declared-end-exceeds-payload'
|
|
291
|
+
| 'mfu-offset-at-or-past-declared-end'
|
|
292
|
+
| 'source-fec-payload-id-width-mismatch'
|
|
293
|
+
| 'declared-trailers-unresolved'
|
|
294
|
+
|
|
295
|
+
export interface MFURejection {
|
|
296
|
+
gate: MFURejectionGate
|
|
297
|
+
/** Human-readable detail, including the byte arithmetic that decided it. */
|
|
298
|
+
detail: string
|
|
299
|
+
}
|
|
300
|
+
|
|
239
301
|
interface MfuPayloadLayout {
|
|
240
302
|
hasMfuDuHeader: boolean
|
|
241
303
|
mfuOffset: number
|
|
@@ -246,23 +308,48 @@ function resolveMfuPayloadLayout(
|
|
|
246
308
|
mpu: MPUHeader,
|
|
247
309
|
fragmentType: number,
|
|
248
310
|
payload: Uint8Array,
|
|
249
|
-
): MfuPayloadLayout |
|
|
250
|
-
if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE)
|
|
311
|
+
): { layout: MfuPayloadLayout } | { rejection: MFURejection } {
|
|
312
|
+
if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {
|
|
313
|
+
return {
|
|
314
|
+
rejection: {
|
|
315
|
+
gate: 'mpu-payload-length-below-header-remainder',
|
|
316
|
+
detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
}
|
|
251
320
|
|
|
252
321
|
// ISO/IEC 23008-1 defines payload_length from immediately after its own
|
|
253
322
|
// two-byte field. It therefore includes the remaining six MPU-header bytes.
|
|
254
323
|
const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength
|
|
255
|
-
if (declaredEnd > payload.length)
|
|
324
|
+
if (declaredEnd > payload.length) {
|
|
325
|
+
return {
|
|
326
|
+
rejection: {
|
|
327
|
+
gate: 'mpu-declared-end-exceeds-payload',
|
|
328
|
+
detail:
|
|
329
|
+
`payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +
|
|
330
|
+
`payload is ${payload.length} bytes`,
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
}
|
|
256
334
|
|
|
257
335
|
const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1
|
|
258
336
|
const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED
|
|
259
337
|
const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)
|
|
260
|
-
if (mfuOffset >= declaredEnd)
|
|
338
|
+
if (mfuOffset >= declaredEnd) {
|
|
339
|
+
return {
|
|
340
|
+
rejection: {
|
|
341
|
+
gate: 'mfu-offset-at-or-past-declared-end',
|
|
342
|
+
detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
}
|
|
261
346
|
|
|
262
347
|
return {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
348
|
+
layout: {
|
|
349
|
+
hasMfuDuHeader: hasHeader,
|
|
350
|
+
mfuOffset,
|
|
351
|
+
dataEnd: declaredEnd,
|
|
352
|
+
},
|
|
266
353
|
}
|
|
267
354
|
}
|
|
268
355
|
|
|
@@ -365,43 +452,172 @@ export interface MFUExtraction {
|
|
|
365
452
|
* draft-ramadan-moq-mmt-00 §3.1).
|
|
366
453
|
* @returns Extracted MFU data + metadata, or null if packet is too small
|
|
367
454
|
*/
|
|
368
|
-
|
|
455
|
+
interface MfuPlan {
|
|
456
|
+
header: MMTPHeader
|
|
457
|
+
payload: Uint8Array
|
|
458
|
+
mpu: MPUHeader
|
|
459
|
+
fragmentType: number
|
|
460
|
+
layout: MfuPayloadLayout
|
|
461
|
+
trailerLayout: DeclaredTrailerLayout
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Single source of truth for whether a packet yields an MFU payload.
|
|
466
|
+
*
|
|
467
|
+
* `extractMFUPayload` and `explainMFURejection` both delegate here, so the
|
|
468
|
+
* reason reported to operators can never drift from the decision the parser
|
|
469
|
+
* actually made.
|
|
470
|
+
*/
|
|
471
|
+
function planMfuExtraction(
|
|
369
472
|
mmtpPacket: Uint8Array,
|
|
370
473
|
options?: { skipFecId?: boolean },
|
|
371
|
-
|
|
372
|
-
|
|
474
|
+
parsedHeader?: MMTPHeader,
|
|
475
|
+
): { plan: MfuPlan } | { rejection: MFURejection } {
|
|
476
|
+
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {
|
|
477
|
+
return {
|
|
478
|
+
rejection: {
|
|
479
|
+
gate: 'packet-shorter-than-mmtp-plus-mpu-header',
|
|
480
|
+
detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
}
|
|
373
484
|
|
|
374
485
|
let header: MMTPHeader
|
|
375
|
-
|
|
376
|
-
header =
|
|
377
|
-
}
|
|
378
|
-
|
|
486
|
+
if (parsedHeader) {
|
|
487
|
+
header = parsedHeader
|
|
488
|
+
} else {
|
|
489
|
+
try {
|
|
490
|
+
header = parseMMTPHeader(mmtpPacket)
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return {
|
|
493
|
+
rejection: {
|
|
494
|
+
gate: 'mmtp-header-unparseable',
|
|
495
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
}
|
|
379
499
|
}
|
|
380
|
-
if (header.packetType !== PACKET_TYPE_MPU)
|
|
500
|
+
if (header.packetType !== PACKET_TYPE_MPU) {
|
|
501
|
+
return {
|
|
502
|
+
rejection: {
|
|
503
|
+
gate: 'not-an-mpu-packet',
|
|
504
|
+
detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
381
509
|
const payload = mmtpPacket.subarray(header.headerLength)
|
|
382
|
-
if (payload.length < MPU_HEADER_SIZE)
|
|
510
|
+
if (payload.length < MPU_HEADER_SIZE) {
|
|
511
|
+
return {
|
|
512
|
+
rejection: {
|
|
513
|
+
gate: 'payload-shorter-than-mpu-header',
|
|
514
|
+
detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
383
519
|
const mpu = parseMPUHeader(payload)
|
|
384
|
-
if (mpu.aggregationFlag)
|
|
520
|
+
if (mpu.aggregationFlag) {
|
|
521
|
+
return {
|
|
522
|
+
rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },
|
|
523
|
+
}
|
|
524
|
+
}
|
|
385
525
|
|
|
386
526
|
// Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)
|
|
387
527
|
const fragmentType = (payload[2]! >> 4) & 0x0f
|
|
388
528
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
529
|
+
const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)
|
|
530
|
+
if ('rejection' in layoutResult) return layoutResult
|
|
531
|
+
const { layout } = layoutResult
|
|
532
|
+
|
|
533
|
+
const trailerLayout = resolveDeclaredTrailers(
|
|
392
534
|
payload,
|
|
535
|
+
layout.dataEnd,
|
|
536
|
+
header.fecType,
|
|
537
|
+
options,
|
|
393
538
|
)
|
|
394
|
-
if (!
|
|
539
|
+
if (!trailerLayout) {
|
|
540
|
+
// The most common cause is a producer whose Source FEC Payload ID width
|
|
541
|
+
// disagrees with ISO/IEC 23008-1 §C.5.2. Name that explicitly rather than
|
|
542
|
+
// leaving operators to infer it from a generic trailer error.
|
|
543
|
+
if (!options?.skipFecId && header.fecType === 1) {
|
|
544
|
+
const observed = payload.length - layout.dataEnd
|
|
545
|
+
if (observed !== SOURCE_FEC_PAYLOAD_ID_SIZE) {
|
|
546
|
+
return {
|
|
547
|
+
rejection: {
|
|
548
|
+
gate: 'source-fec-payload-id-width-mismatch',
|
|
549
|
+
detail:
|
|
550
|
+
`producer wrote a ${observed}-byte Source FEC Payload ID, ` +
|
|
551
|
+
`ISO/IEC 23008-1 §C.5.2 requires ${SOURCE_FEC_PAYLOAD_ID_SIZE} ` +
|
|
552
|
+
`(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
rejection: {
|
|
559
|
+
gate: 'declared-trailers-unresolved',
|
|
560
|
+
detail:
|
|
561
|
+
`trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +
|
|
562
|
+
`nor an exact ALTA trailer (fecType=${header.fecType})`,
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Report why `extractMFUPayload` would refuse this packet, or null if it would
|
|
572
|
+
* accept it.
|
|
573
|
+
*
|
|
574
|
+
* `extractMFUPayload` returns bare `null` on ten distinct conditions, which
|
|
575
|
+
* makes a producer/consumer framing disagreement indistinguishable from a
|
|
576
|
+
* publisher that never started — both render as silence. Use this wherever
|
|
577
|
+
* that distinction matters: diagnostics, contract tests, and operator-facing
|
|
578
|
+
* logs.
|
|
579
|
+
*/
|
|
580
|
+
export function explainMFURejection(
|
|
581
|
+
mmtpPacket: Uint8Array,
|
|
582
|
+
options?: { skipFecId?: boolean },
|
|
583
|
+
): MFURejection | null {
|
|
584
|
+
const result = planMfuExtraction(mmtpPacket, options)
|
|
585
|
+
return 'rejection' in result ? result.rejection : null
|
|
586
|
+
}
|
|
395
587
|
|
|
396
|
-
|
|
397
|
-
|
|
588
|
+
export function extractMFUPayload(
|
|
589
|
+
mmtpPacket: Uint8Array,
|
|
590
|
+
options?: { skipFecId?: boolean },
|
|
591
|
+
): MFUExtraction | null {
|
|
592
|
+
return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* extractMFUPayload for a caller that already holds this packet's parsed MMTP
|
|
597
|
+
* header, so the header is not parsed a second time (BLO-36599).
|
|
598
|
+
*
|
|
599
|
+
* `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the
|
|
600
|
+
* result is then identical to `extractMFUPayload(mmtpPacket, options)`.
|
|
601
|
+
*/
|
|
602
|
+
export function extractMFUPayloadWithHeader(
|
|
603
|
+
mmtpPacket: Uint8Array,
|
|
604
|
+
header: MMTPHeader,
|
|
605
|
+
options?: { skipFecId?: boolean },
|
|
606
|
+
): MFUExtraction | null {
|
|
607
|
+
return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function extractPlannedMfu(
|
|
611
|
+
mmtpPacket: Uint8Array,
|
|
612
|
+
result: { plan: MfuPlan } | { rejection: MFURejection },
|
|
613
|
+
): MFUExtraction | null {
|
|
614
|
+
if ('rejection' in result) return null
|
|
615
|
+
const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan
|
|
398
616
|
|
|
399
617
|
const mfuDuHeaderPresent = layout.hasMfuDuHeader
|
|
400
618
|
const mfuOffset = layout.mfuOffset
|
|
401
619
|
const dataEnd = layout.dataEnd
|
|
402
620
|
|
|
403
|
-
if (mfuOffset >= dataEnd) return null
|
|
404
|
-
|
|
405
621
|
const mfuDuHeader =
|
|
406
622
|
mfuDuHeaderPresent && mpu.timedFlag === 1
|
|
407
623
|
? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)
|
|
@@ -449,24 +665,18 @@ export function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {
|
|
|
449
665
|
* source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.
|
|
450
666
|
*
|
|
451
667
|
* SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet
|
|
452
|
-
* for ssbg_mode0 and ssbg_mode1).
|
|
453
|
-
*
|
|
454
|
-
*
|
|
668
|
+
* for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the
|
|
669
|
+
* configured K; repair SS_Start identifies the first SS_ID of the block and
|
|
670
|
+
* SSB_length records the authored source-symbol count:
|
|
671
|
+
* SBN = floor(SS_ID / K), ESI = SS_ID % K
|
|
455
672
|
* SS_Start = SBN * K (first SS_ID of the block)
|
|
456
673
|
*
|
|
457
674
|
* This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec §6.2),
|
|
458
675
|
* and ATSC A/331 §8.1.2.5 which defers to ISO 23008-1 Annex C.
|
|
459
676
|
*/
|
|
460
677
|
export function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE
|
|
464
|
-
const view = new DataView(
|
|
465
|
-
mmtpPacket.buffer,
|
|
466
|
-
mmtpPacket.byteOffset + off,
|
|
467
|
-
SOURCE_FEC_PAYLOAD_ID_SIZE,
|
|
468
|
-
)
|
|
469
|
-
return { ssId: view.getUint32(0, false) }
|
|
678
|
+
const header = parseHeaderForSourceFecId(mmtpPacket)
|
|
679
|
+
return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null
|
|
470
680
|
}
|
|
471
681
|
|
|
472
682
|
/**
|
|
@@ -474,22 +684,52 @@ export function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } |
|
|
|
474
684
|
* payload contains a four-byte Source FEC Payload ID.
|
|
475
685
|
*/
|
|
476
686
|
export function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {
|
|
477
|
-
|
|
687
|
+
const header = parseHeaderForSourceFecId(mmtpPacket)
|
|
688
|
+
return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null
|
|
689
|
+
}
|
|
478
690
|
|
|
479
|
-
|
|
691
|
+
function parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {
|
|
692
|
+
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null
|
|
480
693
|
try {
|
|
481
|
-
|
|
694
|
+
return parseMMTPHeader(mmtpPacket)
|
|
482
695
|
} catch {
|
|
483
|
-
return
|
|
696
|
+
return null
|
|
484
697
|
}
|
|
485
|
-
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* The Source FEC Payload ID of a packet whose MMTP header the caller already
|
|
702
|
+
* parsed, or null when the packet carries none: present only when FEC_type=1
|
|
703
|
+
* and the exact suffix after the ISO-declared MPU payload holds the four-byte
|
|
704
|
+
* ID (ISO/IEC 23008-1 §C.5.2). Same answer as extractFecPayloadId, without a
|
|
705
|
+
* second header parse (BLO-36599).
|
|
706
|
+
*
|
|
707
|
+
* `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.
|
|
708
|
+
*/
|
|
709
|
+
export function sourceFecPayloadIdOf(
|
|
710
|
+
mmtpPacket: Uint8Array,
|
|
711
|
+
header: MMTPHeader,
|
|
712
|
+
): { ssId: number } | null {
|
|
713
|
+
if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null
|
|
714
|
+
if (header.packetType !== PACKET_TYPE_MPU) return null
|
|
486
715
|
|
|
487
716
|
const payload = mmtpPacket.subarray(header.headerLength)
|
|
488
|
-
if (payload.length < MPU_HEADER_SIZE) return
|
|
489
|
-
const
|
|
490
|
-
if (
|
|
491
|
-
const declaredEnd = MPU_LENGTH_FIELD_SIZE +
|
|
492
|
-
|
|
717
|
+
if (payload.length < MPU_HEADER_SIZE) return null
|
|
718
|
+
const payloadLength = (payload[0]! << 8) | payload[1]!
|
|
719
|
+
if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null
|
|
720
|
+
const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength
|
|
721
|
+
if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null
|
|
722
|
+
|
|
723
|
+
// Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id
|
|
724
|
+
const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE
|
|
725
|
+
return {
|
|
726
|
+
ssId:
|
|
727
|
+
((mmtpPacket[off]! << 24) |
|
|
728
|
+
(mmtpPacket[off + 1]! << 16) |
|
|
729
|
+
(mmtpPacket[off + 2]! << 8) |
|
|
730
|
+
mmtpPacket[off + 3]!) >>>
|
|
731
|
+
0,
|
|
732
|
+
}
|
|
493
733
|
}
|
|
494
734
|
|
|
495
735
|
/**
|
|
@@ -511,22 +751,34 @@ export const extractAltaTrailer = _leafExtractAltaTrailer
|
|
|
511
751
|
/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */
|
|
512
752
|
export const REPAIR_FEC_PID_SIZE = 13
|
|
513
753
|
|
|
514
|
-
/**
|
|
515
|
-
|
|
754
|
+
/**
|
|
755
|
+
* Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.
|
|
756
|
+
* `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this
|
|
757
|
+
* same packet; it spares the second header parse (BLO-36599).
|
|
758
|
+
*/
|
|
759
|
+
function readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {
|
|
516
760
|
packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number
|
|
517
761
|
} | null {
|
|
518
762
|
let header: MMTPHeader
|
|
519
|
-
|
|
520
|
-
header =
|
|
521
|
-
}
|
|
522
|
-
|
|
763
|
+
if (parsedHeader) {
|
|
764
|
+
header = parsedHeader
|
|
765
|
+
} else {
|
|
766
|
+
try {
|
|
767
|
+
header = parseMMTPHeader(mmtpPacket)
|
|
768
|
+
} catch {
|
|
769
|
+
return null
|
|
770
|
+
}
|
|
523
771
|
}
|
|
524
772
|
if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null
|
|
525
773
|
|
|
526
774
|
const off = header.headerLength
|
|
527
775
|
if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null
|
|
528
|
-
const
|
|
529
|
-
|
|
776
|
+
const ssStart =
|
|
777
|
+
((mmtpPacket[off]! << 24) |
|
|
778
|
+
(mmtpPacket[off + 1]! << 16) |
|
|
779
|
+
(mmtpPacket[off + 2]! << 8) |
|
|
780
|
+
mmtpPacket[off + 3]!) >>>
|
|
781
|
+
0 // 32 bits
|
|
530
782
|
const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits
|
|
531
783
|
const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits
|
|
532
784
|
const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits
|
|
@@ -572,7 +824,7 @@ export interface MoqRepairFrame {
|
|
|
572
824
|
rsbLength: number
|
|
573
825
|
/** RS_ID (24 bits): repair symbol index (0-based) */
|
|
574
826
|
rsId: number
|
|
575
|
-
/** SSB_length (24 bits):
|
|
827
|
+
/** SSB_length (24 bits): authored source-symbol count (1..=configured K) */
|
|
576
828
|
ssbLength: number
|
|
577
829
|
/** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */
|
|
578
830
|
symbolData: Uint8Array
|
|
@@ -586,14 +838,36 @@ export interface MoqRepairFrame {
|
|
|
586
838
|
* then SS_Start (4 bytes) → block identifier
|
|
587
839
|
* then RSB_length (3 bytes) → P
|
|
588
840
|
* then RS_ID (3 bytes) → ESI = K + RS_ID
|
|
589
|
-
* then SSB_length (3 bytes) → K
|
|
841
|
+
* then SSB_length (3 bytes) → authored source-symbol count (1..=K)
|
|
590
842
|
* then Repair Symbol Data (T bytes)
|
|
591
843
|
*
|
|
592
844
|
* @param frame - Raw MoQ repair frame bytes
|
|
593
845
|
* @returns Parsed frame or null if invalid
|
|
594
846
|
*/
|
|
595
847
|
export function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {
|
|
596
|
-
|
|
848
|
+
return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP
|
|
853
|
+
* header, so the header is not parsed a second time (BLO-36599). The router's
|
|
854
|
+
* multicast repair branch uses it: one repair packet in every K + P otherwise
|
|
855
|
+
* paid two header parses.
|
|
856
|
+
*
|
|
857
|
+
* `header` must be `parseMMTPHeader(frame)` of this same frame; the result is
|
|
858
|
+
* then identical to `parseMoqRepairFrame(frame)`.
|
|
859
|
+
*/
|
|
860
|
+
export function parseMoqRepairFrameWithHeader(
|
|
861
|
+
frame: Uint8Array,
|
|
862
|
+
header: MMTPHeader,
|
|
863
|
+
): MoqRepairFrame | null {
|
|
864
|
+
return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function moqRepairFrameFrom(
|
|
868
|
+
frame: Uint8Array,
|
|
869
|
+
repairId: ReturnType<typeof readRepairFecPayloadIdFields>,
|
|
870
|
+
): MoqRepairFrame | null {
|
|
597
871
|
if (!repairId) return null
|
|
598
872
|
|
|
599
873
|
const symbolData = frame.subarray(repairId.dataOffset)
|
|
@@ -614,133 +888,5 @@ export function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {
|
|
|
614
888
|
}
|
|
615
889
|
}
|
|
616
890
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*/
|
|
620
|
-
export interface AlFecConfig {
|
|
621
|
-
fecCodeId: number
|
|
622
|
-
fecPayloadIdMode: 0
|
|
623
|
-
repairSymbolSize: number
|
|
624
|
-
maximumK: number
|
|
625
|
-
maximumP: number
|
|
626
|
-
interleaveDepth: number
|
|
627
|
-
oti: Uint8Array
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function canonicalRaptorQOtiK(oti: Uint8Array, repairSymbolSize: number): number | null {
|
|
631
|
-
if (oti.length !== 12 || oti[5] !== 0) return null
|
|
632
|
-
const transferLength = (oti[0]! * 0x1_0000_0000) +
|
|
633
|
-
(((oti[1]! << 24) | (oti[2]! << 16) | (oti[3]! << 8) | oti[4]!) >>> 0)
|
|
634
|
-
const symbolSize = (oti[6]! << 8) | oti[7]!
|
|
635
|
-
const sourceBlocks = oti[8]!
|
|
636
|
-
const subBlocks = (oti[9]! << 8) | oti[10]!
|
|
637
|
-
const alignment = oti[11]!
|
|
638
|
-
if (!Number.isSafeInteger(transferLength) || transferLength <= 0 ||
|
|
639
|
-
symbolSize !== repairSymbolSize || symbolSize === 0 || symbolSize % 8 !== 0 ||
|
|
640
|
-
sourceBlocks !== 1 || subBlocks !== 1 || alignment !== 8 ||
|
|
641
|
-
transferLength % symbolSize !== 0) {
|
|
642
|
-
return null
|
|
643
|
-
}
|
|
644
|
-
const k = transferLength / symbolSize
|
|
645
|
-
return Number.isInteger(k) && k >= 1 && k <= 56_403 ? k : null
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Parse AL-FEC signaling message from a full MMTP signaling packet.
|
|
650
|
-
*
|
|
651
|
-
* Wire format (ISO/IEC 23008-1:2023 Table C.3, Amendment 1:2025):
|
|
652
|
-
* [12-byte MMTP header (packet_type=0x02)]
|
|
653
|
-
* message_id(2) = 0x0203
|
|
654
|
-
* version(1)
|
|
655
|
-
* message_length(2) = length of remaining payload
|
|
656
|
-
* flags(1) = fec_flag(1) | private_fec_flag(1) | reserved(6)
|
|
657
|
-
* fec_flow_descriptor:
|
|
658
|
-
* length(2)
|
|
659
|
-
* num_flows(1)
|
|
660
|
-
* per flow:
|
|
661
|
-
* fec_flow_id(1), source_flow_id(1), num_assets(1), packet_ids(2*N),
|
|
662
|
-
* coding_params(1), repair_symbol_size(2),
|
|
663
|
-
* [one-stage]: repair_flow_id(2), fec_code_id(1),
|
|
664
|
-
* [private_fec_flag]: private_header(1){flag(1)|len(7)},
|
|
665
|
-
* current RaptorQ profile OTI(12) + interleave_depth(1)
|
|
666
|
-
* max_k(3), max_p(3), protection_window_time(4), protection_window_size(4)
|
|
667
|
-
*
|
|
668
|
-
* @param mmtpPacket Full MMTP packet including 12-byte header
|
|
669
|
-
* @returns Parsed config or null if not an AL-FEC signaling message
|
|
670
|
-
*/
|
|
671
|
-
export function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecConfig | null {
|
|
672
|
-
if (mmtpPacket.length < MMTP_HEADER_SIZE + 9) return null
|
|
673
|
-
let header: MMTPHeader
|
|
674
|
-
try {
|
|
675
|
-
header = parseMMTPHeader(mmtpPacket)
|
|
676
|
-
} catch {
|
|
677
|
-
return null
|
|
678
|
-
}
|
|
679
|
-
if (header.packetType !== PACKET_TYPE_SIGNALING || header.fecType !== 0) return null
|
|
680
|
-
const payload = mmtpPacket.subarray(header.headerLength)
|
|
681
|
-
if (payload.length < 9) return null
|
|
682
|
-
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)
|
|
683
|
-
let off = 0
|
|
684
|
-
|
|
685
|
-
const messageId = view.getUint16(off, false); off += 2
|
|
686
|
-
if (messageId !== ALFEC_MESSAGE_ID) return null
|
|
687
|
-
const version = payload[off]!; off += 1
|
|
688
|
-
const messageLength = view.getUint16(off, false); off += 2
|
|
689
|
-
if (version !== 1 || messageLength === 0 || messageLength !== payload.length - off) return null
|
|
690
|
-
|
|
691
|
-
const flags = payload[off]!; off += 1
|
|
692
|
-
const fecFlag = (flags >> 7) & 1
|
|
693
|
-
const privateFecFlag = (flags >> 6) & 1
|
|
694
|
-
if (fecFlag !== 1 || privateFecFlag !== 1 || (flags & 0x3f) !== 0x3f) return null
|
|
695
|
-
|
|
696
|
-
if (off + 2 > payload.length) return null
|
|
697
|
-
const descriptorLength = view.getUint16(off, false); off += 2
|
|
698
|
-
const descriptorEnd = off + descriptorLength
|
|
699
|
-
if (descriptorLength === 0 || descriptorEnd !== payload.length) return null
|
|
700
|
-
|
|
701
|
-
const numFlows = payload[off]!; off += 1
|
|
702
|
-
if (numFlows !== 1 || off + 3 > descriptorEnd) return null
|
|
703
|
-
|
|
704
|
-
off += 2 // fec_flow_id, source_flow_id
|
|
705
|
-
const numAssets = payload[off]!; off += 1
|
|
706
|
-
if (numAssets === 0 || off + (numAssets * 2) > descriptorEnd) return null
|
|
707
|
-
off += numAssets * 2
|
|
708
|
-
|
|
709
|
-
if (off + 3 > descriptorEnd) return null
|
|
710
|
-
const codingParams = payload[off]!; off += 1
|
|
711
|
-
const fecCodingStructure = (codingParams >> 4) & 0x0f
|
|
712
|
-
const ssbgMode = (codingParams >> 2) & 0x03
|
|
713
|
-
const ffsrptsFlag = (codingParams >> 1) & 0x01
|
|
714
|
-
const fecPayloadIdMode = codingParams & 0x01
|
|
715
|
-
if (fecCodingStructure !== 1 || ssbgMode !== 0 || ffsrptsFlag !== 0 || fecPayloadIdMode !== 0) {
|
|
716
|
-
return null
|
|
717
|
-
}
|
|
718
|
-
const repairSymbolSize = view.getUint16(off, false); off += 2
|
|
719
|
-
if (repairSymbolSize === 0 || repairSymbolSize % 8 !== 0) return null
|
|
720
|
-
|
|
721
|
-
if (off + 4 > descriptorEnd) return null
|
|
722
|
-
off += 2 // Amendment 1:2025 replaces Table C.3 with a 16-bit repair_flow_id
|
|
723
|
-
const fecCodeId = payload[off]!; off += 1
|
|
724
|
-
if (fecCodeId !== FEC_CODE_RAPTORQ) return null
|
|
725
|
-
const privateHeader = payload[off]!; off += 1
|
|
726
|
-
const privateFlag = privateHeader >> 7
|
|
727
|
-
const privateFieldLength = privateHeader & 0x7f
|
|
728
|
-
if (privateFlag !== 1 || privateFieldLength !== 13 || off + 13 > descriptorEnd) return null
|
|
729
|
-
const oti = payload.slice(off, off + 12); off += 12
|
|
730
|
-
const interleaveDepth = payload[off]!; off += 1
|
|
731
|
-
const otiK = canonicalRaptorQOtiK(oti, repairSymbolSize)
|
|
732
|
-
if (otiK === null || interleaveDepth === 0) return null
|
|
733
|
-
|
|
734
|
-
if (off + 14 !== descriptorEnd) return null
|
|
735
|
-
const maximumK = (payload[off]! << 16) | (payload[off + 1]! << 8) | payload[off + 2]!
|
|
736
|
-
off += 3
|
|
737
|
-
const maximumP = (payload[off]! << 16) | (payload[off + 1]! << 8) | payload[off + 2]!
|
|
738
|
-
off += 3
|
|
739
|
-
if (maximumK !== otiK || maximumP === 0 || maximumK + maximumP - 1 > 0x00ff_ffff) {
|
|
740
|
-
return null
|
|
741
|
-
}
|
|
742
|
-
off += 8 // protection_window_time, protection_window_size
|
|
743
|
-
if (off !== descriptorEnd) return null
|
|
744
|
-
|
|
745
|
-
return { fecCodeId, fecPayloadIdMode, repairSymbolSize, maximumK, maximumP, interleaveDepth, oti }
|
|
746
|
-
}
|
|
891
|
+
// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,
|
|
892
|
+
// PA/MPT) live in ./signalling.ts.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AMT well-known UDP port (RFC 7450 §4).
|
|
3
|
+
*
|
|
4
|
+
* The single definition for the whole repo. It lives here, in the one package
|
|
5
|
+
* both `@blockcast/mmt-transport` and `@blockcast/mmt-container` already
|
|
6
|
+
* depend on, because the two apply it on opposite sides of a boundary that
|
|
7
|
+
* does not otherwise share code:
|
|
8
|
+
*
|
|
9
|
+
* - container resolves it where `networkSource` semantics live, so a catalog
|
|
10
|
+
* relay that names no port is reachable instead of handing a join a
|
|
11
|
+
* portless relay that fails.
|
|
12
|
+
* - transport applies it when reading a catalog, dialling a gateway, and
|
|
13
|
+
* answering DRIAD — but deliberately not at every boundary. Which call
|
|
14
|
+
* sites do and do not take the default is transport's business; the
|
|
15
|
+
* reasoning lives beside those call sites, not here.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_AMT_RELAY_PORT = 2268;
|