@blockcast/fec-worker 0.1.0-main.64a6b356bf43 → 0.1.0-main.6598eedfb039
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 +10 -6
- package/dist/fec-worker-client.d.ts +10 -6
- package/dist/fec-worker-client.d.ts.map +1 -1
- package/dist/fec-worker-client.js +90 -25
- package/dist/fec-worker-client.js.map +1 -1
- package/dist/fec-worker-types.d.ts +53 -14
- package/dist/fec-worker-types.d.ts.map +1 -1
- package/dist/fec-worker.js +62 -44
- package/dist/fec-worker.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/fec-worker-client.ts +97 -26
- package/src/fec-worker-types.ts +51 -10
- package/src/fec-worker.ts +99 -67
- package/src/index.ts +2 -0
package/src/fec-worker-client.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
FecWorkerCommand,
|
|
13
|
+
FecSymbolCommand,
|
|
13
14
|
FecWorkerEvent,
|
|
14
15
|
FecTrackConfig,
|
|
15
16
|
FecTrackStats,
|
|
@@ -23,7 +24,6 @@ export class FecWorkerClient {
|
|
|
23
24
|
#onFrame: ((data: ArrayBuffer, meta: FrameMeta) => void) | null = null;
|
|
24
25
|
#onSnapshot: ((stats: FecTrackStats) => void) | null = null;
|
|
25
26
|
#onBlockUpdate: ((block: FecBlockSnapshot) => void) | null = null;
|
|
26
|
-
#onGreenFill: ((sbn: number, data: ArrayBuffer) => void) | null = null;
|
|
27
27
|
#onCleanupComplete: ((sbn: number) => void) | null = null;
|
|
28
28
|
#onError: ((message: string, code?: FecWorkerErrorCode) => void) | null = null;
|
|
29
29
|
#cleanupInFlight: Set<number> | null = null;
|
|
@@ -31,6 +31,20 @@ export class FecWorkerClient {
|
|
|
31
31
|
#pendingResets = 0;
|
|
32
32
|
#resetFailed = false;
|
|
33
33
|
#disposed = false;
|
|
34
|
+
// Symbols fed during the current task, sent as one feedSymbols message
|
|
35
|
+
// when the task's microtasks run (BLO-36599). A packet stream delivers many
|
|
36
|
+
// symbols per task, and each postMessage is a structured-clone round trip
|
|
37
|
+
// on the page thread; the batch boundary is the task, not a size or timer.
|
|
38
|
+
#pendingSymbols: FecSymbolCommand[] = [];
|
|
39
|
+
#pendingTransfer: Transferable[] = [];
|
|
40
|
+
#flushQueued = false;
|
|
41
|
+
// Batching starts only once the Worker announced it accepts feedSymbols.
|
|
42
|
+
#batchSymbols = false;
|
|
43
|
+
// The Worker posts blockUpdate events only to a client that subscribed,
|
|
44
|
+
// which this client does once a callback is registered and the Worker
|
|
45
|
+
// announced the command (BLO-36599).
|
|
46
|
+
#blockUpdatesAnnounced = false;
|
|
47
|
+
#blockUpdatesSubscribed = false;
|
|
34
48
|
|
|
35
49
|
/**
|
|
36
50
|
* Create a FecWorkerClient.
|
|
@@ -66,6 +80,7 @@ export class FecWorkerClient {
|
|
|
66
80
|
altaWasmModule?: WebAssembly.Module,
|
|
67
81
|
): void {
|
|
68
82
|
if (this.#disposed) throw new Error("[FecWorkerClient] disposed");
|
|
83
|
+
this.#flushSymbols();
|
|
69
84
|
const cmd: FecWorkerCommand = {
|
|
70
85
|
type: "configure",
|
|
71
86
|
config,
|
|
@@ -84,7 +99,8 @@ export class FecWorkerClient {
|
|
|
84
99
|
|
|
85
100
|
/**
|
|
86
101
|
* Feed a source symbol to the Worker. data ArrayBuffer is transferred (zero-copy).
|
|
87
|
-
* Caller must not access data after this call — it is
|
|
102
|
+
* Caller must not access data after this call — it is transferred with the
|
|
103
|
+
* task's symbol batch, before any later command of this client.
|
|
88
104
|
*
|
|
89
105
|
* Optional `alta` carries the detached ALTA authenticator bytes and the
|
|
90
106
|
* byte offset in `data` where the signed payload ends. The auth buffer is
|
|
@@ -98,18 +114,22 @@ export class FecWorkerClient {
|
|
|
98
114
|
alta?: { auth: ArrayBuffer; payloadEnd: number },
|
|
99
115
|
): void {
|
|
100
116
|
if (this.#disposed) throw new Error("[FecWorkerClient] feedSource on a disposed client");
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
if (alta) {
|
|
118
|
+
this.#queueSymbol(
|
|
119
|
+
{
|
|
103
120
|
type: "feedSource",
|
|
104
121
|
ssId,
|
|
105
122
|
data,
|
|
106
123
|
ts,
|
|
107
124
|
altaAuth: alta.auth,
|
|
108
125
|
altaPayloadEnd: alta.payloadEnd,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
},
|
|
127
|
+
data,
|
|
128
|
+
alta.auth,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
this.#queueSymbol({ type: "feedSource", ssId, data, ts }, data);
|
|
132
|
+
}
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
/**
|
|
@@ -117,8 +137,8 @@ export class FecWorkerClient {
|
|
|
117
137
|
* Caller must not access data after this call — it is detached.
|
|
118
138
|
*
|
|
119
139
|
* Carries the wire-level Repair FEC Payload ID fields (ISO 23008-1 §C.5.3):
|
|
120
|
-
* - ssStart: SS_ID of first source symbol in the block (= SBN *
|
|
121
|
-
* - ssbLength:
|
|
140
|
+
* - ssStart: SS_ID of first source symbol in the block (= SBN * configured K)
|
|
141
|
+
* - ssbLength: authored source-symbol count (1..configured K)
|
|
122
142
|
* - rsId: repair symbol index within the block
|
|
123
143
|
*/
|
|
124
144
|
feedRepair(
|
|
@@ -129,16 +149,38 @@ export class FecWorkerClient {
|
|
|
129
149
|
ts: number,
|
|
130
150
|
): void {
|
|
131
151
|
if (this.#disposed) throw new Error("[FecWorkerClient] feedRepair on a disposed client");
|
|
152
|
+
this.#queueSymbol({ type: "feedRepair", ssStart, ssbLength, rsId, data, ts }, data);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#queueSymbol(symbol: FecSymbolCommand, data: ArrayBuffer, auth?: ArrayBuffer): void {
|
|
156
|
+
if (!this.#batchSymbols) {
|
|
157
|
+
this.#worker.postMessage(symbol, auth ? [data, auth] : [data]);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#pendingSymbols.push(symbol);
|
|
161
|
+
this.#pendingTransfer.push(data);
|
|
162
|
+
if (auth) this.#pendingTransfer.push(auth);
|
|
163
|
+
if (this.#flushQueued) return;
|
|
164
|
+
this.#flushQueued = true;
|
|
165
|
+
queueMicrotask(() => {
|
|
166
|
+
this.#flushQueued = false;
|
|
167
|
+
this.#flushSymbols();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Send the symbols fed so far. Every other command calls this first, so a
|
|
173
|
+
* cleanup barrier, reset or signature still follows all earlier symbols.
|
|
174
|
+
*/
|
|
175
|
+
#flushSymbols(): void {
|
|
176
|
+
if (this.#pendingSymbols.length === 0 || this.#disposed) return;
|
|
177
|
+
const symbols = this.#pendingSymbols;
|
|
178
|
+
const transfer = this.#pendingTransfer;
|
|
179
|
+
this.#pendingSymbols = [];
|
|
180
|
+
this.#pendingTransfer = [];
|
|
132
181
|
this.#worker.postMessage(
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
ssStart,
|
|
136
|
-
ssbLength,
|
|
137
|
-
rsId,
|
|
138
|
-
data,
|
|
139
|
-
ts,
|
|
140
|
-
} satisfies FecWorkerCommand,
|
|
141
|
-
[data],
|
|
182
|
+
{ type: "feedSymbols", symbols } satisfies FecWorkerCommand,
|
|
183
|
+
transfer,
|
|
142
184
|
);
|
|
143
185
|
}
|
|
144
186
|
|
|
@@ -147,6 +189,7 @@ export class FecWorkerClient {
|
|
|
147
189
|
*/
|
|
148
190
|
relayBlockSig(sbn: number, sig: ArrayBuffer, hash: ArrayBuffer): void {
|
|
149
191
|
if (this.#disposed) throw new Error("[FecWorkerClient] relayBlockSig on a disposed client");
|
|
192
|
+
this.#flushSymbols();
|
|
150
193
|
this.#worker.postMessage(
|
|
151
194
|
{ type: "relayBlockSig", sbn, sig, hash } satisfies FecWorkerCommand,
|
|
152
195
|
[sig, hash],
|
|
@@ -178,6 +221,7 @@ export class FecWorkerClient {
|
|
|
178
221
|
/** Reset this track's decoder epoch while preserving Worker configuration. */
|
|
179
222
|
reset(): void {
|
|
180
223
|
if (this.#disposed) throw new Error("[FecWorkerClient] reset on a disposed client");
|
|
224
|
+
this.#flushSymbols();
|
|
181
225
|
this.#pendingResets++;
|
|
182
226
|
this.#cleanupInFlight = null;
|
|
183
227
|
this.#queuedCleanupSbns.clear();
|
|
@@ -185,6 +229,9 @@ export class FecWorkerClient {
|
|
|
185
229
|
}
|
|
186
230
|
|
|
187
231
|
#postCleanup(sbns: Set<number>): void {
|
|
232
|
+
// The cleanup is a queue-drain barrier: every symbol fed before it is
|
|
233
|
+
// posted must reach the Worker first.
|
|
234
|
+
this.#flushSymbols();
|
|
188
235
|
this.#cleanupInFlight = sbns;
|
|
189
236
|
const orderedSbns = [...sbns].sort((a, b) => a - b);
|
|
190
237
|
this.#worker.postMessage(
|
|
@@ -202,14 +249,28 @@ export class FecWorkerClient {
|
|
|
202
249
|
this.#onSnapshot = cb;
|
|
203
250
|
}
|
|
204
251
|
|
|
205
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Register callback for individual block state changes. The Worker posts
|
|
254
|
+
* them only once asked, so registering subscribes (after the Worker
|
|
255
|
+
* announces `subscribeBlockUpdates`). An older Worker that does not
|
|
256
|
+
* announce it posts them unasked.
|
|
257
|
+
*/
|
|
206
258
|
onBlockUpdate(cb: (block: FecBlockSnapshot) => void): void {
|
|
207
259
|
this.#onBlockUpdate = cb;
|
|
260
|
+
this.#subscribeBlockUpdates();
|
|
208
261
|
}
|
|
209
262
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
263
|
+
#subscribeBlockUpdates(): void {
|
|
264
|
+
if (
|
|
265
|
+
this.#disposed ||
|
|
266
|
+
this.#blockUpdatesSubscribed ||
|
|
267
|
+
!this.#blockUpdatesAnnounced ||
|
|
268
|
+
!this.#onBlockUpdate
|
|
269
|
+
) return;
|
|
270
|
+
this.#blockUpdatesSubscribed = true;
|
|
271
|
+
this.#worker.postMessage(
|
|
272
|
+
{ type: "subscribeBlockUpdates" } satisfies FecWorkerCommand,
|
|
273
|
+
);
|
|
213
274
|
}
|
|
214
275
|
|
|
215
276
|
/** Register callback fired after the Worker drains all commands queued before cleanup. */
|
|
@@ -239,6 +300,9 @@ export class FecWorkerClient {
|
|
|
239
300
|
dispose(): void {
|
|
240
301
|
if (this.#disposed) return;
|
|
241
302
|
this.#disposed = true;
|
|
303
|
+
// A terminated Worker decodes nothing; symbols still batched are dropped.
|
|
304
|
+
this.#pendingSymbols = [];
|
|
305
|
+
this.#pendingTransfer = [];
|
|
242
306
|
this.#cleanupInFlight = null;
|
|
243
307
|
this.#queuedCleanupSbns.clear();
|
|
244
308
|
this.#worker.postMessage({ type: "dispose" } satisfies FecWorkerCommand);
|
|
@@ -246,6 +310,16 @@ export class FecWorkerClient {
|
|
|
246
310
|
}
|
|
247
311
|
|
|
248
312
|
#handleEvent(event: FecWorkerEvent): void {
|
|
313
|
+
if (event.type === "capabilities") {
|
|
314
|
+
if (Array.isArray(event.commands) && event.commands.includes("feedSymbols")) {
|
|
315
|
+
this.#batchSymbols = true;
|
|
316
|
+
}
|
|
317
|
+
if (Array.isArray(event.commands) && event.commands.includes("subscribeBlockUpdates")) {
|
|
318
|
+
this.#blockUpdatesAnnounced = true;
|
|
319
|
+
this.#subscribeBlockUpdates();
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
249
323
|
if (event.type === "resetComplete") {
|
|
250
324
|
this.#pendingResets = Math.max(0, this.#pendingResets - 1);
|
|
251
325
|
this.#resetFailed = false;
|
|
@@ -271,9 +345,6 @@ export class FecWorkerClient {
|
|
|
271
345
|
case "blockUpdate":
|
|
272
346
|
this.#onBlockUpdate?.(event.block);
|
|
273
347
|
break;
|
|
274
|
-
case "greenFill":
|
|
275
|
-
this.#onGreenFill?.(event.sbn, event.data);
|
|
276
|
-
break;
|
|
277
348
|
case "cleanupComplete":
|
|
278
349
|
for (const sbn of event.sbns) {
|
|
279
350
|
this.#onCleanupComplete?.(sbn);
|
package/src/fec-worker-types.ts
CHANGED
|
@@ -57,13 +57,15 @@ export interface FecBlockSnapshot {
|
|
|
57
57
|
/** Source block number */
|
|
58
58
|
sbn: number;
|
|
59
59
|
/** Current block state */
|
|
60
|
-
state: "collecting" | "complete" | "recovered" | "failed"
|
|
60
|
+
state: "collecting" | "complete" | "recovered" | "failed";
|
|
61
61
|
/** Number of source symbols received */
|
|
62
62
|
sourceReceived: number;
|
|
63
63
|
/** Number of repair symbols received */
|
|
64
64
|
repairReceived: number;
|
|
65
|
-
/**
|
|
65
|
+
/** Configured source-block geometry (K) */
|
|
66
66
|
k: number;
|
|
67
|
+
/** Authored source symbols (SSB_length) once a repair symbol declared it */
|
|
68
|
+
ssbLength?: number;
|
|
67
69
|
/** Expected repair symbols (P) */
|
|
68
70
|
repairCount: number;
|
|
69
71
|
/** Timestamp of first symbol arrival (ms) */
|
|
@@ -97,8 +99,6 @@ export interface FecTrackStats {
|
|
|
97
99
|
blocksRecovered: number;
|
|
98
100
|
/** Blocks that failed (unrecoverable) */
|
|
99
101
|
blocksFailed: number;
|
|
100
|
-
/** Frames replaced with green-fill */
|
|
101
|
-
greenFillFrames: number;
|
|
102
102
|
/** Recovery success rate 0-100 */
|
|
103
103
|
recoveryRate: number;
|
|
104
104
|
|
|
@@ -143,7 +143,7 @@ export interface FecTrackStats {
|
|
|
143
143
|
liveSbn: number;
|
|
144
144
|
/** Active block snapshots */
|
|
145
145
|
blocks: FecBlockSnapshot[];
|
|
146
|
-
/** Number of blocks in the
|
|
146
|
+
/** Number of blocks in the FEC block span */
|
|
147
147
|
interleaveWindowBlocks: number;
|
|
148
148
|
|
|
149
149
|
/** Average recovery latency (ms) */
|
|
@@ -168,7 +168,9 @@ export interface FecTrackStats {
|
|
|
168
168
|
export type FecWorkerErrorCode =
|
|
169
169
|
| "recovered-symbol-size-unconfigured"
|
|
170
170
|
| "recovered-block-too-short"
|
|
171
|
+
| "recovered-symbol-ssb-length-invalid"
|
|
171
172
|
| "repair-symbol-size-mismatch"
|
|
173
|
+
| "repair-ssb-length-conflict"
|
|
172
174
|
| "reset-failed"
|
|
173
175
|
| "unknown-command"
|
|
174
176
|
| "command-failed"
|
|
@@ -185,8 +187,14 @@ export interface FrameMeta {
|
|
|
185
187
|
recovered: boolean;
|
|
186
188
|
/** Source block number this frame belongs to */
|
|
187
189
|
sbn: number;
|
|
188
|
-
/** Encoding Symbol ID within the recovered source block (0..
|
|
190
|
+
/** Encoding Symbol ID within the recovered source block (0..ssbLength-1). */
|
|
189
191
|
esi?: number;
|
|
192
|
+
/**
|
|
193
|
+
* Authored source-symbol count of the recovered block (the repair
|
|
194
|
+
* symbols' SSB_length, 1..K). Recovered frames are ESI 0..ssbLength-1;
|
|
195
|
+
* ESIs ssbLength..K-1 are encoder zero padding and are never emitted.
|
|
196
|
+
*/
|
|
197
|
+
ssbLength?: number;
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
// --- Worker commands (main -> worker) ---
|
|
@@ -230,17 +238,28 @@ export type FecWorkerCommand =
|
|
|
230
238
|
| {
|
|
231
239
|
type: "feedRepair";
|
|
232
240
|
/** SS_Start: SS_ID of the first source symbol in the block
|
|
233
|
-
* (= SBN *
|
|
241
|
+
* (= SBN * configured K), from the MMTP Repair FEC Payload ID
|
|
234
242
|
* (ISO 23008-1 §C.5.3). */
|
|
235
243
|
ssStart: number;
|
|
236
|
-
/**
|
|
244
|
+
/** Authored source-symbol count for this block; 1..configured K. */
|
|
237
245
|
ssbLength: number;
|
|
238
246
|
/** RS_ID: repair symbol index within the block; WASM maps to
|
|
239
|
-
* repair ESI =
|
|
247
|
+
* repair ESI = configured K + RS_ID. */
|
|
240
248
|
rsId: number;
|
|
241
249
|
data: ArrayBuffer;
|
|
242
250
|
ts: number;
|
|
243
251
|
}
|
|
252
|
+
| {
|
|
253
|
+
/**
|
|
254
|
+
* Every source and repair symbol the page fed in one task, in feed
|
|
255
|
+
* order, sharing one transfer list. The client sends symbols this
|
|
256
|
+
* way instead of one message per symbol (BLO-36599); the Worker
|
|
257
|
+
* handles each record exactly like the single-symbol command of the
|
|
258
|
+
* same `type`, and a record that fails does not stop the others.
|
|
259
|
+
*/
|
|
260
|
+
type: "feedSymbols";
|
|
261
|
+
symbols: FecSymbolCommand[];
|
|
262
|
+
}
|
|
244
263
|
| {
|
|
245
264
|
type: "relayBlockSig";
|
|
246
265
|
sbn: number;
|
|
@@ -253,8 +272,21 @@ export type FecWorkerCommand =
|
|
|
253
272
|
sbns: number[];
|
|
254
273
|
}
|
|
255
274
|
| { type: "reset" }
|
|
275
|
+
/**
|
|
276
|
+
* Ask the Worker to post a blockUpdate on every block transition. Without
|
|
277
|
+
* it the Worker posts none: each one is a structured clone and a task on
|
|
278
|
+
* the page thread, and the player does not consume them (BLO-36599).
|
|
279
|
+
* Sent only to a Worker that announced it in `capabilities`.
|
|
280
|
+
*/
|
|
281
|
+
| { type: "subscribeBlockUpdates" }
|
|
256
282
|
| { type: "dispose" };
|
|
257
283
|
|
|
284
|
+
/** One symbol record of a `feedSymbols` batch. */
|
|
285
|
+
export type FecSymbolCommand = Extract<
|
|
286
|
+
FecWorkerCommand,
|
|
287
|
+
{ type: "feedSource" } | { type: "feedRepair" }
|
|
288
|
+
>;
|
|
289
|
+
|
|
258
290
|
// --- Worker events (worker -> main) ---
|
|
259
291
|
|
|
260
292
|
/** Discriminated union of all events emitted from Worker to main thread. */
|
|
@@ -262,7 +294,16 @@ export type FecWorkerEvent =
|
|
|
262
294
|
| { type: "frame"; data: ArrayBuffer; meta: FrameMeta }
|
|
263
295
|
| { type: "snapshot"; stats: FecTrackStats }
|
|
264
296
|
| { type: "blockUpdate"; block: FecBlockSnapshot }
|
|
265
|
-
| { type: "greenFill"; sbn: number; data: ArrayBuffer }
|
|
266
297
|
| { type: "cleanupComplete"; sbns: number[] }
|
|
267
298
|
| { type: "resetComplete" }
|
|
299
|
+
/**
|
|
300
|
+
* Optional commands this Worker accepts, posted once it is configured.
|
|
301
|
+
* The client uses `feedSymbols` only after the Worker names it, so a page
|
|
302
|
+
* that meets an older cached Worker bundle keeps sending one command per
|
|
303
|
+
* symbol instead of a batch that Worker would reject (BLO-36599).
|
|
304
|
+
*/
|
|
305
|
+
| { type: "capabilities"; commands: FecWorkerOptionalCommand[] }
|
|
268
306
|
| { type: "error"; message: string; code?: FecWorkerErrorCode };
|
|
307
|
+
|
|
308
|
+
/** Commands a client may send only after the Worker announced them. */
|
|
309
|
+
export type FecWorkerOptionalCommand = "feedSymbols" | "subscribeBlockUpdates";
|