@blockcast/mmt-base 0.3.0 → 0.4.0-main.26d61a540c44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Catalog FEC source rules shared by every catalog reader (BLO-40851).
3
+ *
4
+ * These are the dialect-level reads the container resolvers
5
+ * (`resolveRepairLayers`, `catalogFecUnavailable`), the transport endpoint
6
+ * selection (`selectEndpointPacketIds`) and a player's repair inventory all
7
+ * need to agree on. They live here, in the leaf package every one of those
8
+ * depends on, so there is one copy of each rule instead of copies pinned
9
+ * against each other by tests.
10
+ *
11
+ * - `locateCatalogFecSource`: which catalog entry declares a source's `fec`
12
+ * block. hang `video`/`audio` rendition blocks are authoritative over an MSF
13
+ * `tracks[]` entry of the same name.
14
+ * - `catalogFecAvailable`: draft-ramadan-moq-fec §5.1, whether a block's FEC
15
+ * is usable at all (implemented `blockProfile`, usable span).
16
+ * - `catalogFecDeclaresUnconfigurableInstance`: a declared FEC instance whose
17
+ * geometry a decoder cannot configure, which is a malformed catalog rather
18
+ * than "no FEC".
19
+ *
20
+ * Every function is total: it never throws, whatever the input.
21
+ */
22
+ import { isFecBlockProfile } from './fec-block-profile.js'
23
+
24
+ /** The catalog media roles that carry hang renditions. */
25
+ export type CatalogFecRole = 'video' | 'audio'
26
+
27
+ /** One media source's `fec` block, as `locateCatalogFecSource` found it. */
28
+ export type CatalogFecSource =
29
+ | {
30
+ readonly dialect: 'hang'
31
+ readonly role: CatalogFecRole
32
+ /** The rendition object. */
33
+ readonly entry: Record<string, unknown>
34
+ /** The rendition's `fec` value, unvalidated (may be undefined). */
35
+ readonly fec: unknown
36
+ /** JSON path of that `fec` block. */
37
+ readonly fecPath: string
38
+ }
39
+ | {
40
+ readonly dialect: 'msf'
41
+ /** Index of the entry in `tracks[]`. */
42
+ readonly index: number
43
+ /** The `tracks[]` entry (an empty object when the entry is not an object). */
44
+ readonly entry: Record<string, unknown>
45
+ /** The entry's `fec` value, unvalidated (may be undefined). */
46
+ readonly fec: unknown
47
+ /** JSON path of that `fec` block. */
48
+ readonly fecPath: string
49
+ }
50
+
51
+ const ALL_ROLES: readonly CatalogFecRole[] = Object.freeze(['video', 'audio'])
52
+
53
+ function recordOf(value: unknown): Record<string, unknown> | undefined {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
55
+ ? (value as Record<string, unknown>)
56
+ : undefined
57
+ }
58
+
59
+ /**
60
+ * Locate the catalog entry that declares one media source's `fec` block, in
61
+ * either catalog dialect.
62
+ *
63
+ * A hang `video`/`audio` rendition named `source` wins, in `roles` order, over
64
+ * a `tracks[]` entry of the same name: when a catalog carries rendition blocks
65
+ * beside a `tracks[]` switching-set index (the unified ABR ladder catalog),
66
+ * the rendition blocks are authoritative, exactly as they are for hang's own
67
+ * catalog decode. The precedence is keyed on the rendition EXISTING as an
68
+ * object, not on it carrying `fec`: a rendition without `fec` locates with
69
+ * `fec: undefined` and never falls through to `tracks[]`.
70
+ *
71
+ * Otherwise the first `tracks[]` entry named `source` locates. Returns
72
+ * undefined when neither dialect names it, or when `catalog` is not an object.
73
+ *
74
+ * @param roles the hang roles to search, in order (both by default). A typed
75
+ * track reference passes its own role only.
76
+ */
77
+ export function locateCatalogFecSource(
78
+ catalog: unknown,
79
+ source: string,
80
+ roles: readonly CatalogFecRole[] = ALL_ROLES,
81
+ ): CatalogFecSource | undefined {
82
+ const root = recordOf(catalog)
83
+ if (root === undefined) return undefined
84
+ for (const role of roles) {
85
+ const renditions = recordOf(recordOf(root[role])?.['renditions'])
86
+ if (renditions === undefined || !Object.hasOwn(renditions, source)) continue
87
+ const entry = recordOf(renditions[source])
88
+ if (entry === undefined) continue
89
+ return Object.freeze({
90
+ dialect: 'hang' as const,
91
+ role,
92
+ entry,
93
+ fec: entry['fec'],
94
+ fecPath: `$.${role}.renditions[${JSON.stringify(source)}].fec`,
95
+ })
96
+ }
97
+ const tracks = root['tracks']
98
+ if (!Array.isArray(tracks)) return undefined
99
+ const index = tracks.findIndex((track) => recordOf(track)?.['name'] === source)
100
+ if (index < 0) return undefined
101
+ const entry = recordOf(tracks[index]) ?? {}
102
+ return Object.freeze({
103
+ dialect: 'msf' as const,
104
+ index,
105
+ entry,
106
+ fec: entry['fec'],
107
+ fecPath: `$.tracks[${index}].fec`,
108
+ })
109
+ }
110
+
111
+ /**
112
+ * Whether an `iso-ssbg1-v1` block's optional `interleaveDepthMs` span is
113
+ * usable: absent, or a non-negative safe integer. Zero or absence supplies no
114
+ * elapsed deadline and still permits in-band FEC. True for every block that
115
+ * names another profile (the span is not read) and for a non-object.
116
+ */
117
+ export function isoBlockSpanUsable(fec: unknown): boolean {
118
+ const block = recordOf(fec)
119
+ if (block === undefined || block['blockProfile'] !== 'iso-ssbg1-v1') return true
120
+ const ms = block['interleaveDepthMs']
121
+ return ms === undefined || (typeof ms === 'number' && Number.isSafeInteger(ms) && ms >= 0)
122
+ }
123
+
124
+ /**
125
+ * Whether one catalog `fec` block leaves its FEC available
126
+ * (draft-ramadan-moq-fec §5.1): the decision the container's
127
+ * `catalogFecUnavailable` explains with a typed reason.
128
+ *
129
+ * False when the block declares an FEC instance (any `algorithm` but `"none"`)
130
+ * and names no implemented `blockProfile` (the retired `k-padded-v1`
131
+ * included), or carries an `iso-ssbg1-v1` span it cannot use; and when a block
132
+ * declaring no FEC instance carries a profile token that is not implemented.
133
+ * True for a non-object and for every other block. It reads only the profile
134
+ * and the span: malformed geometry (`sourceSymbols`, `symbolSize`) is
135
+ * `catalogFecDeclaresUnconfigurableInstance`'s.
136
+ *
137
+ * A receiver applies no FEC to a source whose block is unavailable and joins
138
+ * none of its repair tracks, without failing the session.
139
+ */
140
+ export function catalogFecAvailable(fec: unknown): boolean {
141
+ const block = recordOf(fec)
142
+ if (block === undefined) return true
143
+ const declaresFecInstance = block['algorithm'] !== 'none'
144
+ const profile = block['blockProfile']
145
+ const profileOk = isFecBlockProfile(profile) || (profile === undefined && !declaresFecInstance)
146
+ return profileOk && (!declaresFecInstance || isoBlockSpanUsable(block))
147
+ }
148
+
149
+ function isPositiveSafeInteger(value: unknown): boolean {
150
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0
151
+ }
152
+
153
+ /**
154
+ * Whether a source's `fec` block declares an FEC instance (any `algorithm`
155
+ * but `"none"`) whose `sourceSymbols` or `symbolSize` is not a positive
156
+ * integer. Geometry a decoder cannot configure is a malformed catalog, not
157
+ * "no FEC": the container's per-track resolver throws `track_fec` for exactly
158
+ * this block, whatever its profile or span.
159
+ */
160
+ export function catalogFecDeclaresUnconfigurableInstance(fec: unknown): boolean {
161
+ const block = recordOf(fec)
162
+ if (block === undefined || block['algorithm'] === 'none') return false
163
+ return !(isPositiveSafeInteger(block['sourceSymbols']) && isPositiveSafeInteger(block['symbolSize']))
164
+ }
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).
3
3
  *
4
- * Every catalog `fec` object that declares an FEC instance names exactly one
5
- * block profile in `fec.blockProfile`. A receiver applies all of a profile's
6
- * rules or none of them, so the token is the one explicit per-stream
7
- * discriminator: it is never defaulted and never inferred from the wire.
4
+ * Every catalog `fec` object that declares an FEC instance names its block
5
+ * profile in `fec.blockProfile`. A receiver applies all of a profile's rules
6
+ * or none of them, so the token is the one explicit per-stream discriminator:
7
+ * it is never defaulted and never inferred from the wire.
8
+ *
9
+ * draft-ramadan-moq-fec §5.1 defines exactly one value, and it is the only
10
+ * profile this library implements:
8
11
  *
9
- * - `k-padded-v1` (legacy, Blockcast-private): a source symbol is the
10
- * protected MMTP packet zero-padded to T with no length field; every block
11
- * is a K-symbol RaptorQ block whose ESIs `SSB_length..K-1` are implicit
12
- * zero padding; `SS_ID = SBN*K + ESI`, so `SS_Start = SBN*K`; repair
13
- * ESI = K + RS_ID; the MoQ repair group id is the SBN.
14
12
  * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is
15
13
  * `u16 length || protected packet || zero padding` to T; a block is a
16
14
  * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;
@@ -19,40 +17,42 @@
19
17
  * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit
20
18
  * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is
21
19
  * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).
20
+ *
21
+ * A track whose `blockProfile` is absent or names any other token (the
22
+ * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver
23
+ * consumes its source without FEC and joins none of its repair tracks
24
+ * (draft-ramadan-moq-fec §5.1). `parseFecBlockProfile` throws for such a
25
+ * token; the per-track catalog resolver turns the same rule into a typed
26
+ * per-track "FEC unavailable" outcome instead of failing the session.
22
27
  */
23
28
  import { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'
24
29
 
25
- /** The two block profiles a catalog `fec.blockProfile` may name. */
26
- export type FecBlockProfile = 'k-padded-v1' | 'iso-ssbg1-v1'
30
+ /** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */
31
+ export type FecBlockProfile = 'iso-ssbg1-v1'
27
32
 
28
- /** Every accepted `fec.blockProfile` token, legacy first. */
33
+ /** Every accepted `fec.blockProfile` token. */
29
34
  export const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([
30
- 'k-padded-v1',
31
35
  'iso-ssbg1-v1',
32
36
  ])
33
37
 
34
- /** True for a known block-profile token. */
38
+ /** True for a block-profile token this library implements. */
35
39
  export function isFecBlockProfile(value: unknown): value is FecBlockProfile {
36
- return value === 'k-padded-v1' || value === 'iso-ssbg1-v1'
40
+ return value === 'iso-ssbg1-v1'
37
41
  }
38
42
 
39
43
  /**
40
- * Read a required `fec.blockProfile`. Throws on an absent or unknown token:
41
- * there is no default and nothing is inferred, because the two profiles carry
42
- * identical wire tuples whose repair math differs.
44
+ * Read a required `fec.blockProfile`. Throws on an absent token and on any
45
+ * token other than `iso-ssbg1-v1`: there is no default and nothing is
46
+ * inferred from the wire (draft-ramadan-moq-fec §5.1).
43
47
  *
44
48
  * @param where the catalog path of the field, for the error message.
45
49
  */
46
50
  export function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {
47
51
  if (isFecBlockProfile(value)) return value
48
52
  if (value === undefined) {
49
- throw new Error(
50
- `${where} is required: name the FEC block profile ("k-padded-v1" or "iso-ssbg1-v1")`,
51
- )
53
+ throw new Error(`${where} is required: name the FEC block profile ("iso-ssbg1-v1")`)
52
54
  }
53
- throw new Error(
54
- `${where} must be "k-padded-v1" or "iso-ssbg1-v1" (got ${JSON.stringify(value)})`,
55
- )
55
+ throw new Error(`${where} must be "iso-ssbg1-v1" (got ${JSON.stringify(value)})`)
56
56
  }
57
57
 
58
58
  /** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */
package/src/index.ts CHANGED
@@ -4,3 +4,4 @@ export * from './ntp-short.js'
4
4
  export * from './signalling.js'
5
5
  export * from './serial-number.js'
6
6
  export * from './fec-block-profile.js'
7
+ export * from './catalog-fec-source.js'
package/src/mmtp.ts CHANGED
@@ -1,10 +1,3 @@
1
- import {
2
- MMTP_EXT_TYPE_ALTA_AUTH as _LEAF_MMTP_EXT_TYPE_ALTA_AUTH,
3
- MMTP_EXT_TRAILER_HEADER_SIZE as _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE,
4
- extractAltaTrailer as _leafExtractAltaTrailer,
5
- type AltaTrailer as _LeafAltaTrailer,
6
- } from '@blockcast/mmt-alta-parse'
7
-
8
1
  /**
9
2
  * @mmt/container - MMTP Parser
10
3
  *
@@ -353,9 +346,17 @@ function resolveMfuPayloadLayout(
353
346
  }
354
347
  }
355
348
 
356
- function isEmptyOrExactAltaTrailer(payload: Uint8Array, start: number, end: number): boolean {
349
+ // Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its
350
+ // producer was removed (BLO-16483) and nothing verifies it any more; provenance
351
+ // is the signed per-group manifest. Receivers still skip an exact suffix so a
352
+ // stale producer degrades to unauthenticated playback instead of dropped
353
+ // packets. ponytail: hard-cut to reject once no deployed producer can emit it.
354
+ const LEGACY_TRAILER_EXT_TYPE = 0xf0a1
355
+ const LEGACY_TRAILER_HEADER_SIZE = 4
356
+
357
+ function isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {
357
358
  if (start === end) return true
358
- if (start < 0 || end > payload.length || start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE > end) {
359
+ if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {
359
360
  return false
360
361
  }
361
362
 
@@ -364,10 +365,10 @@ function isEmptyOrExactAltaTrailer(payload: Uint8Array, start: number, end: numb
364
365
  payload.byteOffset + start,
365
366
  end - start,
366
367
  )
367
- if (view.getUint16(0, false) !== _LEAF_MMTP_EXT_TYPE_ALTA_AUTH) return false
368
+ if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false
368
369
 
369
370
  const authLength = view.getUint16(2, false)
370
- return start + _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE + authLength === end
371
+ return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end
371
372
  }
372
373
 
373
374
  interface DeclaredTrailerLayout {
@@ -383,21 +384,21 @@ function resolveDeclaredTrailers(
383
384
  if (declaredEnd > payload.length) return null
384
385
 
385
386
  if (options?.skipFecId) {
386
- return isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)
387
+ return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)
387
388
  ? { hasFecId: false }
388
389
  : null
389
390
  }
390
391
 
391
392
  if (fecType === 1) {
392
393
  const fecStart = payload.length - SOURCE_FEC_PAYLOAD_ID_SIZE
393
- if (fecStart < declaredEnd || !isEmptyOrExactAltaTrailer(payload, declaredEnd, fecStart)) {
394
+ if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {
394
395
  return null
395
396
  }
396
397
  return { hasFecId: true }
397
398
  }
398
399
  if (fecType !== 0) return null
399
400
 
400
- if (isEmptyOrExactAltaTrailer(payload, declaredEnd, payload.length)) {
401
+ if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {
401
402
  return { hasFecId: false }
402
403
  }
403
404
 
@@ -441,8 +442,8 @@ export interface MFUExtraction {
441
442
  * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)
442
443
  *
443
444
  * `payloadLength` is the ISO length-after-field value: six remaining MPU-header
444
- * bytes, then the MFU DU header when present, then media. ALTA and Source FEC
445
- * trailers are outside that declared span and must consume the exact suffix.
445
+ * bytes, then the MFU DU header when present, then media. The legacy
446
+ * authenticator suffix and the Source FEC trailer are outside that declared span and must consume the exact suffix.
446
447
  *
447
448
  * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)
448
449
  * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID
@@ -559,7 +560,7 @@ function planMfuExtraction(
559
560
  gate: 'declared-trailers-unresolved',
560
561
  detail:
561
562
  `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +
562
- `nor an exact ALTA trailer (fecType=${header.fecType})`,
563
+ `nor an exact legacy authenticator trailer (fecType=${header.fecType})`,
563
564
  },
564
565
  }
565
566
  }
@@ -732,22 +733,6 @@ export function sourceFecPayloadIdOf(
732
733
  }
733
734
  }
734
735
 
735
- /**
736
- * MMTP header-extension ext_type assigned to the detached ALTA authenticator
737
- * trailer (Path 2 / 08-07). Carried in an `[ext_type][ext_length][auth]`
738
- * block appended after the MPU's declared payload bytes, before the 4-byte
739
- * Source FEC Payload ID. MMTP X-bit stays 0 — non-ALTA MMTP receivers treat
740
- * the trailer as bytes past `payload_length` and skip it (per ISO 23008-1
741
- * §9.2.2 "may be discarded without impacting correct processing").
742
- */
743
- // Re-export the ALTA trailer helper from the zero-dep leaf package. The leaf
744
- // owns the implementation so container and FEC consumers share one parser
745
- // without creating a package cycle.
746
- export const MMTP_EXT_TYPE_ALTA_AUTH = _LEAF_MMTP_EXT_TYPE_ALTA_AUTH
747
- export const MMTP_EXT_TRAILER_HEADER_SIZE = _LEAF_MMTP_EXT_TRAILER_HEADER_SIZE
748
- export type AltaTrailer = _LeafAltaTrailer
749
- export const extractAltaTrailer = _leafExtractAltaTrailer
750
-
751
736
  /** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */
752
737
  export const REPAIR_FEC_PID_SIZE = 13
753
738
 
@@ -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
@@ -112,20 +112,17 @@ function total<T>(parse: () => T | null): T | null {
112
112
  // ---------------------------------------------------------------------------
113
113
 
114
114
  /**
115
- * How the signalling payload of an MMTP packet was framed.
115
+ * How the signalling payload of an MMTP packet was framed: always
116
+ * `iso-9.3.4.2`, the 2-byte `f_i(2) RES(4) H(1) A(1) | frag_counter(8)`
117
+ * header of ISO/IEC 23008-1:2023 9.3.4.2 ahead of the message(s).
116
118
  *
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).
119
+ * The pre-remediation Blockcast form, an AL-FEC message directly after the
120
+ * MMTP header, is not accepted (BLO-35597, Phase L). Its first two bytes,
121
+ * `0x02 0x03`, read as a 9.3.4.2 header claiming three further fragments
122
+ * of a complete message, which the header parser refuses: such a packet
123
+ * parses to null, never to a guess.
127
124
  */
128
- export type SignallingPayloadForm = 'iso-9.3.4.2' | 'legacy-bare-alfec'
125
+ export type SignallingPayloadForm = 'iso-9.3.4.2'
129
126
 
130
127
  export interface SignallingPayload {
131
128
  form: SignallingPayloadForm
@@ -158,10 +155,6 @@ export function parseSignallingPayload(mmtpPacket: Uint8Array): SignallingPayloa
158
155
 
159
156
  function splitSignallingPayload(payload: Uint8Array): SignallingPayload {
160
157
  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
158
  const reader = new WireReader(payload)
166
159
  const flags = reader.u8()
167
160
  const fragmentation = flags >> 6
@@ -445,9 +438,9 @@ export function parseAlFecMessageBody(message: Uint8Array): AlFecMessageBody | n
445
438
  }
446
439
 
447
440
  /**
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.
441
+ * Parse the AL-FEC message carried by a full MMTP signalling packet (a
442
+ * 9.3.4.2 payload, see SignallingPayloadForm). Returns null when the packet
443
+ * carries no AL-FEC message, more than one, or a malformed one.
451
444
  */
452
445
  export function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecMessage | null {
453
446
  const packet = parseSignallingPacket(mmtpPacket)
@@ -467,7 +460,9 @@ export function parseAlFecMessage(mmtpPacket: Uint8Array): AlFecMessage | null {
467
460
  export interface AlFecRaptorQFlowConfig {
468
461
  sourcePacketIds: number[]
469
462
  repairFlowId: number
463
+ /** Always `SSBG_MODE_1` (`iso-ssbg1-v1`). */
470
464
  ssbgMode: number
465
+ /** The coding byte's low bit as signalled (see AlFecFlow.fecPayloadIdMode). */
471
466
  fecPayloadIdMode: 0 | 1
472
467
  repairSymbolSize: number
473
468
  maximumK: number
@@ -506,15 +501,21 @@ function canonicalRaptorQOtiK(oti: Uint8Array, repairSymbolSize: number): number
506
501
  * the profile. Nothing is inferred: K comes from the OTI and must equal
507
502
  * maximum_k_for_repair_flow.
508
503
  *
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.
504
+ * The profile is `iso-ssbg1-v1`, whose AL-FEC coding byte is 0x14
505
+ * (draft-ramadan-moq-fec §7.3; ISO/IEC 23008-1 Table C.3 as replaced by
506
+ * Amd 1:2025): one-stage, ssbg_mode1, ffsrpts_flag 0, fec_payload_id_mode 0.
507
+ * 0x15 is accepted too: its low bit is fec_payload_id_mode 1 under Amd
508
+ * 1:2025 but reserved "1" in the unamended 2023 Table C.3, so a 2023 sender
509
+ * of the same profile emits it. No later field depends on the bit, and each
510
+ * source packet's FEC_type says whether it carries a Source FEC Payload ID,
511
+ * so the bit is reported, not interpreted (BLO-35597 N1). Any other coding
512
+ * byte is refused here, though `parseAlFecMessageBody` still reports it:
513
+ * ssbg_mode0 (0x10/0x11, the retired `k-padded-v1` wire) carries no length
514
+ * field; ffsrpts_flag=1 changes the source/repair packet trailer layout.
514
515
  */
515
516
  export function alFecRaptorQFlowConfig(flow: AlFecFlow): AlFecRaptorQFlowConfig | null {
516
517
  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.ssbgMode !== SSBG_MODE_1) return null
518
519
  if (flow.ffsrptsFlag !== 0) return null
519
520
  const repair = flow.repairFlows[0]!
520
521
  if (repair.fecCodeId !== FEC_CODE_RAPTORQ) return null
@@ -847,10 +848,7 @@ function decodeSignallingMessage(bytes: Uint8Array): MmtSignallingMessage {
847
848
  * Parse every signalling message of a full MMTP signalling packet (the
848
849
  * packet_id 0 flow). Returns null when the packet or any message in it is
849
850
  * 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.
851
+ * the packet signalled. Only the 9.3.4.2 payload form is accepted.
854
852
  */
855
853
  export function parseSignallingPacket(mmtpPacket: Uint8Array): MmtSignallingPacket | null {
856
854
  const payload = parseSignallingPayload(mmtpPacket)