@blockcast/mmt-transport 0.2.0 → 0.2.2

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 (98) hide show
  1. package/README.md +264 -0
  2. package/dist/.build-stamp +0 -0
  3. package/dist/abr-controller.d.ts +3 -0
  4. package/dist/abr-controller.d.ts.map +1 -1
  5. package/dist/abr-controller.js +5 -0
  6. package/dist/abr-controller.js.map +1 -1
  7. package/dist/amt-gateway.d.ts.map +1 -1
  8. package/dist/amt-gateway.js +2 -1
  9. package/dist/amt-gateway.js.map +1 -1
  10. package/dist/endpoint-config.d.ts +20 -0
  11. package/dist/endpoint-config.d.ts.map +1 -0
  12. package/dist/endpoint-config.js +103 -0
  13. package/dist/endpoint-config.js.map +1 -0
  14. package/dist/endpoint-config.test.d.ts +2 -0
  15. package/dist/endpoint-config.test.d.ts.map +1 -0
  16. package/dist/endpoint-config.test.js +39 -0
  17. package/dist/endpoint-config.test.js.map +1 -0
  18. package/dist/fec-client.d.ts +2 -2
  19. package/dist/fec-client.d.ts.map +1 -1
  20. package/dist/fec-client.js +10 -5
  21. package/dist/fec-client.js.map +1 -1
  22. package/dist/index.d.ts +8 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/mmt-transport.no-modules.js +1154 -0
  27. package/dist/mmt-transport.no-modules.js.map +7 -0
  28. package/dist/multicast-orchestrator.d.ts +56 -0
  29. package/dist/multicast-orchestrator.d.ts.map +1 -0
  30. package/dist/multicast-orchestrator.js +123 -0
  31. package/dist/multicast-orchestrator.js.map +1 -0
  32. package/dist/multicast-subscription-pool.d.ts +18 -0
  33. package/dist/multicast-subscription-pool.d.ts.map +1 -0
  34. package/dist/multicast-subscription-pool.js +130 -0
  35. package/dist/multicast-subscription-pool.js.map +1 -0
  36. package/dist/multicast-subscription-pool.test.d.ts +2 -0
  37. package/dist/multicast-subscription-pool.test.d.ts.map +1 -0
  38. package/dist/multicast-subscription-pool.test.js +108 -0
  39. package/dist/multicast-subscription-pool.test.js.map +1 -0
  40. package/dist/multicast-tier.d.ts +14 -0
  41. package/dist/multicast-tier.d.ts.map +1 -0
  42. package/dist/multicast-tier.js +22 -0
  43. package/dist/multicast-tier.js.map +1 -0
  44. package/dist/multicast-tier.test.d.ts +2 -0
  45. package/dist/multicast-tier.test.d.ts.map +1 -0
  46. package/dist/multicast-tier.test.js +75 -0
  47. package/dist/multicast-tier.test.js.map +1 -0
  48. package/dist/no-modules.d.ts +29 -0
  49. package/dist/no-modules.d.ts.map +1 -0
  50. package/dist/no-modules.js +35 -0
  51. package/dist/no-modules.js.map +1 -0
  52. package/dist/stall-detector.d.ts +22 -0
  53. package/dist/stall-detector.d.ts.map +1 -0
  54. package/dist/stall-detector.js +46 -0
  55. package/dist/stall-detector.js.map +1 -0
  56. package/dist/stall-detector.test.d.ts +2 -0
  57. package/dist/stall-detector.test.d.ts.map +1 -0
  58. package/dist/stall-detector.test.js +33 -0
  59. package/dist/stall-detector.test.js.map +1 -0
  60. package/dist/takeover.d.ts +49 -0
  61. package/dist/takeover.d.ts.map +1 -0
  62. package/dist/takeover.js +43 -0
  63. package/dist/takeover.js.map +1 -0
  64. package/dist/transport-manager.d.ts +28 -18
  65. package/dist/transport-manager.d.ts.map +1 -1
  66. package/dist/transport-manager.js +192 -138
  67. package/dist/transport-manager.js.map +1 -1
  68. package/dist/transport-manager.test.d.ts +2 -0
  69. package/dist/transport-manager.test.d.ts.map +1 -0
  70. package/dist/transport-manager.test.js +155 -0
  71. package/dist/transport-manager.test.js.map +1 -0
  72. package/dist/types.d.ts +36 -36
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/types.js.map +1 -1
  75. package/dist/unicast-fallback.d.ts +36 -0
  76. package/dist/unicast-fallback.d.ts.map +1 -0
  77. package/dist/unicast-fallback.js +53 -0
  78. package/dist/unicast-fallback.js.map +1 -0
  79. package/package.json +6 -6
  80. package/src/abr-controller.ts +9 -0
  81. package/src/amt-gateway.ts +2 -1
  82. package/src/endpoint-config.test.ts +54 -0
  83. package/src/endpoint-config.ts +121 -0
  84. package/src/fec-client.ts +11 -5
  85. package/src/index.ts +72 -2
  86. package/src/multicast-orchestrator.ts +150 -0
  87. package/src/multicast-subscription-pool.test.ts +115 -0
  88. package/src/multicast-subscription-pool.ts +164 -0
  89. package/src/multicast-tier.test.ts +80 -0
  90. package/src/multicast-tier.ts +32 -0
  91. package/src/no-modules.ts +44 -0
  92. package/src/stall-detector.test.ts +34 -0
  93. package/src/stall-detector.ts +63 -0
  94. package/src/takeover.ts +71 -0
  95. package/src/transport-manager.test.ts +192 -0
  96. package/src/transport-manager.ts +203 -153
  97. package/src/types.ts +50 -31
  98. package/src/unicast-fallback.ts +57 -0
@@ -2,15 +2,15 @@
2
2
  * @blockcast/transport - Transport Manager
3
3
  *
4
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
5
+ * - window.multicast gateway subscriptions
6
+ * - AMT proxy tunneling (RFC 7450) with optional DRIAD relay discovery
7
7
  * - WebTransport/QUIC (MoQ-lite protocol)
8
8
  * - WebSocket fallback
9
9
  *
10
10
  * Transport promotion hierarchy (WebTransport-first for fast startup):
11
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
12
+ * 2. Background discovery - check if window.multicast is available
13
+ * 3. Promote to gateway multicast if available
14
14
  * 4. WebSocket fallback if WebTransport unavailable
15
15
  *
16
16
  * Features:
@@ -26,15 +26,27 @@ import type {
26
26
  TransportCapabilities,
27
27
  TransportState,
28
28
  QualityLevel,
29
+ WindowMulticastCapabilities,
30
+ WindowMulticastGateway,
31
+ WindowMulticastSubscriptionState,
29
32
  } from "./types.js";
30
33
  import { DRIADDiscovery } from "./driad-discovery.js";
31
34
  import { AdaptiveBitrateController } from "./abr-controller.js";
35
+ import {
36
+ MulticastOrchestrator,
37
+ type MulticastTierPacket,
38
+ type OrchestratorCommand,
39
+ type OrchestratorOptions,
40
+ } from "./multicast-orchestrator.js";
41
+ import type { MulticastTierEndpoint } from "./multicast-tier.js";
42
+ import { multicastSubscriptionPool } from "./multicast-subscription-pool.js";
43
+ import { normalizeMulticastEndpoint } from "./endpoint-config.js";
32
44
 
33
45
  /**
34
46
  * Transport Manager
35
47
  *
36
48
  * Manages transport connections with automatic mode selection:
37
- * WebTransport -> Native SSM -> IGMP -> AMT -> WebSocket
49
+ * WebTransport -> window.multicast -> AMT proxy -> WebSocket
38
50
  *
39
51
  * Renamed from SSMTransportManager - the old name is preserved
40
52
  * as a deprecated alias for backwards compatibility.
@@ -52,6 +64,9 @@ export class TransportManager {
52
64
  private subscription: TransportSubscription | null = null;
53
65
  private driad = new DRIADDiscovery();
54
66
  private abr: AdaptiveBitrateController;
67
+ private multicastOrchestrator: MulticastOrchestrator | null = null;
68
+ private onOrchestratorCommand?: (command: OrchestratorCommand) => void;
69
+ private connectionGeneration = 0;
55
70
 
56
71
  private onPacket?: (data: ArrayBuffer) => void;
57
72
  private onError?: (error: Error) => void;
@@ -61,22 +76,55 @@ export class TransportManager {
61
76
  this.abr = new AdaptiveBitrateController();
62
77
  }
63
78
 
79
+ private getWindow(): Window | undefined {
80
+ return typeof window === "undefined" ? undefined : window;
81
+ }
82
+
83
+ private getWindowMulticastGateway(): WindowMulticastGateway | undefined {
84
+ return this.getWindow()?.multicast;
85
+ }
86
+
87
+ private async getGatewayCapabilities(
88
+ gateway: WindowMulticastGateway,
89
+ ): Promise<WindowMulticastCapabilities | undefined> {
90
+ if (!gateway?.capabilities) return undefined;
91
+ try {
92
+ return typeof gateway.capabilities === "function" ?
93
+ await gateway.capabilities() :
94
+ gateway.capabilities;
95
+ } catch (error) {
96
+ console.warn("[Transport] window.multicast capabilities failed:", error);
97
+ return undefined;
98
+ }
99
+ }
100
+
101
+ private async gatewaySupportsBytes(gateway: WindowMulticastGateway | undefined): Promise<boolean> {
102
+ if (typeof gateway?.subscribe !== "function") return false;
103
+ if (!gateway.capabilities) return true;
104
+
105
+ const capabilities = await this.getGatewayCapabilities(gateway);
106
+ if (!capabilities) return false;
107
+ const consumeModes = capabilities.consumeModes;
108
+ return !consumeModes || consumeModes.includes("bytes");
109
+ }
110
+
64
111
  /**
65
112
  * Detect available transport capabilities
66
113
  */
67
114
  async detectCapabilities(): Promise<TransportCapabilities> {
68
115
  if (this.capabilities) return this.capabilities;
69
116
 
70
- const hasMulticast = typeof window !== "undefined" && window.Multicast !== undefined;
71
- const multicastApi = hasMulticast ? window.Multicast : null;
117
+ const gateway = this.getWindowMulticastGateway();
118
+ const hasGatewayBytes = await this.gatewaySupportsBytes(gateway);
119
+ const win = this.getWindow();
72
120
 
73
121
  this.capabilities = {
74
- nativeSSM: !!multicastApi?.subscribeSSM,
75
- nativeIGMP: !!multicastApi?.subscribe,
76
- directSockets: typeof window !== "undefined" && typeof window.UDPSocket !== "undefined",
77
- amt: !!multicastApi?.connectAMT,
122
+ nativeSSM: hasGatewayBytes,
123
+ nativeIGMP: hasGatewayBytes,
124
+ directSockets: typeof win?.UDPSocket !== "undefined",
125
+ amt: false,
78
126
  driad: true, // DRIAD uses HTTP proxy, always available
79
- fecDecode: !!multicastApi?.fecDecode,
127
+ fecDecode: false,
80
128
  };
81
129
 
82
130
  console.log("[Transport] Capabilities:", this.capabilities);
@@ -87,13 +135,45 @@ export class TransportManager {
87
135
  * Connect using best available transport
88
136
  *
89
137
  * 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)
138
+ * 1. window.multicast gateway bytes
139
+ * 2. AMT proxy (specified or discovered via DRIAD)
140
+ * 3. MOQ WebTransport (final fallback)
94
141
  */
95
142
  async connect(config: TransportConfig): Promise<boolean> {
143
+ const generation = ++this.connectionGeneration;
144
+ this.subscription?.close();
145
+ this.subscription = null;
146
+ this.state = {
147
+ mode: "disconnected",
148
+ connected: false,
149
+ packetsReceived: 0,
150
+ bytesReceived: 0,
151
+ lastPacketTime: 0,
152
+ };
153
+ this.onStateChange?.(this.state);
154
+ const endpoint = normalizeMulticastEndpoint({
155
+ groupAddress: config.group,
156
+ sourceAddress: config.source,
157
+ port: config.port,
158
+ mode: config.mode,
159
+ amtRelay: config.amtRelay,
160
+ amtRelayPort: config.amtRelayPort,
161
+ });
162
+ if (!endpoint) {
163
+ const error = new Error("Invalid multicast endpoint configuration");
164
+ this.onError?.(error);
165
+ return false;
166
+ }
167
+ config = {
168
+ ...config,
169
+ group: endpoint.groupAddress,
170
+ source: endpoint.sourceAddress ?? "",
171
+ mode: endpoint.mode,
172
+ amtRelay: endpoint.amtRelay,
173
+ amtRelayPort: endpoint.amtRelayPort,
174
+ };
96
175
  const caps = await this.detectCapabilities();
176
+ if (generation !== this.connectionGeneration) return false;
97
177
 
98
178
  // Initialize ABR with requested quality
99
179
  if (config.initialQuality) {
@@ -109,74 +189,69 @@ export class TransportManager {
109
189
  // Try transport hierarchy
110
190
  let connected = false;
111
191
 
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);
192
+ // 1. Try the canonical browser multicast gateway.
193
+ if (caps.nativeSSM || caps.nativeIGMP) {
194
+ console.log("[Transport] Attempting window.multicast...");
195
+ connected = await this.connectWindowMulticast(config, generation);
126
196
  if (connected) {
127
- this.state.mode = "igmp";
197
+ if (generation !== this.connectionGeneration) return false;
198
+ this.state.mode = "multicast";
128
199
  return true;
129
200
  }
130
201
  }
131
202
 
132
- // 3. Try AMT (with optional DRIAD discovery)
203
+ // 2. Try AMT proxy (with optional DRIAD discovery)
133
204
  if (!connected) {
134
205
  console.log("[Transport] Attempting AMT tunnel...");
135
- connected = await this.connectAMT(config);
206
+ connected = await this.connectAmt(config, generation);
136
207
  if (connected) {
137
- this.state.mode = config.driadEnabled ? "driad" : "amt";
208
+ if (generation !== this.connectionGeneration) return false;
209
+ this.state.mode = "amt";
138
210
  return true;
139
211
  }
140
212
  }
141
213
 
142
- // 4. Fallback to MOQ
214
+ // 3. Fallback to MOQ
143
215
  console.log("[Transport] All multicast transports failed, falling back to MOQ");
144
216
  return false;
145
217
  }
146
218
 
147
219
  /**
148
- * Connect via native SSM (Source-Specific Multicast)
220
+ * Connect via the lowercase window.multicast bytes gateway.
149
221
  */
150
- private async connectNativeSSM(config: TransportConfig): Promise<boolean> {
222
+ private async connectWindowMulticast(config: TransportConfig, generation: number): Promise<boolean> {
151
223
  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,
224
+ const gateway = this.getWindowMulticastGateway();
225
+ if (typeof gateway?.subscribe !== "function") return false;
226
+
227
+ const lease = await multicastSubscriptionPool.acquire(gateway, config, {
228
+ onBytes: (data) => {
229
+ if (generation === this.connectionGeneration) this.handlePacket(data);
230
+ },
231
+ onState: (state: WindowMulticastSubscriptionState) => {
232
+ if (generation !== this.connectionGeneration) return;
233
+ this.state.connected = state !== "closed";
234
+ this.onStateChange?.(this.state);
235
+ },
236
+ onError: (error) => {
237
+ if (generation !== this.connectionGeneration) return;
238
+ console.error("[Transport] window.multicast error:", error);
239
+ this.onError?.(error);
240
+ },
161
241
  });
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
- };
242
+ if (generation !== this.connectionGeneration) {
243
+ await lease.release();
244
+ return false;
245
+ }
171
246
 
172
247
  this.subscription = {
173
- id: `ssm-${Date.now()}`,
248
+ id: lease.key,
174
249
  config,
175
- close: () => subscription.unsubscribe(),
250
+ close: () => { void lease.release(); },
176
251
  };
177
252
 
178
253
  this.state = {
179
- mode: "ssm",
254
+ mode: "multicast",
180
255
  connected: true,
181
256
  group: config.group,
182
257
  source: config.source,
@@ -186,63 +261,19 @@ export class TransportManager {
186
261
  lastPacketTime: 0,
187
262
  };
188
263
 
189
- console.log(`[Transport] Connected via native SSM: (${config.source}, ${config.group})`);
264
+ console.log(`[Transport] Connected via window.multicast: (${config.source}, ${config.group})`);
190
265
  this.onStateChange?.(this.state);
191
266
  return true;
192
267
  } catch (error) {
193
- console.warn("[Transport] Native SSM failed:", error);
268
+ console.warn("[Transport] window.multicast failed:", error);
194
269
  return false;
195
270
  }
196
271
  }
197
272
 
198
273
  /**
199
- * Connect via native IGMP (Any-Source Multicast)
274
+ * Connect via AMT proxy (with optional DRIAD discovery)
200
275
  */
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> {
276
+ private async connectAmt(config: TransportConfig, generation: number): Promise<boolean> {
246
277
  try {
247
278
  let amtRelay = config.amtRelay;
248
279
 
@@ -251,11 +282,13 @@ export class TransportManager {
251
282
  if (!amtRelay && config.driadEnabled !== false && config.source) {
252
283
  console.log(`[Transport] Discovering AMT relay via DRIAD for source ${config.source}...`);
253
284
  const relays = await this.driad.discoverRelays(config.source);
285
+ if (generation !== this.connectionGeneration) return false;
254
286
  const bestRelay = await this.driad.selectBestRelay(relays);
287
+ if (generation !== this.connectionGeneration) return false;
255
288
 
256
289
  if (bestRelay) {
257
290
  amtRelay = bestRelay.host;
258
- this.state.driadRelay = amtRelay;
291
+ this.state.amtRelay = amtRelay;
259
292
  }
260
293
  }
261
294
 
@@ -264,46 +297,8 @@ export class TransportManager {
264
297
  return false;
265
298
  }
266
299
 
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);
300
+ // Use the WebSocket-based AMT proxy when a relay is known.
301
+ return await this.connectAmtProxy(config, amtRelay, generation);
307
302
  } catch (error) {
308
303
  console.warn("[Transport] AMT connection failed:", error);
309
304
  return false;
@@ -313,16 +308,31 @@ export class TransportManager {
313
308
  /**
314
309
  * Connect via WebSocket-based AMT proxy (browser fallback)
315
310
  */
316
- private async connectAMTProxy(config: TransportConfig, amtRelay: string): Promise<boolean> {
311
+ private async connectAmtProxy(config: TransportConfig, amtRelay: string, generation: number): Promise<boolean> {
317
312
  try {
318
- const wsUrl = `wss://${amtRelay}/amt-proxy?group=${config.group}&source=${config.source || ""}&port=${config.port}`;
313
+ const relayHost = amtRelay.includes(":") && !amtRelay.startsWith("[") ? `[${amtRelay}]` : amtRelay;
314
+ const relayAuthority = config.amtRelayPort ? `${relayHost}:${config.amtRelayPort}` : relayHost;
315
+ const wsUrl = new URL(`wss://${relayAuthority}/amt-proxy`);
316
+ wsUrl.searchParams.set("group", config.group);
317
+ wsUrl.searchParams.set("source", config.source || "");
318
+ wsUrl.searchParams.set("port", String(config.port));
319
319
 
320
320
  const ws = new WebSocket(wsUrl);
321
321
 
322
322
  return new Promise((resolve, reject) => {
323
323
  ws.binaryType = "arraybuffer";
324
+ const timeout = setTimeout(() => {
325
+ ws.close();
326
+ reject(new Error("AMT proxy connection timeout"));
327
+ }, 10000);
324
328
 
325
329
  ws.onopen = () => {
330
+ clearTimeout(timeout);
331
+ if (generation !== this.connectionGeneration) {
332
+ ws.close();
333
+ resolve(false);
334
+ return;
335
+ }
326
336
  console.log(`[Transport] Connected to AMT proxy: ${amtRelay}`);
327
337
 
328
338
  this.subscription = {
@@ -348,23 +358,31 @@ export class TransportManager {
348
358
  };
349
359
 
350
360
  ws.onmessage = (event) => {
351
- if (event.data instanceof ArrayBuffer) {
361
+ if (generation === this.connectionGeneration && event.data instanceof ArrayBuffer) {
352
362
  this.handlePacket(event.data);
353
363
  }
354
364
  };
355
365
 
356
366
  ws.onerror = (error) => {
367
+ clearTimeout(timeout);
368
+ if (generation !== this.connectionGeneration) {
369
+ resolve(false);
370
+ return;
371
+ }
357
372
  console.error("[Transport] AMT proxy error:", error);
358
373
  reject(error);
359
374
  };
360
375
 
361
376
  ws.onclose = () => {
377
+ clearTimeout(timeout);
378
+ if (generation !== this.connectionGeneration) {
379
+ resolve(false);
380
+ return;
381
+ }
362
382
  this.state.connected = false;
363
383
  this.onStateChange?.(this.state);
384
+ resolve(false);
364
385
  };
365
-
366
- // Timeout
367
- setTimeout(() => reject(new Error("AMT proxy connection timeout")), 10000);
368
386
  });
369
387
  } catch (error) {
370
388
  console.warn("[Transport] AMT proxy failed:", error);
@@ -408,6 +426,29 @@ export class TransportManager {
408
426
  this.abr.setQuality(name);
409
427
  }
410
428
 
429
+ /** Configures multicast-only ABR while leaving the caller's MoQ ABR untouched. */
430
+ configureMulticastTiers(
431
+ options: OrchestratorOptions,
432
+ onCommand?: (command: OrchestratorCommand) => void,
433
+ ): MulticastTierEndpoint | undefined {
434
+ this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
435
+ this.multicastOrchestrator = new MulticastOrchestrator(options);
436
+ this.onOrchestratorCommand = onCommand;
437
+ return this.multicastOrchestrator.start();
438
+ }
439
+
440
+ recordMulticastGoodput(bitrate: number | undefined): OrchestratorCommand | undefined {
441
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onGoodput({ bitrate }));
442
+ }
443
+
444
+ recordMulticastTierPacket(packet: MulticastTierPacket): OrchestratorCommand | undefined {
445
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.onTierPacket(packet));
446
+ }
447
+
448
+ pollMulticastTierSwitch(): OrchestratorCommand | undefined {
449
+ return this.dispatchOrchestratorCommand(this.multicastOrchestrator?.poll());
450
+ }
451
+
411
452
  /**
412
453
  * Set packet callback
413
454
  */
@@ -433,8 +474,12 @@ export class TransportManager {
433
474
  * Disconnect
434
475
  */
435
476
  disconnect() {
477
+ this.connectionGeneration++;
436
478
  this.subscription?.close();
437
479
  this.subscription = null;
480
+ this.dispatchOrchestratorCommand(this.multicastOrchestrator?.cancel());
481
+ this.multicastOrchestrator = null;
482
+ this.onOrchestratorCommand = undefined;
438
483
  this.state = {
439
484
  mode: "disconnected",
440
485
  connected: false,
@@ -444,6 +489,11 @@ export class TransportManager {
444
489
  };
445
490
  this.onStateChange?.(this.state);
446
491
  }
492
+
493
+ private dispatchOrchestratorCommand(command: OrchestratorCommand | undefined): OrchestratorCommand | undefined {
494
+ if (command) this.onOrchestratorCommand?.(command);
495
+ return command;
496
+ }
447
497
  }
448
498
 
449
499
  // Export singleton instance
package/src/types.ts CHANGED
@@ -4,39 +4,55 @@
4
4
  * Shared interfaces for unified transport (WebTransport, SSM, AMT, DRIAD) and FEC.
5
5
  */
6
6
 
7
- /** window.Multicast API type (from IWA/Chrome Direct Sockets) */
8
- export interface MulticastAPI {
9
- subscribeSSM?: (config: {
10
- group: string;
11
- source: string;
12
- port: number;
13
- protocol?: string;
14
- fec?: boolean;
15
- }) => Promise<MulticastSubscription>;
16
- subscribe?: (config: {
17
- group: string;
18
- port: number;
19
- protocol?: string;
20
- fec?: boolean;
21
- }) => Promise<MulticastSubscription>;
22
- connectAMT?: (config: {
23
- relay: string;
24
- group: string;
25
- source?: string;
26
- port: number;
27
- }) => Promise<{ onpacket?: (event: { data: ArrayBuffer }) => void; close: () => void }>;
28
- fecDecode?: boolean;
29
- }
30
-
31
- export interface MulticastSubscription {
32
- onpacket?: (event: { data: ArrayBuffer }) => void;
33
- onerror?: (error: Error) => void;
34
- unsubscribe: () => void;
7
+ /** window.multicast gateway config for byte subscriptions. */
8
+ export interface WindowMulticastSubscribeConfig {
9
+ group: string;
10
+ port: number;
11
+ source?: string;
12
+ consume: "bytes";
13
+ mode?: "auto" | "native" | "tunnel";
14
+ amtRelay?: string;
15
+ amtRelayPort?: number;
16
+ }
17
+
18
+ export type WindowMulticastSubscriptionState = "connecting" | "connected" | "reconnecting" | "closed";
19
+
20
+ export interface WindowMulticastSubscriptionEventMap {
21
+ bytes: CustomEvent<ArrayBuffer>;
22
+ state: CustomEvent<WindowMulticastSubscriptionState>;
23
+ error: CustomEvent<unknown>;
24
+ }
25
+
26
+ /** window.multicast subscription returned by the IWA gateway surface. */
27
+ export interface WindowMulticastSubscription {
28
+ readonly id?: string;
29
+ readonly state?: WindowMulticastSubscriptionState;
30
+ addEventListener<K extends keyof WindowMulticastSubscriptionEventMap>(
31
+ type: K,
32
+ listener: (ev: WindowMulticastSubscriptionEventMap[K]) => void,
33
+ options?: AddEventListenerOptions | boolean,
34
+ ): void;
35
+ removeEventListener<K extends keyof WindowMulticastSubscriptionEventMap>(
36
+ type: K,
37
+ listener: (ev: WindowMulticastSubscriptionEventMap[K]) => void,
38
+ options?: EventListenerOptions | boolean,
39
+ ): void;
40
+ unsubscribe: () => Promise<void> | void;
41
+ }
42
+
43
+ export interface WindowMulticastCapabilities {
44
+ readonly consumeModes?: ReadonlyArray<"bytes" | string>;
45
+ }
46
+
47
+ /** Lowercase window.multicast gateway surface. */
48
+ export interface WindowMulticastGateway {
49
+ subscribe?: (config: WindowMulticastSubscribeConfig) => Promise<WindowMulticastSubscription>;
50
+ capabilities?: WindowMulticastCapabilities | (() => Promise<WindowMulticastCapabilities>);
35
51
  }
36
52
 
37
53
  declare global {
38
54
  interface Window {
39
- Multicast?: MulticastAPI;
55
+ multicast?: WindowMulticastGateway;
40
56
  UDPSocket?: unknown;
41
57
  }
42
58
  }
@@ -53,6 +69,10 @@ export interface TransportConfig {
53
69
  streamType: "video" | "audio";
54
70
  /** Optional AMT relay host for unicast tunneling */
55
71
  amtRelay?: string;
72
+ /** Optional AMT relay port. */
73
+ amtRelayPort?: number;
74
+ /** Browser gateway transport preference. */
75
+ mode?: "auto" | "native" | "tunnel";
56
76
  /** DRIAD discovery enabled */
57
77
  driadEnabled?: boolean;
58
78
  /** Initial bitrate quality level */
@@ -79,7 +99,7 @@ export interface TransportCapabilities {
79
99
  fecDecode: boolean;
80
100
  }
81
101
 
82
- export type TransportMode = "ssm" | "igmp" | "amt" | "driad" | "moq" | "disconnected";
102
+ export type TransportMode = "multicast" | "amt" | "moq" | "disconnected";
83
103
 
84
104
  export interface TransportState {
85
105
  mode: TransportMode;
@@ -87,7 +107,6 @@ export interface TransportState {
87
107
  group?: string;
88
108
  source?: string;
89
109
  amtRelay?: string;
90
- driadRelay?: string;
91
110
  quality?: QualityLevel;
92
111
  packetsReceived: number;
93
112
  bytesReceived: number;