@blockcast/mmt-fec 0.1.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.
- package/dist/fec-client.d.ts +191 -0
- package/dist/fec-client.d.ts.map +1 -0
- package/dist/fec-client.js +233 -0
- package/dist/fec-client.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/src/fec-client.test.ts +158 -0
- package/src/fec-client.ts +354 -0
- package/src/index.ts +31 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container-Agnostic FEC Client
|
|
3
|
+
*
|
|
4
|
+
* Provides RaptorQ-based Forward Error Correction (RFC 6330) that works with:
|
|
5
|
+
* - MMTP (MMT Protocol) containers
|
|
6
|
+
* - LOC (Low Overhead Container)
|
|
7
|
+
* - Raw NAL units with FEC_CONFIG
|
|
8
|
+
* - MoQ (Media over QUIC)
|
|
9
|
+
*
|
|
10
|
+
* Transport independent - works over both:
|
|
11
|
+
* - Unicast (MOQ/QUIC/WebTransport)
|
|
12
|
+
* - Multicast (SSM/AMT via window.Multicast API)
|
|
13
|
+
*
|
|
14
|
+
* Based on draft-ramadan-moq-fec-00
|
|
15
|
+
*/
|
|
16
|
+
interface WasmFecDecoder {
|
|
17
|
+
configure(oti: Uint8Array): void;
|
|
18
|
+
add_repair(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | undefined;
|
|
19
|
+
add_source(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | undefined;
|
|
20
|
+
cleanup(current_time: number): Uint8Array;
|
|
21
|
+
get_stats(): WasmFecStats;
|
|
22
|
+
reset_stats(): void;
|
|
23
|
+
flush(): void;
|
|
24
|
+
set_timeout(timeout_ms: number): void;
|
|
25
|
+
set_green_fill(enabled: boolean): void;
|
|
26
|
+
pending_blocks(): number;
|
|
27
|
+
}
|
|
28
|
+
interface WasmFecStats {
|
|
29
|
+
source_packets: bigint;
|
|
30
|
+
repair_packets: bigint;
|
|
31
|
+
blocks_complete: bigint;
|
|
32
|
+
blocks_recovered: bigint;
|
|
33
|
+
blocks_failed: bigint;
|
|
34
|
+
recovery_rate(): number;
|
|
35
|
+
}
|
|
36
|
+
interface WasmFecPayloadId {
|
|
37
|
+
sbn: number;
|
|
38
|
+
esi: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* FEC processing statistics (transport & container independent)
|
|
42
|
+
*/
|
|
43
|
+
export interface FecProcessingStats {
|
|
44
|
+
/** Source packets received */
|
|
45
|
+
sourcePackets: number;
|
|
46
|
+
/** Repair packets received */
|
|
47
|
+
repairPackets: number;
|
|
48
|
+
/** Blocks completed (received all source symbols) */
|
|
49
|
+
blocksComplete: number;
|
|
50
|
+
/** Blocks recovered via FEC */
|
|
51
|
+
blocksRecovered: number;
|
|
52
|
+
/** Blocks that failed to recover */
|
|
53
|
+
blocksFailed: number;
|
|
54
|
+
/** Recovery rate (0-100%) */
|
|
55
|
+
recoveryRate: number;
|
|
56
|
+
/** Green-fill frames (unrecoverable data replaced with placeholder) */
|
|
57
|
+
greenFillFrames: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* FEC configuration parameters (RFC 6330 OTI)
|
|
61
|
+
*/
|
|
62
|
+
export interface FecConfig {
|
|
63
|
+
/** Symbol size in bytes (T) */
|
|
64
|
+
symbolSize: number;
|
|
65
|
+
/** Number of source blocks (Z) */
|
|
66
|
+
sourceBlocks: number;
|
|
67
|
+
/** Number of sub-blocks (N) - used for large blocks */
|
|
68
|
+
subBlocks: number;
|
|
69
|
+
/** Symbol alignment (Al) - typically 8 for byte-aligned */
|
|
70
|
+
alignment: number;
|
|
71
|
+
/** Interleave depth - number of blocks that can be "in flight" */
|
|
72
|
+
interleaveDepth: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Options for FecClient
|
|
76
|
+
*/
|
|
77
|
+
export interface FecClientOptions {
|
|
78
|
+
/** Interleave depth for FEC (default: 30 for ~1s at 30fps) */
|
|
79
|
+
interleaveDepth?: number;
|
|
80
|
+
/** Block timeout in milliseconds (default: 2000) */
|
|
81
|
+
blockTimeoutMs?: number;
|
|
82
|
+
/** Enable green-fill for failed frames (default: true) */
|
|
83
|
+
greenFillEnabled?: boolean;
|
|
84
|
+
/** Callback when data is recovered via FEC */
|
|
85
|
+
onRecovered?: (data: Uint8Array, timestamp: bigint) => void;
|
|
86
|
+
/** Callback for green-fill event */
|
|
87
|
+
onGreenFill?: (timestamp: bigint) => void;
|
|
88
|
+
/** Callback for stats update */
|
|
89
|
+
onStatsUpdate?: (stats: FecProcessingStats) => void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* WASM module interface for FecClient
|
|
93
|
+
* Matches mmt-wasm module exports
|
|
94
|
+
*/
|
|
95
|
+
export interface FecWasmModule {
|
|
96
|
+
init: () => Promise<unknown>;
|
|
97
|
+
MmtFecDecoder: new (interleaveDepth?: number | null) => WasmFecDecoder;
|
|
98
|
+
parse_fec_payload_id: (data: Uint8Array) => WasmFecPayloadId;
|
|
99
|
+
is_simd_enabled: () => boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Container-Agnostic FEC Client
|
|
103
|
+
*
|
|
104
|
+
* Provides RaptorQ FEC decoding for any container format.
|
|
105
|
+
* The actual container parsing (MMTP, LOC, etc.) is handled separately.
|
|
106
|
+
*/
|
|
107
|
+
export declare class FecClient {
|
|
108
|
+
private fecDecoder;
|
|
109
|
+
private options;
|
|
110
|
+
private initialized;
|
|
111
|
+
private greenFillCount;
|
|
112
|
+
private cleanupInterval;
|
|
113
|
+
private wasmModule;
|
|
114
|
+
constructor(options?: FecClientOptions);
|
|
115
|
+
/**
|
|
116
|
+
* Set WASM module (call before init)
|
|
117
|
+
*/
|
|
118
|
+
setWasmModule(module: FecWasmModule): void;
|
|
119
|
+
/**
|
|
120
|
+
* Initialize the WASM FEC decoder
|
|
121
|
+
*/
|
|
122
|
+
init(wasmModule?: FecWasmModule): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Check if initialized
|
|
125
|
+
*/
|
|
126
|
+
isInitialized(): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Configure FEC parameters from OTI (Object Transmission Information)
|
|
129
|
+
* Per RFC 6330 (RaptorQ)
|
|
130
|
+
*/
|
|
131
|
+
configure(oti: Uint8Array): void;
|
|
132
|
+
/**
|
|
133
|
+
* Add a source symbol to the FEC decoder
|
|
134
|
+
*
|
|
135
|
+
* @param sbn - Source Block Number
|
|
136
|
+
* @param esi - Encoding Symbol ID (< K for source symbols)
|
|
137
|
+
* @param data - Symbol data
|
|
138
|
+
* @param timestamp - Packet timestamp for ordering
|
|
139
|
+
* @returns Decoded data if block is complete, null otherwise
|
|
140
|
+
*/
|
|
141
|
+
addSource(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | null;
|
|
142
|
+
/**
|
|
143
|
+
* Add a repair symbol to the FEC decoder
|
|
144
|
+
*
|
|
145
|
+
* @param sbn - Source Block Number
|
|
146
|
+
* @param esi - Encoding Symbol ID (>= K for repair symbols)
|
|
147
|
+
* @param data - Repair symbol data
|
|
148
|
+
* @param timestamp - Packet timestamp for ordering
|
|
149
|
+
* @returns Recovered data if FEC decoding succeeded, null otherwise
|
|
150
|
+
*/
|
|
151
|
+
addRepair(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | null;
|
|
152
|
+
/**
|
|
153
|
+
* Process a repair packet with FEC payload ID header
|
|
154
|
+
* Per draft-ramadan-moq-fec-00 Section 6.1
|
|
155
|
+
*
|
|
156
|
+
* @param payload - Repair packet payload (4-byte header + repair symbols)
|
|
157
|
+
* @param timestamp - Packet timestamp
|
|
158
|
+
* @returns Recovered data if FEC decoding succeeded, null otherwise
|
|
159
|
+
*/
|
|
160
|
+
processRepairPacket(payload: Uint8Array, timestamp: number): Uint8Array | null;
|
|
161
|
+
/**
|
|
162
|
+
* Cleanup expired blocks
|
|
163
|
+
* Returns green-fill frame data for expired blocks if green-fill is enabled
|
|
164
|
+
*/
|
|
165
|
+
cleanup(): void;
|
|
166
|
+
/**
|
|
167
|
+
* Get number of pending blocks
|
|
168
|
+
*/
|
|
169
|
+
getPendingBlocks(): number;
|
|
170
|
+
/**
|
|
171
|
+
* Get current FEC statistics
|
|
172
|
+
*/
|
|
173
|
+
getStats(): FecProcessingStats;
|
|
174
|
+
/**
|
|
175
|
+
* Get current FEC configuration
|
|
176
|
+
* Returns the configured interleave depth (OTI params set via configure())
|
|
177
|
+
*/
|
|
178
|
+
getConfig(): FecConfig;
|
|
179
|
+
/**
|
|
180
|
+
* Reset statistics
|
|
181
|
+
*/
|
|
182
|
+
resetStats(): void;
|
|
183
|
+
/**
|
|
184
|
+
* Dispose of resources
|
|
185
|
+
*/
|
|
186
|
+
dispose(): void;
|
|
187
|
+
}
|
|
188
|
+
export declare function getSharedFecClient(wasmModule: FecWasmModule, options?: FecClientOptions): Promise<FecClient>;
|
|
189
|
+
export declare function resetSharedFecClient(): void;
|
|
190
|
+
export {};
|
|
191
|
+
//# sourceMappingURL=fec-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fec-client.d.ts","sourceRoot":"","sources":["../src/fec-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container-Agnostic FEC Client
|
|
3
|
+
*
|
|
4
|
+
* Provides RaptorQ-based Forward Error Correction (RFC 6330) that works with:
|
|
5
|
+
* - MMTP (MMT Protocol) containers
|
|
6
|
+
* - LOC (Low Overhead Container)
|
|
7
|
+
* - Raw NAL units with FEC_CONFIG
|
|
8
|
+
* - MoQ (Media over QUIC)
|
|
9
|
+
*
|
|
10
|
+
* Transport independent - works over both:
|
|
11
|
+
* - Unicast (MOQ/QUIC/WebTransport)
|
|
12
|
+
* - Multicast (SSM/AMT via window.Multicast API)
|
|
13
|
+
*
|
|
14
|
+
* Based on draft-ramadan-moq-fec-00
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Container-Agnostic FEC Client
|
|
18
|
+
*
|
|
19
|
+
* Provides RaptorQ FEC decoding for any container format.
|
|
20
|
+
* The actual container parsing (MMTP, LOC, etc.) is handled separately.
|
|
21
|
+
*/
|
|
22
|
+
export class FecClient {
|
|
23
|
+
fecDecoder = null;
|
|
24
|
+
options;
|
|
25
|
+
initialized = false;
|
|
26
|
+
greenFillCount = 0;
|
|
27
|
+
cleanupInterval = null;
|
|
28
|
+
// WASM module references (set via setWasmModule or init)
|
|
29
|
+
wasmModule = null;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.options = {
|
|
32
|
+
interleaveDepth: options.interleaveDepth ?? 30,
|
|
33
|
+
blockTimeoutMs: options.blockTimeoutMs ?? 2000,
|
|
34
|
+
greenFillEnabled: options.greenFillEnabled ?? true,
|
|
35
|
+
onRecovered: options.onRecovered ?? (() => { }),
|
|
36
|
+
onGreenFill: options.onGreenFill ?? (() => { }),
|
|
37
|
+
onStatsUpdate: options.onStatsUpdate ?? (() => { }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Set WASM module (call before init)
|
|
42
|
+
*/
|
|
43
|
+
setWasmModule(module) {
|
|
44
|
+
this.wasmModule = module;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the WASM FEC decoder
|
|
48
|
+
*/
|
|
49
|
+
async init(wasmModule) {
|
|
50
|
+
if (this.initialized)
|
|
51
|
+
return;
|
|
52
|
+
if (wasmModule) {
|
|
53
|
+
this.wasmModule = wasmModule;
|
|
54
|
+
}
|
|
55
|
+
if (!this.wasmModule) {
|
|
56
|
+
throw new Error("WASM module not set. Call setWasmModule() or pass module to init()");
|
|
57
|
+
}
|
|
58
|
+
await this.wasmModule.init();
|
|
59
|
+
this.fecDecoder = new this.wasmModule.MmtFecDecoder(this.options.interleaveDepth);
|
|
60
|
+
this.fecDecoder.set_timeout(this.options.blockTimeoutMs);
|
|
61
|
+
this.fecDecoder.set_green_fill(this.options.greenFillEnabled);
|
|
62
|
+
// Start periodic cleanup
|
|
63
|
+
this.cleanupInterval = setInterval(() => {
|
|
64
|
+
this.cleanup();
|
|
65
|
+
}, 500);
|
|
66
|
+
this.initialized = true;
|
|
67
|
+
console.log(`[FecClient] Initialized (SIMD: ${this.wasmModule.is_simd_enabled()})`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if initialized
|
|
71
|
+
*/
|
|
72
|
+
isInitialized() {
|
|
73
|
+
return this.initialized;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Configure FEC parameters from OTI (Object Transmission Information)
|
|
77
|
+
* Per RFC 6330 (RaptorQ)
|
|
78
|
+
*/
|
|
79
|
+
configure(oti) {
|
|
80
|
+
if (!this.fecDecoder) {
|
|
81
|
+
throw new Error("FecClient not initialized");
|
|
82
|
+
}
|
|
83
|
+
this.fecDecoder.configure(oti);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Add a source symbol to the FEC decoder
|
|
87
|
+
*
|
|
88
|
+
* @param sbn - Source Block Number
|
|
89
|
+
* @param esi - Encoding Symbol ID (< K for source symbols)
|
|
90
|
+
* @param data - Symbol data
|
|
91
|
+
* @param timestamp - Packet timestamp for ordering
|
|
92
|
+
* @returns Decoded data if block is complete, null otherwise
|
|
93
|
+
*/
|
|
94
|
+
addSource(sbn, esi, data, timestamp) {
|
|
95
|
+
if (!this.fecDecoder)
|
|
96
|
+
return null;
|
|
97
|
+
return this.fecDecoder.add_source(sbn, esi, data, timestamp) ?? null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Add a repair symbol to the FEC decoder
|
|
101
|
+
*
|
|
102
|
+
* @param sbn - Source Block Number
|
|
103
|
+
* @param esi - Encoding Symbol ID (>= K for repair symbols)
|
|
104
|
+
* @param data - Repair symbol data
|
|
105
|
+
* @param timestamp - Packet timestamp for ordering
|
|
106
|
+
* @returns Recovered data if FEC decoding succeeded, null otherwise
|
|
107
|
+
*/
|
|
108
|
+
addRepair(sbn, esi, data, timestamp) {
|
|
109
|
+
if (!this.fecDecoder)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const decoded = this.fecDecoder.add_repair(sbn, esi, data, timestamp);
|
|
113
|
+
if (decoded && decoded.length > 0) {
|
|
114
|
+
const recovered = new Uint8Array(decoded);
|
|
115
|
+
this.options.onRecovered(recovered, BigInt(timestamp));
|
|
116
|
+
return recovered;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.warn("[FecClient] Repair processing error:", e);
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Process a repair packet with FEC payload ID header
|
|
126
|
+
* Per draft-ramadan-moq-fec-00 Section 6.1
|
|
127
|
+
*
|
|
128
|
+
* @param payload - Repair packet payload (4-byte header + repair symbols)
|
|
129
|
+
* @param timestamp - Packet timestamp
|
|
130
|
+
* @returns Recovered data if FEC decoding succeeded, null otherwise
|
|
131
|
+
*/
|
|
132
|
+
processRepairPacket(payload, timestamp) {
|
|
133
|
+
if (!this.fecDecoder || !this.wasmModule || payload.length < 4)
|
|
134
|
+
return null;
|
|
135
|
+
try {
|
|
136
|
+
const { sbn, esi } = this.wasmModule.parse_fec_payload_id(payload.subarray(0, 4));
|
|
137
|
+
const data = payload.subarray(4);
|
|
138
|
+
return this.addRepair(sbn, esi, data, timestamp);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
console.warn("[FecClient] FEC payload parse error:", e);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Cleanup expired blocks
|
|
147
|
+
* Returns green-fill frame data for expired blocks if green-fill is enabled
|
|
148
|
+
*/
|
|
149
|
+
cleanup() {
|
|
150
|
+
if (!this.fecDecoder)
|
|
151
|
+
return;
|
|
152
|
+
const greenFillData = this.fecDecoder.cleanup(performance.now());
|
|
153
|
+
// greenFillData is a Uint8Array returned by WASM for expired blocks
|
|
154
|
+
if (greenFillData && greenFillData.length > 0 && this.options.greenFillEnabled) {
|
|
155
|
+
// Count green-fills based on data returned
|
|
156
|
+
const greenFillsTriggered = Math.ceil(greenFillData.length / 8);
|
|
157
|
+
if (greenFillsTriggered > 0) {
|
|
158
|
+
this.greenFillCount += greenFillsTriggered;
|
|
159
|
+
this.options.onGreenFill(BigInt(performance.now()));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get number of pending blocks
|
|
165
|
+
*/
|
|
166
|
+
getPendingBlocks() {
|
|
167
|
+
return this.fecDecoder?.pending_blocks() ?? 0;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get current FEC statistics
|
|
171
|
+
*/
|
|
172
|
+
getStats() {
|
|
173
|
+
const fecStats = this.fecDecoder?.get_stats();
|
|
174
|
+
return {
|
|
175
|
+
sourcePackets: Number(fecStats?.source_packets ?? 0n),
|
|
176
|
+
repairPackets: Number(fecStats?.repair_packets ?? 0n),
|
|
177
|
+
blocksComplete: Number(fecStats?.blocks_complete ?? 0n),
|
|
178
|
+
blocksRecovered: Number(fecStats?.blocks_recovered ?? 0n),
|
|
179
|
+
blocksFailed: Number(fecStats?.blocks_failed ?? 0n),
|
|
180
|
+
recoveryRate: fecStats?.recovery_rate() ?? 100,
|
|
181
|
+
greenFillFrames: this.greenFillCount,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get current FEC configuration
|
|
186
|
+
* Returns the configured interleave depth (OTI params set via configure())
|
|
187
|
+
*/
|
|
188
|
+
getConfig() {
|
|
189
|
+
return {
|
|
190
|
+
symbolSize: 1280, // Default, configured via OTI
|
|
191
|
+
sourceBlocks: 1, // Default, configured via OTI
|
|
192
|
+
subBlocks: 1, // Default, configured via OTI
|
|
193
|
+
alignment: 8, // Default, configured via OTI
|
|
194
|
+
interleaveDepth: this.options.interleaveDepth,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Reset statistics
|
|
199
|
+
*/
|
|
200
|
+
resetStats() {
|
|
201
|
+
this.fecDecoder?.reset_stats();
|
|
202
|
+
this.greenFillCount = 0;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Dispose of resources
|
|
206
|
+
*/
|
|
207
|
+
dispose() {
|
|
208
|
+
if (this.cleanupInterval) {
|
|
209
|
+
clearInterval(this.cleanupInterval);
|
|
210
|
+
this.cleanupInterval = null;
|
|
211
|
+
}
|
|
212
|
+
this.fecDecoder?.flush();
|
|
213
|
+
this.initialized = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Create a shared FEC client instance (singleton pattern)
|
|
218
|
+
*/
|
|
219
|
+
let sharedFecClient = null;
|
|
220
|
+
export async function getSharedFecClient(wasmModule, options) {
|
|
221
|
+
if (!sharedFecClient) {
|
|
222
|
+
sharedFecClient = new FecClient(options);
|
|
223
|
+
await sharedFecClient.init(wasmModule);
|
|
224
|
+
}
|
|
225
|
+
return sharedFecClient;
|
|
226
|
+
}
|
|
227
|
+
export function resetSharedFecClient() {
|
|
228
|
+
if (sharedFecClient) {
|
|
229
|
+
sharedFecClient.dispose();
|
|
230
|
+
sharedFecClient = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=fec-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fec-client.js","sourceRoot":"","sources":["../src/fec-client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mmt/fec - Container-agnostic FEC client for RaptorQ (RFC 6330)
|
|
3
|
+
*
|
|
4
|
+
* This package provides a TypeScript wrapper around the mmt-wasm WASM bindings
|
|
5
|
+
* for Forward Error Correction using RaptorQ (RFC 6330).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { FecClient } from '@mmt/fec';
|
|
10
|
+
* import * as wasmModule from '@mmt/wasm';
|
|
11
|
+
*
|
|
12
|
+
* const client = new FecClient({ interleaveDepth: 30 });
|
|
13
|
+
* await client.init(wasmModule);
|
|
14
|
+
*
|
|
15
|
+
* // Configure from OTI
|
|
16
|
+
* client.configure(otiBytes);
|
|
17
|
+
*
|
|
18
|
+
* // Add source/repair symbols
|
|
19
|
+
* const recovered = client.addRepair(sbn, esi, data, timestamp);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export { FecClient, getSharedFecClient, resetSharedFecClient, type FecConfig, type FecClientOptions, type FecProcessingStats, type FecWasmModule, } from "./fec-client.js";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACN,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GAClB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mmt/fec - Container-agnostic FEC client for RaptorQ (RFC 6330)
|
|
3
|
+
*
|
|
4
|
+
* This package provides a TypeScript wrapper around the mmt-wasm WASM bindings
|
|
5
|
+
* for Forward Error Correction using RaptorQ (RFC 6330).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { FecClient } from '@mmt/fec';
|
|
10
|
+
* import * as wasmModule from '@mmt/wasm';
|
|
11
|
+
*
|
|
12
|
+
* const client = new FecClient({ interleaveDepth: 30 });
|
|
13
|
+
* await client.init(wasmModule);
|
|
14
|
+
*
|
|
15
|
+
* // Configure from OTI
|
|
16
|
+
* client.configure(otiBytes);
|
|
17
|
+
*
|
|
18
|
+
* // Add source/repair symbols
|
|
19
|
+
* const recovered = client.addRepair(sbn, esi, data, timestamp);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export { FecClient, getSharedFecClient, resetSharedFecClient, } from "./fec-client.js";
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACN,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GAKpB,MAAM,iBAAiB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blockcast/mmt-fec",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Container-agnostic FEC client for RaptorQ (RFC 6330) decoding via WASM",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"mmt-wasm": "file:../../mmt-wasm/pkg"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.3.0",
|
|
31
|
+
"vitest": "^1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"fec",
|
|
35
|
+
"raptorq",
|
|
36
|
+
"rfc6330",
|
|
37
|
+
"wasm",
|
|
38
|
+
"forward-error-correction",
|
|
39
|
+
"streaming"
|
|
40
|
+
],
|
|
41
|
+
"license": "Apache-2.0",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/Blockcast/libmmt.git",
|
|
45
|
+
"directory": "packages/fec"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for shared FecClient
|
|
3
|
+
*
|
|
4
|
+
* Note: Full WASM integration is tested in mmt-wasm.
|
|
5
|
+
* These tests cover the TypeScript wrapper behavior.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it, beforeEach } from "vitest";
|
|
9
|
+
import { FecClient, type FecConfig, type FecProcessingStats } from "./fec-client.js";
|
|
10
|
+
|
|
11
|
+
describe("FecConfig interface", () => {
|
|
12
|
+
it("should have correct shape for RFC 6330 OTI parameters", () => {
|
|
13
|
+
const config: FecConfig = {
|
|
14
|
+
symbolSize: 1280,
|
|
15
|
+
sourceBlocks: 1,
|
|
16
|
+
subBlocks: 1,
|
|
17
|
+
alignment: 8,
|
|
18
|
+
interleaveDepth: 30,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
expect(config.symbolSize).toBe(1280);
|
|
22
|
+
expect(config.sourceBlocks).toBe(1);
|
|
23
|
+
expect(config.subBlocks).toBe(1);
|
|
24
|
+
expect(config.alignment).toBe(8);
|
|
25
|
+
expect(config.interleaveDepth).toBe(30);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("FecProcessingStats interface", () => {
|
|
30
|
+
it("should have correct shape", () => {
|
|
31
|
+
const stats: FecProcessingStats = {
|
|
32
|
+
sourcePackets: 100,
|
|
33
|
+
repairPackets: 30,
|
|
34
|
+
blocksComplete: 10,
|
|
35
|
+
blocksRecovered: 2,
|
|
36
|
+
blocksFailed: 0,
|
|
37
|
+
recoveryRate: 100,
|
|
38
|
+
greenFillFrames: 0,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
expect(stats.sourcePackets).toBe(100);
|
|
42
|
+
expect(stats.repairPackets).toBe(30);
|
|
43
|
+
expect(stats.blocksComplete).toBe(10);
|
|
44
|
+
expect(stats.blocksRecovered).toBe(2);
|
|
45
|
+
expect(stats.blocksFailed).toBe(0);
|
|
46
|
+
expect(stats.recoveryRate).toBe(100);
|
|
47
|
+
expect(stats.greenFillFrames).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("FecClient", () => {
|
|
52
|
+
let client: FecClient;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
client = new FecClient({
|
|
56
|
+
interleaveDepth: 30,
|
|
57
|
+
blockTimeoutMs: 2000,
|
|
58
|
+
greenFillEnabled: true,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should create with default options", () => {
|
|
63
|
+
const defaultClient = new FecClient();
|
|
64
|
+
expect(defaultClient.isInitialized()).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should not be initialized without calling init()", () => {
|
|
68
|
+
expect(client.isInitialized()).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should throw when configuring without initialization", () => {
|
|
72
|
+
expect(() => client.configure(new Uint8Array(12))).toThrow(
|
|
73
|
+
"FecClient not initialized"
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return default stats when not initialized", () => {
|
|
78
|
+
const stats = client.getStats();
|
|
79
|
+
|
|
80
|
+
expect(stats.sourcePackets).toBe(0);
|
|
81
|
+
expect(stats.repairPackets).toBe(0);
|
|
82
|
+
expect(stats.blocksComplete).toBe(0);
|
|
83
|
+
expect(stats.blocksRecovered).toBe(0);
|
|
84
|
+
expect(stats.blocksFailed).toBe(0);
|
|
85
|
+
expect(stats.recoveryRate).toBe(100);
|
|
86
|
+
expect(stats.greenFillFrames).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should return default config when not initialized", () => {
|
|
90
|
+
const config = client.getConfig();
|
|
91
|
+
|
|
92
|
+
expect(config.symbolSize).toBe(1280);
|
|
93
|
+
expect(config.sourceBlocks).toBe(1);
|
|
94
|
+
expect(config.subBlocks).toBe(1);
|
|
95
|
+
expect(config.alignment).toBe(8);
|
|
96
|
+
expect(config.interleaveDepth).toBe(30);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should no-op setInterleaveDepth when not initialized", () => {
|
|
100
|
+
client.setInterleaveDepth(10);
|
|
101
|
+
const config = client.getConfig();
|
|
102
|
+
expect(config.interleaveDepth).toBe(30); // Unchanged since not init'd
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should return empty array from cleanupByInterleave when not initialized", () => {
|
|
106
|
+
const expired = client.cleanupByInterleave(100);
|
|
107
|
+
expect(expired).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should return null from addRepair when not initialized", () => {
|
|
111
|
+
const result = client.addRepair(0, 0, new Uint8Array(100), 0);
|
|
112
|
+
expect(result).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should return null from addSource when not initialized", () => {
|
|
116
|
+
const result = client.addSource(0, 0, new Uint8Array(100), 0);
|
|
117
|
+
expect(result).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should return null from processRepairPacket when not initialized", () => {
|
|
121
|
+
const result = client.processRepairPacket(new Uint8Array(10), 0);
|
|
122
|
+
expect(result).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should return null from processRepairPacket with short payload", () => {
|
|
126
|
+
const result = client.processRepairPacket(new Uint8Array(3), 0);
|
|
127
|
+
expect(result).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should dispose cleanly without initialization", () => {
|
|
131
|
+
client.dispose();
|
|
132
|
+
expect(client.isInitialized()).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should reset stats when not initialized", () => {
|
|
136
|
+
client.resetStats();
|
|
137
|
+
const stats = client.getStats();
|
|
138
|
+
expect(stats.greenFillFrames).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should throw when init without WASM module", async () => {
|
|
142
|
+
await expect(client.init()).rejects.toThrow(
|
|
143
|
+
"WASM module not set"
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("FecClient callbacks", () => {
|
|
149
|
+
it("should store callbacks from options", () => {
|
|
150
|
+
const client = new FecClient({
|
|
151
|
+
onRecovered: () => {},
|
|
152
|
+
onGreenFill: () => {},
|
|
153
|
+
onStatsUpdate: () => {},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(client.isInitialized()).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container-Agnostic FEC Client
|
|
3
|
+
*
|
|
4
|
+
* Provides RaptorQ-based Forward Error Correction (RFC 6330) that works with:
|
|
5
|
+
* - MMTP (MMT Protocol) containers
|
|
6
|
+
* - LOC (Low Overhead Container)
|
|
7
|
+
* - Raw NAL units with FEC_CONFIG
|
|
8
|
+
* - MoQ (Media over QUIC)
|
|
9
|
+
*
|
|
10
|
+
* Transport independent - works over both:
|
|
11
|
+
* - Unicast (MOQ/QUIC/WebTransport)
|
|
12
|
+
* - Multicast (SSM/AMT via window.Multicast API)
|
|
13
|
+
*
|
|
14
|
+
* Based on draft-ramadan-moq-fec-00
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// WASM module types - matches mmt-wasm MmtFecDecoder
|
|
18
|
+
interface WasmFecDecoder {
|
|
19
|
+
configure(oti: Uint8Array): void;
|
|
20
|
+
add_repair(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | undefined;
|
|
21
|
+
add_source(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | undefined;
|
|
22
|
+
cleanup(current_time: number): Uint8Array;
|
|
23
|
+
get_stats(): WasmFecStats;
|
|
24
|
+
reset_stats(): void;
|
|
25
|
+
flush(): void;
|
|
26
|
+
set_timeout(timeout_ms: number): void;
|
|
27
|
+
set_green_fill(enabled: boolean): void;
|
|
28
|
+
pending_blocks(): number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface WasmFecStats {
|
|
32
|
+
source_packets: bigint;
|
|
33
|
+
repair_packets: bigint;
|
|
34
|
+
blocks_complete: bigint;
|
|
35
|
+
blocks_recovered: bigint;
|
|
36
|
+
blocks_failed: bigint;
|
|
37
|
+
recovery_rate(): number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface WasmFecPayloadId {
|
|
41
|
+
sbn: number;
|
|
42
|
+
esi: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* FEC processing statistics (transport & container independent)
|
|
47
|
+
*/
|
|
48
|
+
export interface FecProcessingStats {
|
|
49
|
+
/** Source packets received */
|
|
50
|
+
sourcePackets: number;
|
|
51
|
+
/** Repair packets received */
|
|
52
|
+
repairPackets: number;
|
|
53
|
+
/** Blocks completed (received all source symbols) */
|
|
54
|
+
blocksComplete: number;
|
|
55
|
+
/** Blocks recovered via FEC */
|
|
56
|
+
blocksRecovered: number;
|
|
57
|
+
/** Blocks that failed to recover */
|
|
58
|
+
blocksFailed: number;
|
|
59
|
+
/** Recovery rate (0-100%) */
|
|
60
|
+
recoveryRate: number;
|
|
61
|
+
/** Green-fill frames (unrecoverable data replaced with placeholder) */
|
|
62
|
+
greenFillFrames: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* FEC configuration parameters (RFC 6330 OTI)
|
|
67
|
+
*/
|
|
68
|
+
export interface FecConfig {
|
|
69
|
+
/** Symbol size in bytes (T) */
|
|
70
|
+
symbolSize: number;
|
|
71
|
+
/** Number of source blocks (Z) */
|
|
72
|
+
sourceBlocks: number;
|
|
73
|
+
/** Number of sub-blocks (N) - used for large blocks */
|
|
74
|
+
subBlocks: number;
|
|
75
|
+
/** Symbol alignment (Al) - typically 8 for byte-aligned */
|
|
76
|
+
alignment: number;
|
|
77
|
+
/** Interleave depth - number of blocks that can be "in flight" */
|
|
78
|
+
interleaveDepth: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Options for FecClient
|
|
83
|
+
*/
|
|
84
|
+
export interface FecClientOptions {
|
|
85
|
+
/** Interleave depth for FEC (default: 30 for ~1s at 30fps) */
|
|
86
|
+
interleaveDepth?: number;
|
|
87
|
+
/** Block timeout in milliseconds (default: 2000) */
|
|
88
|
+
blockTimeoutMs?: number;
|
|
89
|
+
/** Enable green-fill for failed frames (default: true) */
|
|
90
|
+
greenFillEnabled?: boolean;
|
|
91
|
+
/** Callback when data is recovered via FEC */
|
|
92
|
+
onRecovered?: (data: Uint8Array, timestamp: bigint) => void;
|
|
93
|
+
/** Callback for green-fill event */
|
|
94
|
+
onGreenFill?: (timestamp: bigint) => void;
|
|
95
|
+
/** Callback for stats update */
|
|
96
|
+
onStatsUpdate?: (stats: FecProcessingStats) => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* WASM module interface for FecClient
|
|
101
|
+
* Matches mmt-wasm module exports
|
|
102
|
+
*/
|
|
103
|
+
export interface FecWasmModule {
|
|
104
|
+
init: () => Promise<unknown>;
|
|
105
|
+
MmtFecDecoder: new (interleaveDepth?: number | null) => WasmFecDecoder;
|
|
106
|
+
parse_fec_payload_id: (data: Uint8Array) => WasmFecPayloadId;
|
|
107
|
+
is_simd_enabled: () => boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Container-Agnostic FEC Client
|
|
112
|
+
*
|
|
113
|
+
* Provides RaptorQ FEC decoding for any container format.
|
|
114
|
+
* The actual container parsing (MMTP, LOC, etc.) is handled separately.
|
|
115
|
+
*/
|
|
116
|
+
export class FecClient {
|
|
117
|
+
private fecDecoder: WasmFecDecoder | null = null;
|
|
118
|
+
private options: Required<FecClientOptions>;
|
|
119
|
+
private initialized = false;
|
|
120
|
+
private greenFillCount = 0;
|
|
121
|
+
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
122
|
+
|
|
123
|
+
// WASM module references (set via setWasmModule or init)
|
|
124
|
+
private wasmModule: FecWasmModule | null = null;
|
|
125
|
+
|
|
126
|
+
constructor(options: FecClientOptions = {}) {
|
|
127
|
+
this.options = {
|
|
128
|
+
interleaveDepth: options.interleaveDepth ?? 30,
|
|
129
|
+
blockTimeoutMs: options.blockTimeoutMs ?? 2000,
|
|
130
|
+
greenFillEnabled: options.greenFillEnabled ?? true,
|
|
131
|
+
onRecovered: options.onRecovered ?? (() => {}),
|
|
132
|
+
onGreenFill: options.onGreenFill ?? (() => {}),
|
|
133
|
+
onStatsUpdate: options.onStatsUpdate ?? (() => {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Set WASM module (call before init)
|
|
139
|
+
*/
|
|
140
|
+
setWasmModule(module: FecWasmModule): void {
|
|
141
|
+
this.wasmModule = module;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Initialize the WASM FEC decoder
|
|
146
|
+
*/
|
|
147
|
+
async init(wasmModule?: FecWasmModule): Promise<void> {
|
|
148
|
+
if (this.initialized) return;
|
|
149
|
+
|
|
150
|
+
if (wasmModule) {
|
|
151
|
+
this.wasmModule = wasmModule;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!this.wasmModule) {
|
|
155
|
+
throw new Error("WASM module not set. Call setWasmModule() or pass module to init()");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await this.wasmModule.init();
|
|
159
|
+
|
|
160
|
+
this.fecDecoder = new this.wasmModule.MmtFecDecoder(this.options.interleaveDepth);
|
|
161
|
+
this.fecDecoder.set_timeout(this.options.blockTimeoutMs);
|
|
162
|
+
this.fecDecoder.set_green_fill(this.options.greenFillEnabled);
|
|
163
|
+
|
|
164
|
+
// Start periodic cleanup
|
|
165
|
+
this.cleanupInterval = setInterval(() => {
|
|
166
|
+
this.cleanup();
|
|
167
|
+
}, 500);
|
|
168
|
+
|
|
169
|
+
this.initialized = true;
|
|
170
|
+
console.log(`[FecClient] Initialized (SIMD: ${this.wasmModule.is_simd_enabled()})`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if initialized
|
|
175
|
+
*/
|
|
176
|
+
isInitialized(): boolean {
|
|
177
|
+
return this.initialized;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Configure FEC parameters from OTI (Object Transmission Information)
|
|
182
|
+
* Per RFC 6330 (RaptorQ)
|
|
183
|
+
*/
|
|
184
|
+
configure(oti: Uint8Array): void {
|
|
185
|
+
if (!this.fecDecoder) {
|
|
186
|
+
throw new Error("FecClient not initialized");
|
|
187
|
+
}
|
|
188
|
+
this.fecDecoder.configure(oti);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Add a source symbol to the FEC decoder
|
|
193
|
+
*
|
|
194
|
+
* @param sbn - Source Block Number
|
|
195
|
+
* @param esi - Encoding Symbol ID (< K for source symbols)
|
|
196
|
+
* @param data - Symbol data
|
|
197
|
+
* @param timestamp - Packet timestamp for ordering
|
|
198
|
+
* @returns Decoded data if block is complete, null otherwise
|
|
199
|
+
*/
|
|
200
|
+
addSource(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | null {
|
|
201
|
+
if (!this.fecDecoder) return null;
|
|
202
|
+
return this.fecDecoder.add_source(sbn, esi, data, timestamp) ?? null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Add a repair symbol to the FEC decoder
|
|
207
|
+
*
|
|
208
|
+
* @param sbn - Source Block Number
|
|
209
|
+
* @param esi - Encoding Symbol ID (>= K for repair symbols)
|
|
210
|
+
* @param data - Repair symbol data
|
|
211
|
+
* @param timestamp - Packet timestamp for ordering
|
|
212
|
+
* @returns Recovered data if FEC decoding succeeded, null otherwise
|
|
213
|
+
*/
|
|
214
|
+
addRepair(sbn: number, esi: number, data: Uint8Array, timestamp: number): Uint8Array | null {
|
|
215
|
+
if (!this.fecDecoder) return null;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const decoded = this.fecDecoder.add_repair(sbn, esi, data, timestamp);
|
|
219
|
+
|
|
220
|
+
if (decoded && decoded.length > 0) {
|
|
221
|
+
const recovered = new Uint8Array(decoded);
|
|
222
|
+
this.options.onRecovered(recovered, BigInt(timestamp));
|
|
223
|
+
return recovered;
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.warn("[FecClient] Repair processing error:", e);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Process a repair packet with FEC payload ID header
|
|
234
|
+
* Per draft-ramadan-moq-fec-00 Section 6.1
|
|
235
|
+
*
|
|
236
|
+
* @param payload - Repair packet payload (4-byte header + repair symbols)
|
|
237
|
+
* @param timestamp - Packet timestamp
|
|
238
|
+
* @returns Recovered data if FEC decoding succeeded, null otherwise
|
|
239
|
+
*/
|
|
240
|
+
processRepairPacket(payload: Uint8Array, timestamp: number): Uint8Array | null {
|
|
241
|
+
if (!this.fecDecoder || !this.wasmModule || payload.length < 4) return null;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const { sbn, esi } = this.wasmModule.parse_fec_payload_id(payload.subarray(0, 4));
|
|
245
|
+
const data = payload.subarray(4);
|
|
246
|
+
|
|
247
|
+
return this.addRepair(sbn, esi, data, timestamp);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
console.warn("[FecClient] FEC payload parse error:", e);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Cleanup expired blocks
|
|
256
|
+
* Returns green-fill frame data for expired blocks if green-fill is enabled
|
|
257
|
+
*/
|
|
258
|
+
cleanup(): void {
|
|
259
|
+
if (!this.fecDecoder) return;
|
|
260
|
+
|
|
261
|
+
const greenFillData = this.fecDecoder.cleanup(performance.now());
|
|
262
|
+
|
|
263
|
+
// greenFillData is a Uint8Array returned by WASM for expired blocks
|
|
264
|
+
if (greenFillData && greenFillData.length > 0 && this.options.greenFillEnabled) {
|
|
265
|
+
// Count green-fills based on data returned
|
|
266
|
+
const greenFillsTriggered = Math.ceil(greenFillData.length / 8);
|
|
267
|
+
if (greenFillsTriggered > 0) {
|
|
268
|
+
this.greenFillCount += greenFillsTriggered;
|
|
269
|
+
this.options.onGreenFill(BigInt(performance.now()));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get number of pending blocks
|
|
276
|
+
*/
|
|
277
|
+
getPendingBlocks(): number {
|
|
278
|
+
return this.fecDecoder?.pending_blocks() ?? 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get current FEC statistics
|
|
283
|
+
*/
|
|
284
|
+
getStats(): FecProcessingStats {
|
|
285
|
+
const fecStats = this.fecDecoder?.get_stats();
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
sourcePackets: Number(fecStats?.source_packets ?? 0n),
|
|
289
|
+
repairPackets: Number(fecStats?.repair_packets ?? 0n),
|
|
290
|
+
blocksComplete: Number(fecStats?.blocks_complete ?? 0n),
|
|
291
|
+
blocksRecovered: Number(fecStats?.blocks_recovered ?? 0n),
|
|
292
|
+
blocksFailed: Number(fecStats?.blocks_failed ?? 0n),
|
|
293
|
+
recoveryRate: fecStats?.recovery_rate() ?? 100,
|
|
294
|
+
greenFillFrames: this.greenFillCount,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get current FEC configuration
|
|
300
|
+
* Returns the configured interleave depth (OTI params set via configure())
|
|
301
|
+
*/
|
|
302
|
+
getConfig(): FecConfig {
|
|
303
|
+
return {
|
|
304
|
+
symbolSize: 1280, // Default, configured via OTI
|
|
305
|
+
sourceBlocks: 1, // Default, configured via OTI
|
|
306
|
+
subBlocks: 1, // Default, configured via OTI
|
|
307
|
+
alignment: 8, // Default, configured via OTI
|
|
308
|
+
interleaveDepth: this.options.interleaveDepth,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Reset statistics
|
|
314
|
+
*/
|
|
315
|
+
resetStats(): void {
|
|
316
|
+
this.fecDecoder?.reset_stats();
|
|
317
|
+
this.greenFillCount = 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Dispose of resources
|
|
322
|
+
*/
|
|
323
|
+
dispose(): void {
|
|
324
|
+
if (this.cleanupInterval) {
|
|
325
|
+
clearInterval(this.cleanupInterval);
|
|
326
|
+
this.cleanupInterval = null;
|
|
327
|
+
}
|
|
328
|
+
this.fecDecoder?.flush();
|
|
329
|
+
this.initialized = false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create a shared FEC client instance (singleton pattern)
|
|
335
|
+
*/
|
|
336
|
+
let sharedFecClient: FecClient | null = null;
|
|
337
|
+
|
|
338
|
+
export async function getSharedFecClient(
|
|
339
|
+
wasmModule: FecWasmModule,
|
|
340
|
+
options?: FecClientOptions
|
|
341
|
+
): Promise<FecClient> {
|
|
342
|
+
if (!sharedFecClient) {
|
|
343
|
+
sharedFecClient = new FecClient(options);
|
|
344
|
+
await sharedFecClient.init(wasmModule);
|
|
345
|
+
}
|
|
346
|
+
return sharedFecClient;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function resetSharedFecClient(): void {
|
|
350
|
+
if (sharedFecClient) {
|
|
351
|
+
sharedFecClient.dispose();
|
|
352
|
+
sharedFecClient = null;
|
|
353
|
+
}
|
|
354
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mmt/fec - Container-agnostic FEC client for RaptorQ (RFC 6330)
|
|
3
|
+
*
|
|
4
|
+
* This package provides a TypeScript wrapper around the mmt-wasm WASM bindings
|
|
5
|
+
* for Forward Error Correction using RaptorQ (RFC 6330).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { FecClient } from '@mmt/fec';
|
|
10
|
+
* import * as wasmModule from '@mmt/wasm';
|
|
11
|
+
*
|
|
12
|
+
* const client = new FecClient({ interleaveDepth: 30 });
|
|
13
|
+
* await client.init(wasmModule);
|
|
14
|
+
*
|
|
15
|
+
* // Configure from OTI
|
|
16
|
+
* client.configure(otiBytes);
|
|
17
|
+
*
|
|
18
|
+
* // Add source/repair symbols
|
|
19
|
+
* const recovered = client.addRepair(sbn, esi, data, timestamp);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
FecClient,
|
|
25
|
+
getSharedFecClient,
|
|
26
|
+
resetSharedFecClient,
|
|
27
|
+
type FecConfig,
|
|
28
|
+
type FecClientOptions,
|
|
29
|
+
type FecProcessingStats,
|
|
30
|
+
type FecWasmModule,
|
|
31
|
+
} from "./fec-client.js";
|