@blockcast/mmt-base 0.3.0 → 0.4.0-main.1d3ea1cc8ff2
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 +8 -18
- package/dist/catalog-fec-source.d.ts +88 -0
- package/dist/catalog-fec-source.d.ts.map +1 -0
- package/dist/catalog-fec-source.js +154 -0
- package/dist/catalog-fec-source.js.map +1 -0
- package/dist/fec-block-profile.d.ts +7 -7
- package/dist/fec-block-profile.d.ts.map +1 -1
- package/dist/fec-block-profile.js +22 -18
- package/dist/fec-block-profile.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mmtp.d.ts +28 -24
- package/dist/mmtp.d.ts.map +1 -1
- package/dist/mmtp.js +63 -41
- package/dist/mmtp.js.map +1 -1
- package/dist/multicast-catalog.d.ts +75 -0
- package/dist/multicast-catalog.d.ts.map +1 -0
- package/dist/multicast-catalog.js +318 -0
- package/dist/multicast-catalog.js.map +1 -0
- package/dist/serial-number.d.ts +0 -29
- package/dist/serial-number.d.ts.map +1 -1
- package/dist/serial-number.js +0 -54
- package/dist/serial-number.js.map +1 -1
- package/dist/signalling.d.ts +49 -33
- package/dist/signalling.d.ts.map +1 -1
- package/dist/signalling.js +72 -58
- package/dist/signalling.js.map +1 -1
- package/package.json +2 -4
- package/src/catalog-fec-source.ts +183 -0
- package/src/fec-block-profile.ts +24 -24
- package/src/index.ts +2 -0
- package/src/mmtp.ts +78 -59
- package/src/multicast-catalog.ts +350 -0
- package/src/serial-number.ts +0 -58
- package/src/signalling.ts +87 -75
package/src/serial-number.ts
CHANGED
|
@@ -47,61 +47,3 @@ export function serial32Precedes(a: number, b: number): boolean {
|
|
|
47
47
|
const distance = serial32Distance(a, b)
|
|
48
48
|
return distance !== 0 && distance < SERIAL32_HALF
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* First SS_ID of block `sbn` under the K-stride numbering every current
|
|
53
|
-
* sender uses (`SS_ID = SBN * K + ESI`, C.5.2), reduced mod 2^32.
|
|
54
|
-
*/
|
|
55
|
-
export function kStrideBlockStart(sbn: number, k: number): number {
|
|
56
|
-
assertUint32(sbn, 'sbn')
|
|
57
|
-
if (!Number.isInteger(k) || k < 1 || k >= SERIAL32_HALF) {
|
|
58
|
-
throw new RangeError(`k must be a positive integer below 2^31 (got ${k})`)
|
|
59
|
-
}
|
|
60
|
-
// sbn < 2^32 and k < 2^31 keep the product below 2^63, but a double is
|
|
61
|
-
// exact only to 2^53; split the product so every step is exact.
|
|
62
|
-
const high = Math.floor(sbn / 0x1_0000) // < 2^16
|
|
63
|
-
const low = sbn % 0x1_0000 // < 2^16
|
|
64
|
-
return (((high * k) % SERIAL32_SPAN) * 0x1_0000 + low * k) % SERIAL32_SPAN
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Whether block `a` precedes block `b`, ordering the blocks by their first
|
|
69
|
-
* SS_ID in serial arithmetic. Identical to `a < b` for any two blocks of a
|
|
70
|
-
* stream that has not wrapped SS_ID.
|
|
71
|
-
*/
|
|
72
|
-
export function kStrideBlockPrecedes(a: number, b: number, k: number): boolean {
|
|
73
|
-
return serial32Precedes(kStrideBlockStart(a, k), kStrideBlockStart(b, k))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* How many blocks `a` lies behind `b`: 0 when `a` does not precede `b`,
|
|
78
|
-
* otherwise `ceil(distance / K)` over the SS_ID distance between their first
|
|
79
|
-
* symbols. Without a wrap that is exactly `b - a`; across the wrap the one
|
|
80
|
-
* short pre-wrap block (2^32 mod K symbols) still counts as one block.
|
|
81
|
-
*/
|
|
82
|
-
export function kStrideBlocksBehind(a: number, b: number, k: number): number {
|
|
83
|
-
const from = kStrideBlockStart(a, k)
|
|
84
|
-
const to = kStrideBlockStart(b, k)
|
|
85
|
-
if (!serial32Precedes(from, to)) return 0
|
|
86
|
-
return Math.ceil(serial32Distance(from, to) / k)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Whether `sbn` and `k` describe a block of the K-stride numbering:
|
|
91
|
-
* `sbn` a uint32, `k` an integer in 1..2^31-1.
|
|
92
|
-
*/
|
|
93
|
-
export function isKStrideBlock(sbn: number, k: number): boolean {
|
|
94
|
-
return Number.isInteger(sbn) && sbn >= 0 && sbn < SERIAL32_SPAN &&
|
|
95
|
-
Number.isInteger(k) && k >= 1 && k < SERIAL32_HALF
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The block after `sbn`: `sbn + 1`, or 0 once `(sbn + 1) * K` reaches 2^32
|
|
100
|
-
* and the next SS_ID wraps (C.5.2). There are `ceil(2^32 / K)` blocks per
|
|
101
|
-
* wrap, the last of them short when K does not divide 2^32.
|
|
102
|
-
*/
|
|
103
|
-
export function kStrideNextBlock(sbn: number, k: number): number {
|
|
104
|
-
assertUint32(sbn, 'sbn')
|
|
105
|
-
kStrideBlockStart(sbn, k) // validates k
|
|
106
|
-
return (sbn + 1) * k >= SERIAL32_SPAN ? 0 : sbn + 1
|
|
107
|
-
}
|
package/src/signalling.ts
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
import {
|
|
24
24
|
ALFEC_MESSAGE_ID,
|
|
25
25
|
FEC_CODE_RAPTORQ,
|
|
26
|
+
FEC_CODE_REED_SOLOMON,
|
|
27
|
+
MMTP_SIGNALING_PACKET_ID,
|
|
26
28
|
PACKET_TYPE_SIGNALING,
|
|
27
29
|
parseMMTPHeader,
|
|
28
30
|
type MMTPHeader,
|
|
@@ -112,20 +114,17 @@ function total<T>(parse: () => T | null): T | null {
|
|
|
112
114
|
// ---------------------------------------------------------------------------
|
|
113
115
|
|
|
114
116
|
/**
|
|
115
|
-
* How the signalling payload of an MMTP packet was framed
|
|
117
|
+
* How the signalling payload of an MMTP packet was framed: always
|
|
118
|
+
* `iso-9.3.4.2`, the 2-byte `f_i(2) RES(4) H(1) A(1) | frag_counter(8)`
|
|
119
|
+
* header of ISO/IEC 23008-1:2023 9.3.4.2 ahead of the message(s).
|
|
116
120
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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).
|
|
121
|
+
* The pre-remediation Blockcast form, an AL-FEC message directly after the
|
|
122
|
+
* MMTP header, is not accepted (BLO-35597, Phase L). Its first two bytes,
|
|
123
|
+
* `0x02 0x03`, read as a 9.3.4.2 header claiming three further fragments
|
|
124
|
+
* of a complete message, which the header parser refuses: such a packet
|
|
125
|
+
* parses to null, never to a guess.
|
|
127
126
|
*/
|
|
128
|
-
export type SignallingPayloadForm = 'iso-9.3.4.2'
|
|
127
|
+
export type SignallingPayloadForm = 'iso-9.3.4.2'
|
|
129
128
|
|
|
130
129
|
export interface SignallingPayload {
|
|
131
130
|
form: SignallingPayloadForm
|
|
@@ -158,10 +157,6 @@ export function parseSignallingPayload(mmtpPacket: Uint8Array): SignallingPayloa
|
|
|
158
157
|
|
|
159
158
|
function splitSignallingPayload(payload: Uint8Array): SignallingPayload {
|
|
160
159
|
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
160
|
const reader = new WireReader(payload)
|
|
166
161
|
const flags = reader.u8()
|
|
167
162
|
const fragmentation = flags >> 6
|
|
@@ -445,9 +440,9 @@ export function parseAlFecMessageBody(message: Uint8Array): AlFecMessageBody | n
|
|
|
445
440
|
}
|
|
446
441
|
|
|
447
442
|
/**
|
|
448
|
-
* Parse the AL-FEC message carried by a full MMTP signalling packet
|
|
449
|
-
*
|
|
450
|
-
*
|
|
443
|
+
* Parse the AL-FEC message carried by a full MMTP signalling packet (a
|
|
444
|
+
* 9.3.4.2 payload, see SignallingPayloadForm). Returns null when the packet
|
|
445
|
+
* carries no AL-FEC message, more than one, or a malformed one.
|
|
451
446
|
*/
|
|
452
447
|
export function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecMessage | null {
|
|
453
448
|
const packet = parseSignallingPacket(mmtpPacket)
|
|
@@ -460,92 +455,112 @@ export function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecMessage | null {
|
|
|
460
455
|
}
|
|
461
456
|
|
|
462
457
|
/**
|
|
463
|
-
* Decoder geometry of one FEC flow
|
|
464
|
-
*
|
|
465
|
-
*
|
|
458
|
+
* Decoder geometry of one FEC flow's AL-FEC message entry under
|
|
459
|
+
* draft-ramadan-moq-mmt-01 Section 8.3 ("iso-ssbg1-v1"): one Asset, coding
|
|
460
|
+
* byte 0x14, fec_code_id_for_repair_flow 3 (RaptorQ) or 1 (Reed-Solomon),
|
|
461
|
+
* and no OTI (draft-ramadan-moq-mmt-01 Section 7.2). K, T and P come from
|
|
462
|
+
* the entry's own fields; a private field, if present, is ignored.
|
|
466
463
|
*/
|
|
467
464
|
export interface AlFecRaptorQFlowConfig {
|
|
465
|
+
/** The flow's one Asset packet_id (number_of_assets is 1). */
|
|
468
466
|
sourcePacketIds: number[]
|
|
469
467
|
repairFlowId: number
|
|
468
|
+
/** `FEC_CODE_RAPTORQ` (3) or `FEC_CODE_REED_SOLOMON` (1). */
|
|
469
|
+
fecCodeId: number
|
|
470
|
+
/** Always `SSBG_MODE_1` (`iso-ssbg1-v1`). */
|
|
470
471
|
ssbgMode: number
|
|
472
|
+
/** Always 0: the profile's coding byte is exactly 0x14. */
|
|
471
473
|
fecPayloadIdMode: 0 | 1
|
|
474
|
+
/** T, from length_of_repair_symbol. */
|
|
472
475
|
repairSymbolSize: number
|
|
476
|
+
/** K, from maximum_k_for_repair_flow. */
|
|
473
477
|
maximumK: number
|
|
478
|
+
/** P, from maximum_p_for_repair_flow: the RSB_length total over every published layer. */
|
|
474
479
|
maximumP: number
|
|
475
|
-
interleaveDepth: number
|
|
476
|
-
oti: Uint8Array
|
|
477
480
|
protectionWindowTime: number
|
|
478
481
|
protectionWindowSize: number
|
|
479
482
|
}
|
|
480
483
|
|
|
481
|
-
const RAPTORQ_PRIVATE_FIELD_LENGTH = 13
|
|
482
484
|
const RAPTORQ_MAX_SOURCE_SYMBOLS = 56_403
|
|
483
485
|
const REPAIR_SYMBOL_ID_MAX = 0x00ff_ffff
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
}
|
|
486
|
+
/** The 12-octet MMTP header plus the 2-octet length of the T - 2 construction (moq-fec-01 Section 7.3). */
|
|
487
|
+
const MIN_REPAIR_SYMBOL_SIZE = 14
|
|
488
|
+
const REED_SOLOMON_MAX_BLOCK = 255
|
|
502
489
|
|
|
503
490
|
/**
|
|
504
|
-
* Validate one parsed FEC flow against
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* maximum_k_for_repair_flow.
|
|
491
|
+
* Validate one parsed FEC flow against draft-ramadan-moq-mmt-01 Sections 8.3
|
|
492
|
+
* and 12.2 and return its decoder geometry, or null when the entry is
|
|
493
|
+
* outside them. Fail closed on everything else:
|
|
508
494
|
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
495
|
+
* - coding byte other than 0x14 (one-stage, ssbg_mode1, ffsrpts_flag 0,
|
|
496
|
+
* fec_payload_id_mode 0). 0x15 is fec_payload_id_mode 1 under Amd 1:2025,
|
|
497
|
+
* which "iso-ssbg1-v1" does not describe (moq-fec-01 Section 7.3);
|
|
498
|
+
* - number_of_assets other than 1;
|
|
499
|
+
* - fec_code_id_for_repair_flow other than 3 or 1, the retired Blockcast
|
|
500
|
+
* code 2 included (ISO/IEC 23008-10 Table 1);
|
|
501
|
+
* - T < 14, K = 0, P = 0, K + P - 1 > 2^24 - 1;
|
|
502
|
+
* - RaptorQ: T not a multiple of 8, or K > 56403;
|
|
503
|
+
* - Reed-Solomon: K + P > 255;
|
|
504
|
+
* - repair_flow_id 0 (the signalling flow) or the flow's own packet_id.
|
|
505
|
+
*
|
|
506
|
+
* Checks that span entries (another entry's packet_id, a duplicate
|
|
507
|
+
* repair_flow_id or Asset) are `alFecMessageFlowConfigs`'.
|
|
514
508
|
*/
|
|
515
509
|
export function alFecRaptorQFlowConfig(flow: AlFecFlow): AlFecRaptorQFlowConfig | null {
|
|
516
510
|
if (flow.fecCodingStructure !== FEC_CODING_STRUCTURE_ONE_STAGE || flow.repairFlows.length !== 1) return null
|
|
517
|
-
if (flow.ssbgMode !==
|
|
518
|
-
if (flow.
|
|
511
|
+
if (flow.ssbgMode !== SSBG_MODE_1 || flow.ffsrptsFlag !== 0 || flow.fecPayloadIdMode !== 0) return null
|
|
512
|
+
if (flow.packetIds.length !== 1) return null
|
|
519
513
|
const repair = flow.repairFlows[0]!
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
return null
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
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) {
|
|
514
|
+
const { fecCodeId, maximumK: k, maximumP: p } = repair
|
|
515
|
+
const t = flow.repairSymbolSize
|
|
516
|
+
if (t < MIN_REPAIR_SYMBOL_SIZE || k < 1 || p < 1 || k + p - 1 > REPAIR_SYMBOL_ID_MAX) return null
|
|
517
|
+
if (fecCodeId === FEC_CODE_RAPTORQ) {
|
|
518
|
+
if (t % 8 !== 0 || k > RAPTORQ_MAX_SOURCE_SYMBOLS) return null
|
|
519
|
+
} else if (fecCodeId === FEC_CODE_REED_SOLOMON) {
|
|
520
|
+
if (k + p > REED_SOLOMON_MAX_BLOCK) return null
|
|
521
|
+
} else {
|
|
532
522
|
return null
|
|
533
523
|
}
|
|
524
|
+
if (repair.repairFlowId === MMTP_SIGNALING_PACKET_ID || repair.repairFlowId === flow.packetIds[0]) return null
|
|
534
525
|
return {
|
|
535
526
|
sourcePacketIds: [...flow.packetIds],
|
|
536
527
|
repairFlowId: repair.repairFlowId,
|
|
528
|
+
fecCodeId,
|
|
537
529
|
ssbgMode: flow.ssbgMode,
|
|
538
530
|
fecPayloadIdMode: flow.fecPayloadIdMode,
|
|
539
|
-
repairSymbolSize:
|
|
540
|
-
maximumK:
|
|
541
|
-
maximumP:
|
|
542
|
-
interleaveDepth,
|
|
543
|
-
oti,
|
|
531
|
+
repairSymbolSize: t,
|
|
532
|
+
maximumK: k,
|
|
533
|
+
maximumP: p,
|
|
544
534
|
protectionWindowTime: repair.protectionWindowTime,
|
|
545
535
|
protectionWindowSize: repair.protectionWindowSize,
|
|
546
536
|
}
|
|
547
537
|
}
|
|
548
538
|
|
|
539
|
+
/**
|
|
540
|
+
* `alFecRaptorQFlowConfig` for every entry of a message, index for index,
|
|
541
|
+
* plus the draft-ramadan-moq-mmt-01 Section 12.2 checks that span entries: an
|
|
542
|
+
* entry whose repair_flow_id is another entry's Asset packet_id, and every
|
|
543
|
+
* entry of two that name one repair_flow_id or list one Asset, is null.
|
|
544
|
+
*/
|
|
545
|
+
export function alFecMessageFlowConfigs(message: AlFecMessageBody): (AlFecRaptorQFlowConfig | null)[] {
|
|
546
|
+
const assetCount = new Map<number, number>()
|
|
547
|
+
const repairCount = new Map<number, number>()
|
|
548
|
+
const bump = (map: Map<number, number>, key: number) => map.set(key, (map.get(key) ?? 0) + 1)
|
|
549
|
+
for (const flow of message.flows) {
|
|
550
|
+
for (const id of flow.packetIds) bump(assetCount, id)
|
|
551
|
+
for (const repair of flow.repairFlows) bump(repairCount, repair.repairFlowId)
|
|
552
|
+
}
|
|
553
|
+
return message.flows.map(flow => {
|
|
554
|
+
const config = alFecRaptorQFlowConfig(flow)
|
|
555
|
+
if (!config) return null
|
|
556
|
+
const [asset] = config.sourcePacketIds
|
|
557
|
+
if (assetCount.get(asset!) !== 1 || repairCount.get(config.repairFlowId) !== 1) return null
|
|
558
|
+
// Its own packet_id is already refused, so any count here is another entry's.
|
|
559
|
+
if (assetCount.has(config.repairFlowId)) return null
|
|
560
|
+
return config
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
|
|
549
564
|
// ---------------------------------------------------------------------------
|
|
550
565
|
// 10.6 — location, asset_id and identifier mapping
|
|
551
566
|
// ---------------------------------------------------------------------------
|
|
@@ -847,10 +862,7 @@ function decodeSignallingMessage(bytes: Uint8Array): MmtSignallingMessage {
|
|
|
847
862
|
* Parse every signalling message of a full MMTP signalling packet (the
|
|
848
863
|
* packet_id 0 flow). Returns null when the packet or any message in it is
|
|
849
864
|
* 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.
|
|
865
|
+
* the packet signalled. Only the 9.3.4.2 payload form is accepted.
|
|
854
866
|
*/
|
|
855
867
|
export function parseSignallingPacket(mmtpPacket: Uint8Array): MmtSignallingPacket | null {
|
|
856
868
|
const payload = parseSignallingPayload(mmtpPacket)
|