@blockcast/fec-worker 0.3.0-main.5bb7058c73ff → 0.3.0-main.dd5b3a8dcbc8
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 +6 -6
- package/dist/block-order.d.ts +3 -19
- package/dist/block-order.d.ts.map +1 -1
- package/dist/block-order.js +3 -33
- package/dist/block-order.js.map +1 -1
- package/dist/fec-worker-client.d.ts +2 -3
- package/dist/fec-worker-client.d.ts.map +1 -1
- package/dist/fec-worker-client.js +2 -3
- package/dist/fec-worker-client.js.map +1 -1
- package/dist/fec-worker-types.d.ts +31 -38
- package/dist/fec-worker-types.d.ts.map +1 -1
- package/dist/fec-worker.js +22 -117
- package/dist/fec-worker.js.map +2 -2
- package/package.json +2 -2
- package/src/block-order.ts +3 -37
- package/src/fec-worker-client.ts +2 -3
- package/src/fec-worker-types.ts +31 -38
- package/src/fec-worker.ts +41 -157
package/src/block-order.ts
CHANGED
|
@@ -1,46 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Order of
|
|
3
|
-
* C.5.2; RFC 1982 serial arithmetic, SERIAL_BITS = 32; BLO-35597 iso:5).
|
|
4
|
-
*
|
|
5
|
-
* Block `sbn` starts at SS_ID `sbn * K mod 2^32`, and blocks are ordered by
|
|
6
|
-
* serial comparison of their starts. That is identical to comparing SBNs
|
|
7
|
-
* with < for any two blocks less than 2^31 SS_IDs apart, so only the order
|
|
8
|
-
* across the SS_ID wrap changes.
|
|
9
|
-
*
|
|
10
|
-
* The canonical helpers are @blockcast/mmt-base `kStrideBlockPrecedes` /
|
|
11
|
-
* `kStrideBlocksBehind`. This worker bundle does not depend on mmt-base, so
|
|
12
|
-
* it carries these two, and block-order.test.ts pins them to the same wrap
|
|
13
|
-
* vectors.
|
|
2
|
+
* Order of iso-ssbg1-v1 FEC blocks on the 32-bit SS_ID (ISO/IEC 23008-1:2023
|
|
3
|
+
* C.5.2/C.5.3; RFC 1982 serial arithmetic, SERIAL_BITS = 32; BLO-35597 iso:5).
|
|
4
|
+
* The K-stride (SBN * K) order of the retired k-padded-v1 profile is gone.
|
|
14
5
|
*/
|
|
15
6
|
|
|
16
7
|
const SPAN = 0x1_0000_0000;
|
|
17
8
|
const HALF = 0x8000_0000;
|
|
18
9
|
|
|
19
|
-
function blockStart(sbn: number, k: number): number {
|
|
20
|
-
// sbn < 2^32 and k <= 56 403 keep the product below 2^48: exact.
|
|
21
|
-
return (sbn * k) % SPAN;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Forward SS_ID distance from block `a`'s start to block `b`'s, or null when `a` does not precede `b`. */
|
|
25
|
-
function precedingDistance(a: number, b: number, k: number): number | null {
|
|
26
|
-
const distance = (blockStart(b, k) - blockStart(a, k) + SPAN) % SPAN;
|
|
27
|
-
return distance !== 0 && distance < HALF ? distance : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Whether block `a` precedes block `b`. */
|
|
31
|
-
export function blockPrecedes(a: number, b: number, k: number): boolean {
|
|
32
|
-
return precedingDistance(a, b, k) !== null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* How many blocks `a` lies behind `b`: 0 unless `a` precedes `b`. Across the
|
|
37
|
-
* wrap the short pre-wrap block (2^32 mod K symbols) counts as one block.
|
|
38
|
-
*/
|
|
39
|
-
export function blocksBehind(a: number, b: number, k: number): number {
|
|
40
|
-
const distance = precedingDistance(a, b, k);
|
|
41
|
-
return distance === null ? 0 : Math.ceil(distance / k);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
10
|
// --- iso-ssbg1-v1 (ISO/IEC 23008-1 C.3.3 ssbg_mode1; C.5.2/C.5.3) ---------
|
|
45
11
|
//
|
|
46
12
|
// Under the ISO profile SS_ID increments by one per FEC source packet and
|
package/src/fec-worker-client.ts
CHANGED
|
@@ -137,8 +137,7 @@ export class FecWorkerClient {
|
|
|
137
137
|
* Caller must not access data after this call — it is detached.
|
|
138
138
|
*
|
|
139
139
|
* Carries the wire-level Repair FEC Payload ID fields (ISO 23008-1 §C.5.3):
|
|
140
|
-
* - ssStart: SS_ID of first source symbol in the block (
|
|
141
|
-
* under `k-padded-v1`; any uint32, the block key, under `iso-ssbg1-v1`)
|
|
140
|
+
* - ssStart: SS_ID of first source symbol in the block (any uint32; the block key)
|
|
142
141
|
* - ssbLength: authored source-symbol count (1..configured K)
|
|
143
142
|
* - rsId: repair symbol index within the block
|
|
144
143
|
*/
|
|
@@ -199,7 +198,7 @@ export class FecWorkerClient {
|
|
|
199
198
|
|
|
200
199
|
/**
|
|
201
200
|
* Trigger cleanup after a block's inactivity deadline. `currentSbn` is the
|
|
202
|
-
* block key
|
|
201
|
+
* block key, the block's SS_Start.
|
|
203
202
|
*
|
|
204
203
|
* While one queue-drain barrier is in flight, later SBNs are coalesced into
|
|
205
204
|
* the next command without losing their individual acknowledgement identity.
|
package/src/fec-worker-types.ts
CHANGED
|
@@ -12,19 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C): the catalog
|
|
15
|
-
* `fec.blockProfile` token.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* packet mod 2^32; a block is `[SS_Start, SS_Start + SSB_length)` keyed by
|
|
22
|
-
* SS_Start; `k` is only the maximum SSB_length; repair ESI =
|
|
23
|
-
* SSB_length + RS_ID.
|
|
15
|
+
* `fec.blockProfile` token. Required and never inferred; the only profile is
|
|
16
|
+
* `iso-ssbg1-v1` (C.3.3 ssbg_mode1): SS_ID increments by one per source
|
|
17
|
+
* packet mod 2^32; a block is `[SS_Start, SS_Start + SSB_length)` keyed by
|
|
18
|
+
* SS_Start; `k` is only the maximum SSB_length; repair ESI =
|
|
19
|
+
* SSB_length + RS_ID. The retired `k-padded-v1` token is refused like any
|
|
20
|
+
* unknown one.
|
|
24
21
|
*
|
|
25
22
|
* Mirrors @blockcast/mmt-base `FecBlockProfile`.
|
|
26
23
|
*/
|
|
27
|
-
export type FecBlockProfile = "
|
|
24
|
+
export type FecBlockProfile = "iso-ssbg1-v1";
|
|
28
25
|
|
|
29
26
|
/** Per-track FEC configuration. Matches design spec section 3.2 FecTrackConfig. */
|
|
30
27
|
export interface FecTrackConfig {
|
|
@@ -51,12 +48,11 @@ export interface FecTrackConfig {
|
|
|
51
48
|
repairCount: number;
|
|
52
49
|
/** Bytes per symbol */
|
|
53
50
|
symbolSize: number;
|
|
54
|
-
/** Retained decoder horizon as a BLOCK COUNT
|
|
55
|
-
*
|
|
56
|
-
* `
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* (block-order.ts), the lower bound every ISO receiver layer shares.
|
|
51
|
+
/** Retained decoder horizon as a BLOCK COUNT, not D. `isRetiredSbn`
|
|
52
|
+
* retires blocks more than this many blocks behind the live SS_ID, by
|
|
53
|
+
* `isoBlocksBehind(SS_ID, live SS_ID, k)` (block-order.ts), the lower
|
|
54
|
+
* bound every ISO receiver layer shares, and `MmtFecDecoder::new`
|
|
55
|
+
* documents it as "the number of interleaved blocks".
|
|
60
56
|
* Never assign `deriveFecInterleaveDepth()` here — that returns media units
|
|
61
57
|
* per block, which is a different quantity (BLO-35597). */
|
|
62
58
|
interleaveDepth: number;
|
|
@@ -79,8 +75,7 @@ export interface FecTrackConfig {
|
|
|
79
75
|
|
|
80
76
|
/** Per-block snapshot for live block visualization and stats. */
|
|
81
77
|
export interface FecBlockSnapshot {
|
|
82
|
-
/** Block key: the
|
|
83
|
-
* SS_Start under `iso-ssbg1-v1`. */
|
|
78
|
+
/** Block key: the block's SS_Start. */
|
|
84
79
|
sbn: number;
|
|
85
80
|
/** Current block state */
|
|
86
81
|
state: "collecting" | "complete" | "recovered" | "failed";
|
|
@@ -168,8 +163,7 @@ export interface FecTrackStats {
|
|
|
168
163
|
/** Blocks with invalid relay signature */
|
|
169
164
|
relayBlocksFailed: number;
|
|
170
165
|
|
|
171
|
-
/** Live media position: the newest
|
|
172
|
-
* `k-padded-v1`, the newest accepted SS_ID / SS_Start under `iso-ssbg1-v1`. */
|
|
166
|
+
/** Live media position: the newest accepted SS_ID / SS_Start. */
|
|
173
167
|
liveSbn: number;
|
|
174
168
|
/** Active block snapshots */
|
|
175
169
|
blocks: FecBlockSnapshot[];
|
|
@@ -220,13 +214,14 @@ export interface FecTrackStats {
|
|
|
220
214
|
*/
|
|
221
215
|
sourceDroppedDuplicate?: number;
|
|
222
216
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
217
|
+
* Accepted source symbols whose block never became known (no repair
|
|
218
|
+
* symbol named a range containing them) before their SS_ID fell behind
|
|
219
|
+
* the retained horizon. They were already fed to the decoder and routed
|
|
220
|
+
* directly; this counts the bookkeeping retired with them, i.e. evidence
|
|
221
|
+
* that a block's repair symbols never arrived. The decoder retires its
|
|
222
|
+
* own copies by the same horizon inside `cleanup_by_ss_start`.
|
|
228
223
|
*/
|
|
229
|
-
sourceUnplacedRetired
|
|
224
|
+
sourceUnplacedRetired: number;
|
|
230
225
|
/** Native decoder counters, converted to numbers so browser diagnostics remain JSON-safe. */
|
|
231
226
|
decoder?: {
|
|
232
227
|
sourcePackets: number;
|
|
@@ -266,7 +261,7 @@ export interface FrameMeta {
|
|
|
266
261
|
altaVerified: boolean | null;
|
|
267
262
|
/** Whether this frame was recovered via FEC (not received directly) */
|
|
268
263
|
recovered: boolean;
|
|
269
|
-
/** Block key this frame belongs to:
|
|
264
|
+
/** Block key this frame belongs to: the block's SS_Start */
|
|
270
265
|
sbn: number;
|
|
271
266
|
/** Encoding Symbol ID within the recovered source block (0..ssbLength-1).
|
|
272
267
|
* Under `iso-ssbg1-v1` a recovered frame is the full T-byte ssbg_mode1
|
|
@@ -301,13 +296,12 @@ export type FecWorkerCommand =
|
|
|
301
296
|
| {
|
|
302
297
|
type: "feedSource";
|
|
303
298
|
/** Flat 32-bit Source Symbol ID from the MMTP Source FEC Payload ID
|
|
304
|
-
* (ISO 23008-1 §C.5.2).
|
|
305
|
-
*
|
|
306
|
-
* pre-compute
|
|
307
|
-
* packet mod 2^32 and the block comes from a repair's range. */
|
|
299
|
+
* (ISO 23008-1 §C.5.2). SS_IDs are consecutive per source packet mod
|
|
300
|
+
* 2^32 and the block comes from a repair's range — do NOT
|
|
301
|
+
* pre-compute a block. */
|
|
308
302
|
ssId: number;
|
|
309
|
-
/**
|
|
310
|
-
*
|
|
303
|
+
/** The protected packet exactly, unpadded and without its SS_ID (at
|
|
304
|
+
* most T-2 bytes). */
|
|
311
305
|
data: ArrayBuffer;
|
|
312
306
|
ts: number;
|
|
313
307
|
/** Optional ALTA authenticator bytes (detached mode — 08-07 MMTP
|
|
@@ -326,14 +320,13 @@ export type FecWorkerCommand =
|
|
|
326
320
|
| {
|
|
327
321
|
type: "feedRepair";
|
|
328
322
|
/** SS_Start: SS_ID of the first source symbol in the block, from the
|
|
329
|
-
* MMTP Repair FEC Payload ID (ISO 23008-1 §C.5.3)
|
|
330
|
-
*
|
|
323
|
+
* MMTP Repair FEC Payload ID (ISO 23008-1 §C.5.3): any uint32; the
|
|
324
|
+
* block key. */
|
|
331
325
|
ssStart: number;
|
|
332
326
|
/** Authored source-symbol count for this block; 1..configured K. */
|
|
333
327
|
ssbLength: number;
|
|
334
328
|
/** RS_ID: repair symbol index within the block; WASM maps to repair
|
|
335
|
-
* ESI =
|
|
336
|
-
* (`iso-ssbg1-v1`). */
|
|
329
|
+
* ESI = SSB_length + RS_ID. */
|
|
337
330
|
rsId: number;
|
|
338
331
|
data: ArrayBuffer;
|
|
339
332
|
ts: number;
|
|
@@ -358,7 +351,7 @@ export type FecWorkerCommand =
|
|
|
358
351
|
| {
|
|
359
352
|
type: "cleanup";
|
|
360
353
|
/** Every timed-out block covered by this coalesced queue-drain barrier,
|
|
361
|
-
* by block key
|
|
354
|
+
* by block key (SS_Start). */
|
|
362
355
|
sbns: number[];
|
|
363
356
|
}
|
|
364
357
|
| { type: "reset" }
|
package/src/fec-worker.ts
CHANGED
|
@@ -22,8 +22,6 @@ import type {
|
|
|
22
22
|
} from "./fec-worker-types.js";
|
|
23
23
|
import { toOwnedArrayBuffer } from "./transfer.js";
|
|
24
24
|
import {
|
|
25
|
-
blockPrecedes,
|
|
26
|
-
blocksBehind,
|
|
27
25
|
isoBlockContains,
|
|
28
26
|
isoBlocksBehind,
|
|
29
27
|
isoBlocksOverlap,
|
|
@@ -36,10 +34,9 @@ import {
|
|
|
36
34
|
// The decoder derives SBN/ESI internally — do not pre-compute.
|
|
37
35
|
interface WasmFecDecoder {
|
|
38
36
|
/**
|
|
39
|
-
* ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* holds it until a repair names the block containing it.
|
|
37
|
+
* ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID; data is
|
|
38
|
+
* the protected packet, unpadded. The decoder builds the ssbg_mode1 symbol
|
|
39
|
+
* and holds it until a repair names the block containing it.
|
|
43
40
|
*/
|
|
44
41
|
add_source(
|
|
45
42
|
ss_id: number,
|
|
@@ -47,11 +44,9 @@ interface WasmFecDecoder {
|
|
|
47
44
|
timestamp: number,
|
|
48
45
|
): Uint8Array | undefined;
|
|
49
46
|
/**
|
|
50
|
-
* ISO 23008-1 §C.5.3
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* ESI = ssb_length + rs_id; a recovered block is ssb_length x T bytes of
|
|
54
|
-
* ssbg_mode1 symbols.
|
|
47
|
+
* ISO 23008-1 §C.5.3: the block is `[ss_start, ss_start + ssb_length)`
|
|
48
|
+
* with K' = ssb_length and repair ESI = ssb_length + rs_id; a recovered
|
|
49
|
+
* block is ssb_length x T bytes of ssbg_mode1 symbols.
|
|
55
50
|
*/
|
|
56
51
|
add_repair(
|
|
57
52
|
ss_start: number,
|
|
@@ -68,14 +63,12 @@ interface WasmFecDecoder {
|
|
|
68
63
|
*/
|
|
69
64
|
cleanup_blocks_detailed(sbns: Uint32Array): void;
|
|
70
65
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
* more than interleaveDepth `isoBlocksBehind` the live SS_ID; returns the
|
|
78
|
-
* SS_Starts of the blocks it removed.
|
|
66
|
+
* Remove decoder blocks, and held unplaced sources, more than
|
|
67
|
+
* interleaveDepth `isoBlocksBehind` the live SS_ID; returns the SS_Starts
|
|
68
|
+
* of the blocks it removed. It runs the decoder's `retire_unplaced` sweep
|
|
69
|
+
* itself, so the Worker never calls that separately: synchronizeDecoderHorizon
|
|
70
|
+
* calls this after every source and repair it accepts, which is what keeps
|
|
71
|
+
* the decoder's unplaced pool bounded when a block's repairs never arrive.
|
|
79
72
|
*/
|
|
80
73
|
cleanup_by_ss_start(current: number): Uint32Array;
|
|
81
74
|
/** The block profile the decoder was constructed with. */
|
|
@@ -255,15 +248,9 @@ function clearBlockTracking(): void {
|
|
|
255
248
|
*/
|
|
256
249
|
function isRetiredSbn(sbn: number): boolean {
|
|
257
250
|
if (config === null || !liveSbnKnown) return false;
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
return config.
|
|
261
|
-
? isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth
|
|
262
|
-
: blocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function isIsoProfile(): boolean {
|
|
266
|
-
return config?.blockProfile === "iso-ssbg1-v1";
|
|
251
|
+
// `sbn` is an SS_ID (a block's SS_Start or a source's own SS_ID) and
|
|
252
|
+
// liveSbn the live SS_ID; K is only the maximum block length.
|
|
253
|
+
return isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
|
|
267
254
|
}
|
|
268
255
|
|
|
269
256
|
/**
|
|
@@ -291,9 +278,7 @@ function findIsoBlock(ssId: number): FecBlockSnapshot | undefined {
|
|
|
291
278
|
* retired, after which the SS_ID retirement check refuses every member.
|
|
292
279
|
*/
|
|
293
280
|
function isRetiredBlock(key: number, block: FecBlockSnapshot): boolean {
|
|
294
|
-
return
|
|
295
|
-
? isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN)
|
|
296
|
-
: isRetiredSbn(key);
|
|
281
|
+
return isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN);
|
|
297
282
|
}
|
|
298
283
|
|
|
299
284
|
/**
|
|
@@ -310,15 +295,10 @@ function trimBlockRetention(): void {
|
|
|
310
295
|
}
|
|
311
296
|
}
|
|
312
297
|
|
|
313
|
-
/** Advance the live position: an
|
|
298
|
+
/** Advance the live position: an SS_ID / SS_Start, in serial order. */
|
|
314
299
|
function advanceMediaHorizon(sbn: number): void {
|
|
315
300
|
if (!config) return;
|
|
316
|
-
if (liveSbnKnown)
|
|
317
|
-
const ahead = config.blockProfile === "iso-ssbg1-v1"
|
|
318
|
-
? serialPrecedes(liveSbn, sbn)
|
|
319
|
-
: blockPrecedes(liveSbn, sbn, config.k);
|
|
320
|
-
if (!ahead) return;
|
|
321
|
-
}
|
|
301
|
+
if (liveSbnKnown && !serialPrecedes(liveSbn, sbn)) return;
|
|
322
302
|
liveSbn = sbn;
|
|
323
303
|
liveSbnKnown = true;
|
|
324
304
|
trimBlockRetention();
|
|
@@ -336,10 +316,7 @@ function markBlockTerminal(
|
|
|
336
316
|
|
|
337
317
|
function synchronizeDecoderHorizon(): void {
|
|
338
318
|
if (!wasmDecoder || !liveSbnKnown) return;
|
|
339
|
-
const
|
|
340
|
-
const expired = iso
|
|
341
|
-
? wasmDecoder.cleanup_by_ss_start(liveSbn)
|
|
342
|
-
: wasmDecoder.cleanup_by_sbn(liveSbn);
|
|
319
|
+
const expired = wasmDecoder.cleanup_by_ss_start(liveSbn);
|
|
343
320
|
for (const sbn of expired) {
|
|
344
321
|
const block = blocks.get(sbn);
|
|
345
322
|
if (!block || isTerminalBlock(block)) {
|
|
@@ -350,7 +327,7 @@ function synchronizeDecoderHorizon(): void {
|
|
|
350
327
|
blocksFailed++;
|
|
351
328
|
emitBlockUpdate(block);
|
|
352
329
|
}
|
|
353
|
-
|
|
330
|
+
retireUnplacedSources();
|
|
354
331
|
trimBlockRetention();
|
|
355
332
|
}
|
|
356
333
|
|
|
@@ -583,8 +560,10 @@ function buildStatsSnapshot(): FecTrackStats {
|
|
|
583
560
|
|
|
584
561
|
// Cap block array to interleave window
|
|
585
562
|
const maxBlocks = Math.max(config.interleaveDepth * 3, 24);
|
|
563
|
+
// Newest first in SS_ID serial order, so the cap keeps the newest blocks
|
|
564
|
+
// across the 2^32 wrap too: every block start precedes the live SS_ID.
|
|
586
565
|
const blockArray = Array.from(blocks.values())
|
|
587
|
-
.sort((a, b) =>
|
|
566
|
+
.sort((a, b) => serialDistance(a.sbn, liveSbn) - serialDistance(b.sbn, liveSbn))
|
|
588
567
|
.slice(0, maxBlocks);
|
|
589
568
|
|
|
590
569
|
return {
|
|
@@ -620,7 +599,7 @@ function buildStatsSnapshot(): FecTrackStats {
|
|
|
620
599
|
sourceDroppedRetired,
|
|
621
600
|
sourceDroppedTerminal,
|
|
622
601
|
sourceDroppedDuplicate,
|
|
623
|
-
|
|
602
|
+
sourceUnplacedRetired,
|
|
624
603
|
decoder,
|
|
625
604
|
};
|
|
626
605
|
}
|
|
@@ -662,22 +641,19 @@ function emitRecoveredSymbols(
|
|
|
662
641
|
return;
|
|
663
642
|
}
|
|
664
643
|
|
|
665
|
-
const iso = config.blockProfile === "iso-ssbg1-v1";
|
|
666
644
|
for (let i = 0; i < ssbLength; i++) {
|
|
667
645
|
const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);
|
|
668
646
|
// ssbg_mode1 symbols are emitted whole: the router trims them by their
|
|
669
647
|
// length field. A field past T-2 or non-zero padding means the
|
|
670
648
|
// recovered symbol is not what was sent, so it is refused, not trimmed.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
649
|
+
const invalid = invalidSsbg1Symbol(symbol, symbolSize);
|
|
650
|
+
if (invalid !== null) {
|
|
651
|
+
emit({
|
|
652
|
+
type: "error",
|
|
653
|
+
code: "recovered-ssbg1-symbol-invalid",
|
|
654
|
+
message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`,
|
|
655
|
+
});
|
|
656
|
+
continue;
|
|
681
657
|
}
|
|
682
658
|
|
|
683
659
|
// Source authentication now runs once at router admission in
|
|
@@ -990,12 +966,12 @@ async function handleConfigure(
|
|
|
990
966
|
const blockProfile: unknown = cmd.config.blockProfile;
|
|
991
967
|
if (blockProfile === undefined) {
|
|
992
968
|
throw new Error(
|
|
993
|
-
'[FecWorker] configure: blockProfile is required: name the FEC block profile ("
|
|
969
|
+
'[FecWorker] configure: blockProfile is required: name the FEC block profile ("iso-ssbg1-v1")',
|
|
994
970
|
);
|
|
995
971
|
}
|
|
996
|
-
if (blockProfile !== "
|
|
972
|
+
if (blockProfile !== "iso-ssbg1-v1") {
|
|
997
973
|
throw new Error(
|
|
998
|
-
`[FecWorker] configure: blockProfile must be "
|
|
974
|
+
`[FecWorker] configure: blockProfile must be "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`,
|
|
999
975
|
);
|
|
1000
976
|
}
|
|
1001
977
|
if (
|
|
@@ -1134,51 +1110,7 @@ function handleFeedSource(
|
|
|
1134
1110
|
throw new Error("[FecWorker] feedSource: invalid source symbol");
|
|
1135
1111
|
}
|
|
1136
1112
|
|
|
1137
|
-
|
|
1138
|
-
if (config.blockProfile === "iso-ssbg1-v1") {
|
|
1139
|
-
feedIsoSource(cmd, view, wasmDecoder, config);
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// Derive SBN locally for block-level accounting. WASM derives the same
|
|
1144
|
-
// value internally (ISO 23008-1 §C.5.2: SBN = floor(SS_ID / K)).
|
|
1145
|
-
const sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;
|
|
1146
|
-
const esi = cmd.ssId % config.k;
|
|
1147
|
-
if (isRetiredSbn(sbn)) {
|
|
1148
|
-
sourceDroppedRetired++;
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
const existingBlock = blocks.get(sbn);
|
|
1152
|
-
if (existingBlock && isTerminalBlock(existingBlock)) {
|
|
1153
|
-
sourceDroppedTerminal++;
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
if (acceptedSourceEsis.get(sbn)?.has(esi)) {
|
|
1157
|
-
sourceDroppedDuplicate++;
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);
|
|
1162
|
-
advanceMediaHorizon(sbn);
|
|
1163
|
-
let sourceEsis = acceptedSourceEsis.get(sbn);
|
|
1164
|
-
if (!sourceEsis) {
|
|
1165
|
-
sourceEsis = new Set<number>();
|
|
1166
|
-
acceptedSourceEsis.set(sbn, sourceEsis);
|
|
1167
|
-
}
|
|
1168
|
-
sourceEsis.add(esi);
|
|
1169
|
-
|
|
1170
|
-
// Update block state
|
|
1171
|
-
const block = getOrCreateBlock(sbn);
|
|
1172
|
-
block.sourceReceived++;
|
|
1173
|
-
block.lastSymbolTs = performance.now();
|
|
1174
|
-
sourceSymbols++;
|
|
1175
|
-
|
|
1176
|
-
// The FEC worker must remain decode-only on the per-symbol hot path. ALTA
|
|
1177
|
-
// WASM is retained temporarily for low-rate signaling users, but media is
|
|
1178
|
-
// authenticated by the canonical manifest verifier at router admission.
|
|
1179
|
-
if (altaEnabled) altaPending++;
|
|
1180
|
-
|
|
1181
|
-
settleSourceArrival(sbn, block, result);
|
|
1113
|
+
feedIsoSource(cmd, new Uint8Array(cmd.data), wasmDecoder, config);
|
|
1182
1114
|
}
|
|
1183
1115
|
|
|
1184
1116
|
function handleFeedRepair(
|
|
@@ -1190,17 +1122,14 @@ function handleFeedRepair(
|
|
|
1190
1122
|
if (!config) {
|
|
1191
1123
|
throw new Error("[FecWorker] feedRepair: config missing");
|
|
1192
1124
|
}
|
|
1193
|
-
// k-padded-v1 blocks start at multiples of K and repair ESI = K + RS_ID;
|
|
1194
1125
|
// iso-ssbg1-v1 blocks start at any SS_ID and repair ESI = SSB_length + RS_ID.
|
|
1195
|
-
const iso = config.blockProfile === "iso-ssbg1-v1";
|
|
1196
1126
|
if (
|
|
1197
1127
|
!isUint32(cmd.ssStart) ||
|
|
1198
1128
|
!isUint32(cmd.ssbLength) ||
|
|
1199
1129
|
cmd.ssbLength <= 0 ||
|
|
1200
1130
|
cmd.ssbLength > config.k ||
|
|
1201
|
-
(!iso && cmd.ssStart % config.k !== 0) ||
|
|
1202
1131
|
!isUint32(cmd.rsId) ||
|
|
1203
|
-
|
|
1132
|
+
cmd.ssbLength + cmd.rsId > UINT24_MAX ||
|
|
1204
1133
|
!(cmd.data instanceof ArrayBuffer)
|
|
1205
1134
|
) {
|
|
1206
1135
|
throw new Error("[FecWorker] feedRepair: invalid repair symbol");
|
|
@@ -1218,51 +1147,7 @@ function handleFeedRepair(
|
|
|
1218
1147
|
}
|
|
1219
1148
|
return;
|
|
1220
1149
|
}
|
|
1221
|
-
|
|
1222
|
-
feedIsoRepair(cmd, view, wasmDecoder);
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
const sbn = Math.floor(cmd.ssStart / config.k);
|
|
1226
|
-
const repairEsi = config.k + cmd.rsId;
|
|
1227
|
-
// The repair path's mirror drops stay silent and uncounted: BLO-35597
|
|
1228
|
-
// defect 2 is a source-symbol shortfall, so only the source side needed
|
|
1229
|
-
// evidence. The asymmetry is deliberate, not an oversight — extend these
|
|
1230
|
-
// the same way if a repair-side shortfall ever needs attributing.
|
|
1231
|
-
if (isRetiredSbn(sbn)) return;
|
|
1232
|
-
const existingBlock = blocks.get(sbn);
|
|
1233
|
-
if (existingBlock && isTerminalBlock(existingBlock)) return;
|
|
1234
|
-
if (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;
|
|
1235
|
-
// One block has one authored SSB_length; a repair symbol naming another
|
|
1236
|
-
// is dropped before it reaches the decoder (which rejects it too).
|
|
1237
|
-
if (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {
|
|
1238
|
-
emit({
|
|
1239
|
-
type: "error",
|
|
1240
|
-
code: "repair-ssb-length-conflict",
|
|
1241
|
-
message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,
|
|
1242
|
-
});
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const result = wasmDecoder.add_repair(
|
|
1247
|
-
cmd.ssStart,
|
|
1248
|
-
cmd.ssbLength,
|
|
1249
|
-
cmd.rsId,
|
|
1250
|
-
view,
|
|
1251
|
-
cmd.ts,
|
|
1252
|
-
);
|
|
1253
|
-
advanceMediaHorizon(sbn);
|
|
1254
|
-
let repairEsis = acceptedRepairEsis.get(sbn);
|
|
1255
|
-
if (!repairEsis) {
|
|
1256
|
-
repairEsis = new Set<number>();
|
|
1257
|
-
acceptedRepairEsis.set(sbn, repairEsis);
|
|
1258
|
-
}
|
|
1259
|
-
repairEsis.add(repairEsi);
|
|
1260
|
-
const block = getOrCreateBlock(sbn);
|
|
1261
|
-
block.ssbLength = cmd.ssbLength;
|
|
1262
|
-
block.repairReceived++;
|
|
1263
|
-
block.lastSymbolTs = performance.now();
|
|
1264
|
-
repairSymbols++;
|
|
1265
|
-
settleRepairArrival(sbn, block, cmd.ssbLength, result);
|
|
1150
|
+
feedIsoRepair(cmd, view, wasmDecoder);
|
|
1266
1151
|
}
|
|
1267
1152
|
|
|
1268
1153
|
function handleRelayBlockSig(
|
|
@@ -1315,10 +1200,9 @@ function handleCleanup(
|
|
|
1315
1200
|
continue;
|
|
1316
1201
|
}
|
|
1317
1202
|
currentBlock ??= getOrCreateBlock(sbn);
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
const authoredLength = isIsoProfile() ? currentBlock.ssbLength : currentBlock.k;
|
|
1203
|
+
// Compare with the block's SSB_length; a key no repair declared has
|
|
1204
|
+
// none, so a block the Worker never knew can only have failed.
|
|
1205
|
+
const authoredLength = currentBlock.ssbLength;
|
|
1322
1206
|
if (authoredLength !== undefined && currentBlock.sourceReceived >= authoredLength) {
|
|
1323
1207
|
markBlockTerminal(currentBlock, "complete");
|
|
1324
1208
|
blocksComplete++;
|