@blockcast/fec-worker 0.1.0-main.0c300647d0a5
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 +213 -0
- package/dist/.build-stamp +0 -0
- package/dist/fec-worker-client.d.ts +80 -0
- package/dist/fec-worker-client.d.ts.map +1 -0
- package/dist/fec-worker-client.js +270 -0
- package/dist/fec-worker-client.js.map +1 -0
- package/dist/fec-worker-types.d.ts +252 -0
- package/dist/fec-worker-types.d.ts.map +1 -0
- package/dist/fec-worker-types.js +11 -0
- package/dist/fec-worker-types.js.map +1 -0
- package/dist/fec-worker.d.ts +14 -0
- package/dist/fec-worker.d.ts.map +1 -0
- package/dist/fec-worker.js +565 -0
- package/dist/fec-worker.js.map +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/shred-fec-worker.d.ts +20 -0
- package/dist/shred-fec-worker.d.ts.map +1 -0
- package/dist/shred-fec-worker.js +349 -0
- package/dist/shred-fec-worker.js.map +7 -0
- package/dist/shred-worker-client.d.ts +104 -0
- package/dist/shred-worker-client.d.ts.map +1 -0
- package/dist/shred-worker-client.js +181 -0
- package/dist/shred-worker-client.js.map +1 -0
- package/dist/shred-worker-types.d.ts +179 -0
- package/dist/shred-worker-types.d.ts.map +1 -0
- package/dist/shred-worker-types.js +47 -0
- package/dist/shred-worker-types.js.map +1 -0
- package/dist/transfer.d.ts +9 -0
- package/dist/transfer.d.ts.map +1 -0
- package/dist/transfer.js +13 -0
- package/dist/transfer.js.map +1 -0
- package/package.json +59 -0
- package/src/fec-worker-client.ts +309 -0
- package/src/fec-worker-types.ts +268 -0
- package/src/fec-worker.ts +1048 -0
- package/src/index.ts +32 -0
- package/src/shred-fec-worker.ts +558 -0
- package/src/shred-worker-client.ts +222 -0
- package/src/shred-worker-types.ts +194 -0
- package/src/transfer.ts +12 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blockcast/fec-worker — Main-Thread Client
|
|
3
|
+
*
|
|
4
|
+
* Wraps one Worker per track instance. Provides typed API for the
|
|
5
|
+
* FecWorkerCommand/FecWorkerEvent protocol with zero-copy ArrayBuffer transfers.
|
|
6
|
+
*
|
|
7
|
+
* Constructor accepts either a URL (creates Worker internally) or a pre-created
|
|
8
|
+
* Worker instance (for testing via constructor injection).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
FecWorkerCommand,
|
|
13
|
+
FecWorkerEvent,
|
|
14
|
+
FecTrackConfig,
|
|
15
|
+
FecTrackStats,
|
|
16
|
+
FecBlockSnapshot,
|
|
17
|
+
FrameMeta,
|
|
18
|
+
FecWorkerErrorCode,
|
|
19
|
+
} from "./fec-worker-types.js";
|
|
20
|
+
|
|
21
|
+
export class FecWorkerClient {
|
|
22
|
+
readonly #worker: Worker;
|
|
23
|
+
#onFrame: ((data: ArrayBuffer, meta: FrameMeta) => void) | null = null;
|
|
24
|
+
#onSnapshot: ((stats: FecTrackStats) => void) | null = null;
|
|
25
|
+
#onBlockUpdate: ((block: FecBlockSnapshot) => void) | null = null;
|
|
26
|
+
#onGreenFill: ((sbn: number, data: ArrayBuffer) => void) | null = null;
|
|
27
|
+
#onCleanupComplete: ((sbn: number) => void) | null = null;
|
|
28
|
+
#onError: ((message: string, code?: FecWorkerErrorCode) => void) | null = null;
|
|
29
|
+
#cleanupInFlight: Set<number> | null = null;
|
|
30
|
+
#queuedCleanupSbns = new Set<number>();
|
|
31
|
+
#pendingResets = 0;
|
|
32
|
+
#resetFailed = false;
|
|
33
|
+
#disposed = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a FecWorkerClient.
|
|
37
|
+
* @param workerOrUrl - Either a Worker instance (for testing) or a URL to create one
|
|
38
|
+
*/
|
|
39
|
+
constructor(workerOrUrl: Worker | URL) {
|
|
40
|
+
if (workerOrUrl instanceof URL) {
|
|
41
|
+
this.#worker = new Worker(workerOrUrl, { type: "module" });
|
|
42
|
+
} else {
|
|
43
|
+
this.#worker = workerOrUrl;
|
|
44
|
+
}
|
|
45
|
+
this.#worker.onmessage = (e: MessageEvent<FecWorkerEvent>) =>
|
|
46
|
+
this.#handleEvent(e.data);
|
|
47
|
+
this.#worker.onerror = (e) => {
|
|
48
|
+
this.#onError?.(formatWorkerError(e), "worker-runtime");
|
|
49
|
+
};
|
|
50
|
+
this.#worker.onmessageerror = (e) => {
|
|
51
|
+
this.#onError?.(formatWorkerMessageError(e), "worker-message");
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configure the Worker with FEC parameters and WASM module.
|
|
57
|
+
* wasmModule is transferred (not neutered — WebAssembly.Module is shareable).
|
|
58
|
+
* altaPublicKey is PKCS#8 SPKI DER bytes (self-describing — algorithm detected from OID).
|
|
59
|
+
* Transferred (detached from sender).
|
|
60
|
+
* altaWasmModule is structured-cloneable (NOT transferable — do not add to transfer list).
|
|
61
|
+
*/
|
|
62
|
+
configure(
|
|
63
|
+
config: FecTrackConfig,
|
|
64
|
+
wasmModule: WebAssembly.Module,
|
|
65
|
+
altaPublicKey?: ArrayBuffer,
|
|
66
|
+
altaWasmModule?: WebAssembly.Module,
|
|
67
|
+
): void {
|
|
68
|
+
if (this.#disposed) throw new Error("[FecWorkerClient] disposed");
|
|
69
|
+
const cmd: FecWorkerCommand = {
|
|
70
|
+
type: "configure",
|
|
71
|
+
config,
|
|
72
|
+
wasmModule,
|
|
73
|
+
altaPublicKey,
|
|
74
|
+
altaWasmModule,
|
|
75
|
+
};
|
|
76
|
+
// WebAssembly.Module is structured-cloneable, NOT transferable.
|
|
77
|
+
// Pass it in the message body (Chrome clones it automatically).
|
|
78
|
+
// Only ArrayBuffers (like altaPublicKey) go in the transfer list.
|
|
79
|
+
const transferList: Transferable[] = [];
|
|
80
|
+
if (altaPublicKey) transferList.push(altaPublicKey);
|
|
81
|
+
// altaWasmModule is structured-cloneable — NOT added to transferList
|
|
82
|
+
this.#worker.postMessage(cmd, transferList);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Feed a source symbol to the Worker. data ArrayBuffer is transferred (zero-copy).
|
|
87
|
+
* Caller must not access data after this call — it is detached.
|
|
88
|
+
*
|
|
89
|
+
* Optional `alta` carries the detached ALTA authenticator bytes and the
|
|
90
|
+
* byte offset in `data` where the signed payload ends. The auth buffer is
|
|
91
|
+
* transferred zero-copy alongside `data`. Use {@link getMmtpAltaTrailer}
|
|
92
|
+
* upstream (mmtp-router) to extract it from the wire packet.
|
|
93
|
+
*/
|
|
94
|
+
feedSource(
|
|
95
|
+
ssId: number,
|
|
96
|
+
data: ArrayBuffer,
|
|
97
|
+
ts: number,
|
|
98
|
+
alta?: { auth: ArrayBuffer; payloadEnd: number },
|
|
99
|
+
): void {
|
|
100
|
+
if (this.#disposed) return;
|
|
101
|
+
const cmd: FecWorkerCommand = alta
|
|
102
|
+
? {
|
|
103
|
+
type: "feedSource",
|
|
104
|
+
ssId,
|
|
105
|
+
data,
|
|
106
|
+
ts,
|
|
107
|
+
altaAuth: alta.auth,
|
|
108
|
+
altaPayloadEnd: alta.payloadEnd,
|
|
109
|
+
}
|
|
110
|
+
: { type: "feedSource", ssId, data, ts };
|
|
111
|
+
const transfer: Transferable[] = alta ? [data, alta.auth] : [data];
|
|
112
|
+
this.#worker.postMessage(cmd, transfer);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Feed a repair symbol to the Worker. data ArrayBuffer is transferred (zero-copy).
|
|
117
|
+
* Caller must not access data after this call — it is detached.
|
|
118
|
+
*
|
|
119
|
+
* 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 * SSB_length)
|
|
121
|
+
* - ssbLength: K for this block
|
|
122
|
+
* - rsId: repair symbol index within the block
|
|
123
|
+
*/
|
|
124
|
+
feedRepair(
|
|
125
|
+
ssStart: number,
|
|
126
|
+
ssbLength: number,
|
|
127
|
+
rsId: number,
|
|
128
|
+
data: ArrayBuffer,
|
|
129
|
+
ts: number,
|
|
130
|
+
): void {
|
|
131
|
+
if (this.#disposed) return;
|
|
132
|
+
this.#worker.postMessage(
|
|
133
|
+
{
|
|
134
|
+
type: "feedRepair",
|
|
135
|
+
ssStart,
|
|
136
|
+
ssbLength,
|
|
137
|
+
rsId,
|
|
138
|
+
data,
|
|
139
|
+
ts,
|
|
140
|
+
} satisfies FecWorkerCommand,
|
|
141
|
+
[data],
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Submit relay block signature for verification. sig and hash are transferred.
|
|
147
|
+
*/
|
|
148
|
+
relayBlockSig(sbn: number, sig: ArrayBuffer, hash: ArrayBuffer): void {
|
|
149
|
+
if (this.#disposed) return;
|
|
150
|
+
this.#worker.postMessage(
|
|
151
|
+
{ type: "relayBlockSig", sbn, sig, hash } satisfies FecWorkerCommand,
|
|
152
|
+
[sig, hash],
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Trigger cleanup after a block's inactivity deadline.
|
|
158
|
+
*
|
|
159
|
+
* While one queue-drain barrier is in flight, later SBNs are coalesced into
|
|
160
|
+
* the next command without losing their individual acknowledgement identity.
|
|
161
|
+
*/
|
|
162
|
+
cleanup(currentSbn: number): void {
|
|
163
|
+
if (this.#disposed) return;
|
|
164
|
+
if (
|
|
165
|
+
!Number.isInteger(currentSbn) ||
|
|
166
|
+
currentSbn < 0 ||
|
|
167
|
+
currentSbn > 0xffff_ffff
|
|
168
|
+
) {
|
|
169
|
+
throw new Error("[FecWorkerClient] cleanup: SBN must be a uint32");
|
|
170
|
+
}
|
|
171
|
+
if (this.#cleanupInFlight) {
|
|
172
|
+
this.#queuedCleanupSbns.add(currentSbn);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.#postCleanup(new Set([currentSbn]));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Reset this track's decoder epoch while preserving Worker configuration. */
|
|
179
|
+
reset(): void {
|
|
180
|
+
if (this.#disposed) return;
|
|
181
|
+
this.#pendingResets++;
|
|
182
|
+
this.#cleanupInFlight = null;
|
|
183
|
+
this.#queuedCleanupSbns.clear();
|
|
184
|
+
this.#worker.postMessage({ type: "reset" } satisfies FecWorkerCommand);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#postCleanup(sbns: Set<number>): void {
|
|
188
|
+
this.#cleanupInFlight = sbns;
|
|
189
|
+
const orderedSbns = [...sbns].sort((a, b) => a - b);
|
|
190
|
+
this.#worker.postMessage(
|
|
191
|
+
{ type: "cleanup", sbns: orderedSbns } satisfies FecWorkerCommand,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Register callback for decoded/recovered frames (FEC-03). */
|
|
196
|
+
onFrame(cb: (data: ArrayBuffer, meta: FrameMeta) => void): void {
|
|
197
|
+
this.#onFrame = cb;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Register callback for periodic stats snapshots (FEC-04). */
|
|
201
|
+
onSnapshot(cb: (stats: FecTrackStats) => void): void {
|
|
202
|
+
this.#onSnapshot = cb;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Register callback for individual block state changes. */
|
|
206
|
+
onBlockUpdate(cb: (block: FecBlockSnapshot) => void): void {
|
|
207
|
+
this.#onBlockUpdate = cb;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Register callback for green-fill frames. */
|
|
211
|
+
onGreenFill(cb: (sbn: number, data: ArrayBuffer) => void): void {
|
|
212
|
+
this.#onGreenFill = cb;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Register callback fired after the Worker drains all commands queued before cleanup. */
|
|
216
|
+
onCleanupComplete(cb: (sbn: number) => void): void {
|
|
217
|
+
this.#onCleanupComplete = cb;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Register callback for Worker errors. */
|
|
221
|
+
onError(cb: (message: string, code?: FecWorkerErrorCode) => void): void {
|
|
222
|
+
this.#onError = cb;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Dispose the Worker. Sends dispose command then terminates.
|
|
227
|
+
* Idempotent — safe to call multiple times.
|
|
228
|
+
*/
|
|
229
|
+
dispose(): void {
|
|
230
|
+
if (this.#disposed) return;
|
|
231
|
+
this.#disposed = true;
|
|
232
|
+
this.#cleanupInFlight = null;
|
|
233
|
+
this.#queuedCleanupSbns.clear();
|
|
234
|
+
this.#worker.postMessage({ type: "dispose" } satisfies FecWorkerCommand);
|
|
235
|
+
this.#worker.terminate();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#handleEvent(event: FecWorkerEvent): void {
|
|
239
|
+
if (event.type === "resetComplete") {
|
|
240
|
+
this.#pendingResets = Math.max(0, this.#pendingResets - 1);
|
|
241
|
+
this.#resetFailed = false;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (event.type === "error") {
|
|
245
|
+
if (event.code === "reset-failed") {
|
|
246
|
+
this.#pendingResets = Math.max(0, this.#pendingResets - 1);
|
|
247
|
+
this.#resetFailed = true;
|
|
248
|
+
}
|
|
249
|
+
if (event.code) this.#onError?.(event.message, event.code);
|
|
250
|
+
else this.#onError?.(event.message);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (this.#pendingResets > 0 || this.#resetFailed) return;
|
|
254
|
+
switch (event.type) {
|
|
255
|
+
case "frame":
|
|
256
|
+
this.#onFrame?.(event.data, event.meta);
|
|
257
|
+
break;
|
|
258
|
+
case "snapshot":
|
|
259
|
+
this.#onSnapshot?.(event.stats);
|
|
260
|
+
break;
|
|
261
|
+
case "blockUpdate":
|
|
262
|
+
this.#onBlockUpdate?.(event.block);
|
|
263
|
+
break;
|
|
264
|
+
case "greenFill":
|
|
265
|
+
this.#onGreenFill?.(event.sbn, event.data);
|
|
266
|
+
break;
|
|
267
|
+
case "cleanupComplete":
|
|
268
|
+
for (const sbn of event.sbns) {
|
|
269
|
+
this.#onCleanupComplete?.(sbn);
|
|
270
|
+
}
|
|
271
|
+
this.#cleanupInFlight = null;
|
|
272
|
+
if (this.#queuedCleanupSbns.size > 0) {
|
|
273
|
+
const next = this.#queuedCleanupSbns;
|
|
274
|
+
this.#queuedCleanupSbns = new Set();
|
|
275
|
+
this.#postCleanup(next);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function formatWorkerError(event: ErrorEvent): string {
|
|
283
|
+
const parts = ["Worker error"];
|
|
284
|
+
if (event.message) parts.push(event.message);
|
|
285
|
+
if (event.filename) {
|
|
286
|
+
const location = [event.filename];
|
|
287
|
+
if (event.lineno) location.push(String(event.lineno));
|
|
288
|
+
if (event.colno) location.push(String(event.colno));
|
|
289
|
+
parts.push(`at ${location.join(":")}`);
|
|
290
|
+
}
|
|
291
|
+
if (event.error instanceof Error) {
|
|
292
|
+
if (event.error.message && event.error.message !== event.message) {
|
|
293
|
+
parts.push(event.error.message);
|
|
294
|
+
}
|
|
295
|
+
if (event.error.stack) parts.push(event.error.stack);
|
|
296
|
+
} else if (event.error != null) {
|
|
297
|
+
parts.push(String(event.error));
|
|
298
|
+
}
|
|
299
|
+
return parts.join(": ");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function formatWorkerMessageError(event: MessageEvent): string {
|
|
303
|
+
const data = event.data;
|
|
304
|
+
const detail =
|
|
305
|
+
data == null ? "" :
|
|
306
|
+
data instanceof Error ? `: ${data.message}` :
|
|
307
|
+
`: ${Object.prototype.toString.call(data)}`;
|
|
308
|
+
return `Worker messageerror: failed to deserialize worker message${detail}`;
|
|
309
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blockcast/fec-worker — Shared Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Defines the Worker message protocol (commands/events), FecTrackStats shape
|
|
5
|
+
* consumed by fec-panel (Phase 5) and CmcdProducer (Phase 6), and auth
|
|
6
|
+
* interface stubs (ALTA + relay block sig).
|
|
7
|
+
*
|
|
8
|
+
* Types here are importable from both main thread and Worker contexts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// --- Config ---
|
|
12
|
+
|
|
13
|
+
/** Per-track FEC configuration. Matches design spec section 3.2 FecTrackConfig. */
|
|
14
|
+
export interface FecTrackConfig {
|
|
15
|
+
/** Codec string, e.g. 'avc1.64001f', 'mp4a.40.2' */
|
|
16
|
+
codec: string;
|
|
17
|
+
/** Track name from catalog */
|
|
18
|
+
trackName: string;
|
|
19
|
+
/** Track media type */
|
|
20
|
+
trackType: "video" | "audio";
|
|
21
|
+
/** Video resolution (omitted for audio) */
|
|
22
|
+
resolution?: { w: number; h: number };
|
|
23
|
+
/** Video framerate (omitted for audio) */
|
|
24
|
+
framerate?: number;
|
|
25
|
+
/** Audio sample rate (omitted for video) */
|
|
26
|
+
sampleRate?: number;
|
|
27
|
+
|
|
28
|
+
/** FEC algorithm identifier: 'raptor', 'reed-solomon', 'none' */
|
|
29
|
+
algorithm: string;
|
|
30
|
+
/** Source symbols per block (K) */
|
|
31
|
+
k: number;
|
|
32
|
+
/** Repair symbols per block (P). NOT symbol size. */
|
|
33
|
+
repairCount: number;
|
|
34
|
+
/** Bytes per symbol */
|
|
35
|
+
symbolSize: number;
|
|
36
|
+
/** Interleave depth in frames (D) */
|
|
37
|
+
interleaveDepth: number;
|
|
38
|
+
/** Interleave depth in milliseconds (D * frameDurationMs) */
|
|
39
|
+
interleaveMs: number;
|
|
40
|
+
/** Catalog-derived source+repair delivery window used for decoder cleanup. */
|
|
41
|
+
deliveryWindowMs: number;
|
|
42
|
+
/** FEC encoding mode */
|
|
43
|
+
fecMode: "subframe" | "object";
|
|
44
|
+
/** MoQ repair track name (optional) */
|
|
45
|
+
repairTrack?: string;
|
|
46
|
+
|
|
47
|
+
/** Whether ALTA publisher auth is enabled (AUTH-01) */
|
|
48
|
+
altaEnabled: boolean;
|
|
49
|
+
/** Whether relay block signature verification is enabled (AUTH-01) */
|
|
50
|
+
relayBlockSigEnabled: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Block state ---
|
|
54
|
+
|
|
55
|
+
/** Per-block snapshot for live block visualization and stats. */
|
|
56
|
+
export interface FecBlockSnapshot {
|
|
57
|
+
/** Source block number */
|
|
58
|
+
sbn: number;
|
|
59
|
+
/** Current block state */
|
|
60
|
+
state: "collecting" | "complete" | "recovered" | "failed" | "green-fill";
|
|
61
|
+
/** Number of source symbols received */
|
|
62
|
+
sourceReceived: number;
|
|
63
|
+
/** Number of repair symbols received */
|
|
64
|
+
repairReceived: number;
|
|
65
|
+
/** Expected source symbols (K) */
|
|
66
|
+
k: number;
|
|
67
|
+
/** Expected repair symbols (P) */
|
|
68
|
+
repairCount: number;
|
|
69
|
+
/** Timestamp of first symbol arrival (ms) */
|
|
70
|
+
firstSymbolTs: number;
|
|
71
|
+
/** Timestamp of last symbol arrival (ms) */
|
|
72
|
+
lastSymbolTs: number;
|
|
73
|
+
/** Timestamp when block completed (ms), undefined if not yet complete */
|
|
74
|
+
completedTs?: number;
|
|
75
|
+
/** Deadline timestamp for block completion (ms) */
|
|
76
|
+
deadlineTs: number;
|
|
77
|
+
/** Count of source symbols with valid ALTA signature (AUTH-02) */
|
|
78
|
+
altaVerified: number;
|
|
79
|
+
/** Relay block signature validity: true/false after verification, null if not yet verified (AUTH-02) */
|
|
80
|
+
relayBlockSigValid: boolean | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Stats ---
|
|
84
|
+
|
|
85
|
+
/** Aggregate per-track FEC statistics. Consumed by fec-panel and CmcdProducer. */
|
|
86
|
+
export interface FecTrackStats {
|
|
87
|
+
/** Track configuration */
|
|
88
|
+
config: FecTrackConfig;
|
|
89
|
+
|
|
90
|
+
/** Total source symbols received */
|
|
91
|
+
sourceSymbols: number;
|
|
92
|
+
/** Total repair symbols received */
|
|
93
|
+
repairSymbols: number;
|
|
94
|
+
/** Blocks completed without recovery needed */
|
|
95
|
+
blocksComplete: number;
|
|
96
|
+
/** Blocks recovered via FEC */
|
|
97
|
+
blocksRecovered: number;
|
|
98
|
+
/** Blocks that failed (unrecoverable) */
|
|
99
|
+
blocksFailed: number;
|
|
100
|
+
/** Frames replaced with green-fill */
|
|
101
|
+
greenFillFrames: number;
|
|
102
|
+
/** Recovery success rate 0-100 */
|
|
103
|
+
recoveryRate: number;
|
|
104
|
+
|
|
105
|
+
/** Packets with valid publisher ALTA signature */
|
|
106
|
+
altaVerified: number;
|
|
107
|
+
/** Packets with MAC / signature / hash-chain mismatch (tamper-confirmed) */
|
|
108
|
+
altaFailed: number;
|
|
109
|
+
/** Packets that couldn't be verified due to missing trailer / not-ready verifier /
|
|
110
|
+
* late-joiner bootstrap gap. Neither tamper nor error — just unverifiable. */
|
|
111
|
+
altaPending: number;
|
|
112
|
+
/** Deferred-queue entries evicted by cap (queue full). Separate counter so
|
|
113
|
+
* cap misconfiguration is distinguishable from natural bootstrap-gap noise. */
|
|
114
|
+
altaPendingEvictedCap: number;
|
|
115
|
+
/** Deferred-queue entries evicted by TTL (retry window expired). */
|
|
116
|
+
altaPendingEvictedTtl: number;
|
|
117
|
+
/** verifyDetached / parse threw an exception (WASM trap, OOM, unmarshal panic).
|
|
118
|
+
* Not counted as altaFailed because the cause isn't crypto — helps ops
|
|
119
|
+
* distinguish infrastructure flake from tamper. */
|
|
120
|
+
altaError: number;
|
|
121
|
+
/** True if ALTA was enabled at configure time but the verifier WASM init
|
|
122
|
+
* failed. Ops alerting should treat this as "ALTA disabled due to error",
|
|
123
|
+
* distinct from "ALTA disabled by config" (altaEnabled flag not present). */
|
|
124
|
+
altaInitFailed: boolean;
|
|
125
|
+
/** Diagnostic: last ≤16 altaFailed events with full context (seq, error
|
|
126
|
+
* string from the verifier, auth+payload lengths, and which code path
|
|
127
|
+
* raised the failure). Bounded so it can't blow memory. Intended for
|
|
128
|
+
* investigation — not a stable API; consumers should treat it as
|
|
129
|
+
* opt-in and defensive (may be undefined). */
|
|
130
|
+
altaFailedDetails?: Array<{
|
|
131
|
+
seq: number;
|
|
132
|
+
error: string;
|
|
133
|
+
authLen: number;
|
|
134
|
+
payloadLen: number;
|
|
135
|
+
path: string;
|
|
136
|
+
}>;
|
|
137
|
+
/** Blocks with valid relay signature */
|
|
138
|
+
relayBlocksVerified: number;
|
|
139
|
+
/** Blocks with invalid relay signature */
|
|
140
|
+
relayBlocksFailed: number;
|
|
141
|
+
|
|
142
|
+
/** Current live source block number */
|
|
143
|
+
liveSbn: number;
|
|
144
|
+
/** Active block snapshots */
|
|
145
|
+
blocks: FecBlockSnapshot[];
|
|
146
|
+
/** Number of blocks in the interleave window */
|
|
147
|
+
interleaveWindowBlocks: number;
|
|
148
|
+
|
|
149
|
+
/** Average recovery latency (ms) */
|
|
150
|
+
avgRecoveryMs: number;
|
|
151
|
+
/** Maximum recovery latency (ms) */
|
|
152
|
+
maxRecoveryMs: number;
|
|
153
|
+
/** Repair symbols rejected before WASM because their byte length did not match T. */
|
|
154
|
+
repairSymbolSizeMismatch?: number;
|
|
155
|
+
/** Native decoder counters, converted to numbers so browser diagnostics remain JSON-safe. */
|
|
156
|
+
decoder?: {
|
|
157
|
+
sourcePackets: number;
|
|
158
|
+
repairPackets: number;
|
|
159
|
+
blocksComplete: number;
|
|
160
|
+
blocksRecovered: number;
|
|
161
|
+
blocksFailed: number;
|
|
162
|
+
bytesRecovered: number;
|
|
163
|
+
pendingBlocks: number;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Stable identities for Worker failures whose message text contains live values. */
|
|
168
|
+
export type FecWorkerErrorCode =
|
|
169
|
+
| "recovered-symbol-size-unconfigured"
|
|
170
|
+
| "recovered-block-too-short"
|
|
171
|
+
| "repair-symbol-size-mismatch"
|
|
172
|
+
| "reset-failed"
|
|
173
|
+
| "unknown-command"
|
|
174
|
+
| "command-failed"
|
|
175
|
+
| "worker-runtime"
|
|
176
|
+
| "worker-message";
|
|
177
|
+
|
|
178
|
+
// --- Frame metadata ---
|
|
179
|
+
|
|
180
|
+
/** Per-frame metadata emitted with frame events from Worker. */
|
|
181
|
+
export interface FrameMeta {
|
|
182
|
+
/** ALTA verification result: true/false after check, null when ALTA not configured (AUTH-04) */
|
|
183
|
+
altaVerified: boolean | null;
|
|
184
|
+
/** Whether this frame was recovered via FEC (not received directly) */
|
|
185
|
+
recovered: boolean;
|
|
186
|
+
/** Source block number this frame belongs to */
|
|
187
|
+
sbn: number;
|
|
188
|
+
/** Encoding Symbol ID within the recovered source block (0..K-1). */
|
|
189
|
+
esi?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Worker commands (main -> worker) ---
|
|
193
|
+
|
|
194
|
+
/** Discriminated union of all commands sent from main thread to Worker. */
|
|
195
|
+
export type FecWorkerCommand =
|
|
196
|
+
| {
|
|
197
|
+
type: "configure";
|
|
198
|
+
config: FecTrackConfig;
|
|
199
|
+
wasmModule: WebAssembly.Module;
|
|
200
|
+
/**
|
|
201
|
+
* ALTA public key as PKCS#8 SubjectPublicKeyInfo DER bytes (self-describing).
|
|
202
|
+
* Algorithm (Ed25519 or ECDSA P-256) is auto-detected from the SPKI OID.
|
|
203
|
+
* Ed25519 SPKI is 44 bytes; P-256 uncompressed is 91 bytes.
|
|
204
|
+
*/
|
|
205
|
+
altaPublicKey?: ArrayBuffer;
|
|
206
|
+
/** Pre-compiled ALTA WASM module. Structured-cloneable (NOT transferable) — do not add to transfer list. */
|
|
207
|
+
altaWasmModule?: WebAssembly.Module;
|
|
208
|
+
}
|
|
209
|
+
| {
|
|
210
|
+
type: "feedSource";
|
|
211
|
+
/** Flat 32-bit Source Symbol ID from the MMTP Source FEC Payload ID
|
|
212
|
+
* (ISO 23008-1 §C.5.2). The WASM decoder derives SBN = floor(ssId/K)
|
|
213
|
+
* and ESI = ssId % K internally — do NOT pre-compute. */
|
|
214
|
+
ssId: number;
|
|
215
|
+
data: ArrayBuffer;
|
|
216
|
+
ts: number;
|
|
217
|
+
/** Optional ALTA authenticator bytes (detached mode — 08-07 MMTP
|
|
218
|
+
* trailer). Present when the source packet carried a
|
|
219
|
+
* `[0xF0A1][len][auth]` trailer and ALTA is enabled at the worker.
|
|
220
|
+
* When omitted, the worker skips verification and the packet
|
|
221
|
+
* counts as `altaPending`. */
|
|
222
|
+
altaAuth?: ArrayBuffer;
|
|
223
|
+
/** Byte offset within `data` where the signed MMTP payload ends
|
|
224
|
+
* (== where the trailer began on the wire). Required when
|
|
225
|
+
* `altaAuth` is present. Worker constructs
|
|
226
|
+
* `payload = new Uint8Array(data, 0, altaPayloadEnd)` for
|
|
227
|
+
* `verifyDetached(altaAuth, payload)`. */
|
|
228
|
+
altaPayloadEnd?: number;
|
|
229
|
+
}
|
|
230
|
+
| {
|
|
231
|
+
type: "feedRepair";
|
|
232
|
+
/** SS_Start: SS_ID of the first source symbol in the block
|
|
233
|
+
* (= SBN * SSB_length), from the MMTP Repair FEC Payload ID
|
|
234
|
+
* (ISO 23008-1 §C.5.3). */
|
|
235
|
+
ssStart: number;
|
|
236
|
+
/** SSB_length: K for this block (variable-K allowed per spec). */
|
|
237
|
+
ssbLength: number;
|
|
238
|
+
/** RS_ID: repair symbol index within the block; WASM maps to
|
|
239
|
+
* repair ESI = SSB_length + RS_ID. */
|
|
240
|
+
rsId: number;
|
|
241
|
+
data: ArrayBuffer;
|
|
242
|
+
ts: number;
|
|
243
|
+
}
|
|
244
|
+
| {
|
|
245
|
+
type: "relayBlockSig";
|
|
246
|
+
sbn: number;
|
|
247
|
+
sig: ArrayBuffer;
|
|
248
|
+
hash: ArrayBuffer;
|
|
249
|
+
}
|
|
250
|
+
| {
|
|
251
|
+
type: "cleanup";
|
|
252
|
+
/** Every timed-out block covered by this coalesced queue-drain barrier. */
|
|
253
|
+
sbns: number[];
|
|
254
|
+
}
|
|
255
|
+
| { type: "reset" }
|
|
256
|
+
| { type: "dispose" };
|
|
257
|
+
|
|
258
|
+
// --- Worker events (worker -> main) ---
|
|
259
|
+
|
|
260
|
+
/** Discriminated union of all events emitted from Worker to main thread. */
|
|
261
|
+
export type FecWorkerEvent =
|
|
262
|
+
| { type: "frame"; data: ArrayBuffer; meta: FrameMeta }
|
|
263
|
+
| { type: "snapshot"; stats: FecTrackStats }
|
|
264
|
+
| { type: "blockUpdate"; block: FecBlockSnapshot }
|
|
265
|
+
| { type: "greenFill"; sbn: number; data: ArrayBuffer }
|
|
266
|
+
| { type: "cleanupComplete"; sbns: number[] }
|
|
267
|
+
| { type: "resetComplete" }
|
|
268
|
+
| { type: "error"; message: string; code?: FecWorkerErrorCode };
|