@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
package/src/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @blockcast/transport
3
+ *
4
+ * Unified transport manager for MoQ players with WebTransport, multicast, AMT, DRIAD, and FEC support.
5
+ *
6
+ * Transport modes:
7
+ * - Native multicast (ASM/SSM via IGMPv3/MLD + PIM-SSM)
8
+ * - AMT tunneling (RFC 7450) with optional DRIAD relay discovery
9
+ * - WebTransport/QUIC (MoQ-lite protocol)
10
+ * - WebSocket fallback
11
+ *
12
+ * Transport promotion hierarchy (WebTransport-first for fast startup):
13
+ * 1. WebTransport - immediate connection for fast channel select
14
+ * 2. Background discovery - check if multicast available
15
+ * 3. Promote to multicast (Native SSM > IGMP > AMT) if available
16
+ * 4. WebSocket fallback if WebTransport unavailable
17
+ *
18
+ * Features:
19
+ * - Progressive adaptive bitrate based on network conditions
20
+ * - FEC (Forward Error Correction) for burst loss recovery
21
+ * - Seamless transport switching with session continuity
22
+ * - Unified FEC/ABR pipeline across all transport modes
23
+ * - A/V synchronization via SharedClock
24
+ */
25
+
26
+ // Types (new generic names)
27
+ export type {
28
+ TransportConfig,
29
+ TransportSubscription,
30
+ QualityLevel,
31
+ TransportCapabilities,
32
+ TransportMode,
33
+ TransportState,
34
+ DRIADRelay,
35
+ AmtRelayInfo,
36
+ GroupKey,
37
+ PacketMetadata,
38
+ MfuFragment,
39
+ ReassembledMfu,
40
+ ProcessingStats,
41
+ MmtFecClientOptions,
42
+ Micro,
43
+ SyncDecision,
44
+ MulticastAPI,
45
+ MulticastSubscription,
46
+ // Consolidated from @blockcast/multicast
47
+ MulticastTransportMode,
48
+ MulticastConfig,
49
+ PacketSource,
50
+ SubscriptionState,
51
+ SubscriptionStats,
52
+ MulticastSubscriptionHandle,
53
+ FecConfig,
54
+ FecStats,
55
+ MulticastConfigWithFec,
56
+ MulticastBridge,
57
+ // FEC types per draft-ramadan-moq-fec-00
58
+ MoqFecConfig,
59
+ RepairObjectHeader,
60
+ // @deprecated - use TransportConfig and TransportSubscription instead
61
+ SSMConfig,
62
+ SSMSubscription,
63
+ } from "./types.js";
64
+
65
+ // FEC constants and utilities per draft-ramadan-moq-fec-00
66
+ export {
67
+ FEC_CONFIG_MESSAGE_TYPE,
68
+ REPAIR_TRACK_SUFFIX,
69
+ repairTrackPath,
70
+ isRepairTrack,
71
+ sourceTrackFromRepair,
72
+ MoqFecAlgorithm,
73
+ } from "./types.js";
74
+
75
+ // Transport Manager (renamed from SSMTransportManager)
76
+ export { TransportManager, transportManager } from "./transport-manager.js";
77
+
78
+ // @deprecated - Use TransportManager and transportManager instead
79
+ export { SSMTransportManager, ssmTransport } from "./transport-manager.js";
80
+
81
+ // DRIAD Discovery
82
+ export { DRIADDiscovery, driadDiscovery } from "./driad-discovery.js";
83
+
84
+ // Adaptive Bitrate Controller
85
+ export {
86
+ AdaptiveBitrateController,
87
+ QUALITY_LEVELS,
88
+ type FecStatsForAbr,
89
+ } from "./abr-controller.js";
90
+
91
+ // AMT Gateway
92
+ export {
93
+ AMTGatewayManager,
94
+ AMTGateway,
95
+ amtGatewayManager,
96
+ type AMTSubscriptionConfig,
97
+ type AMTGatewayOptions,
98
+ } from "./amt-gateway.js";
99
+
100
+ // FEC Client
101
+ export {
102
+ MmtFecClient,
103
+ createMulticastFecHandler,
104
+ setupMmtFec,
105
+ // MoQ FEC Client (for WebTransport)
106
+ MoqFecClient,
107
+ createMoqFecClient,
108
+ loadMmtWasm,
109
+ type MoqFecClientConfig,
110
+ type MoqFecClientStats,
111
+ // FEC decoder configuration (RFC 6330 OTI parameters)
112
+ type FecDecoderConfig,
113
+ } from "./fec-client.js";
114
+
115
+ // A/V Sync Clock
116
+ export {
117
+ SharedClock,
118
+ getSharedClock,
119
+ resetSharedClock,
120
+ SYNC_TOLERANCE_US,
121
+ PTS_OFFSET_US,
122
+ } from "./clock.js";
@@ -0,0 +1,460 @@
1
+ /**
2
+ * @blockcast/transport - Transport Manager
3
+ *
4
+ * Unified transport abstraction for MoQ players supporting:
5
+ * - Native multicast (ASM/SSM via IGMPv3/MLD + PIM-SSM)
6
+ * - AMT tunneling (RFC 7450) with optional DRIAD relay discovery
7
+ * - WebTransport/QUIC (MoQ-lite protocol)
8
+ * - WebSocket fallback
9
+ *
10
+ * Transport promotion hierarchy (WebTransport-first for fast startup):
11
+ * 1. WebTransport - immediate connection for fast channel select
12
+ * 2. Background discovery - check if multicast available
13
+ * 3. Promote to multicast (Native SSM > IGMP > AMT) if available
14
+ * 4. WebSocket fallback if WebTransport unavailable
15
+ *
16
+ * Features:
17
+ * - Progressive adaptive bitrate based on network conditions
18
+ * - FEC (Forward Error Correction) for burst loss recovery
19
+ * - Seamless transport switching with session continuity
20
+ * - Unified FEC/ABR pipeline across all transport modes
21
+ */
22
+
23
+ import type {
24
+ TransportConfig,
25
+ TransportSubscription,
26
+ TransportCapabilities,
27
+ TransportState,
28
+ QualityLevel,
29
+ } from "./types.js";
30
+ import { DRIADDiscovery } from "./driad-discovery.js";
31
+ import { AdaptiveBitrateController } from "./abr-controller.js";
32
+
33
+ /**
34
+ * Transport Manager
35
+ *
36
+ * Manages transport connections with automatic mode selection:
37
+ * WebTransport -> Native SSM -> IGMP -> AMT -> WebSocket
38
+ *
39
+ * Renamed from SSMTransportManager - the old name is preserved
40
+ * as a deprecated alias for backwards compatibility.
41
+ */
42
+ export class TransportManager {
43
+ private state: TransportState = {
44
+ mode: "disconnected",
45
+ connected: false,
46
+ packetsReceived: 0,
47
+ bytesReceived: 0,
48
+ lastPacketTime: 0,
49
+ };
50
+
51
+ private capabilities: TransportCapabilities | null = null;
52
+ private subscription: TransportSubscription | null = null;
53
+ private driad = new DRIADDiscovery();
54
+ private abr: AdaptiveBitrateController;
55
+
56
+ private onPacket?: (data: ArrayBuffer) => void;
57
+ private onError?: (error: Error) => void;
58
+ private onStateChange?: (state: TransportState) => void;
59
+
60
+ constructor() {
61
+ this.abr = new AdaptiveBitrateController();
62
+ }
63
+
64
+ /**
65
+ * Detect available transport capabilities
66
+ */
67
+ async detectCapabilities(): Promise<TransportCapabilities> {
68
+ if (this.capabilities) return this.capabilities;
69
+
70
+ const hasMulticast = typeof window !== "undefined" && window.Multicast !== undefined;
71
+ const multicastApi = hasMulticast ? window.Multicast : null;
72
+
73
+ this.capabilities = {
74
+ nativeSSM: !!multicastApi?.subscribeSSM,
75
+ nativeIGMP: !!multicastApi?.subscribe,
76
+ directSockets: typeof window !== "undefined" && typeof window.UDPSocket !== "undefined",
77
+ amt: !!multicastApi?.connectAMT,
78
+ driad: true, // DRIAD uses HTTP proxy, always available
79
+ fecDecode: !!multicastApi?.fecDecode,
80
+ };
81
+
82
+ console.log("[Transport] Capabilities:", this.capabilities);
83
+ return this.capabilities;
84
+ }
85
+
86
+ /**
87
+ * Connect using best available transport
88
+ *
89
+ * Promotion hierarchy:
90
+ * 1. Native SSM (IWA with Direct Sockets)
91
+ * 2. Native IGMP (IWA fallback)
92
+ * 3. AMT Gateway (specified or discovered via DRIAD)
93
+ * 4. MOQ WebTransport (final fallback)
94
+ */
95
+ async connect(config: TransportConfig): Promise<boolean> {
96
+ const caps = await this.detectCapabilities();
97
+
98
+ // Initialize ABR with requested quality
99
+ if (config.initialQuality) {
100
+ this.abr = new AdaptiveBitrateController(config.initialQuality);
101
+ }
102
+
103
+ this.abr.setCallback((quality) => {
104
+ this.state.quality = quality;
105
+ this.subscription?.onqualitychange?.(quality);
106
+ this.onStateChange?.(this.state);
107
+ });
108
+
109
+ // Try transport hierarchy
110
+ let connected = false;
111
+
112
+ // 1. Try native SSM (best option)
113
+ if (caps.nativeSSM && config.source) {
114
+ console.log("[Transport] Attempting native SSM...");
115
+ connected = await this.connectNativeSSM(config);
116
+ if (connected) {
117
+ this.state.mode = "ssm";
118
+ return true;
119
+ }
120
+ }
121
+
122
+ // 2. Try native IGMP (if SSM not available)
123
+ if (caps.nativeIGMP && !connected) {
124
+ console.log("[Transport] Attempting native IGMP...");
125
+ connected = await this.connectNativeIGMP(config);
126
+ if (connected) {
127
+ this.state.mode = "igmp";
128
+ return true;
129
+ }
130
+ }
131
+
132
+ // 3. Try AMT (with optional DRIAD discovery)
133
+ if (!connected) {
134
+ console.log("[Transport] Attempting AMT tunnel...");
135
+ connected = await this.connectAMT(config);
136
+ if (connected) {
137
+ this.state.mode = config.driadEnabled ? "driad" : "amt";
138
+ return true;
139
+ }
140
+ }
141
+
142
+ // 4. Fallback to MOQ
143
+ console.log("[Transport] All multicast transports failed, falling back to MOQ");
144
+ return false;
145
+ }
146
+
147
+ /**
148
+ * Connect via native SSM (Source-Specific Multicast)
149
+ */
150
+ private async connectNativeSSM(config: TransportConfig): Promise<boolean> {
151
+ try {
152
+ const multicastApi = window.Multicast;
153
+ if (!multicastApi?.subscribeSSM) return false;
154
+
155
+ const subscription = await multicastApi.subscribeSSM({
156
+ group: config.group,
157
+ source: config.source,
158
+ port: config.port,
159
+ protocol: "mmtp",
160
+ fec: true,
161
+ });
162
+
163
+ subscription.onpacket = (event: { data: ArrayBuffer }) => {
164
+ this.handlePacket(event.data);
165
+ };
166
+
167
+ subscription.onerror = (error: Error) => {
168
+ console.error("[Transport] Native SSM error:", error);
169
+ this.onError?.(error);
170
+ };
171
+
172
+ this.subscription = {
173
+ id: `ssm-${Date.now()}`,
174
+ config,
175
+ close: () => subscription.unsubscribe(),
176
+ };
177
+
178
+ this.state = {
179
+ mode: "ssm",
180
+ connected: true,
181
+ group: config.group,
182
+ source: config.source,
183
+ quality: this.abr.getCurrentQuality(),
184
+ packetsReceived: 0,
185
+ bytesReceived: 0,
186
+ lastPacketTime: 0,
187
+ };
188
+
189
+ console.log(`[Transport] Connected via native SSM: (${config.source}, ${config.group})`);
190
+ this.onStateChange?.(this.state);
191
+ return true;
192
+ } catch (error) {
193
+ console.warn("[Transport] Native SSM failed:", error);
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Connect via native IGMP (Any-Source Multicast)
200
+ */
201
+ private async connectNativeIGMP(config: TransportConfig): Promise<boolean> {
202
+ try {
203
+ const multicastApi = window.Multicast;
204
+ if (!multicastApi?.subscribe) return false;
205
+
206
+ const subscription = await multicastApi.subscribe({
207
+ group: config.group,
208
+ port: config.port,
209
+ protocol: "mmtp",
210
+ fec: true,
211
+ });
212
+
213
+ subscription.onpacket = (event: { data: ArrayBuffer }) => {
214
+ this.handlePacket(event.data);
215
+ };
216
+
217
+ this.subscription = {
218
+ id: `igmp-${Date.now()}`,
219
+ config,
220
+ close: () => subscription.unsubscribe(),
221
+ };
222
+
223
+ this.state = {
224
+ mode: "igmp",
225
+ connected: true,
226
+ group: config.group,
227
+ quality: this.abr.getCurrentQuality(),
228
+ packetsReceived: 0,
229
+ bytesReceived: 0,
230
+ lastPacketTime: 0,
231
+ };
232
+
233
+ console.log(`[Transport] Connected via IGMP: ${config.group}`);
234
+ this.onStateChange?.(this.state);
235
+ return true;
236
+ } catch (error) {
237
+ console.warn("[Transport] Native IGMP failed:", error);
238
+ return false;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Connect via AMT Gateway (with optional DRIAD discovery)
244
+ */
245
+ private async connectAMT(config: TransportConfig): Promise<boolean> {
246
+ try {
247
+ let amtRelay = config.amtRelay;
248
+
249
+ // Use DRIAD to discover relay if not specified (RFC 8777)
250
+ // DRIAD discovers relays based on SOURCE address, not group
251
+ if (!amtRelay && config.driadEnabled !== false && config.source) {
252
+ console.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);
253
+ const relays = await this.driad.discoverRelays(config.source);
254
+ const bestRelay = await this.driad.selectBestRelay(relays);
255
+
256
+ if (bestRelay) {
257
+ amtRelay = bestRelay.host;
258
+ this.state.driadRelay = amtRelay;
259
+ }
260
+ }
261
+
262
+ if (!amtRelay) {
263
+ console.warn("[Transport] No AMT relay available (DRIAD requires source address for SSM)");
264
+ return false;
265
+ }
266
+
267
+ // Connect via AMT proxy/gateway
268
+ const multicastApi = window.Multicast;
269
+
270
+ if (multicastApi?.connectAMT) {
271
+ const gateway = await multicastApi.connectAMT({
272
+ relay: amtRelay,
273
+ group: config.group,
274
+ source: config.source,
275
+ port: config.port,
276
+ });
277
+
278
+ gateway.onpacket = (event: { data: ArrayBuffer }) => {
279
+ this.handlePacket(event.data);
280
+ };
281
+
282
+ this.subscription = {
283
+ id: `amt-${Date.now()}`,
284
+ config,
285
+ close: () => gateway.close(),
286
+ };
287
+
288
+ this.state = {
289
+ mode: config.driadEnabled ? "driad" : "amt",
290
+ connected: true,
291
+ group: config.group,
292
+ source: config.source,
293
+ amtRelay,
294
+ quality: this.abr.getCurrentQuality(),
295
+ packetsReceived: 0,
296
+ bytesReceived: 0,
297
+ lastPacketTime: 0,
298
+ };
299
+
300
+ console.log(`[Transport] Connected via AMT: ${amtRelay}`);
301
+ this.onStateChange?.(this.state);
302
+ return true;
303
+ }
304
+
305
+ // Fallback: Use WebSocket-based AMT proxy
306
+ return await this.connectAMTProxy(config, amtRelay);
307
+ } catch (error) {
308
+ console.warn("[Transport] AMT connection failed:", error);
309
+ return false;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Connect via WebSocket-based AMT proxy (browser fallback)
315
+ */
316
+ private async connectAMTProxy(config: TransportConfig, amtRelay: string): Promise<boolean> {
317
+ try {
318
+ const wsUrl = `wss://${amtRelay}/amt-proxy?group=${config.group}&source=${config.source || ""}&port=${config.port}`;
319
+
320
+ const ws = new WebSocket(wsUrl);
321
+
322
+ return new Promise((resolve, reject) => {
323
+ ws.binaryType = "arraybuffer";
324
+
325
+ ws.onopen = () => {
326
+ console.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);
327
+
328
+ this.subscription = {
329
+ id: `amt-proxy-${Date.now()}`,
330
+ config,
331
+ close: () => ws.close(),
332
+ };
333
+
334
+ this.state = {
335
+ mode: "amt",
336
+ connected: true,
337
+ group: config.group,
338
+ source: config.source,
339
+ amtRelay,
340
+ quality: this.abr.getCurrentQuality(),
341
+ packetsReceived: 0,
342
+ bytesReceived: 0,
343
+ lastPacketTime: 0,
344
+ };
345
+
346
+ this.onStateChange?.(this.state);
347
+ resolve(true);
348
+ };
349
+
350
+ ws.onmessage = (event) => {
351
+ if (event.data instanceof ArrayBuffer) {
352
+ this.handlePacket(event.data);
353
+ }
354
+ };
355
+
356
+ ws.onerror = (error) => {
357
+ console.error("[Transport] AMT proxy error:", error);
358
+ reject(error);
359
+ };
360
+
361
+ ws.onclose = () => {
362
+ this.state.connected = false;
363
+ this.onStateChange?.(this.state);
364
+ };
365
+
366
+ // Timeout
367
+ setTimeout(() => reject(new Error("AMT proxy connection timeout")), 10000);
368
+ });
369
+ } catch (error) {
370
+ console.warn("[Transport] AMT proxy failed:", error);
371
+ return false;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Handle received packet
377
+ */
378
+ private handlePacket(data: ArrayBuffer) {
379
+ this.state.packetsReceived++;
380
+ this.state.bytesReceived += data.byteLength;
381
+ this.state.lastPacketTime = Date.now();
382
+
383
+ // Update ABR with sample
384
+ this.abr.recordSample(data.byteLength, 0);
385
+
386
+ // Forward to callback
387
+ this.onPacket?.(data);
388
+ }
389
+
390
+ /**
391
+ * Get current transport state
392
+ */
393
+ getState(): TransportState {
394
+ return { ...this.state };
395
+ }
396
+
397
+ /**
398
+ * Get current quality level
399
+ */
400
+ getQuality(): QualityLevel {
401
+ return this.abr.getCurrentQuality();
402
+ }
403
+
404
+ /**
405
+ * Force quality level
406
+ */
407
+ setQuality(name: string) {
408
+ this.abr.setQuality(name);
409
+ }
410
+
411
+ /**
412
+ * Set packet callback
413
+ */
414
+ setPacketCallback(callback: (data: ArrayBuffer) => void) {
415
+ this.onPacket = callback;
416
+ }
417
+
418
+ /**
419
+ * Set error callback
420
+ */
421
+ setErrorCallback(callback: (error: Error) => void) {
422
+ this.onError = callback;
423
+ }
424
+
425
+ /**
426
+ * Set state change callback
427
+ */
428
+ setStateCallback(callback: (state: TransportState) => void) {
429
+ this.onStateChange = callback;
430
+ }
431
+
432
+ /**
433
+ * Disconnect
434
+ */
435
+ disconnect() {
436
+ this.subscription?.close();
437
+ this.subscription = null;
438
+ this.state = {
439
+ mode: "disconnected",
440
+ connected: false,
441
+ packetsReceived: 0,
442
+ bytesReceived: 0,
443
+ lastPacketTime: 0,
444
+ };
445
+ this.onStateChange?.(this.state);
446
+ }
447
+ }
448
+
449
+ // Export singleton instance
450
+ export const transportManager = new TransportManager();
451
+
452
+ /**
453
+ * @deprecated Use `TransportManager` instead. This alias is preserved for backwards compatibility.
454
+ */
455
+ export const SSMTransportManager = TransportManager;
456
+
457
+ /**
458
+ * @deprecated Use `transportManager` instead. This alias is preserved for backwards compatibility.
459
+ */
460
+ export const ssmTransport = transportManager;