@blockcast/mmt-transport 0.2.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.
Files changed (46) hide show
  1. package/dist/abr-controller.d.ts +94 -0
  2. package/dist/abr-controller.d.ts.map +1 -0
  3. package/dist/abr-controller.js +174 -0
  4. package/dist/abr-controller.js.map +1 -0
  5. package/dist/amt-gateway.d.ts +160 -0
  6. package/dist/amt-gateway.d.ts.map +1 -0
  7. package/dist/amt-gateway.js +390 -0
  8. package/dist/amt-gateway.js.map +1 -0
  9. package/dist/clock.d.ts +104 -0
  10. package/dist/clock.d.ts.map +1 -0
  11. package/dist/clock.js +183 -0
  12. package/dist/clock.js.map +1 -0
  13. package/dist/driad-discovery.d.ts +50 -0
  14. package/dist/driad-discovery.d.ts.map +1 -0
  15. package/dist/driad-discovery.js +170 -0
  16. package/dist/driad-discovery.js.map +1 -0
  17. package/dist/fec-client.d.ts +442 -0
  18. package/dist/fec-client.d.ts.map +1 -0
  19. package/dist/fec-client.js +784 -0
  20. package/dist/fec-client.js.map +1 -0
  21. package/dist/fec-client.test.d.ts +8 -0
  22. package/dist/fec-client.test.d.ts.map +1 -0
  23. package/dist/fec-client.test.js +112 -0
  24. package/dist/fec-client.test.js.map +1 -0
  25. package/dist/index.d.ts +34 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +43 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/transport-manager.d.ts +114 -0
  30. package/dist/transport-manager.d.ts.map +1 -0
  31. package/dist/transport-manager.js +396 -0
  32. package/dist/transport-manager.js.map +1 -0
  33. package/dist/types.d.ts +356 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/types.js +85 -0
  36. package/dist/types.js.map +1 -0
  37. package/package.json +60 -0
  38. package/src/abr-controller.ts +227 -0
  39. package/src/amt-gateway.ts +511 -0
  40. package/src/clock.ts +212 -0
  41. package/src/driad-discovery.ts +193 -0
  42. package/src/fec-client.test.ts +140 -0
  43. package/src/fec-client.ts +1097 -0
  44. package/src/index.ts +122 -0
  45. package/src/transport-manager.ts +460 -0
  46. package/src/types.ts +420 -0
@@ -0,0 +1,1097 @@
1
+ /**
2
+ * @blockcast/transport - MMT FEC Client
3
+ *
4
+ * MMT FEC Client for Browser Players
5
+ *
6
+ * Integrates libmmt WASM FEC decoder with:
7
+ * - MFU fragment reassembly
8
+ * - window.Multicast API from pim-multicast-gateway
9
+ * - Green-fill fallback for unrecoverable frames
10
+ *
11
+ * Designed for hang and moqtail players.
12
+ */
13
+
14
+ import type {
15
+ MfuFragment,
16
+ ReassembledMfu,
17
+ ProcessingStats,
18
+ MmtFecClientOptions,
19
+ } from "./types.js";
20
+
21
+ /**
22
+ * MFU Reassembler (TypeScript implementation for WASM compatibility)
23
+ */
24
+ class MfuReassembler {
25
+ private fragmentBuffers = new Map<number, MfuFragment[]>();
26
+ private timeouts = new Map<number, ReturnType<typeof setTimeout>>();
27
+ private readonly timeoutMs: number;
28
+ private readonly maxMfus: number;
29
+
30
+ public stats = {
31
+ complete: 0,
32
+ reassembled: 0,
33
+ timedOut: 0,
34
+ };
35
+
36
+ constructor(maxMfus = 100, timeoutMs = 1000) {
37
+ this.maxMfus = maxMfus;
38
+ this.timeoutMs = timeoutMs;
39
+ }
40
+
41
+ addFragment(fragment: MfuFragment): ReassembledMfu | null {
42
+ const { mpuSequenceNumber, fragmentationIndicator } = fragment;
43
+
44
+ // FI=0: Complete MFU in single packet
45
+ if (fragmentationIndicator === 0) {
46
+ this.stats.complete++;
47
+ return {
48
+ mpuSequenceNumber,
49
+ data: fragment.data,
50
+ timestamp: fragment.timestamp,
51
+ fragmentCount: 1,
52
+ recoveredViaFec: false,
53
+ };
54
+ }
55
+
56
+ // Get or create buffer for this MPU
57
+ let fragments = this.fragmentBuffers.get(mpuSequenceNumber);
58
+ if (!fragments) {
59
+ fragments = [];
60
+ this.fragmentBuffers.set(mpuSequenceNumber, fragments);
61
+ this.setTimeout(mpuSequenceNumber);
62
+ }
63
+
64
+ // Check for duplicate
65
+ const existingIdx = fragments.findIndex((f) => f.fragmentCounter === fragment.fragmentCounter);
66
+ if (existingIdx >= 0) {
67
+ fragments[existingIdx] = fragment;
68
+ } else {
69
+ fragments.push(fragment);
70
+ }
71
+
72
+ // Enforce memory limit
73
+ this.enforceLimit();
74
+
75
+ // Try to reassemble
76
+ return this.tryReassemble(mpuSequenceNumber);
77
+ }
78
+
79
+ private tryReassemble(mpuSeq: number): ReassembledMfu | null {
80
+ const fragments = this.fragmentBuffers.get(mpuSeq);
81
+ if (!fragments || fragments.length === 0) return null;
82
+
83
+ // Sort by FI
84
+ fragments.sort((a, b) => a.fragmentationIndicator - b.fragmentationIndicator);
85
+
86
+ const first = fragments[0].fragmentationIndicator;
87
+ const last = fragments[fragments.length - 1].fragmentationIndicator;
88
+
89
+ // Need FI=1 (first) and FI=3 (last)
90
+ if (first !== 1 || last !== 3) return null;
91
+
92
+ // Verify middles are FI=2
93
+ for (let i = 1; i < fragments.length - 1; i++) {
94
+ if (fragments[i].fragmentationIndicator !== 2) return null;
95
+ }
96
+
97
+ // Reassemble
98
+ const totalSize = fragments.reduce((sum, f) => sum + f.data.length, 0);
99
+ const reassembled = new Uint8Array(totalSize);
100
+ let offset = 0;
101
+ for (const fragment of fragments) {
102
+ reassembled.set(fragment.data, offset);
103
+ offset += fragment.data.length;
104
+ }
105
+
106
+ // Cleanup
107
+ this.clearTimeout(mpuSeq);
108
+ this.fragmentBuffers.delete(mpuSeq);
109
+ this.stats.reassembled++;
110
+
111
+ return {
112
+ mpuSequenceNumber: mpuSeq,
113
+ data: reassembled,
114
+ timestamp: fragments[0].timestamp,
115
+ fragmentCount: fragments.length,
116
+ recoveredViaFec: false,
117
+ };
118
+ }
119
+
120
+ private setTimeout(mpuSeq: number): void {
121
+ const id = setTimeout(() => this.onTimeout(mpuSeq), this.timeoutMs);
122
+ this.timeouts.set(mpuSeq, id);
123
+ }
124
+
125
+ private clearTimeout(mpuSeq: number): void {
126
+ const id = this.timeouts.get(mpuSeq);
127
+ if (id !== undefined) {
128
+ clearTimeout(id);
129
+ this.timeouts.delete(mpuSeq);
130
+ }
131
+ }
132
+
133
+ private onTimeout(mpuSeq: number): void {
134
+ this.fragmentBuffers.delete(mpuSeq);
135
+ this.timeouts.delete(mpuSeq);
136
+ this.stats.timedOut++;
137
+ }
138
+
139
+ private enforceLimit(): void {
140
+ if (this.fragmentBuffers.size <= this.maxMfus) return;
141
+
142
+ const sequences = Array.from(this.fragmentBuffers.keys()).sort((a, b) => a - b);
143
+ const toRemove = sequences.slice(0, sequences.length - this.maxMfus);
144
+
145
+ for (const seq of toRemove) {
146
+ this.clearTimeout(seq);
147
+ this.fragmentBuffers.delete(seq);
148
+ this.stats.timedOut++;
149
+ }
150
+ }
151
+
152
+ flush(): void {
153
+ for (const seq of this.fragmentBuffers.keys()) {
154
+ this.clearTimeout(seq);
155
+ }
156
+ this.fragmentBuffers.clear();
157
+ this.timeouts.clear();
158
+ }
159
+ }
160
+
161
+ /**
162
+ * FEC decoder configuration parameters (RFC 6330 OTI)
163
+ * This is distinct from the high-level FecConfig in types.ts which is for enabling/configuring FEC.
164
+ */
165
+ export interface FecDecoderConfig {
166
+ /** Symbol size in bytes (T) */
167
+ symbolSize: number;
168
+ /** Number of source blocks (Z) */
169
+ sourceBlocks: number;
170
+ /** Number of sub-blocks (N) - used for large blocks */
171
+ subBlocks: number;
172
+ /** Symbol alignment (Al) - typically 8 for byte-aligned */
173
+ alignment: number;
174
+ /** Interleave depth - number of blocks that can be "in flight" */
175
+ interleaveDepth: number;
176
+ }
177
+
178
+ /**
179
+ * FEC Decoder interface (WASM module)
180
+ */
181
+ interface FecDecoder {
182
+ configure(oti: Uint8Array): void;
183
+ add_repair(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | null;
184
+ cleanup(timestamp: number): number[];
185
+ cleanup_by_interleave(current_sbn: number): number[];
186
+ get_stats(): {
187
+ source_packets: bigint;
188
+ repair_packets: bigint;
189
+ blocks_complete: bigint;
190
+ blocks_recovered: bigint;
191
+ blocks_failed: bigint;
192
+ recovery_rate(): number;
193
+ };
194
+ reset_stats(): void;
195
+ flush(): void;
196
+ set_timeout(ms: number): void;
197
+ set_green_fill(enabled: boolean): void;
198
+ set_interleave_depth(depth: number): void;
199
+ get_interleave_depth(): number;
200
+ get_symbol_size(): number;
201
+ get_source_blocks(): number;
202
+ get_sub_blocks(): number;
203
+ get_alignment(): number;
204
+ }
205
+
206
+ /**
207
+ * FEC Payload ID parser interface
208
+ */
209
+ interface FecPayloadId {
210
+ sbn: number;
211
+ esi: number;
212
+ }
213
+
214
+ /**
215
+ * MMT FEC Client
216
+ *
217
+ * Combines WASM FEC decoder with MFU reassembler for complete
218
+ * MMT packet processing in browser players.
219
+ */
220
+ export class MmtFecClient {
221
+ private fecDecoder: FecDecoder | null = null;
222
+ private mfuReassembler: MfuReassembler;
223
+ private options: Required<MmtFecClientOptions>;
224
+ private initialized = false;
225
+ private greenFillCount = 0;
226
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
227
+
228
+ // WASM module references (set via init)
229
+ private wasmInit?: () => Promise<void>;
230
+ private wasmFecDecoder?: new (interleaveDepth: number) => FecDecoder;
231
+ private wasmParseFecPayloadId?: (data: Uint8Array) => FecPayloadId;
232
+ private wasmIsSimdEnabled?: () => boolean;
233
+
234
+ constructor(options: MmtFecClientOptions = {}) {
235
+ this.options = {
236
+ interleaveDepth: options.interleaveDepth ?? 30,
237
+ blockTimeoutMs: options.blockTimeoutMs ?? 2000,
238
+ mfuTimeoutMs: options.mfuTimeoutMs ?? 1000,
239
+ greenFillEnabled: options.greenFillEnabled ?? true,
240
+ onMfuReady: options.onMfuReady ?? (() => {}),
241
+ onGreenFill: options.onGreenFill ?? (() => {}),
242
+ onStatsUpdate: options.onStatsUpdate ?? (() => {}),
243
+ };
244
+
245
+ this.mfuReassembler = new MfuReassembler(100, this.options.mfuTimeoutMs);
246
+ }
247
+
248
+ /**
249
+ * Set WASM module functions (called before init)
250
+ */
251
+ setWasmModule(module: {
252
+ init: () => Promise<void>;
253
+ MmtFecDecoder: new (interleaveDepth: number) => FecDecoder;
254
+ parse_fec_payload_id: (data: Uint8Array) => FecPayloadId;
255
+ is_simd_enabled: () => boolean;
256
+ }): void {
257
+ this.wasmInit = module.init;
258
+ this.wasmFecDecoder = module.MmtFecDecoder;
259
+ this.wasmParseFecPayloadId = module.parse_fec_payload_id;
260
+ this.wasmIsSimdEnabled = module.is_simd_enabled;
261
+ }
262
+
263
+ /**
264
+ * Initialize the WASM module
265
+ */
266
+ async init(): Promise<void> {
267
+ if (this.initialized) return;
268
+
269
+ if (!this.wasmInit || !this.wasmFecDecoder) {
270
+ throw new Error("WASM module not set. Call setWasmModule() first.");
271
+ }
272
+
273
+ await this.wasmInit();
274
+
275
+ this.fecDecoder = new this.wasmFecDecoder(this.options.interleaveDepth);
276
+ this.fecDecoder.set_timeout(this.options.blockTimeoutMs);
277
+ this.fecDecoder.set_green_fill(this.options.greenFillEnabled);
278
+
279
+ // Start periodic cleanup
280
+ this.cleanupInterval = setInterval(() => {
281
+ this.cleanup();
282
+ }, 500);
283
+
284
+ this.initialized = true;
285
+ console.log(`[MmtFecClient] Initialized (SIMD: ${this.wasmIsSimdEnabled?.() ?? false})`);
286
+ }
287
+
288
+ /**
289
+ * Configure FEC from OTI
290
+ */
291
+ configureFec(oti: Uint8Array): void {
292
+ if (!this.fecDecoder) {
293
+ throw new Error("MmtFecClient not initialized");
294
+ }
295
+ this.fecDecoder.configure(oti);
296
+ }
297
+
298
+ /**
299
+ * Process an MMTP packet
300
+ *
301
+ * Handles FEC and MFU reassembly automatically.
302
+ * Calls onMfuReady when a complete MFU is available.
303
+ */
304
+ processPacket(packet: Uint8Array): void {
305
+ if (!this.fecDecoder) {
306
+ throw new Error("MmtFecClient not initialized");
307
+ }
308
+
309
+ const timestamp = performance.now();
310
+
311
+ // Parse MMTP header (12 bytes minimum)
312
+ if (packet.length < 12) return;
313
+
314
+ const fecType = (packet[0] >> 3) & 0x03;
315
+ const mmtpTimestamp = BigInt((packet[4] << 24) | (packet[5] << 16) | (packet[6] << 8) | packet[7]);
316
+
317
+ const payload = packet.subarray(12);
318
+ if (payload.length < 8) return;
319
+
320
+ // Check if FEC packet
321
+ if (fecType !== 0) {
322
+ this.processFecPacket(payload, timestamp, mmtpTimestamp);
323
+ return;
324
+ }
325
+
326
+ // Parse MPU header
327
+ const mpuSeq = (payload[0] << 24) | (payload[1] << 16) | (payload[2] << 8) | payload[3];
328
+ const fi = payload[5] & 0x03;
329
+ const fragCounter = (payload[6] << 8) | payload[7];
330
+
331
+ // MFU DU header present for FI=0 (complete) or FI=1 (first)
332
+ const hasMfuHeader = fi === 0 || fi === 1;
333
+ const headerSize = 8 + (hasMfuHeader ? 14 : 0);
334
+
335
+ if (payload.length < headerSize) return;
336
+
337
+ const data = payload.subarray(headerSize);
338
+
339
+ // Add fragment to reassembler
340
+ const fragment: MfuFragment = {
341
+ mpuSequenceNumber: mpuSeq,
342
+ fragmentationIndicator: fi,
343
+ fragmentCounter: fragCounter,
344
+ data: new Uint8Array(data),
345
+ timestamp: mmtpTimestamp,
346
+ };
347
+
348
+ const mfu = this.mfuReassembler.addFragment(fragment);
349
+ if (mfu) {
350
+ this.options.onMfuReady(mfu);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Process FEC repair packet
356
+ */
357
+ private processFecPacket(payload: Uint8Array, timestamp: number, mmtpTimestamp: bigint): void {
358
+ if (!this.fecDecoder || !this.wasmParseFecPayloadId || payload.length < 4) return;
359
+
360
+ try {
361
+ const { sbn, esi } = this.wasmParseFecPayloadId(payload.subarray(0, 4));
362
+ const data = payload.subarray(4);
363
+
364
+ // Add repair packet to FEC decoder
365
+ const decoded = this.fecDecoder.add_repair(sbn, esi, data, timestamp);
366
+
367
+ if (decoded && decoded.length > 0) {
368
+ // FEC recovery successful - create MFU from decoded data
369
+ const mfu: ReassembledMfu = {
370
+ mpuSequenceNumber: sbn, // Use SBN as sequence
371
+ data: new Uint8Array(decoded),
372
+ timestamp: mmtpTimestamp,
373
+ fragmentCount: 0,
374
+ recoveredViaFec: true,
375
+ };
376
+ this.options.onMfuReady(mfu);
377
+ }
378
+ } catch (e) {
379
+ console.warn("[MmtFecClient] FEC packet parse error:", e);
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Cleanup expired blocks and MFUs
385
+ */
386
+ cleanup(): void {
387
+ if (!this.fecDecoder) return;
388
+
389
+ const greenFills = this.fecDecoder.cleanup(performance.now());
390
+
391
+ if (greenFills.length > 0 && this.options.greenFillEnabled) {
392
+ this.greenFillCount++;
393
+ this.options.onGreenFill(BigInt(performance.now()));
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Get current statistics
399
+ */
400
+ getStats(): ProcessingStats {
401
+ const fecStats = this.fecDecoder?.get_stats();
402
+ const mfuStats = this.mfuReassembler.stats;
403
+
404
+ return {
405
+ mfuComplete: mfuStats.complete,
406
+ mfuReassembled: mfuStats.reassembled,
407
+ mfuTimedOut: mfuStats.timedOut,
408
+ fecSourcePackets: Number(fecStats?.source_packets ?? 0n),
409
+ fecRepairPackets: Number(fecStats?.repair_packets ?? 0n),
410
+ fecBlocksComplete: Number(fecStats?.blocks_complete ?? 0n),
411
+ fecBlocksRecovered: Number(fecStats?.blocks_recovered ?? 0n),
412
+ fecBlocksFailed: Number(fecStats?.blocks_failed ?? 0n),
413
+ fecRecoveryRate: fecStats?.recovery_rate() ?? 100,
414
+ greenFillFrames: this.greenFillCount,
415
+ };
416
+ }
417
+
418
+ /**
419
+ * Reset statistics
420
+ */
421
+ resetStats(): void {
422
+ this.fecDecoder?.reset_stats();
423
+ this.greenFillCount = 0;
424
+ }
425
+
426
+ /**
427
+ * Get current FEC configuration
428
+ * Returns the OTI parameters and interleave depth
429
+ */
430
+ getConfig(): FecDecoderConfig {
431
+ return {
432
+ symbolSize: this.fecDecoder?.get_symbol_size() ?? 1280,
433
+ sourceBlocks: this.fecDecoder?.get_source_blocks() ?? 1,
434
+ subBlocks: this.fecDecoder?.get_sub_blocks() ?? 1,
435
+ alignment: this.fecDecoder?.get_alignment() ?? 8,
436
+ interleaveDepth: this.fecDecoder?.get_interleave_depth() ?? this.options.interleaveDepth,
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Set interleave depth
442
+ *
443
+ * The interleave depth determines how many blocks can be "in flight" at once.
444
+ * Higher values provide better burst loss protection but increase latency.
445
+ *
446
+ * @param depth - Number of interleaved blocks (min: 1, typical: 30 for ~1s at 30fps)
447
+ */
448
+ setInterleaveDepth(depth: number): void {
449
+ if (!this.fecDecoder) return;
450
+ this.fecDecoder.set_interleave_depth(Math.max(1, depth));
451
+ this.options.interleaveDepth = Math.max(1, depth);
452
+ }
453
+
454
+ /**
455
+ * Cleanup blocks that are too old based on interleave depth
456
+ *
457
+ * This method expires blocks that are more than `interleaveDepth` behind
458
+ * the current block number. Call this when processing a new source block
459
+ * to maintain memory bounds.
460
+ *
461
+ * @param currentSbn - Current source block number being processed
462
+ * @returns Array of expired block numbers
463
+ */
464
+ cleanupByInterleave(currentSbn: number): number[] {
465
+ if (!this.fecDecoder) return [];
466
+ return Array.from(this.fecDecoder.cleanup_by_interleave(currentSbn));
467
+ }
468
+
469
+ /**
470
+ * Check if initialized
471
+ */
472
+ isInitialized(): boolean {
473
+ return this.initialized;
474
+ }
475
+
476
+ /**
477
+ * Dispose of resources
478
+ */
479
+ dispose(): void {
480
+ if (this.cleanupInterval) {
481
+ clearInterval(this.cleanupInterval);
482
+ }
483
+ this.mfuReassembler.flush();
484
+ this.fecDecoder?.flush();
485
+ this.initialized = false;
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Integration with window.Multicast API (pim-multicast-gateway)
491
+ */
492
+ export function createMulticastFecHandler(
493
+ client: MmtFecClient,
494
+ options?: { onPacket?: (data: ArrayBuffer) => void },
495
+ ): (data: ArrayBuffer) => void {
496
+ return (data: ArrayBuffer) => {
497
+ const packet = new Uint8Array(data);
498
+ client.processPacket(packet);
499
+ options?.onPacket?.(data);
500
+ };
501
+ }
502
+
503
+ /**
504
+ * Quick setup for hang/moqtail integration
505
+ */
506
+ export async function setupMmtFec(options?: MmtFecClientOptions): Promise<MmtFecClient> {
507
+ const client = new MmtFecClient(options);
508
+
509
+ // Integrate with window.Multicast if available
510
+ if (typeof window !== "undefined" && (window as unknown as { Multicast?: unknown }).Multicast) {
511
+ console.log("[MmtFecClient] window.Multicast detected, ready for integration");
512
+ }
513
+
514
+ return client;
515
+ }
516
+
517
+ // =============================================================================
518
+ // MoQ FEC Client (for WebTransport)
519
+ // =============================================================================
520
+
521
+ /**
522
+ * MoQ FEC configuration
523
+ */
524
+ export interface MoqFecClientConfig {
525
+ /** Symbol size in bytes (typically 1280 for MTU) */
526
+ symbolSize: number;
527
+ /** Source symbols per block (K) */
528
+ k: number;
529
+ /** Repair symbols per block (P) */
530
+ p: number;
531
+ /** Interleave depth for burst loss protection */
532
+ interleaveDepth: number;
533
+ }
534
+
535
+ /**
536
+ * MoQ FEC statistics for ABR
537
+ */
538
+ export interface MoqFecClientStats {
539
+ /** Blocks successfully decoded */
540
+ blocksComplete: number;
541
+ /** Blocks with some symbols but not yet complete */
542
+ blocksIncomplete: number;
543
+ /** Total symbols received */
544
+ totalSymbolsReceived: number;
545
+ /** Source symbols per block (K) */
546
+ k: number;
547
+ /** Repair symbols per block (P) */
548
+ p: number;
549
+ /** Interleave depth */
550
+ interleaveDepth: number;
551
+ /** Effective loss rate after FEC recovery (0.0-1.0) */
552
+ effectiveLossRate: number;
553
+ /** Recovery rate (0.0-1.0) */
554
+ recoveryRate: number;
555
+ }
556
+
557
+ /**
558
+ * MoQ FEC Encoder interface (WASM module)
559
+ */
560
+ interface WasmMoqFecEncoder {
561
+ encodeBlock(data: Uint8Array): {
562
+ block_id: number;
563
+ original_len: number;
564
+ k: number;
565
+ p: number;
566
+ getOti(): Uint8Array;
567
+ sourceSymbolCount(): number;
568
+ getSourceSymbol(index: number): Uint8Array | null;
569
+ repairSymbolCount(): number;
570
+ getRepairSymbol(index: number): Uint8Array | null;
571
+ };
572
+ blockCount(): number;
573
+ getOti(): Uint8Array;
574
+ }
575
+
576
+ /**
577
+ * MoQ FEC Decoder interface (WASM module)
578
+ */
579
+ interface WasmMoqFecDecoder {
580
+ addSourceSymbol(
581
+ blockId: number,
582
+ esi: number,
583
+ symbol: Uint8Array,
584
+ originalLen?: number,
585
+ ): Uint8Array | null;
586
+ addRepairSymbol(blockId: number, esi: number, symbol: Uint8Array): Uint8Array | null;
587
+ isBlockComplete(blockId: number): boolean;
588
+ getBlockData(blockId: number): Uint8Array;
589
+ resetBlock(blockId: number): void;
590
+ getStats(): {
591
+ blocksComplete: number;
592
+ blocksIncomplete: number;
593
+ totalSymbolsReceived: number;
594
+ k: number;
595
+ p: number;
596
+ interleaveDepth: number;
597
+ effectiveLossRate(): number;
598
+ recoveryRate(): number;
599
+ };
600
+ }
601
+
602
+ /**
603
+ * MoQ FEC Config interface (WASM module)
604
+ */
605
+ interface WasmMoqFecConfig {
606
+ maxBlockSize(): number;
607
+ overheadRatio(): number;
608
+ symbol_size: number;
609
+ k: number;
610
+ p: number;
611
+ interleaveDepth: number;
612
+ }
613
+
614
+ /**
615
+ * MoQ FEC Client
616
+ *
617
+ * Wraps mmt-wasm MoQ FEC bindings for use in TypeScript applications.
618
+ * Designed for WebTransport-based MoQ streaming with FEC protection.
619
+ *
620
+ * ## Usage
621
+ *
622
+ * ```typescript
623
+ * import { MoqFecClient } from '@blockcast/ssm-transport';
624
+ *
625
+ * const client = new MoqFecClient({
626
+ * symbolSize: 1280,
627
+ * k: 10,
628
+ * p: 3,
629
+ * interleaveDepth: 30,
630
+ * });
631
+ *
632
+ * await client.init();
633
+ *
634
+ * // Encoder (publisher side)
635
+ * const encoded = client.encodeBlock(groupData);
636
+ * encoded.sourceSymbols.forEach((symbol, esi) => {
637
+ * sendSourceSymbol(encoded.blockId, esi, symbol);
638
+ * });
639
+ * encoded.repairSymbols.forEach((symbol, i) => {
640
+ * sendRepairSymbol(encoded.blockId, encoded.k + i, symbol);
641
+ * });
642
+ *
643
+ * // Decoder (subscriber side)
644
+ * client.setOti(encoded.oti);
645
+ * onSourceSymbol((blockId, esi, symbol) => {
646
+ * const decoded = client.addSourceSymbol(blockId, esi, symbol);
647
+ * if (decoded) processGroup(decoded);
648
+ * });
649
+ * ```
650
+ */
651
+ // Global WASM module cache
652
+ let wasmModuleCache: {
653
+ init: () => Promise<void>;
654
+ MoqFecConfig: new (symbolSize: number, k: number, p: number, interleaveDepth: number) => WasmMoqFecConfig;
655
+ MoqFecEncoder: new (config: WasmMoqFecConfig) => WasmMoqFecEncoder;
656
+ MoqFecDecoder: new (config: WasmMoqFecConfig, oti: Uint8Array) => WasmMoqFecDecoder;
657
+ } | null = null;
658
+
659
+ /**
660
+ * Load the mmt-wasm module dynamically
661
+ *
662
+ * @param wasmPath - Path to the mmt-wasm module (default: tries common locations)
663
+ * @returns The loaded WASM module
664
+ */
665
+ export async function loadMmtWasm(wasmPath?: string): Promise<typeof wasmModuleCache> {
666
+ if (wasmModuleCache) {
667
+ return wasmModuleCache;
668
+ }
669
+
670
+ // Try multiple paths to find the WASM module
671
+ const paths = wasmPath
672
+ ? [wasmPath]
673
+ : [
674
+ // NPM package names (preferred - resolved by bundler/node)
675
+ "mmt-wasm",
676
+ "@mmt/wasm",
677
+ "@blockcast/mmt-wasm",
678
+ // Relative paths from common project structures
679
+ "../mmt-wasm/pkg/mmt_wasm.js",
680
+ "../../mmt-wasm/pkg/mmt_wasm.js",
681
+ "../../../libmmt/mmt-wasm/pkg/mmt_wasm.js",
682
+ // Absolute paths for web serving
683
+ "/libs/mmt-wasm/pkg/mmt_wasm.js",
684
+ "/mmt-wasm/pkg/mmt_wasm.js",
685
+ "/pkg/mmt_wasm.js",
686
+ "./mmt_wasm.js",
687
+ ];
688
+
689
+ for (const path of paths) {
690
+ try {
691
+ // Dynamic import
692
+ const module = await import(/* webpackIgnore: true */ path);
693
+
694
+ // Check if it has the expected exports
695
+ if (module.MoqFecConfig && module.MoqFecEncoder && module.MoqFecDecoder) {
696
+ // Initialize the WASM module
697
+ if (module.default) {
698
+ await module.default();
699
+ }
700
+
701
+ wasmModuleCache = {
702
+ init: async () => {
703
+ /* already initialized */
704
+ },
705
+ MoqFecConfig: module.MoqFecConfig,
706
+ MoqFecEncoder: module.MoqFecEncoder,
707
+ MoqFecDecoder: module.MoqFecDecoder,
708
+ };
709
+
710
+ console.log(`[MoqFecClient] Loaded mmt-wasm from ${path}`);
711
+ return wasmModuleCache;
712
+ }
713
+ } catch {
714
+ // Try next path
715
+ continue;
716
+ }
717
+ }
718
+
719
+ throw new Error(
720
+ `Failed to load mmt-wasm module. Tried paths: ${paths.join(", ")}. ` +
721
+ `Make sure mmt-wasm is built and accessible.`,
722
+ );
723
+ }
724
+
725
+ /**
726
+ * Create a MoqFecClient with auto-loaded WASM module
727
+ *
728
+ * @param config - FEC configuration
729
+ * @param wasmPath - Optional path to mmt-wasm module
730
+ * @returns Initialized MoqFecClient
731
+ */
732
+ export async function createMoqFecClient(
733
+ config: Partial<MoqFecClientConfig> = {},
734
+ wasmPath?: string,
735
+ ): Promise<MoqFecClient> {
736
+ const client = new MoqFecClient(config);
737
+
738
+ try {
739
+ const wasmModule = await loadMmtWasm(wasmPath);
740
+ if (wasmModule) {
741
+ client.setWasmModule(wasmModule);
742
+ await client.init();
743
+ }
744
+ } catch (err) {
745
+ console.warn("[MoqFecClient] Failed to auto-load WASM module:", err);
746
+ // Client is created but not initialized - caller can set WASM module manually
747
+ }
748
+
749
+ return client;
750
+ }
751
+
752
+ export class MoqFecClient {
753
+ private config: MoqFecClientConfig;
754
+ private encoder: WasmMoqFecEncoder | null = null;
755
+ private decoder: WasmMoqFecDecoder | null = null;
756
+ private wasmConfig: WasmMoqFecConfig | null = null;
757
+ private oti: Uint8Array | null = null;
758
+ private initialized = false;
759
+
760
+ // Callbacks for decoded blocks
761
+ private onBlockDecoded?: (blockId: number, data: Uint8Array) => void;
762
+
763
+ // WASM module references
764
+ private wasmInit?: () => Promise<void>;
765
+ private wasmMoqFecConfig?: new (
766
+ symbolSize: number,
767
+ k: number,
768
+ p: number,
769
+ interleaveDepth: number,
770
+ ) => WasmMoqFecConfig;
771
+ private wasmMoqFecEncoder?: new (config: WasmMoqFecConfig) => WasmMoqFecEncoder;
772
+ private wasmMoqFecDecoder?: new (config: WasmMoqFecConfig, oti: Uint8Array) => WasmMoqFecDecoder;
773
+
774
+ constructor(config: Partial<MoqFecClientConfig> = {}) {
775
+ this.config = {
776
+ symbolSize: config.symbolSize ?? 1280,
777
+ k: config.k ?? 10,
778
+ p: config.p ?? 3,
779
+ interleaveDepth: config.interleaveDepth ?? 30,
780
+ };
781
+ }
782
+
783
+ /**
784
+ * Create and initialize a MoqFecClient with auto-loaded WASM
785
+ *
786
+ * This is a convenience static method that handles WASM loading automatically.
787
+ *
788
+ * @param config - FEC configuration
789
+ * @param wasmPath - Optional path to mmt-wasm module
790
+ * @returns Initialized MoqFecClient
791
+ */
792
+ static async create(config: Partial<MoqFecClientConfig> = {}, wasmPath?: string): Promise<MoqFecClient> {
793
+ return createMoqFecClient(config, wasmPath);
794
+ }
795
+
796
+ /**
797
+ * Set WASM module functions (called before init)
798
+ */
799
+ setWasmModule(module: {
800
+ init: () => Promise<void>;
801
+ MoqFecConfig: new (
802
+ symbolSize: number,
803
+ k: number,
804
+ p: number,
805
+ interleaveDepth: number,
806
+ ) => WasmMoqFecConfig;
807
+ MoqFecEncoder: new (config: WasmMoqFecConfig) => WasmMoqFecEncoder;
808
+ MoqFecDecoder: new (config: WasmMoqFecConfig, oti: Uint8Array) => WasmMoqFecDecoder;
809
+ }): void {
810
+ this.wasmInit = module.init;
811
+ this.wasmMoqFecConfig = module.MoqFecConfig;
812
+ this.wasmMoqFecEncoder = module.MoqFecEncoder;
813
+ this.wasmMoqFecDecoder = module.MoqFecDecoder;
814
+ }
815
+
816
+ /**
817
+ * Initialize the WASM module and create encoder
818
+ */
819
+ async init(): Promise<void> {
820
+ if (this.initialized) return;
821
+
822
+ if (!this.wasmInit || !this.wasmMoqFecConfig || !this.wasmMoqFecEncoder) {
823
+ throw new Error("WASM module not set. Call setWasmModule() first.");
824
+ }
825
+
826
+ await this.wasmInit();
827
+
828
+ this.wasmConfig = new this.wasmMoqFecConfig(
829
+ this.config.symbolSize,
830
+ this.config.k,
831
+ this.config.p,
832
+ this.config.interleaveDepth,
833
+ );
834
+
835
+ this.encoder = new this.wasmMoqFecEncoder(this.wasmConfig);
836
+ this.oti = new Uint8Array(this.encoder.getOti());
837
+
838
+ this.initialized = true;
839
+ console.log(
840
+ `[MoqFecClient] Initialized: K=${this.config.k}, P=${this.config.p}, ` +
841
+ `symbolSize=${this.config.symbolSize}, depth=${this.config.interleaveDepth}`,
842
+ );
843
+ }
844
+
845
+ /**
846
+ * Set OTI from encoder (required for decoder)
847
+ * Call this when receiving FEC config from publisher
848
+ */
849
+ setOti(oti: Uint8Array): void {
850
+ if (oti.length !== 12) {
851
+ throw new Error("OTI must be exactly 12 bytes");
852
+ }
853
+ this.oti = new Uint8Array(oti);
854
+
855
+ // Create decoder with OTI
856
+ if (this.wasmMoqFecDecoder && this.wasmConfig) {
857
+ this.decoder = new this.wasmMoqFecDecoder(this.wasmConfig, this.oti);
858
+ console.log("[MoqFecClient] Decoder initialized with OTI");
859
+ }
860
+ }
861
+
862
+ /**
863
+ * Get the current OTI
864
+ */
865
+ getOti(): Uint8Array | null {
866
+ return this.oti;
867
+ }
868
+
869
+ /**
870
+ * Set callback for decoded blocks
871
+ */
872
+ setBlockDecodedCallback(callback: (blockId: number, data: Uint8Array) => void): void {
873
+ this.onBlockDecoded = callback;
874
+ }
875
+
876
+ /**
877
+ * Encode a data block (publisher side)
878
+ *
879
+ * @param data - The data block to encode (e.g., MoQ Group)
880
+ * @returns Encoded block with source and repair symbols
881
+ */
882
+ encodeBlock(data: Uint8Array): {
883
+ blockId: number;
884
+ originalLen: number;
885
+ k: number;
886
+ p: number;
887
+ oti: Uint8Array;
888
+ sourceSymbols: Uint8Array[];
889
+ repairSymbols: Uint8Array[];
890
+ } {
891
+ if (!this.encoder) {
892
+ throw new Error("MoqFecClient not initialized");
893
+ }
894
+
895
+ const encoded = this.encoder.encodeBlock(data);
896
+
897
+ // Collect source symbols
898
+ const sourceSymbols: Uint8Array[] = [];
899
+ for (let i = 0; i < encoded.sourceSymbolCount(); i++) {
900
+ const symbol = encoded.getSourceSymbol(i);
901
+ if (symbol) sourceSymbols.push(new Uint8Array(symbol));
902
+ }
903
+
904
+ // Collect repair symbols
905
+ const repairSymbols: Uint8Array[] = [];
906
+ for (let i = 0; i < encoded.repairSymbolCount(); i++) {
907
+ const symbol = encoded.getRepairSymbol(i);
908
+ if (symbol) repairSymbols.push(new Uint8Array(symbol));
909
+ }
910
+
911
+ return {
912
+ blockId: encoded.block_id,
913
+ originalLen: encoded.original_len,
914
+ k: encoded.k,
915
+ p: encoded.p,
916
+ oti: new Uint8Array(encoded.getOti()),
917
+ sourceSymbols,
918
+ repairSymbols,
919
+ };
920
+ }
921
+
922
+ /**
923
+ * Add a source symbol (subscriber side)
924
+ *
925
+ * @param blockId - Block this symbol belongs to
926
+ * @param esi - Encoding Symbol ID (0 to K-1)
927
+ * @param symbol - Symbol data
928
+ * @param originalLen - Original data length (optional)
929
+ * @returns Decoded data if block is complete, null otherwise
930
+ */
931
+ addSourceSymbol(
932
+ blockId: number,
933
+ esi: number,
934
+ symbol: Uint8Array,
935
+ originalLen?: number,
936
+ ): Uint8Array | null {
937
+ if (!this.decoder) {
938
+ console.warn("[MoqFecClient] Decoder not initialized, call setOti() first");
939
+ return null;
940
+ }
941
+
942
+ const decoded = this.decoder.addSourceSymbol(blockId, esi, symbol, originalLen);
943
+
944
+ if (decoded) {
945
+ this.onBlockDecoded?.(blockId, decoded);
946
+ }
947
+
948
+ return decoded;
949
+ }
950
+
951
+ /**
952
+ * Add a repair symbol (subscriber side)
953
+ *
954
+ * @param blockId - Block this symbol belongs to
955
+ * @param esi - Encoding Symbol ID (K to K+P-1)
956
+ * @param symbol - Symbol data
957
+ * @returns Decoded data if block is recovered, null otherwise
958
+ */
959
+ addRepairSymbol(blockId: number, esi: number, symbol: Uint8Array): Uint8Array | null {
960
+ if (!this.decoder) {
961
+ console.warn("[MoqFecClient] Decoder not initialized");
962
+ return null;
963
+ }
964
+
965
+ const decoded = this.decoder.addRepairSymbol(blockId, esi, symbol);
966
+
967
+ if (decoded) {
968
+ this.onBlockDecoded?.(blockId, decoded);
969
+ }
970
+
971
+ return decoded;
972
+ }
973
+
974
+ /**
975
+ * Check if a block is complete
976
+ */
977
+ isBlockComplete(blockId: number): boolean {
978
+ return this.decoder?.isBlockComplete(blockId) ?? false;
979
+ }
980
+
981
+ /**
982
+ * Get decoded data for a complete block
983
+ */
984
+ getBlockData(blockId: number): Uint8Array | null {
985
+ if (!this.decoder) return null;
986
+ try {
987
+ return this.decoder.getBlockData(blockId);
988
+ } catch {
989
+ return null;
990
+ }
991
+ }
992
+
993
+ /**
994
+ * Reset a block slot for reuse
995
+ */
996
+ resetBlock(blockId: number): void {
997
+ this.decoder?.resetBlock(blockId);
998
+ }
999
+
1000
+ /**
1001
+ * Get FEC statistics for ABR decisions
1002
+ */
1003
+ getStats(): MoqFecClientStats {
1004
+ if (!this.decoder) {
1005
+ return {
1006
+ blocksComplete: 0,
1007
+ blocksIncomplete: 0,
1008
+ totalSymbolsReceived: 0,
1009
+ k: this.config.k,
1010
+ p: this.config.p,
1011
+ interleaveDepth: this.config.interleaveDepth,
1012
+ effectiveLossRate: 0,
1013
+ recoveryRate: 1,
1014
+ };
1015
+ }
1016
+
1017
+ const stats = this.decoder.getStats();
1018
+ return {
1019
+ blocksComplete: stats.blocksComplete,
1020
+ blocksIncomplete: stats.blocksIncomplete,
1021
+ totalSymbolsReceived: stats.totalSymbolsReceived,
1022
+ k: stats.k,
1023
+ p: stats.p,
1024
+ interleaveDepth: stats.interleaveDepth,
1025
+ effectiveLossRate: stats.effectiveLossRate(),
1026
+ recoveryRate: stats.recoveryRate(),
1027
+ };
1028
+ }
1029
+
1030
+ /**
1031
+ * Get ABR-compatible statistics
1032
+ * Returns stats in format suitable for ABR controller
1033
+ */
1034
+ getAbrStats(): {
1035
+ effectiveLossRate: number;
1036
+ rawLossRate: number;
1037
+ recoveryRate: number;
1038
+ fecEnabled: boolean;
1039
+ } {
1040
+ const stats = this.getStats();
1041
+ const totalBlocks = stats.blocksComplete + stats.blocksIncomplete;
1042
+ const rawLossRate = totalBlocks > 0 ? stats.blocksIncomplete / totalBlocks : 0;
1043
+
1044
+ return {
1045
+ effectiveLossRate: stats.effectiveLossRate,
1046
+ rawLossRate,
1047
+ recoveryRate: stats.recoveryRate,
1048
+ fecEnabled: this.initialized && this.decoder !== null,
1049
+ };
1050
+ }
1051
+
1052
+ /**
1053
+ * Get the FEC configuration
1054
+ */
1055
+ getConfig(): MoqFecClientConfig {
1056
+ return { ...this.config };
1057
+ }
1058
+
1059
+ /**
1060
+ * Get max block size
1061
+ */
1062
+ getMaxBlockSize(): number {
1063
+ return this.wasmConfig?.maxBlockSize() ?? this.config.k * this.config.symbolSize;
1064
+ }
1065
+
1066
+ /**
1067
+ * Get overhead ratio (P/K)
1068
+ */
1069
+ getOverheadRatio(): number {
1070
+ return this.wasmConfig?.overheadRatio() ?? this.config.p / this.config.k;
1071
+ }
1072
+
1073
+ /**
1074
+ * Check if initialized
1075
+ */
1076
+ isInitialized(): boolean {
1077
+ return this.initialized;
1078
+ }
1079
+
1080
+ /**
1081
+ * Check if decoder is ready
1082
+ */
1083
+ isDecoderReady(): boolean {
1084
+ return this.decoder !== null;
1085
+ }
1086
+
1087
+ /**
1088
+ * Dispose of resources
1089
+ */
1090
+ dispose(): void {
1091
+ this.encoder = null;
1092
+ this.decoder = null;
1093
+ this.wasmConfig = null;
1094
+ this.oti = null;
1095
+ this.initialized = false;
1096
+ }
1097
+ }