@blockcast/mmt-base 0.3.0 → 0.4.0-main.2654dbc2ed6d

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.
@@ -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
- * - `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).
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' | 'legacy-bare-alfec'
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, 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.
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 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).
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
- 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
- }
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 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.
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
- * 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.
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 !== SSBG_MODE_0 && flow.ssbgMode !== SSBG_MODE_1) return null
518
- if (flow.ffsrptsFlag !== 0) return null
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
- 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) {
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: flow.repairSymbolSize,
540
- maximumK: repair.maximumK,
541
- maximumP: repair.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)