@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.
- package/README.md +264 -0
- package/dist/.build-stamp +0 -0
- package/dist/abr-controller.d.ts +3 -0
- package/dist/abr-controller.d.ts.map +1 -1
- package/dist/abr-controller.js +5 -0
- package/dist/abr-controller.js.map +1 -1
- package/dist/amt-gateway.d.ts.map +1 -1
- package/dist/amt-gateway.js +2 -1
- package/dist/amt-gateway.js.map +1 -1
- package/dist/endpoint-config.d.ts +20 -0
- package/dist/endpoint-config.d.ts.map +1 -0
- package/dist/endpoint-config.js +103 -0
- package/dist/endpoint-config.js.map +1 -0
- package/dist/endpoint-config.test.d.ts +2 -0
- package/dist/endpoint-config.test.d.ts.map +1 -0
- package/dist/endpoint-config.test.js +39 -0
- package/dist/endpoint-config.test.js.map +1 -0
- package/dist/fec-client.d.ts +2 -2
- package/dist/fec-client.d.ts.map +1 -1
- package/dist/fec-client.js +10 -5
- package/dist/fec-client.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/mmt-transport.no-modules.js +1154 -0
- package/dist/mmt-transport.no-modules.js.map +7 -0
- package/dist/multicast-orchestrator.d.ts +56 -0
- package/dist/multicast-orchestrator.d.ts.map +1 -0
- package/dist/multicast-orchestrator.js +123 -0
- package/dist/multicast-orchestrator.js.map +1 -0
- package/dist/multicast-subscription-pool.d.ts +18 -0
- package/dist/multicast-subscription-pool.d.ts.map +1 -0
- package/dist/multicast-subscription-pool.js +130 -0
- package/dist/multicast-subscription-pool.js.map +1 -0
- package/dist/multicast-subscription-pool.test.d.ts +2 -0
- package/dist/multicast-subscription-pool.test.d.ts.map +1 -0
- package/dist/multicast-subscription-pool.test.js +108 -0
- package/dist/multicast-subscription-pool.test.js.map +1 -0
- package/dist/multicast-tier.d.ts +14 -0
- package/dist/multicast-tier.d.ts.map +1 -0
- package/dist/multicast-tier.js +22 -0
- package/dist/multicast-tier.js.map +1 -0
- package/dist/multicast-tier.test.d.ts +2 -0
- package/dist/multicast-tier.test.d.ts.map +1 -0
- package/dist/multicast-tier.test.js +75 -0
- package/dist/multicast-tier.test.js.map +1 -0
- package/dist/no-modules.d.ts +29 -0
- package/dist/no-modules.d.ts.map +1 -0
- package/dist/no-modules.js +35 -0
- package/dist/no-modules.js.map +1 -0
- package/dist/stall-detector.d.ts +22 -0
- package/dist/stall-detector.d.ts.map +1 -0
- package/dist/stall-detector.js +46 -0
- package/dist/stall-detector.js.map +1 -0
- package/dist/stall-detector.test.d.ts +2 -0
- package/dist/stall-detector.test.d.ts.map +1 -0
- package/dist/stall-detector.test.js +33 -0
- package/dist/stall-detector.test.js.map +1 -0
- package/dist/takeover.d.ts +49 -0
- package/dist/takeover.d.ts.map +1 -0
- package/dist/takeover.js +43 -0
- package/dist/takeover.js.map +1 -0
- package/dist/transport-manager.d.ts +28 -18
- package/dist/transport-manager.d.ts.map +1 -1
- package/dist/transport-manager.js +192 -138
- package/dist/transport-manager.js.map +1 -1
- package/dist/transport-manager.test.d.ts +2 -0
- package/dist/transport-manager.test.d.ts.map +1 -0
- package/dist/transport-manager.test.js +155 -0
- package/dist/transport-manager.test.js.map +1 -0
- package/dist/types.d.ts +36 -36
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/unicast-fallback.d.ts +36 -0
- package/dist/unicast-fallback.d.ts.map +1 -0
- package/dist/unicast-fallback.js +53 -0
- package/dist/unicast-fallback.js.map +1 -0
- package/package.json +6 -6
- package/src/abr-controller.ts +9 -0
- package/src/amt-gateway.ts +2 -1
- package/src/endpoint-config.test.ts +54 -0
- package/src/endpoint-config.ts +121 -0
- package/src/fec-client.ts +11 -5
- package/src/index.ts +72 -2
- package/src/multicast-orchestrator.ts +150 -0
- package/src/multicast-subscription-pool.test.ts +115 -0
- package/src/multicast-subscription-pool.ts +164 -0
- package/src/multicast-tier.test.ts +80 -0
- package/src/multicast-tier.ts +32 -0
- package/src/no-modules.ts +44 -0
- package/src/stall-detector.test.ts +34 -0
- package/src/stall-detector.ts +63 -0
- package/src/takeover.ts +71 -0
- package/src/transport-manager.test.ts +192 -0
- package/src/transport-manager.ts +203 -153
- package/src/types.ts +50 -31
- package/src/unicast-fallback.ts +57 -0
package/src/transport-manager.ts
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* @blockcast/transport - Transport Manager
|
|
3
3
|
*
|
|
4
4
|
* Unified transport abstraction for MoQ players supporting:
|
|
5
|
-
* -
|
|
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
|
|
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 ->
|
|
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
|
|
71
|
-
const
|
|
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:
|
|
75
|
-
nativeIGMP:
|
|
76
|
-
directSockets: typeof
|
|
77
|
-
amt:
|
|
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:
|
|
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.
|
|
91
|
-
* 2.
|
|
92
|
-
* 3.
|
|
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
|
|
113
|
-
if (caps.nativeSSM
|
|
114
|
-
console.log("[Transport] Attempting
|
|
115
|
-
connected = await this.
|
|
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.
|
|
197
|
+
if (generation !== this.connectionGeneration) return false;
|
|
198
|
+
this.state.mode = "multicast";
|
|
128
199
|
return true;
|
|
129
200
|
}
|
|
130
201
|
}
|
|
131
202
|
|
|
132
|
-
//
|
|
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.
|
|
206
|
+
connected = await this.connectAmt(config, generation);
|
|
136
207
|
if (connected) {
|
|
137
|
-
|
|
208
|
+
if (generation !== this.connectionGeneration) return false;
|
|
209
|
+
this.state.mode = "amt";
|
|
138
210
|
return true;
|
|
139
211
|
}
|
|
140
212
|
}
|
|
141
213
|
|
|
142
|
-
//
|
|
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
|
|
220
|
+
* Connect via the lowercase window.multicast bytes gateway.
|
|
149
221
|
*/
|
|
150
|
-
private async
|
|
222
|
+
private async connectWindowMulticast(config: TransportConfig, generation: number): Promise<boolean> {
|
|
151
223
|
try {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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:
|
|
248
|
+
id: lease.key,
|
|
174
249
|
config,
|
|
175
|
-
close: () =>
|
|
250
|
+
close: () => { void lease.release(); },
|
|
176
251
|
};
|
|
177
252
|
|
|
178
253
|
this.state = {
|
|
179
|
-
mode: "
|
|
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
|
|
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]
|
|
268
|
+
console.warn("[Transport] window.multicast failed:", error);
|
|
194
269
|
return false;
|
|
195
270
|
}
|
|
196
271
|
}
|
|
197
272
|
|
|
198
273
|
/**
|
|
199
|
-
* Connect via
|
|
274
|
+
* Connect via AMT proxy (with optional DRIAD discovery)
|
|
200
275
|
*/
|
|
201
|
-
private async
|
|
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.
|
|
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
|
-
//
|
|
268
|
-
|
|
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
|
|
311
|
+
private async connectAmtProxy(config: TransportConfig, amtRelay: string, generation: number): Promise<boolean> {
|
|
317
312
|
try {
|
|
318
|
-
const
|
|
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.
|
|
8
|
-
export interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 = "
|
|
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;
|