@blockcast/mmt-fec 0.3.0 → 0.4.0
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 +4 -4
- package/dist/block-processor.d.ts +19 -20
- package/dist/block-processor.d.ts.map +1 -1
- package/dist/block-processor.js +27 -93
- package/dist/block-processor.js.map +1 -1
- package/dist/fec-block-mapper.d.ts +16 -72
- package/dist/fec-block-mapper.d.ts.map +1 -1
- package/dist/fec-block-mapper.js +17 -96
- package/dist/fec-block-mapper.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/oti.d.ts +17 -8
- package/dist/oti.d.ts.map +1 -1
- package/dist/oti.js +9 -1
- package/dist/oti.js.map +1 -1
- package/dist/repair-group.d.ts +86 -0
- package/dist/repair-group.d.ts.map +1 -1
- package/dist/repair-group.js +135 -0
- package/dist/repair-group.js.map +1 -1
- package/package.json +5 -6
- package/src/block-processor.ts +31 -110
- package/src/fec-block-mapper.ts +23 -108
- package/src/index.ts +6 -1
- package/src/oti.ts +20 -9
- package/src/repair-group.ts +176 -0
- package/test-vectors/repair-epoch.json +119 -0
- package/node_modules/@blockcast/mmt-alta-parse/README.md +0 -181
- package/node_modules/@blockcast/mmt-alta-parse/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts +0 -57
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js +0 -115
- package/node_modules/@blockcast/mmt-alta-parse/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-alta-parse/package.json +0 -50
- package/node_modules/@blockcast/mmt-alta-parse/src/index.ts +0 -136
- package/node_modules/@blockcast/mmt-base/README.md +0 -134
- package/node_modules/@blockcast/mmt-base/dist/.build-stamp +0 -0
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts +0 -55
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js +0 -128
- package/node_modules/@blockcast/mmt-base/dist/fec-block-profile.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts +0 -7
- package/node_modules/@blockcast/mmt-base/dist/index.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/index.js +0 -7
- package/node_modules/@blockcast/mmt-base/dist/index.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts +0 -292
- package/node_modules/@blockcast/mmt-base/dist/mmtp.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js +0 -612
- package/node_modules/@blockcast/mmt-base/dist/mmtp.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts +0 -18
- package/node_modules/@blockcast/mmt-base/dist/net-constants.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js +0 -18
- package/node_modules/@blockcast/mmt-base/dist/net-constants.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts +0 -47
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js +0 -54
- package/node_modules/@blockcast/mmt-base/dist/ntp-short.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts +0 -57
- package/node_modules/@blockcast/mmt-base/dist/serial-number.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js +0 -98
- package/node_modules/@blockcast/mmt-base/dist/serial-number.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts +0 -312
- package/node_modules/@blockcast/mmt-base/dist/signalling.d.ts.map +0 -1
- package/node_modules/@blockcast/mmt-base/dist/signalling.js +0 -637
- package/node_modules/@blockcast/mmt-base/dist/signalling.js.map +0 -1
- package/node_modules/@blockcast/mmt-base/package.json +0 -49
- package/node_modules/@blockcast/mmt-base/src/fec-block-profile.ts +0 -145
- package/node_modules/@blockcast/mmt-base/src/index.ts +0 -6
- package/node_modules/@blockcast/mmt-base/src/mmtp.ts +0 -892
- package/node_modules/@blockcast/mmt-base/src/net-constants.ts +0 -17
- package/node_modules/@blockcast/mmt-base/src/ntp-short.ts +0 -67
- package/node_modules/@blockcast/mmt-base/src/serial-number.ts +0 -107
- package/node_modules/@blockcast/mmt-base/src/signalling.ts +0 -862
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/mmt-fec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Container-agnostic FEC client for RaptorQ (RFC 6330) decoding via WASM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,11 +33,13 @@
|
|
|
33
33
|
"./loc": {
|
|
34
34
|
"import": "./dist/loc-symbols.js",
|
|
35
35
|
"types": "./dist/loc-symbols.d.ts"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"./test-vectors/repair-epoch.json": "./test-vectors/repair-epoch.json"
|
|
37
38
|
},
|
|
38
39
|
"files": [
|
|
39
40
|
"dist",
|
|
40
41
|
"src",
|
|
42
|
+
"test-vectors",
|
|
41
43
|
"!src/**/*.test.ts",
|
|
42
44
|
"!src/**/*.spec.ts",
|
|
43
45
|
"!src/**/__tests__",
|
|
@@ -53,11 +55,8 @@
|
|
|
53
55
|
"test:watch": "vitest"
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|
|
56
|
-
"@blockcast/mmt-base": "^0.
|
|
58
|
+
"@blockcast/mmt-base": "^0.4.0"
|
|
57
59
|
},
|
|
58
|
-
"bundledDependencies": [
|
|
59
|
-
"@blockcast/mmt-base"
|
|
60
|
-
],
|
|
61
60
|
"devDependencies": {
|
|
62
61
|
"typescript": "^5.3.0",
|
|
63
62
|
"vitest": "^1.0.0"
|
package/src/block-processor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FEC Block Processor
|
|
3
3
|
*
|
|
4
|
-
* Splits decoded FEC blocks (
|
|
4
|
+
* Splits decoded FEC blocks (SSB_length symbols concatenated by the WASM decoder)
|
|
5
5
|
* into individual MFU fragments ready for reassembly and decoding.
|
|
6
6
|
*
|
|
7
7
|
* Extracted from moqtail.html's inline processFecBlock() for reuse across
|
|
@@ -9,14 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
MMTP_EXT_TRAILER_HEADER_SIZE,
|
|
13
|
-
MMTP_EXT_TYPE_ALTA_AUTH,
|
|
14
12
|
MMTP_HEADER_SIZE,
|
|
15
|
-
MPU_HEADER_SIZE,
|
|
16
13
|
SSBG1_LENGTH_FIELD_BYTES,
|
|
17
14
|
extractMFUPayload,
|
|
18
15
|
parseFecBlockProfile,
|
|
19
|
-
parseMPUHeader,
|
|
20
16
|
parseMMTPHeader,
|
|
21
17
|
trimSsbg1SourceSymbol,
|
|
22
18
|
type FecBlockProfile,
|
|
@@ -66,9 +62,9 @@ export interface ProcessFecBlockResult {
|
|
|
66
62
|
/** Options for {@link processFecSymbol}. */
|
|
67
63
|
export interface ProcessFecSymbolOptions {
|
|
68
64
|
/**
|
|
69
|
-
* The stream's catalog `fec.blockProfile`. Required, never defaulted:
|
|
70
|
-
*
|
|
71
|
-
*
|
|
65
|
+
* The stream's catalog `fec.blockProfile`. Required, never defaulted:
|
|
66
|
+
* the profile fixes how a packet is laid into a symbol, and the only one
|
|
67
|
+
* implemented is `iso-ssbg1-v1`.
|
|
72
68
|
*/
|
|
73
69
|
blockProfile: FecBlockProfile;
|
|
74
70
|
/** Symbol index within the block, used only for diagnostics. */
|
|
@@ -84,62 +80,15 @@ export interface ProcessFecBlockOptions {
|
|
|
84
80
|
/** Symbol size T in bytes (catalog `fec.symbolSize`). Required: there is no default T. */
|
|
85
81
|
symbolSize: number;
|
|
86
82
|
/**
|
|
87
|
-
* Source symbols in the decoded block: K
|
|
88
|
-
*
|
|
89
|
-
*
|
|
83
|
+
* Source symbols in the decoded block: its SSB_length (K'). When omitted
|
|
84
|
+
* the block must be a whole number of T-byte symbols and K' is that
|
|
85
|
+
* number; a partial symbol throws.
|
|
90
86
|
*/
|
|
91
87
|
k?: number;
|
|
92
88
|
/** Keep only symbols for this packetId. */
|
|
93
89
|
ownerPacketId?: number;
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
function hasUniformPaddingFrom(data: Uint8Array, start: number): boolean {
|
|
97
|
-
if (start >= data.length) return false;
|
|
98
|
-
const pad = data[start]!;
|
|
99
|
-
if (pad !== 0x00 && pad !== 0xee) return false;
|
|
100
|
-
for (let i = start + 1; i < data.length; i++) {
|
|
101
|
-
if (data[i] !== pad) return false;
|
|
102
|
-
}
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function maybeExtendAltaTrailer(symbolData: Uint8Array, packetEnd: number): number {
|
|
107
|
-
if (packetEnd + MMTP_EXT_TRAILER_HEADER_SIZE > symbolData.length) return packetEnd;
|
|
108
|
-
const view = new DataView(
|
|
109
|
-
symbolData.buffer,
|
|
110
|
-
symbolData.byteOffset + packetEnd,
|
|
111
|
-
symbolData.byteLength - packetEnd,
|
|
112
|
-
);
|
|
113
|
-
const extType = view.getUint16(0, false);
|
|
114
|
-
if (extType !== MMTP_EXT_TYPE_ALTA_AUTH) return packetEnd;
|
|
115
|
-
|
|
116
|
-
const extLen = view.getUint16(2, false);
|
|
117
|
-
const trailerEnd = packetEnd + MMTP_EXT_TRAILER_HEADER_SIZE + extLen;
|
|
118
|
-
return trailerEnd <= symbolData.length ? trailerEnd : packetEnd;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function trimDecodedSourceSymbol(symbolData: Uint8Array): Uint8Array | null {
|
|
122
|
-
if (symbolData.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null;
|
|
123
|
-
|
|
124
|
-
const headerLength = parseMMTPHeader(symbolData).headerLength;
|
|
125
|
-
if (symbolData.length < headerLength + MPU_HEADER_SIZE) return null;
|
|
126
|
-
const payload = symbolData.subarray(headerLength);
|
|
127
|
-
const mpu = parseMPUHeader(payload);
|
|
128
|
-
const trailingMpuHeaderSize = MPU_HEADER_SIZE - 2;
|
|
129
|
-
if (mpu.payloadLength < trailingMpuHeaderSize) return null;
|
|
130
|
-
|
|
131
|
-
// Recovered source symbols are fixed-width and may be padded, but their wire
|
|
132
|
-
// boundary is still the ISO length-after-field coordinate. ALTA may extend the
|
|
133
|
-
// packet from exactly that point; no MFU-content or alternate-length guessing.
|
|
134
|
-
const declaredEnd = headerLength + 2 + mpu.payloadLength;
|
|
135
|
-
if (declaredEnd > symbolData.length) return null;
|
|
136
|
-
const packetEnd = maybeExtendAltaTrailer(symbolData, declaredEnd);
|
|
137
|
-
if (packetEnd < symbolData.length && !hasUniformPaddingFrom(symbolData, packetEnd)) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
return symbolData.subarray(0, packetEnd);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
92
|
function emptyProcessResult(): ProcessFecBlockResult {
|
|
144
93
|
return {
|
|
145
94
|
symbols: [],
|
|
@@ -172,8 +121,8 @@ function resultFromSymbols(
|
|
|
172
121
|
parsed.sort(comparePacketSequence);
|
|
173
122
|
|
|
174
123
|
// When ownerPacketId is set, only keep symbols matching the track that owns
|
|
175
|
-
// this FEC block. Other packetIds
|
|
176
|
-
//
|
|
124
|
+
// this FEC block. Other packetIds are garbage and must be rejected to
|
|
125
|
+
// prevent decoder crashes.
|
|
177
126
|
const symbols = ownerPacketId != null
|
|
178
127
|
? parsed.filter((s) => s.packetId === ownerPacketId)
|
|
179
128
|
: parsed;
|
|
@@ -186,9 +135,8 @@ type FailureCounter = (reason: string) => void;
|
|
|
186
135
|
/**
|
|
187
136
|
* Gate one candidate MMTP packet on its fixed header: long enough, version 0,
|
|
188
137
|
* parseable, and an MPU packet. Returns the parsed header, or null after
|
|
189
|
-
* counting why it was refused.
|
|
190
|
-
*
|
|
191
|
-
* length-delimited packet under iso-ssbg1-v1).
|
|
138
|
+
* counting why it was refused. It sees the length-delimited protected packet,
|
|
139
|
+
* never the padded symbol.
|
|
192
140
|
*/
|
|
193
141
|
function gateMpuHeader(data: Uint8Array, bump: FailureCounter): MMTPHeader | null {
|
|
194
142
|
if (data.length < MMTP_HEADER_SIZE) {
|
|
@@ -223,7 +171,7 @@ function parseProtectedPacket(
|
|
|
223
171
|
// packet (fecType=1 per draft-ramadan-moq-mmt-00 §3.1), but the 4-byte
|
|
224
172
|
// Source FEC Payload ID (ISO 23008-1 §C.5.2) is never part of the protected
|
|
225
173
|
// packet (C.4.2). Tell extractMFUPayload to skip FEC PID subtraction so it
|
|
226
|
-
// does not truncate video data; any
|
|
174
|
+
// does not truncate video data; any legacy authenticator trailer must then consume the
|
|
227
175
|
// exact suffix after the declared payload.
|
|
228
176
|
const extraction = extractMFUPayload(packetData, { skipFecId: true });
|
|
229
177
|
if (!extraction) {
|
|
@@ -256,26 +204,6 @@ function parseProtectedPacket(
|
|
|
256
204
|
};
|
|
257
205
|
}
|
|
258
206
|
|
|
259
|
-
/**
|
|
260
|
-
* k-padded-v1: the symbol is the protected packet zero-padded to T with no
|
|
261
|
-
* length field, so the packet ends at its ISO-declared MPU boundary (plus an
|
|
262
|
-
* ALTA trailer starting exactly there) and the rest must be uniform padding.
|
|
263
|
-
*/
|
|
264
|
-
function parseKPaddedSymbol(
|
|
265
|
-
symbolData: Uint8Array,
|
|
266
|
-
index: number,
|
|
267
|
-
bump: FailureCounter,
|
|
268
|
-
): ParsedFecSymbol | null {
|
|
269
|
-
const hdr = gateMpuHeader(symbolData, bump);
|
|
270
|
-
if (!hdr) return null;
|
|
271
|
-
const packetData = trimDecodedSourceSymbol(symbolData);
|
|
272
|
-
if (!packetData) {
|
|
273
|
-
bump("bad_length");
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
return parseProtectedPacket(packetData, hdr, index, bump);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
207
|
/**
|
|
280
208
|
* iso-ssbg1-v1 (ISO/IEC 23008-1 C.3.3 ssbg_mode1): the symbol is
|
|
281
209
|
* `u16be(length) || protected packet || 00h padding`. The length field alone
|
|
@@ -300,22 +228,19 @@ function parseSsbg1Symbol(
|
|
|
300
228
|
/**
|
|
301
229
|
* Parse one recovered FEC source symbol into an MFU fragment.
|
|
302
230
|
*
|
|
303
|
-
* The symbol is
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
231
|
+
* The symbol is the `iso-ssbg1-v1` ssbg_mode1 `u16 length || packet || zero
|
|
232
|
+
* padding` (ISO/IEC 23008-1 C.3.3): the length field alone delimits the
|
|
233
|
+
* protected packet, which is then parsed as an MMTP MPU packet. Failures are
|
|
234
|
+
* counted by reason (`bad_ssbg1_length` is the length/padding refusal), never
|
|
235
|
+
* thrown.
|
|
308
236
|
*
|
|
309
|
-
* @throws when `options.blockProfile` is absent or not
|
|
237
|
+
* @throws when `options.blockProfile` is absent or not `iso-ssbg1-v1`.
|
|
310
238
|
*/
|
|
311
239
|
export function processFecSymbol(
|
|
312
240
|
symbolData: Uint8Array,
|
|
313
241
|
options: ProcessFecSymbolOptions,
|
|
314
242
|
): ProcessFecBlockResult {
|
|
315
|
-
|
|
316
|
-
options?.blockProfile,
|
|
317
|
-
"processFecSymbol options.blockProfile",
|
|
318
|
-
);
|
|
243
|
+
parseFecBlockProfile(options?.blockProfile, "processFecSymbol options.blockProfile");
|
|
319
244
|
const failReasons: Record<string, number> = {};
|
|
320
245
|
let failedCount = 0;
|
|
321
246
|
const bump: FailureCounter = (reason) => {
|
|
@@ -328,9 +253,7 @@ export function processFecSymbol(
|
|
|
328
253
|
const index = options.index ?? 0;
|
|
329
254
|
let parsed: ParsedFecSymbol | null;
|
|
330
255
|
try {
|
|
331
|
-
parsed =
|
|
332
|
-
? parseSsbg1Symbol(symbolData, index, bump)
|
|
333
|
-
: parseKPaddedSymbol(symbolData, index, bump);
|
|
256
|
+
parsed = parseSsbg1Symbol(symbolData, index, bump);
|
|
334
257
|
} catch {
|
|
335
258
|
bump("exception");
|
|
336
259
|
parsed = null;
|
|
@@ -343,14 +266,14 @@ export function processFecSymbol(
|
|
|
343
266
|
);
|
|
344
267
|
}
|
|
345
268
|
|
|
346
|
-
function requireSymbolSize(symbolSize: unknown
|
|
269
|
+
function requireSymbolSize(symbolSize: unknown): number {
|
|
347
270
|
if (symbolSize === undefined) {
|
|
348
271
|
throw new Error("processFecBlock options.symbolSize is required: name the catalog fec.symbolSize (T)");
|
|
349
272
|
}
|
|
350
273
|
if (typeof symbolSize !== "number" || !Number.isSafeInteger(symbolSize) || symbolSize <= 0) {
|
|
351
274
|
throw new Error(`processFecBlock options.symbolSize must be a positive integer (got ${String(symbolSize)})`);
|
|
352
275
|
}
|
|
353
|
-
if (
|
|
276
|
+
if (symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {
|
|
354
277
|
throw new Error(
|
|
355
278
|
`processFecBlock options.symbolSize must exceed the ${SSBG1_LENGTH_FIELD_BYTES}-byte ssbg_mode1 length field (got ${symbolSize})`,
|
|
356
279
|
);
|
|
@@ -362,7 +285,6 @@ function requireSourceSymbolCount(
|
|
|
362
285
|
k: number | undefined,
|
|
363
286
|
blockLength: number,
|
|
364
287
|
symbolSize: number,
|
|
365
|
-
blockProfile: FecBlockProfile,
|
|
366
288
|
): number {
|
|
367
289
|
if (k === undefined) {
|
|
368
290
|
if (blockLength % symbolSize !== 0) {
|
|
@@ -377,7 +299,7 @@ function requireSourceSymbolCount(
|
|
|
377
299
|
}
|
|
378
300
|
// An iso-ssbg1-v1 block is exactly K' = SSB_length symbols of T bytes
|
|
379
301
|
// (C.5.3); any other length is not the block the repair symbols described.
|
|
380
|
-
if (
|
|
302
|
+
if (blockLength !== k * symbolSize) {
|
|
381
303
|
throw new Error(
|
|
382
304
|
`processFecBlock: an iso-ssbg1-v1 block of ${k} symbols of ${symbolSize} bytes is ${k * symbolSize} bytes (got ${blockLength})`,
|
|
383
305
|
);
|
|
@@ -389,17 +311,16 @@ function requireSourceSymbolCount(
|
|
|
389
311
|
* Process a decoded FEC block into individual MFU fragments.
|
|
390
312
|
*
|
|
391
313
|
* The WASM RaptorQ decoder returns every source symbol of the block
|
|
392
|
-
* concatenated (
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
* structured results sorted for reassembly.
|
|
314
|
+
* concatenated (SSB_length x T bytes). This function splits them apart,
|
|
315
|
+
* recovers each protected packet from its ssbg_mode1 length field (see
|
|
316
|
+
* {@link processFecSymbol}), extracts the MFU payload via shared
|
|
317
|
+
* extractMFUPayload, and returns structured results sorted for reassembly.
|
|
397
318
|
*
|
|
398
319
|
* @param blockData - Concatenated source symbols from the WASM decoder
|
|
399
320
|
* @param options - Block profile and symbol size T are required; see {@link ProcessFecBlockOptions}
|
|
400
321
|
* @throws when the block profile or symbol size is absent or invalid, when an
|
|
401
|
-
* omitted `k` cannot be read exactly from the block length, or when
|
|
402
|
-
*
|
|
322
|
+
* omitted `k` cannot be read exactly from the block length, or when the
|
|
323
|
+
* block is not exactly `k` symbols long.
|
|
403
324
|
*/
|
|
404
325
|
export function processFecBlock(
|
|
405
326
|
blockData: Uint8Array,
|
|
@@ -409,11 +330,11 @@ export function processFecBlock(
|
|
|
409
330
|
options?.blockProfile,
|
|
410
331
|
"processFecBlock options.blockProfile",
|
|
411
332
|
);
|
|
412
|
-
const symbolSize = requireSymbolSize(options.symbolSize
|
|
333
|
+
const symbolSize = requireSymbolSize(options.symbolSize);
|
|
413
334
|
|
|
414
335
|
if (!blockData || blockData.length === 0) return emptyProcessResult();
|
|
415
336
|
|
|
416
|
-
const k = requireSourceSymbolCount(options.k, blockData.length, symbolSize
|
|
337
|
+
const k = requireSourceSymbolCount(options.k, blockData.length, symbolSize);
|
|
417
338
|
|
|
418
339
|
const failReasons: Record<string, number> = {};
|
|
419
340
|
let failedCount = 0;
|
package/src/fec-block-mapper.ts
CHANGED
|
@@ -8,17 +8,10 @@
|
|
|
8
8
|
* It deliberately does NOT map MoQ media groups to FEC blocks. A recovered
|
|
9
9
|
* packet's media group is read from its own MMTP/MPU header
|
|
10
10
|
* ({@link recoveredPacketMediaGroup}), never from SBN arithmetic, and a source
|
|
11
|
-
* symbol's block comes from the wire:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - `iso-ssbg1-v1`: a block is the SS_ID range `[SS_Start, SS_Start +
|
|
16
|
-
* SSB_length)` its Repair FEC Payload ID states (ISO/IEC 23008-1 C.5.3), so
|
|
17
|
-
* SS_ID alone locates nothing and `fromSsId` throws.
|
|
18
|
-
*
|
|
19
|
-
* Object FEC (draft-ramadan-moq-fec §9.1) is the one exception: there each MoQ
|
|
20
|
-
* group IS its own FEC block, so {@link ObjectBlockMapper} keeps its identity
|
|
21
|
-
* `derive`.
|
|
11
|
+
* symbol's block comes from the wire: under `iso-ssbg1-v1` a block is the SS_ID
|
|
12
|
+
* range `[SS_Start, SS_Start + SSB_length)` its Repair FEC Payload ID states
|
|
13
|
+
* (ISO/IEC 23008-1 C.5.3), so SS_ID alone locates nothing and `fromSsId`
|
|
14
|
+
* throws for every stream with an FEC instance.
|
|
22
15
|
*/
|
|
23
16
|
|
|
24
17
|
import {
|
|
@@ -36,10 +29,7 @@ export interface FecBlockConfig {
|
|
|
36
29
|
interleaveDepthMs: number
|
|
37
30
|
/** GOP/frame duration (milliseconds). Measured from actual MMTP timestamps. */
|
|
38
31
|
gopDurationMs: number
|
|
39
|
-
/**
|
|
40
|
-
* K from catalog fec.sourceSymbols: the source symbols per block under
|
|
41
|
-
* `k-padded-v1`, the maximum SSB_length under `iso-ssbg1-v1`.
|
|
42
|
-
*/
|
|
32
|
+
/** K from catalog fec.sourceSymbols: the maximum SSB_length of a block. */
|
|
43
33
|
k: number
|
|
44
34
|
/**
|
|
45
35
|
* The catalog `fec.blockProfile`. Required and validated with
|
|
@@ -48,7 +38,7 @@ export interface FecBlockConfig {
|
|
|
48
38
|
blockProfile: FecBlockProfile
|
|
49
39
|
}
|
|
50
40
|
|
|
51
|
-
/** FEC coordinates of one MoQ object, for the
|
|
41
|
+
/** FEC coordinates of one MoQ object, for the mapper whose groups are blocks. */
|
|
52
42
|
export interface FecCoordinates {
|
|
53
43
|
/** Source Block Number */
|
|
54
44
|
sbn: number
|
|
@@ -98,7 +88,7 @@ interface NoFecInstanceConfig {
|
|
|
98
88
|
export class FecBlockMapper {
|
|
99
89
|
/** Media units (GOPs/frames) per FEC block, recovered with positive round-half-up semantics. */
|
|
100
90
|
readonly d: number
|
|
101
|
-
/** K:
|
|
91
|
+
/** K: the maximum SSB_length of a block (catalog `fec.sourceSymbols`). */
|
|
102
92
|
readonly k: number
|
|
103
93
|
/** Block time span (ms) — used as pacing threshold */
|
|
104
94
|
readonly interleaveDepthMs: number
|
|
@@ -127,24 +117,17 @@ export class FecBlockMapper {
|
|
|
127
117
|
}
|
|
128
118
|
|
|
129
119
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
120
|
+
* SS_ID → (SBN, ESI). Only {@link NullBlockMapper} answers: a stream with an
|
|
121
|
+
* FEC instance is `iso-ssbg1-v1`, whose blocks are located by
|
|
122
|
+
* `(SS_Start, SSB_length)` range membership and never from SS_ID alone.
|
|
133
123
|
*
|
|
134
|
-
* @throws
|
|
135
|
-
* `(SS_Start, SSB_length)` range membership and never from SS_ID alone
|
|
124
|
+
* @throws for every stream with an FEC instance
|
|
136
125
|
*/
|
|
137
|
-
fromSsId(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
return {
|
|
145
|
-
sbn: Math.floor(ssId / this.k),
|
|
146
|
-
esi: ssId % this.k,
|
|
147
|
-
}
|
|
126
|
+
fromSsId(_ssId: number): { sbn: number; esi: number } {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`fromSsId: a ${this.blockProfile ?? 'non-FEC'} stream locates a block by ` +
|
|
129
|
+
'(SS_Start, SSB_length) range membership, never from SS_ID alone',
|
|
130
|
+
)
|
|
148
131
|
}
|
|
149
132
|
|
|
150
133
|
/**
|
|
@@ -200,77 +183,6 @@ export class NullBlockMapper extends FecBlockMapper {
|
|
|
200
183
|
}
|
|
201
184
|
}
|
|
202
185
|
|
|
203
|
-
/**
|
|
204
|
-
* Object-level FEC (§9.1): each MoQ group (frame) is its own FEC block.
|
|
205
|
-
* K_actual = ceil(frame_bytes / symbolSize) — varies per block.
|
|
206
|
-
*
|
|
207
|
-
* SBN = group_id, ESI = object_id.
|
|
208
|
-
* SS_ID = Group_ID * sourceSymbols + Object_ID (§9.1.2).
|
|
209
|
-
*
|
|
210
|
-
* That SS_ID is the K-stride numbering of `k-padded-v1`; `iso-ssbg1-v1`
|
|
211
|
-
* numbers SS_IDs consecutively per source packet (ISO/IEC 23008-1 C.5.2), so
|
|
212
|
-
* §9.1 object FEC is defined only under `k-padded-v1` and the constructor
|
|
213
|
-
* refuses any other profile.
|
|
214
|
-
*
|
|
215
|
-
* The stride of that numbering is the catalog `sourceSymbols`, never a
|
|
216
|
-
* block's own K: a short block (K_actual < sourceSymbols) still starts at
|
|
217
|
-
* `Group_ID * sourceSymbols` (its repair's SS_Start) and leaves the rest of
|
|
218
|
-
* its stride unused, which is exactly `k-padded-v1` with `SSB_length =
|
|
219
|
-
* K_actual`. So the SS_ID locator is the inherited
|
|
220
|
-
* {@link FecBlockMapper.fromSsId} over `k = sourceSymbols`, the exact inverse
|
|
221
|
-
* of {@link ObjectBlockMapper.derive}; it needs no per-block K and infers
|
|
222
|
-
* nothing.
|
|
223
|
-
*
|
|
224
|
-
* This mapper does NOT carry per-block K. Each block discovers its own K from:
|
|
225
|
-
* 1. Clean reception: counting source objects per group
|
|
226
|
-
* 2. Repair packets: SSB_length in the Repair FEC Payload ID (ISO 23008-1 C.5.3)
|
|
227
|
-
*
|
|
228
|
-
* Callers MUST pass per-block K to FecBlockManager.addSourcePacket/addRepairPacket.
|
|
229
|
-
* Created once per stream, GC'd on stream close (Effect cleanup).
|
|
230
|
-
*/
|
|
231
|
-
export class ObjectBlockMapper extends FecBlockMapper {
|
|
232
|
-
/** Catalog sourceSymbols (max K) — used ONLY for SS_ID encoding (§9.1.2).
|
|
233
|
-
* Actual per-block K varies: K_actual = ceil(frame_bytes / symbolSize). */
|
|
234
|
-
readonly sourceSymbolsMax: number
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* @param gopDurationMs - GOP/frame duration (ms)
|
|
238
|
-
* @param sourceSymbolsMax - Catalog `fec.sourceSymbols`
|
|
239
|
-
* @param blockProfile - Catalog `fec.blockProfile`; must be `k-padded-v1`
|
|
240
|
-
*/
|
|
241
|
-
constructor(gopDurationMs: number, sourceSymbolsMax: number, blockProfile: FecBlockProfile) {
|
|
242
|
-
if (sourceSymbolsMax <= 0) throw new Error('ObjectBlockMapper: sourceSymbolsMax must be > 0 (from catalog)')
|
|
243
|
-
const profile = parseFecBlockProfile(blockProfile, 'ObjectBlockMapper fec.blockProfile')
|
|
244
|
-
if (profile !== 'k-padded-v1') {
|
|
245
|
-
throw new Error(
|
|
246
|
-
`ObjectBlockMapper: §9.1 object FEC numbers SS_ID = Group_ID * K + Object_ID, the k-padded-v1 stride; ` +
|
|
247
|
-
`it is not defined under ${profile}`,
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
// D=1, K=sourceSymbolsMax for SS_ID math only — actual block K varies
|
|
251
|
-
super({ interleaveDepthMs: gopDurationMs, gopDurationMs, k: sourceSymbolsMax, blockProfile: profile })
|
|
252
|
-
this.sourceSymbolsMax = sourceSymbolsMax
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* SBN = group_id, ESI = object_id. SS_ID per §9.1.2. Its inverse is the
|
|
257
|
-
* inherited `fromSsId` (SBN = floor(SS_ID / sourceSymbols), ESI = SS_ID mod
|
|
258
|
-
* sourceSymbols).
|
|
259
|
-
*/
|
|
260
|
-
derive(groupId: number, objectId: number): FecCoordinates {
|
|
261
|
-
return {
|
|
262
|
-
sbn: groupId,
|
|
263
|
-
esi: objectId,
|
|
264
|
-
ssId: groupId * this.sourceSymbolsMax + objectId,
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** One frame of latency: each frame is its own block. */
|
|
269
|
-
override get pacingThresholdMs(): number {
|
|
270
|
-
return this.gopDurationMs // one frame latency
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
186
|
/**
|
|
275
187
|
* Create a FecBlockMapper from catalog FEC config + measured GOP duration.
|
|
276
188
|
*
|
|
@@ -278,8 +190,10 @@ export class ObjectBlockMapper extends FecBlockMapper {
|
|
|
278
190
|
* {@link NullBlockMapper}; `fec.blockProfile` is not read, since a stream
|
|
279
191
|
* without FEC names none.
|
|
280
192
|
* - Otherwise `fec.blockProfile` is required (validated with
|
|
281
|
-
* `parseFecBlockProfile`, throwing
|
|
282
|
-
*
|
|
193
|
+
* `parseFecBlockProfile`, throwing unless it is `iso-ssbg1-v1`) and the
|
|
194
|
+
* result is a {@link FecBlockMapper}. `mode: 'object'` (per-group object
|
|
195
|
+
* FEC, SS_ID = Group_ID * K + Object_ID) throws: that numbering is the
|
|
196
|
+
* retired `k-padded-v1` stride, which `iso-ssbg1-v1` does not define.
|
|
283
197
|
*/
|
|
284
198
|
export function createMapper(
|
|
285
199
|
fecConfig: {
|
|
@@ -295,9 +209,10 @@ export function createMapper(
|
|
|
295
209
|
return new NullBlockMapper(gopDurationMs)
|
|
296
210
|
}
|
|
297
211
|
const blockProfile = parseFecBlockProfile(fecConfig.blockProfile, 'fec.blockProfile')
|
|
298
|
-
// extensions feature — see moqcast-draft docs/fec-extensions-material.md, not in public -00
|
|
299
212
|
if (fecConfig.mode === 'object') {
|
|
300
|
-
|
|
213
|
+
throw new Error(
|
|
214
|
+
`createMapper: object FEC numbers SS_ID = Group_ID * K + Object_ID, which ${blockProfile} does not define`,
|
|
215
|
+
)
|
|
301
216
|
}
|
|
302
217
|
return new FecBlockMapper({
|
|
303
218
|
interleaveDepthMs: fecConfig.interleaveDepthMs && fecConfig.interleaveDepthMs > 0
|
package/src/index.ts
CHANGED
|
@@ -66,13 +66,14 @@ export {
|
|
|
66
66
|
deriveFecRetentionBlocks,
|
|
67
67
|
tryDeriveFecRetentionBlocks,
|
|
68
68
|
MAX_FEC_RETENTION_BLOCKS,
|
|
69
|
+
LOCAL_FEC_RETENTION_BLOCKS,
|
|
70
|
+
LOCAL_FEC_TIMEOUT_MS,
|
|
69
71
|
playbackLatencyMs,
|
|
70
72
|
} from "./oti.js";
|
|
71
73
|
|
|
72
74
|
export {
|
|
73
75
|
FecBlockMapper,
|
|
74
76
|
NullBlockMapper,
|
|
75
|
-
ObjectBlockMapper,
|
|
76
77
|
createMapper,
|
|
77
78
|
recoveredPacketMediaGroup,
|
|
78
79
|
type FecBlockConfig,
|
|
@@ -89,6 +90,10 @@ export {
|
|
|
89
90
|
extendSsId,
|
|
90
91
|
repairFetchRange,
|
|
91
92
|
repairGroupIdMatches,
|
|
93
|
+
RepairEpochReferences,
|
|
94
|
+
type RepairEpochStats,
|
|
92
95
|
type RepairFetchAnchor,
|
|
93
96
|
type RepairFetchRange,
|
|
97
|
+
type RepairGroupPlan,
|
|
98
|
+
type RepairLargestOutcome,
|
|
94
99
|
} from "./repair-group.js";
|
package/src/oti.ts
CHANGED
|
@@ -118,15 +118,16 @@ function validateOTIFields(F: number, T: number, Z: number, N: number, Al: numbe
|
|
|
118
118
|
* Resolve a catalog FEC block's span in milliseconds.
|
|
119
119
|
*
|
|
120
120
|
* The catalog carries exactly ONE interleave field: `interleaveDepthMs`, the
|
|
121
|
-
* full FEC block span in MILLISECONDS (draft-ramadan-moq-fec §5.1).
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* full FEC block send span in MILLISECONDS (draft-ramadan-moq-fec §5.1).
|
|
122
|
+
* Block boundaries still come from the wire's SS_Start and SSB_length; this
|
|
123
|
+
* field never supplies a group count or replaces those coordinates.
|
|
124
124
|
*
|
|
125
|
-
* Absent
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
125
|
+
* Absent → undefined. An explicit zero is returned as zero: it is the
|
|
126
|
+
* catalog's no-elapsed-deadline sentinel, not an invitation to infer a span.
|
|
127
|
+
* Positive values must be safe integer milliseconds. Finiteness is checked
|
|
128
|
+
* separately from the range because `Infinity` satisfies `> 0`: a JSON catalog
|
|
129
|
+
* cannot spell `Infinity`, but any overflowing literal (`1e999`) parses to it,
|
|
130
|
+
* and it would then reach `fecBlockBudgetMs` as an unbounded span. The legacy
|
|
130
131
|
* `interleaveDepth` frame-count key and
|
|
131
132
|
* the `interleaveMs` alias were removed in the BLO-14526 clean cut — producers
|
|
132
133
|
* emit milliseconds only, and there is deliberately NO fallback read.
|
|
@@ -139,7 +140,7 @@ export function catalogFecInterleaveMs(
|
|
|
139
140
|
fec: FecInterleaveCatalog | undefined,
|
|
140
141
|
): number | undefined {
|
|
141
142
|
const ms = fec?.interleaveDepthMs;
|
|
142
|
-
return typeof ms === "number" &&
|
|
143
|
+
return typeof ms === "number" && Number.isSafeInteger(ms) && ms >= 0 ? ms : undefined;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/**
|
|
@@ -390,6 +391,16 @@ export function deriveFecRetentionBlocks(
|
|
|
390
391
|
*/
|
|
391
392
|
export const MAX_FEC_RETENTION_BLOCKS = 0xff;
|
|
392
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Receiver-local block retention used when the catalog carries no elapsed-time
|
|
396
|
+
* FEC window. This is a memory/cleanup bound only; it is never derived from
|
|
397
|
+
* media rate and never treated as a catalog deadline.
|
|
398
|
+
*/
|
|
399
|
+
export const LOCAL_FEC_RETENTION_BLOCKS = 2;
|
|
400
|
+
|
|
401
|
+
/** Receiver-local cleanup budget used when the catalog has no elapsed span. */
|
|
402
|
+
export const LOCAL_FEC_TIMEOUT_MS = 500;
|
|
403
|
+
|
|
393
404
|
/**
|
|
394
405
|
* `deriveFecRetentionBlocks` without the throw: the block count, or `undefined`
|
|
395
406
|
* when the geometry cannot define one.
|