@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,784 @@
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
+ * MFU Reassembler (TypeScript implementation for WASM compatibility)
15
+ */
16
+ class MfuReassembler {
17
+ fragmentBuffers = new Map();
18
+ timeouts = new Map();
19
+ timeoutMs;
20
+ maxMfus;
21
+ stats = {
22
+ complete: 0,
23
+ reassembled: 0,
24
+ timedOut: 0,
25
+ };
26
+ constructor(maxMfus = 100, timeoutMs = 1000) {
27
+ this.maxMfus = maxMfus;
28
+ this.timeoutMs = timeoutMs;
29
+ }
30
+ addFragment(fragment) {
31
+ const { mpuSequenceNumber, fragmentationIndicator } = fragment;
32
+ // FI=0: Complete MFU in single packet
33
+ if (fragmentationIndicator === 0) {
34
+ this.stats.complete++;
35
+ return {
36
+ mpuSequenceNumber,
37
+ data: fragment.data,
38
+ timestamp: fragment.timestamp,
39
+ fragmentCount: 1,
40
+ recoveredViaFec: false,
41
+ };
42
+ }
43
+ // Get or create buffer for this MPU
44
+ let fragments = this.fragmentBuffers.get(mpuSequenceNumber);
45
+ if (!fragments) {
46
+ fragments = [];
47
+ this.fragmentBuffers.set(mpuSequenceNumber, fragments);
48
+ this.setTimeout(mpuSequenceNumber);
49
+ }
50
+ // Check for duplicate
51
+ const existingIdx = fragments.findIndex((f) => f.fragmentCounter === fragment.fragmentCounter);
52
+ if (existingIdx >= 0) {
53
+ fragments[existingIdx] = fragment;
54
+ }
55
+ else {
56
+ fragments.push(fragment);
57
+ }
58
+ // Enforce memory limit
59
+ this.enforceLimit();
60
+ // Try to reassemble
61
+ return this.tryReassemble(mpuSequenceNumber);
62
+ }
63
+ tryReassemble(mpuSeq) {
64
+ const fragments = this.fragmentBuffers.get(mpuSeq);
65
+ if (!fragments || fragments.length === 0)
66
+ return null;
67
+ // Sort by FI
68
+ fragments.sort((a, b) => a.fragmentationIndicator - b.fragmentationIndicator);
69
+ const first = fragments[0].fragmentationIndicator;
70
+ const last = fragments[fragments.length - 1].fragmentationIndicator;
71
+ // Need FI=1 (first) and FI=3 (last)
72
+ if (first !== 1 || last !== 3)
73
+ return null;
74
+ // Verify middles are FI=2
75
+ for (let i = 1; i < fragments.length - 1; i++) {
76
+ if (fragments[i].fragmentationIndicator !== 2)
77
+ return null;
78
+ }
79
+ // Reassemble
80
+ const totalSize = fragments.reduce((sum, f) => sum + f.data.length, 0);
81
+ const reassembled = new Uint8Array(totalSize);
82
+ let offset = 0;
83
+ for (const fragment of fragments) {
84
+ reassembled.set(fragment.data, offset);
85
+ offset += fragment.data.length;
86
+ }
87
+ // Cleanup
88
+ this.clearTimeout(mpuSeq);
89
+ this.fragmentBuffers.delete(mpuSeq);
90
+ this.stats.reassembled++;
91
+ return {
92
+ mpuSequenceNumber: mpuSeq,
93
+ data: reassembled,
94
+ timestamp: fragments[0].timestamp,
95
+ fragmentCount: fragments.length,
96
+ recoveredViaFec: false,
97
+ };
98
+ }
99
+ setTimeout(mpuSeq) {
100
+ const id = setTimeout(() => this.onTimeout(mpuSeq), this.timeoutMs);
101
+ this.timeouts.set(mpuSeq, id);
102
+ }
103
+ clearTimeout(mpuSeq) {
104
+ const id = this.timeouts.get(mpuSeq);
105
+ if (id !== undefined) {
106
+ clearTimeout(id);
107
+ this.timeouts.delete(mpuSeq);
108
+ }
109
+ }
110
+ onTimeout(mpuSeq) {
111
+ this.fragmentBuffers.delete(mpuSeq);
112
+ this.timeouts.delete(mpuSeq);
113
+ this.stats.timedOut++;
114
+ }
115
+ enforceLimit() {
116
+ if (this.fragmentBuffers.size <= this.maxMfus)
117
+ return;
118
+ const sequences = Array.from(this.fragmentBuffers.keys()).sort((a, b) => a - b);
119
+ const toRemove = sequences.slice(0, sequences.length - this.maxMfus);
120
+ for (const seq of toRemove) {
121
+ this.clearTimeout(seq);
122
+ this.fragmentBuffers.delete(seq);
123
+ this.stats.timedOut++;
124
+ }
125
+ }
126
+ flush() {
127
+ for (const seq of this.fragmentBuffers.keys()) {
128
+ this.clearTimeout(seq);
129
+ }
130
+ this.fragmentBuffers.clear();
131
+ this.timeouts.clear();
132
+ }
133
+ }
134
+ /**
135
+ * MMT FEC Client
136
+ *
137
+ * Combines WASM FEC decoder with MFU reassembler for complete
138
+ * MMT packet processing in browser players.
139
+ */
140
+ export class MmtFecClient {
141
+ fecDecoder = null;
142
+ mfuReassembler;
143
+ options;
144
+ initialized = false;
145
+ greenFillCount = 0;
146
+ cleanupInterval = null;
147
+ // WASM module references (set via init)
148
+ wasmInit;
149
+ wasmFecDecoder;
150
+ wasmParseFecPayloadId;
151
+ wasmIsSimdEnabled;
152
+ constructor(options = {}) {
153
+ this.options = {
154
+ interleaveDepth: options.interleaveDepth ?? 30,
155
+ blockTimeoutMs: options.blockTimeoutMs ?? 2000,
156
+ mfuTimeoutMs: options.mfuTimeoutMs ?? 1000,
157
+ greenFillEnabled: options.greenFillEnabled ?? true,
158
+ onMfuReady: options.onMfuReady ?? (() => { }),
159
+ onGreenFill: options.onGreenFill ?? (() => { }),
160
+ onStatsUpdate: options.onStatsUpdate ?? (() => { }),
161
+ };
162
+ this.mfuReassembler = new MfuReassembler(100, this.options.mfuTimeoutMs);
163
+ }
164
+ /**
165
+ * Set WASM module functions (called before init)
166
+ */
167
+ setWasmModule(module) {
168
+ this.wasmInit = module.init;
169
+ this.wasmFecDecoder = module.MmtFecDecoder;
170
+ this.wasmParseFecPayloadId = module.parse_fec_payload_id;
171
+ this.wasmIsSimdEnabled = module.is_simd_enabled;
172
+ }
173
+ /**
174
+ * Initialize the WASM module
175
+ */
176
+ async init() {
177
+ if (this.initialized)
178
+ return;
179
+ if (!this.wasmInit || !this.wasmFecDecoder) {
180
+ throw new Error("WASM module not set. Call setWasmModule() first.");
181
+ }
182
+ await this.wasmInit();
183
+ this.fecDecoder = new this.wasmFecDecoder(this.options.interleaveDepth);
184
+ this.fecDecoder.set_timeout(this.options.blockTimeoutMs);
185
+ this.fecDecoder.set_green_fill(this.options.greenFillEnabled);
186
+ // Start periodic cleanup
187
+ this.cleanupInterval = setInterval(() => {
188
+ this.cleanup();
189
+ }, 500);
190
+ this.initialized = true;
191
+ console.log(`[MmtFecClient] Initialized (SIMD: ${this.wasmIsSimdEnabled?.() ?? false})`);
192
+ }
193
+ /**
194
+ * Configure FEC from OTI
195
+ */
196
+ configureFec(oti) {
197
+ if (!this.fecDecoder) {
198
+ throw new Error("MmtFecClient not initialized");
199
+ }
200
+ this.fecDecoder.configure(oti);
201
+ }
202
+ /**
203
+ * Process an MMTP packet
204
+ *
205
+ * Handles FEC and MFU reassembly automatically.
206
+ * Calls onMfuReady when a complete MFU is available.
207
+ */
208
+ processPacket(packet) {
209
+ if (!this.fecDecoder) {
210
+ throw new Error("MmtFecClient not initialized");
211
+ }
212
+ const timestamp = performance.now();
213
+ // Parse MMTP header (12 bytes minimum)
214
+ if (packet.length < 12)
215
+ return;
216
+ const fecType = (packet[0] >> 3) & 0x03;
217
+ const mmtpTimestamp = BigInt((packet[4] << 24) | (packet[5] << 16) | (packet[6] << 8) | packet[7]);
218
+ const payload = packet.subarray(12);
219
+ if (payload.length < 8)
220
+ return;
221
+ // Check if FEC packet
222
+ if (fecType !== 0) {
223
+ this.processFecPacket(payload, timestamp, mmtpTimestamp);
224
+ return;
225
+ }
226
+ // Parse MPU header
227
+ const mpuSeq = (payload[0] << 24) | (payload[1] << 16) | (payload[2] << 8) | payload[3];
228
+ const fi = payload[5] & 0x03;
229
+ const fragCounter = (payload[6] << 8) | payload[7];
230
+ // MFU DU header present for FI=0 (complete) or FI=1 (first)
231
+ const hasMfuHeader = fi === 0 || fi === 1;
232
+ const headerSize = 8 + (hasMfuHeader ? 14 : 0);
233
+ if (payload.length < headerSize)
234
+ return;
235
+ const data = payload.subarray(headerSize);
236
+ // Add fragment to reassembler
237
+ const fragment = {
238
+ mpuSequenceNumber: mpuSeq,
239
+ fragmentationIndicator: fi,
240
+ fragmentCounter: fragCounter,
241
+ data: new Uint8Array(data),
242
+ timestamp: mmtpTimestamp,
243
+ };
244
+ const mfu = this.mfuReassembler.addFragment(fragment);
245
+ if (mfu) {
246
+ this.options.onMfuReady(mfu);
247
+ }
248
+ }
249
+ /**
250
+ * Process FEC repair packet
251
+ */
252
+ processFecPacket(payload, timestamp, mmtpTimestamp) {
253
+ if (!this.fecDecoder || !this.wasmParseFecPayloadId || payload.length < 4)
254
+ return;
255
+ try {
256
+ const { sbn, esi } = this.wasmParseFecPayloadId(payload.subarray(0, 4));
257
+ const data = payload.subarray(4);
258
+ // Add repair packet to FEC decoder
259
+ const decoded = this.fecDecoder.add_repair(sbn, esi, data, timestamp);
260
+ if (decoded && decoded.length > 0) {
261
+ // FEC recovery successful - create MFU from decoded data
262
+ const mfu = {
263
+ mpuSequenceNumber: sbn, // Use SBN as sequence
264
+ data: new Uint8Array(decoded),
265
+ timestamp: mmtpTimestamp,
266
+ fragmentCount: 0,
267
+ recoveredViaFec: true,
268
+ };
269
+ this.options.onMfuReady(mfu);
270
+ }
271
+ }
272
+ catch (e) {
273
+ console.warn("[MmtFecClient] FEC packet parse error:", e);
274
+ }
275
+ }
276
+ /**
277
+ * Cleanup expired blocks and MFUs
278
+ */
279
+ cleanup() {
280
+ if (!this.fecDecoder)
281
+ return;
282
+ const greenFills = this.fecDecoder.cleanup(performance.now());
283
+ if (greenFills.length > 0 && this.options.greenFillEnabled) {
284
+ this.greenFillCount++;
285
+ this.options.onGreenFill(BigInt(performance.now()));
286
+ }
287
+ }
288
+ /**
289
+ * Get current statistics
290
+ */
291
+ getStats() {
292
+ const fecStats = this.fecDecoder?.get_stats();
293
+ const mfuStats = this.mfuReassembler.stats;
294
+ return {
295
+ mfuComplete: mfuStats.complete,
296
+ mfuReassembled: mfuStats.reassembled,
297
+ mfuTimedOut: mfuStats.timedOut,
298
+ fecSourcePackets: Number(fecStats?.source_packets ?? 0n),
299
+ fecRepairPackets: Number(fecStats?.repair_packets ?? 0n),
300
+ fecBlocksComplete: Number(fecStats?.blocks_complete ?? 0n),
301
+ fecBlocksRecovered: Number(fecStats?.blocks_recovered ?? 0n),
302
+ fecBlocksFailed: Number(fecStats?.blocks_failed ?? 0n),
303
+ fecRecoveryRate: fecStats?.recovery_rate() ?? 100,
304
+ greenFillFrames: this.greenFillCount,
305
+ };
306
+ }
307
+ /**
308
+ * Reset statistics
309
+ */
310
+ resetStats() {
311
+ this.fecDecoder?.reset_stats();
312
+ this.greenFillCount = 0;
313
+ }
314
+ /**
315
+ * Get current FEC configuration
316
+ * Returns the OTI parameters and interleave depth
317
+ */
318
+ getConfig() {
319
+ return {
320
+ symbolSize: this.fecDecoder?.get_symbol_size() ?? 1280,
321
+ sourceBlocks: this.fecDecoder?.get_source_blocks() ?? 1,
322
+ subBlocks: this.fecDecoder?.get_sub_blocks() ?? 1,
323
+ alignment: this.fecDecoder?.get_alignment() ?? 8,
324
+ interleaveDepth: this.fecDecoder?.get_interleave_depth() ?? this.options.interleaveDepth,
325
+ };
326
+ }
327
+ /**
328
+ * Set interleave depth
329
+ *
330
+ * The interleave depth determines how many blocks can be "in flight" at once.
331
+ * Higher values provide better burst loss protection but increase latency.
332
+ *
333
+ * @param depth - Number of interleaved blocks (min: 1, typical: 30 for ~1s at 30fps)
334
+ */
335
+ setInterleaveDepth(depth) {
336
+ if (!this.fecDecoder)
337
+ return;
338
+ this.fecDecoder.set_interleave_depth(Math.max(1, depth));
339
+ this.options.interleaveDepth = Math.max(1, depth);
340
+ }
341
+ /**
342
+ * Cleanup blocks that are too old based on interleave depth
343
+ *
344
+ * This method expires blocks that are more than `interleaveDepth` behind
345
+ * the current block number. Call this when processing a new source block
346
+ * to maintain memory bounds.
347
+ *
348
+ * @param currentSbn - Current source block number being processed
349
+ * @returns Array of expired block numbers
350
+ */
351
+ cleanupByInterleave(currentSbn) {
352
+ if (!this.fecDecoder)
353
+ return [];
354
+ return Array.from(this.fecDecoder.cleanup_by_interleave(currentSbn));
355
+ }
356
+ /**
357
+ * Check if initialized
358
+ */
359
+ isInitialized() {
360
+ return this.initialized;
361
+ }
362
+ /**
363
+ * Dispose of resources
364
+ */
365
+ dispose() {
366
+ if (this.cleanupInterval) {
367
+ clearInterval(this.cleanupInterval);
368
+ }
369
+ this.mfuReassembler.flush();
370
+ this.fecDecoder?.flush();
371
+ this.initialized = false;
372
+ }
373
+ }
374
+ /**
375
+ * Integration with window.Multicast API (pim-multicast-gateway)
376
+ */
377
+ export function createMulticastFecHandler(client, options) {
378
+ return (data) => {
379
+ const packet = new Uint8Array(data);
380
+ client.processPacket(packet);
381
+ options?.onPacket?.(data);
382
+ };
383
+ }
384
+ /**
385
+ * Quick setup for hang/moqtail integration
386
+ */
387
+ export async function setupMmtFec(options) {
388
+ const client = new MmtFecClient(options);
389
+ // Integrate with window.Multicast if available
390
+ if (typeof window !== "undefined" && window.Multicast) {
391
+ console.log("[MmtFecClient] window.Multicast detected, ready for integration");
392
+ }
393
+ return client;
394
+ }
395
+ /**
396
+ * MoQ FEC Client
397
+ *
398
+ * Wraps mmt-wasm MoQ FEC bindings for use in TypeScript applications.
399
+ * Designed for WebTransport-based MoQ streaming with FEC protection.
400
+ *
401
+ * ## Usage
402
+ *
403
+ * ```typescript
404
+ * import { MoqFecClient } from '@blockcast/ssm-transport';
405
+ *
406
+ * const client = new MoqFecClient({
407
+ * symbolSize: 1280,
408
+ * k: 10,
409
+ * p: 3,
410
+ * interleaveDepth: 30,
411
+ * });
412
+ *
413
+ * await client.init();
414
+ *
415
+ * // Encoder (publisher side)
416
+ * const encoded = client.encodeBlock(groupData);
417
+ * encoded.sourceSymbols.forEach((symbol, esi) => {
418
+ * sendSourceSymbol(encoded.blockId, esi, symbol);
419
+ * });
420
+ * encoded.repairSymbols.forEach((symbol, i) => {
421
+ * sendRepairSymbol(encoded.blockId, encoded.k + i, symbol);
422
+ * });
423
+ *
424
+ * // Decoder (subscriber side)
425
+ * client.setOti(encoded.oti);
426
+ * onSourceSymbol((blockId, esi, symbol) => {
427
+ * const decoded = client.addSourceSymbol(blockId, esi, symbol);
428
+ * if (decoded) processGroup(decoded);
429
+ * });
430
+ * ```
431
+ */
432
+ // Global WASM module cache
433
+ let wasmModuleCache = null;
434
+ /**
435
+ * Load the mmt-wasm module dynamically
436
+ *
437
+ * @param wasmPath - Path to the mmt-wasm module (default: tries common locations)
438
+ * @returns The loaded WASM module
439
+ */
440
+ export async function loadMmtWasm(wasmPath) {
441
+ if (wasmModuleCache) {
442
+ return wasmModuleCache;
443
+ }
444
+ // Try multiple paths to find the WASM module
445
+ const paths = wasmPath
446
+ ? [wasmPath]
447
+ : [
448
+ // NPM package names (preferred - resolved by bundler/node)
449
+ "mmt-wasm",
450
+ "@mmt/wasm",
451
+ "@blockcast/mmt-wasm",
452
+ // Relative paths from common project structures
453
+ "../mmt-wasm/pkg/mmt_wasm.js",
454
+ "../../mmt-wasm/pkg/mmt_wasm.js",
455
+ "../../../libmmt/mmt-wasm/pkg/mmt_wasm.js",
456
+ // Absolute paths for web serving
457
+ "/libs/mmt-wasm/pkg/mmt_wasm.js",
458
+ "/mmt-wasm/pkg/mmt_wasm.js",
459
+ "/pkg/mmt_wasm.js",
460
+ "./mmt_wasm.js",
461
+ ];
462
+ for (const path of paths) {
463
+ try {
464
+ // Dynamic import
465
+ const module = await import(/* webpackIgnore: true */ path);
466
+ // Check if it has the expected exports
467
+ if (module.MoqFecConfig && module.MoqFecEncoder && module.MoqFecDecoder) {
468
+ // Initialize the WASM module
469
+ if (module.default) {
470
+ await module.default();
471
+ }
472
+ wasmModuleCache = {
473
+ init: async () => {
474
+ /* already initialized */
475
+ },
476
+ MoqFecConfig: module.MoqFecConfig,
477
+ MoqFecEncoder: module.MoqFecEncoder,
478
+ MoqFecDecoder: module.MoqFecDecoder,
479
+ };
480
+ console.log(`[MoqFecClient] Loaded mmt-wasm from ${path}`);
481
+ return wasmModuleCache;
482
+ }
483
+ }
484
+ catch {
485
+ // Try next path
486
+ continue;
487
+ }
488
+ }
489
+ throw new Error(`Failed to load mmt-wasm module. Tried paths: ${paths.join(", ")}. ` +
490
+ `Make sure mmt-wasm is built and accessible.`);
491
+ }
492
+ /**
493
+ * Create a MoqFecClient with auto-loaded WASM module
494
+ *
495
+ * @param config - FEC configuration
496
+ * @param wasmPath - Optional path to mmt-wasm module
497
+ * @returns Initialized MoqFecClient
498
+ */
499
+ export async function createMoqFecClient(config = {}, wasmPath) {
500
+ const client = new MoqFecClient(config);
501
+ try {
502
+ const wasmModule = await loadMmtWasm(wasmPath);
503
+ if (wasmModule) {
504
+ client.setWasmModule(wasmModule);
505
+ await client.init();
506
+ }
507
+ }
508
+ catch (err) {
509
+ console.warn("[MoqFecClient] Failed to auto-load WASM module:", err);
510
+ // Client is created but not initialized - caller can set WASM module manually
511
+ }
512
+ return client;
513
+ }
514
+ export class MoqFecClient {
515
+ config;
516
+ encoder = null;
517
+ decoder = null;
518
+ wasmConfig = null;
519
+ oti = null;
520
+ initialized = false;
521
+ // Callbacks for decoded blocks
522
+ onBlockDecoded;
523
+ // WASM module references
524
+ wasmInit;
525
+ wasmMoqFecConfig;
526
+ wasmMoqFecEncoder;
527
+ wasmMoqFecDecoder;
528
+ constructor(config = {}) {
529
+ this.config = {
530
+ symbolSize: config.symbolSize ?? 1280,
531
+ k: config.k ?? 10,
532
+ p: config.p ?? 3,
533
+ interleaveDepth: config.interleaveDepth ?? 30,
534
+ };
535
+ }
536
+ /**
537
+ * Create and initialize a MoqFecClient with auto-loaded WASM
538
+ *
539
+ * This is a convenience static method that handles WASM loading automatically.
540
+ *
541
+ * @param config - FEC configuration
542
+ * @param wasmPath - Optional path to mmt-wasm module
543
+ * @returns Initialized MoqFecClient
544
+ */
545
+ static async create(config = {}, wasmPath) {
546
+ return createMoqFecClient(config, wasmPath);
547
+ }
548
+ /**
549
+ * Set WASM module functions (called before init)
550
+ */
551
+ setWasmModule(module) {
552
+ this.wasmInit = module.init;
553
+ this.wasmMoqFecConfig = module.MoqFecConfig;
554
+ this.wasmMoqFecEncoder = module.MoqFecEncoder;
555
+ this.wasmMoqFecDecoder = module.MoqFecDecoder;
556
+ }
557
+ /**
558
+ * Initialize the WASM module and create encoder
559
+ */
560
+ async init() {
561
+ if (this.initialized)
562
+ return;
563
+ if (!this.wasmInit || !this.wasmMoqFecConfig || !this.wasmMoqFecEncoder) {
564
+ throw new Error("WASM module not set. Call setWasmModule() first.");
565
+ }
566
+ await this.wasmInit();
567
+ this.wasmConfig = new this.wasmMoqFecConfig(this.config.symbolSize, this.config.k, this.config.p, this.config.interleaveDepth);
568
+ this.encoder = new this.wasmMoqFecEncoder(this.wasmConfig);
569
+ this.oti = new Uint8Array(this.encoder.getOti());
570
+ this.initialized = true;
571
+ console.log(`[MoqFecClient] Initialized: K=${this.config.k}, P=${this.config.p}, ` +
572
+ `symbolSize=${this.config.symbolSize}, depth=${this.config.interleaveDepth}`);
573
+ }
574
+ /**
575
+ * Set OTI from encoder (required for decoder)
576
+ * Call this when receiving FEC config from publisher
577
+ */
578
+ setOti(oti) {
579
+ if (oti.length !== 12) {
580
+ throw new Error("OTI must be exactly 12 bytes");
581
+ }
582
+ this.oti = new Uint8Array(oti);
583
+ // Create decoder with OTI
584
+ if (this.wasmMoqFecDecoder && this.wasmConfig) {
585
+ this.decoder = new this.wasmMoqFecDecoder(this.wasmConfig, this.oti);
586
+ console.log("[MoqFecClient] Decoder initialized with OTI");
587
+ }
588
+ }
589
+ /**
590
+ * Get the current OTI
591
+ */
592
+ getOti() {
593
+ return this.oti;
594
+ }
595
+ /**
596
+ * Set callback for decoded blocks
597
+ */
598
+ setBlockDecodedCallback(callback) {
599
+ this.onBlockDecoded = callback;
600
+ }
601
+ /**
602
+ * Encode a data block (publisher side)
603
+ *
604
+ * @param data - The data block to encode (e.g., MoQ Group)
605
+ * @returns Encoded block with source and repair symbols
606
+ */
607
+ encodeBlock(data) {
608
+ if (!this.encoder) {
609
+ throw new Error("MoqFecClient not initialized");
610
+ }
611
+ const encoded = this.encoder.encodeBlock(data);
612
+ // Collect source symbols
613
+ const sourceSymbols = [];
614
+ for (let i = 0; i < encoded.sourceSymbolCount(); i++) {
615
+ const symbol = encoded.getSourceSymbol(i);
616
+ if (symbol)
617
+ sourceSymbols.push(new Uint8Array(symbol));
618
+ }
619
+ // Collect repair symbols
620
+ const repairSymbols = [];
621
+ for (let i = 0; i < encoded.repairSymbolCount(); i++) {
622
+ const symbol = encoded.getRepairSymbol(i);
623
+ if (symbol)
624
+ repairSymbols.push(new Uint8Array(symbol));
625
+ }
626
+ return {
627
+ blockId: encoded.block_id,
628
+ originalLen: encoded.original_len,
629
+ k: encoded.k,
630
+ p: encoded.p,
631
+ oti: new Uint8Array(encoded.getOti()),
632
+ sourceSymbols,
633
+ repairSymbols,
634
+ };
635
+ }
636
+ /**
637
+ * Add a source symbol (subscriber side)
638
+ *
639
+ * @param blockId - Block this symbol belongs to
640
+ * @param esi - Encoding Symbol ID (0 to K-1)
641
+ * @param symbol - Symbol data
642
+ * @param originalLen - Original data length (optional)
643
+ * @returns Decoded data if block is complete, null otherwise
644
+ */
645
+ addSourceSymbol(blockId, esi, symbol, originalLen) {
646
+ if (!this.decoder) {
647
+ console.warn("[MoqFecClient] Decoder not initialized, call setOti() first");
648
+ return null;
649
+ }
650
+ const decoded = this.decoder.addSourceSymbol(blockId, esi, symbol, originalLen);
651
+ if (decoded) {
652
+ this.onBlockDecoded?.(blockId, decoded);
653
+ }
654
+ return decoded;
655
+ }
656
+ /**
657
+ * Add a repair symbol (subscriber side)
658
+ *
659
+ * @param blockId - Block this symbol belongs to
660
+ * @param esi - Encoding Symbol ID (K to K+P-1)
661
+ * @param symbol - Symbol data
662
+ * @returns Decoded data if block is recovered, null otherwise
663
+ */
664
+ addRepairSymbol(blockId, esi, symbol) {
665
+ if (!this.decoder) {
666
+ console.warn("[MoqFecClient] Decoder not initialized");
667
+ return null;
668
+ }
669
+ const decoded = this.decoder.addRepairSymbol(blockId, esi, symbol);
670
+ if (decoded) {
671
+ this.onBlockDecoded?.(blockId, decoded);
672
+ }
673
+ return decoded;
674
+ }
675
+ /**
676
+ * Check if a block is complete
677
+ */
678
+ isBlockComplete(blockId) {
679
+ return this.decoder?.isBlockComplete(blockId) ?? false;
680
+ }
681
+ /**
682
+ * Get decoded data for a complete block
683
+ */
684
+ getBlockData(blockId) {
685
+ if (!this.decoder)
686
+ return null;
687
+ try {
688
+ return this.decoder.getBlockData(blockId);
689
+ }
690
+ catch {
691
+ return null;
692
+ }
693
+ }
694
+ /**
695
+ * Reset a block slot for reuse
696
+ */
697
+ resetBlock(blockId) {
698
+ this.decoder?.resetBlock(blockId);
699
+ }
700
+ /**
701
+ * Get FEC statistics for ABR decisions
702
+ */
703
+ getStats() {
704
+ if (!this.decoder) {
705
+ return {
706
+ blocksComplete: 0,
707
+ blocksIncomplete: 0,
708
+ totalSymbolsReceived: 0,
709
+ k: this.config.k,
710
+ p: this.config.p,
711
+ interleaveDepth: this.config.interleaveDepth,
712
+ effectiveLossRate: 0,
713
+ recoveryRate: 1,
714
+ };
715
+ }
716
+ const stats = this.decoder.getStats();
717
+ return {
718
+ blocksComplete: stats.blocksComplete,
719
+ blocksIncomplete: stats.blocksIncomplete,
720
+ totalSymbolsReceived: stats.totalSymbolsReceived,
721
+ k: stats.k,
722
+ p: stats.p,
723
+ interleaveDepth: stats.interleaveDepth,
724
+ effectiveLossRate: stats.effectiveLossRate(),
725
+ recoveryRate: stats.recoveryRate(),
726
+ };
727
+ }
728
+ /**
729
+ * Get ABR-compatible statistics
730
+ * Returns stats in format suitable for ABR controller
731
+ */
732
+ getAbrStats() {
733
+ const stats = this.getStats();
734
+ const totalBlocks = stats.blocksComplete + stats.blocksIncomplete;
735
+ const rawLossRate = totalBlocks > 0 ? stats.blocksIncomplete / totalBlocks : 0;
736
+ return {
737
+ effectiveLossRate: stats.effectiveLossRate,
738
+ rawLossRate,
739
+ recoveryRate: stats.recoveryRate,
740
+ fecEnabled: this.initialized && this.decoder !== null,
741
+ };
742
+ }
743
+ /**
744
+ * Get the FEC configuration
745
+ */
746
+ getConfig() {
747
+ return { ...this.config };
748
+ }
749
+ /**
750
+ * Get max block size
751
+ */
752
+ getMaxBlockSize() {
753
+ return this.wasmConfig?.maxBlockSize() ?? this.config.k * this.config.symbolSize;
754
+ }
755
+ /**
756
+ * Get overhead ratio (P/K)
757
+ */
758
+ getOverheadRatio() {
759
+ return this.wasmConfig?.overheadRatio() ?? this.config.p / this.config.k;
760
+ }
761
+ /**
762
+ * Check if initialized
763
+ */
764
+ isInitialized() {
765
+ return this.initialized;
766
+ }
767
+ /**
768
+ * Check if decoder is ready
769
+ */
770
+ isDecoderReady() {
771
+ return this.decoder !== null;
772
+ }
773
+ /**
774
+ * Dispose of resources
775
+ */
776
+ dispose() {
777
+ this.encoder = null;
778
+ this.decoder = null;
779
+ this.wasmConfig = null;
780
+ this.oti = null;
781
+ this.initialized = false;
782
+ }
783
+ }
784
+ //# sourceMappingURL=fec-client.js.map