@reactor-team/js-sdk 2.6.0 → 2.8.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/index.d.mts +124 -172
- package/dist/index.d.ts +124 -172
- package/dist/index.js +1031 -1016
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1030 -1012
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
package/dist/index.mjs
CHANGED
|
@@ -51,12 +51,6 @@ var __async = (__this, __arguments, generator) => {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
// src/types.ts
|
|
54
|
-
function video(name, _options) {
|
|
55
|
-
return { name, kind: "video" };
|
|
56
|
-
}
|
|
57
|
-
function audio(name, _options) {
|
|
58
|
-
return { name, kind: "audio" };
|
|
59
|
-
}
|
|
60
54
|
var ConflictError = class extends Error {
|
|
61
55
|
constructor(message) {
|
|
62
56
|
super(message);
|
|
@@ -73,300 +67,163 @@ function isAbortError(error) {
|
|
|
73
67
|
|
|
74
68
|
// src/core/types.ts
|
|
75
69
|
import { z } from "zod";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
|
|
71
|
+
// package.json
|
|
72
|
+
var package_default = {
|
|
73
|
+
name: "@reactor-team/js-sdk",
|
|
74
|
+
version: "2.8.0",
|
|
75
|
+
description: "Reactor JavaScript frontend SDK",
|
|
76
|
+
main: "dist/index.js",
|
|
77
|
+
module: "dist/index.mjs",
|
|
78
|
+
types: "dist/index.d.ts",
|
|
79
|
+
exports: {
|
|
80
|
+
".": {
|
|
81
|
+
types: "./dist/index.d.ts",
|
|
82
|
+
import: "./dist/index.mjs",
|
|
83
|
+
require: "./dist/index.js"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
files: [
|
|
87
|
+
"dist",
|
|
88
|
+
"README.md"
|
|
89
|
+
],
|
|
90
|
+
scripts: {
|
|
91
|
+
build: "tsup",
|
|
92
|
+
dev: "tsup --watch",
|
|
93
|
+
test: "vitest run",
|
|
94
|
+
"test:watch": "vitest",
|
|
95
|
+
"test:unit": "vitest run __tests__/unit",
|
|
96
|
+
"test:integration": "vitest run __tests__/integration",
|
|
97
|
+
format: "prettier --write .",
|
|
98
|
+
"format:check": "prettier --check ."
|
|
99
|
+
},
|
|
100
|
+
keywords: [
|
|
101
|
+
"reactor",
|
|
102
|
+
"frontend",
|
|
103
|
+
"sdk"
|
|
104
|
+
],
|
|
105
|
+
author: "Reactor Technologies, Inc.",
|
|
106
|
+
reactor: {
|
|
107
|
+
apiVersion: 1,
|
|
108
|
+
webrtcVersion: "1.0"
|
|
109
|
+
},
|
|
110
|
+
license: "MIT",
|
|
111
|
+
repository: {
|
|
112
|
+
type: "git",
|
|
113
|
+
url: "https://github.com/reactor-team/js-sdk"
|
|
114
|
+
},
|
|
115
|
+
packageManager: "pnpm@10.12.1",
|
|
116
|
+
devDependencies: {
|
|
117
|
+
"@roamhq/wrtc": "^0.8.0",
|
|
118
|
+
"@types/inquirer": "^9.0.9",
|
|
119
|
+
"@types/node": "^20",
|
|
120
|
+
"@types/react": "^18.2.8",
|
|
121
|
+
"@types/ws": "^8.18.1",
|
|
122
|
+
prettier: "^3.6.2",
|
|
123
|
+
tsup: "^8.5.0",
|
|
124
|
+
typescript: "^5.8.3",
|
|
125
|
+
vitest: "^3.0.0"
|
|
126
|
+
},
|
|
127
|
+
dependencies: {
|
|
128
|
+
"@bufbuild/protobuf": "^2.0.0",
|
|
129
|
+
chalk: "^5.3.0",
|
|
130
|
+
inquirer: "^9.3.0",
|
|
131
|
+
"simple-git": "^3.24.0",
|
|
132
|
+
ws: "^8.18.3",
|
|
133
|
+
zod: "^4.0.5"
|
|
134
|
+
},
|
|
135
|
+
peerDependencies: {
|
|
136
|
+
react: "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
137
|
+
zustand: "^5.0.6"
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/core/types.ts
|
|
142
|
+
var REACTOR_SDK_VERSION = package_default.version;
|
|
143
|
+
var REACTOR_API_VERSION = package_default.reactor.apiVersion;
|
|
144
|
+
var REACTOR_WEBRTC_VERSION = package_default.reactor.webrtcVersion;
|
|
145
|
+
var REACTOR_SDK_TYPE = "js";
|
|
146
|
+
var API_VERSION_HEADER = "Reactor-API-Version";
|
|
147
|
+
var API_ACCEPT_VERSION_HEADER = "Reactor-API-Accept-Version";
|
|
148
|
+
var WEBRTC_VERSION_HEADER = "Reactor-WebRTC-Version";
|
|
149
|
+
var VERSION_ERROR_CODES = {
|
|
150
|
+
426: "CLIENT_VERSION_TOO_OLD",
|
|
151
|
+
501: "SERVER_VERSION_TOO_OLD"
|
|
152
|
+
};
|
|
153
|
+
var ClientInfoSchema = z.object({
|
|
154
|
+
sdk_version: z.string(),
|
|
155
|
+
sdk_type: z.literal("js")
|
|
156
|
+
});
|
|
157
|
+
var TransportDeclarationSchema = z.object({
|
|
158
|
+
protocol: z.string(),
|
|
159
|
+
version: z.string()
|
|
160
|
+
});
|
|
161
|
+
var TrackCapabilitySchema = z.object({
|
|
162
|
+
name: z.string(),
|
|
163
|
+
kind: z.enum(["video", "audio"]),
|
|
164
|
+
direction: z.enum(["recvonly", "sendonly"])
|
|
165
|
+
});
|
|
166
|
+
var TrackMappingEntrySchema = TrackCapabilitySchema.extend({
|
|
167
|
+
mid: z.string()
|
|
88
168
|
});
|
|
89
169
|
var CreateSessionRequestSchema = z.object({
|
|
90
|
-
model:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
model: z.object({ name: z.string() }),
|
|
171
|
+
client_info: ClientInfoSchema,
|
|
172
|
+
supported_transports: z.array(TransportDeclarationSchema),
|
|
173
|
+
extra_args: z.record(z.string(), z.any()).optional()
|
|
94
174
|
});
|
|
95
|
-
var
|
|
96
|
-
|
|
175
|
+
var CommandCapabilitySchema = z.object({
|
|
176
|
+
name: z.string(),
|
|
177
|
+
description: z.string(),
|
|
178
|
+
schema: z.record(z.string(), z.any()).optional()
|
|
97
179
|
});
|
|
98
|
-
var
|
|
99
|
-
|
|
100
|
-
|
|
180
|
+
var CapabilitiesSchema = z.object({
|
|
181
|
+
protocol_version: z.string(),
|
|
182
|
+
tracks: z.array(TrackCapabilitySchema),
|
|
183
|
+
commands: z.array(CommandCapabilitySchema).optional(),
|
|
184
|
+
emission_fps: z.number().nullable().optional()
|
|
101
185
|
});
|
|
102
|
-
var SessionInfoResponseSchema =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
186
|
+
var SessionInfoResponseSchema = z.object({
|
|
187
|
+
session_id: z.string(),
|
|
188
|
+
state: z.string(),
|
|
189
|
+
cluster: z.string()
|
|
106
190
|
});
|
|
107
|
-
var
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Dictionary
|
|
191
|
+
var CreateSessionResponseSchema = SessionInfoResponseSchema.extend({
|
|
192
|
+
model: z.object({ name: z.string(), version: z.string().optional() }),
|
|
193
|
+
server_info: z.object({ server_version: z.string() })
|
|
111
194
|
});
|
|
112
|
-
var
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
195
|
+
var SessionResponseSchema = CreateSessionResponseSchema.extend({
|
|
196
|
+
selected_transport: TransportDeclarationSchema.optional(),
|
|
197
|
+
capabilities: CapabilitiesSchema.optional()
|
|
198
|
+
});
|
|
199
|
+
var TerminateSessionRequestSchema = z.object({
|
|
200
|
+
reason: z.string().optional()
|
|
201
|
+
});
|
|
202
|
+
var IceServerCredentialsSchema = z.object({
|
|
203
|
+
username: z.string(),
|
|
204
|
+
password: z.string()
|
|
205
|
+
});
|
|
206
|
+
var IceServerSchema = z.object({
|
|
207
|
+
uris: z.array(z.string()),
|
|
208
|
+
credentials: IceServerCredentialsSchema.optional()
|
|
116
209
|
});
|
|
117
210
|
var IceServersResponseSchema = z.object({
|
|
118
|
-
ice_servers: z.array(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
211
|
+
ice_servers: z.array(IceServerSchema)
|
|
212
|
+
});
|
|
213
|
+
var WebRTCSdpOfferRequestSchema = z.object({
|
|
214
|
+
sdp_offer: z.string(),
|
|
215
|
+
client_info: ClientInfoSchema.optional(),
|
|
216
|
+
track_mapping: z.array(TrackMappingEntrySchema)
|
|
217
|
+
});
|
|
218
|
+
var WebRTCSdpAnswerResponseSchema = z.object({
|
|
219
|
+
sdp_answer: z.string()
|
|
127
220
|
});
|
|
128
|
-
|
|
129
|
-
// src/utils/webrtc.ts
|
|
130
|
-
var DEFAULT_DATA_CHANNEL_LABEL = "data";
|
|
131
|
-
var FORCE_RELAY_MODE = false;
|
|
132
|
-
var DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
|
|
133
|
-
function createPeerConnection(config) {
|
|
134
|
-
return new RTCPeerConnection({
|
|
135
|
-
iceServers: config.iceServers,
|
|
136
|
-
iceTransportPolicy: FORCE_RELAY_MODE ? "relay" : "all"
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
function createDataChannel(pc, label) {
|
|
140
|
-
return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
|
|
141
|
-
}
|
|
142
|
-
function rewriteMids(sdp, trackNames) {
|
|
143
|
-
const lines = sdp.split("\r\n");
|
|
144
|
-
let mediaIdx = 0;
|
|
145
|
-
const replacements = /* @__PURE__ */ new Map();
|
|
146
|
-
let inApplication = false;
|
|
147
|
-
for (let i = 0; i < lines.length; i++) {
|
|
148
|
-
if (lines[i].startsWith("m=")) {
|
|
149
|
-
inApplication = lines[i].startsWith("m=application");
|
|
150
|
-
}
|
|
151
|
-
if (!inApplication && lines[i].startsWith("a=mid:")) {
|
|
152
|
-
const oldMid = lines[i].substring("a=mid:".length);
|
|
153
|
-
if (mediaIdx < trackNames.length) {
|
|
154
|
-
const newMid = trackNames[mediaIdx];
|
|
155
|
-
replacements.set(oldMid, newMid);
|
|
156
|
-
lines[i] = `a=mid:${newMid}`;
|
|
157
|
-
mediaIdx++;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
for (let i = 0; i < lines.length; i++) {
|
|
162
|
-
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
163
|
-
const parts = lines[i].split(" ");
|
|
164
|
-
for (let j = 1; j < parts.length; j++) {
|
|
165
|
-
const replacement = replacements.get(parts[j]);
|
|
166
|
-
if (replacement !== void 0) {
|
|
167
|
-
parts[j] = replacement;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
lines[i] = parts.join(" ");
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return lines.join("\r\n");
|
|
175
|
-
}
|
|
176
|
-
function createOffer(pc, trackNames) {
|
|
177
|
-
return __async(this, null, function* () {
|
|
178
|
-
const offer = yield pc.createOffer();
|
|
179
|
-
let needsAnswerRestore = false;
|
|
180
|
-
if (trackNames && trackNames.length > 0 && offer.sdp) {
|
|
181
|
-
const munged = rewriteMids(offer.sdp, trackNames);
|
|
182
|
-
try {
|
|
183
|
-
yield pc.setLocalDescription(
|
|
184
|
-
new RTCSessionDescription({ type: "offer", sdp: munged })
|
|
185
|
-
);
|
|
186
|
-
} catch (e) {
|
|
187
|
-
yield pc.setLocalDescription(offer);
|
|
188
|
-
needsAnswerRestore = true;
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
yield pc.setLocalDescription(offer);
|
|
192
|
-
}
|
|
193
|
-
yield waitForIceGathering(pc);
|
|
194
|
-
const localDescription = pc.localDescription;
|
|
195
|
-
if (!localDescription) {
|
|
196
|
-
throw new Error("Failed to create local description");
|
|
197
|
-
}
|
|
198
|
-
let sdp = localDescription.sdp;
|
|
199
|
-
if (needsAnswerRestore && trackNames && trackNames.length > 0) {
|
|
200
|
-
sdp = rewriteMids(sdp, trackNames);
|
|
201
|
-
}
|
|
202
|
-
return { sdp, needsAnswerRestore };
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
function buildMidMapping(transceivers) {
|
|
206
|
-
var _a;
|
|
207
|
-
const localToRemote = /* @__PURE__ */ new Map();
|
|
208
|
-
const remoteToLocal = /* @__PURE__ */ new Map();
|
|
209
|
-
for (const entry of transceivers) {
|
|
210
|
-
const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
|
|
211
|
-
if (mid) {
|
|
212
|
-
localToRemote.set(mid, entry.name);
|
|
213
|
-
remoteToLocal.set(entry.name, mid);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return { localToRemote, remoteToLocal };
|
|
217
|
-
}
|
|
218
|
-
function restoreAnswerMids(sdp, remoteToLocal) {
|
|
219
|
-
const lines = sdp.split("\r\n");
|
|
220
|
-
for (let i = 0; i < lines.length; i++) {
|
|
221
|
-
if (lines[i].startsWith("a=mid:")) {
|
|
222
|
-
const remoteMid = lines[i].substring("a=mid:".length);
|
|
223
|
-
const localMid = remoteToLocal.get(remoteMid);
|
|
224
|
-
if (localMid !== void 0) {
|
|
225
|
-
lines[i] = `a=mid:${localMid}`;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
229
|
-
const parts = lines[i].split(" ");
|
|
230
|
-
for (let j = 1; j < parts.length; j++) {
|
|
231
|
-
const localMid = remoteToLocal.get(parts[j]);
|
|
232
|
-
if (localMid !== void 0) {
|
|
233
|
-
parts[j] = localMid;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
lines[i] = parts.join(" ");
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return lines.join("\r\n");
|
|
240
|
-
}
|
|
241
|
-
function setRemoteDescription(pc, sdp) {
|
|
242
|
-
return __async(this, null, function* () {
|
|
243
|
-
const sessionDescription = new RTCSessionDescription({
|
|
244
|
-
sdp,
|
|
245
|
-
type: "answer"
|
|
246
|
-
});
|
|
247
|
-
yield pc.setRemoteDescription(sessionDescription);
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
function getLocalDescription(pc) {
|
|
251
|
-
const desc = pc.localDescription;
|
|
252
|
-
if (!desc) return void 0;
|
|
253
|
-
return desc.sdp;
|
|
254
|
-
}
|
|
255
|
-
function transformIceServers(response) {
|
|
256
|
-
return response.ice_servers.map((server) => {
|
|
257
|
-
const rtcServer = {
|
|
258
|
-
urls: server.uris
|
|
259
|
-
};
|
|
260
|
-
if (server.credentials) {
|
|
261
|
-
rtcServer.username = server.credentials.username;
|
|
262
|
-
rtcServer.credential = server.credentials.password;
|
|
263
|
-
}
|
|
264
|
-
return rtcServer;
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
function waitForIceGathering(pc, timeoutMs = 5e3) {
|
|
268
|
-
return new Promise((resolve) => {
|
|
269
|
-
if (pc.iceGatheringState === "complete") {
|
|
270
|
-
resolve();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const onGatheringStateChange = () => {
|
|
274
|
-
if (pc.iceGatheringState === "complete") {
|
|
275
|
-
pc.removeEventListener(
|
|
276
|
-
"icegatheringstatechange",
|
|
277
|
-
onGatheringStateChange
|
|
278
|
-
);
|
|
279
|
-
resolve();
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
pc.addEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
283
|
-
setTimeout(() => {
|
|
284
|
-
pc.removeEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
285
|
-
resolve();
|
|
286
|
-
}, timeoutMs);
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
function sendMessage(channel, command, data, scope = "application", maxBytes = DEFAULT_MAX_MESSAGE_BYTES) {
|
|
290
|
-
if (channel.readyState !== "open") {
|
|
291
|
-
throw new Error(`Data channel not open: ${channel.readyState}`);
|
|
292
|
-
}
|
|
293
|
-
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
|
294
|
-
const inner = { type: command, data: jsonData };
|
|
295
|
-
const payload = { scope, data: inner };
|
|
296
|
-
const serialized = JSON.stringify(payload);
|
|
297
|
-
const byteLength = new TextEncoder().encode(serialized).byteLength;
|
|
298
|
-
if (byteLength > maxBytes) {
|
|
299
|
-
throw new Error(
|
|
300
|
-
`Data channel message too large: ${byteLength} bytes exceeds limit of ${maxBytes} bytes (command: "${command}")`
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
channel.send(serialized);
|
|
304
|
-
}
|
|
305
|
-
function parseMessage(data) {
|
|
306
|
-
if (typeof data === "string") {
|
|
307
|
-
try {
|
|
308
|
-
return JSON.parse(data);
|
|
309
|
-
} catch (e) {
|
|
310
|
-
return data;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return data;
|
|
314
|
-
}
|
|
315
|
-
function closePeerConnection(pc) {
|
|
316
|
-
pc.close();
|
|
317
|
-
}
|
|
318
|
-
function extractConnectionStats(report) {
|
|
319
|
-
let rtt;
|
|
320
|
-
let availableOutgoingBitrate;
|
|
321
|
-
let localCandidateId;
|
|
322
|
-
let framesPerSecond;
|
|
323
|
-
let jitter;
|
|
324
|
-
let packetLossRatio;
|
|
325
|
-
report.forEach((stat) => {
|
|
326
|
-
if (stat.type === "candidate-pair" && stat.state === "succeeded") {
|
|
327
|
-
if (stat.currentRoundTripTime !== void 0) {
|
|
328
|
-
rtt = stat.currentRoundTripTime * 1e3;
|
|
329
|
-
}
|
|
330
|
-
if (stat.availableOutgoingBitrate !== void 0) {
|
|
331
|
-
availableOutgoingBitrate = stat.availableOutgoingBitrate;
|
|
332
|
-
}
|
|
333
|
-
localCandidateId = stat.localCandidateId;
|
|
334
|
-
}
|
|
335
|
-
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
336
|
-
if (stat.framesPerSecond !== void 0) {
|
|
337
|
-
framesPerSecond = stat.framesPerSecond;
|
|
338
|
-
}
|
|
339
|
-
if (stat.jitter !== void 0) {
|
|
340
|
-
jitter = stat.jitter;
|
|
341
|
-
}
|
|
342
|
-
if (stat.packetsReceived !== void 0 && stat.packetsLost !== void 0 && stat.packetsReceived + stat.packetsLost > 0) {
|
|
343
|
-
packetLossRatio = stat.packetsLost / (stat.packetsReceived + stat.packetsLost);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
let candidateType;
|
|
348
|
-
if (localCandidateId) {
|
|
349
|
-
const localCandidate = report.get(localCandidateId);
|
|
350
|
-
if (localCandidate == null ? void 0 : localCandidate.candidateType) {
|
|
351
|
-
candidateType = localCandidate.candidateType;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return {
|
|
355
|
-
rtt,
|
|
356
|
-
candidateType,
|
|
357
|
-
availableOutgoingBitrate,
|
|
358
|
-
framesPerSecond,
|
|
359
|
-
packetLossRatio,
|
|
360
|
-
jitter,
|
|
361
|
-
timestamp: Date.now()
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
221
|
|
|
365
222
|
// src/core/CoordinatorClient.ts
|
|
366
|
-
var
|
|
367
|
-
var
|
|
368
|
-
var
|
|
369
|
-
var
|
|
223
|
+
var SESSION_POLL_INITIAL_BACKOFF_MS = 200;
|
|
224
|
+
var SESSION_POLL_MAX_BACKOFF_MS = 1e4;
|
|
225
|
+
var SESSION_POLL_BACKOFF_MULTIPLIER = 2;
|
|
226
|
+
var SESSION_POLL_DEFAULT_MAX_ATTEMPTS = 20;
|
|
370
227
|
var CoordinatorClient = class {
|
|
371
228
|
constructor(options) {
|
|
372
229
|
this.baseUrl = options.baseUrl;
|
|
@@ -375,7 +232,7 @@ var CoordinatorClient = class {
|
|
|
375
232
|
this.abortController = new AbortController();
|
|
376
233
|
}
|
|
377
234
|
/**
|
|
378
|
-
* Aborts any in-flight HTTP requests
|
|
235
|
+
* Aborts any in-flight HTTP requests.
|
|
379
236
|
* A fresh AbortController is created so the client remains reusable.
|
|
380
237
|
*/
|
|
381
238
|
abort() {
|
|
@@ -383,69 +240,88 @@ var CoordinatorClient = class {
|
|
|
383
240
|
this.abortController = new AbortController();
|
|
384
241
|
}
|
|
385
242
|
/**
|
|
386
|
-
* The current abort signal, passed to every fetch()
|
|
243
|
+
* The current abort signal, passed to every fetch() call.
|
|
387
244
|
* Protected so subclasses can forward it to their own fetch calls.
|
|
388
245
|
*/
|
|
389
246
|
get signal() {
|
|
390
247
|
return this.abortController.signal;
|
|
391
248
|
}
|
|
392
249
|
/**
|
|
393
|
-
* Returns
|
|
250
|
+
* Returns authorization + versioning headers for all coordinator requests.
|
|
394
251
|
*/
|
|
395
|
-
|
|
252
|
+
getHeaders() {
|
|
396
253
|
return {
|
|
397
|
-
Authorization: `Bearer ${this.jwtToken}
|
|
254
|
+
Authorization: `Bearer ${this.jwtToken}`,
|
|
255
|
+
[API_VERSION_HEADER]: String(REACTOR_API_VERSION),
|
|
256
|
+
[API_ACCEPT_VERSION_HEADER]: String(REACTOR_API_VERSION)
|
|
398
257
|
};
|
|
399
258
|
}
|
|
400
259
|
/**
|
|
401
|
-
*
|
|
402
|
-
*
|
|
260
|
+
* Checks an HTTP response for version mismatch errors (426, 501).
|
|
261
|
+
* Logs a clear message and throws with a descriptive error code.
|
|
403
262
|
*/
|
|
404
|
-
|
|
263
|
+
checkVersionMismatch(response) {
|
|
405
264
|
return __async(this, null, function* () {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
{
|
|
410
|
-
method: "GET",
|
|
411
|
-
headers: this.getAuthHeaders(),
|
|
412
|
-
signal: this.signal
|
|
413
|
-
}
|
|
414
|
-
);
|
|
415
|
-
if (!response.ok) {
|
|
416
|
-
throw new Error(`Failed to fetch ICE servers: ${response.status}`);
|
|
265
|
+
if (response.status === 426) {
|
|
266
|
+
const msg = `Client API version (${REACTOR_API_VERSION}) is too old. Server requires a newer version. Please upgrade @reactor-team/js-sdk.`;
|
|
267
|
+
console.error(`[Reactor]`, msg);
|
|
268
|
+
throw new Error(`${VERSION_ERROR_CODES[426]}: ${msg}`);
|
|
417
269
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
270
|
+
if (response.status === 501) {
|
|
271
|
+
const msg = `Server does not support API version ${REACTOR_API_VERSION}. The server may need to be updated.`;
|
|
272
|
+
console.error(`[Reactor]`, msg);
|
|
273
|
+
throw new Error(`${VERSION_ERROR_CODES[501]}: ${msg}`);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
sleep(ms) {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const { signal } = this;
|
|
280
|
+
if (signal.aborted) {
|
|
281
|
+
reject(new AbortError("Sleep aborted"));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const timer = setTimeout(() => {
|
|
285
|
+
signal.removeEventListener("abort", onAbort);
|
|
286
|
+
resolve();
|
|
287
|
+
}, ms);
|
|
288
|
+
const onAbort = () => {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
reject(new AbortError("Sleep aborted"));
|
|
291
|
+
};
|
|
292
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
426
293
|
});
|
|
427
294
|
}
|
|
428
295
|
/**
|
|
429
296
|
* Creates a new session with the coordinator.
|
|
430
|
-
*
|
|
431
|
-
*
|
|
297
|
+
* No SDP is sent — transport signaling is decoupled from session creation.
|
|
298
|
+
*
|
|
299
|
+
* The POST response is a slim acknowledgment (session_id, model name, status).
|
|
300
|
+
* Capabilities and transport details are populated later once the Runtime
|
|
301
|
+
* accepts the session — use {@link pollSessionReady} to wait for them.
|
|
432
302
|
*/
|
|
433
|
-
createSession(
|
|
303
|
+
createSession(extraArgs) {
|
|
434
304
|
return __async(this, null, function* () {
|
|
435
305
|
console.debug("[CoordinatorClient] Creating session...");
|
|
436
|
-
const requestBody = {
|
|
306
|
+
const requestBody = __spreadValues({
|
|
437
307
|
model: { name: this.model },
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
308
|
+
client_info: {
|
|
309
|
+
sdk_version: REACTOR_SDK_VERSION,
|
|
310
|
+
sdk_type: REACTOR_SDK_TYPE
|
|
311
|
+
},
|
|
312
|
+
supported_transports: [
|
|
313
|
+
{ protocol: "webrtc", version: REACTOR_WEBRTC_VERSION }
|
|
314
|
+
]
|
|
315
|
+
}, extraArgs ? { extra_args: extraArgs } : {});
|
|
441
316
|
const response = yield fetch(`${this.baseUrl}/sessions`, {
|
|
442
317
|
method: "POST",
|
|
443
|
-
headers: __spreadProps(__spreadValues({}, this.
|
|
318
|
+
headers: __spreadProps(__spreadValues({}, this.getHeaders()), {
|
|
444
319
|
"Content-Type": "application/json"
|
|
445
320
|
}),
|
|
446
321
|
body: JSON.stringify(requestBody),
|
|
447
322
|
signal: this.signal
|
|
448
323
|
});
|
|
324
|
+
yield this.checkVersionMismatch(response);
|
|
449
325
|
if (!response.ok) {
|
|
450
326
|
const errorText = yield response.text();
|
|
451
327
|
throw new Error(
|
|
@@ -453,339 +329,464 @@ var CoordinatorClient = class {
|
|
|
453
329
|
);
|
|
454
330
|
}
|
|
455
331
|
const data = yield response.json();
|
|
456
|
-
|
|
332
|
+
const parsed = CreateSessionResponseSchema.parse(data);
|
|
333
|
+
this.currentSessionId = parsed.session_id;
|
|
457
334
|
console.debug(
|
|
458
|
-
"[CoordinatorClient] Session created
|
|
459
|
-
this.currentSessionId
|
|
335
|
+
"[CoordinatorClient] Session created:",
|
|
336
|
+
this.currentSessionId,
|
|
337
|
+
"state:",
|
|
338
|
+
parsed.state
|
|
460
339
|
);
|
|
461
|
-
return
|
|
340
|
+
return parsed;
|
|
462
341
|
});
|
|
463
342
|
}
|
|
464
343
|
/**
|
|
465
|
-
*
|
|
466
|
-
*
|
|
344
|
+
* Polls GET /sessions/{id} until the Runtime has accepted the session
|
|
345
|
+
* and populated capabilities and selected_transport.
|
|
467
346
|
*/
|
|
468
|
-
|
|
347
|
+
pollSessionReady(opts) {
|
|
469
348
|
return __async(this, null, function* () {
|
|
349
|
+
var _a;
|
|
470
350
|
if (!this.currentSessionId) {
|
|
471
351
|
throw new Error("No active session. Call createSession() first.");
|
|
472
352
|
}
|
|
353
|
+
const maxAttempts = (_a = opts == null ? void 0 : opts.maxAttempts) != null ? _a : SESSION_POLL_DEFAULT_MAX_ATTEMPTS;
|
|
354
|
+
let backoffMs = SESSION_POLL_INITIAL_BACKOFF_MS;
|
|
355
|
+
let attempt = 0;
|
|
473
356
|
console.debug(
|
|
474
|
-
"[CoordinatorClient]
|
|
475
|
-
this.currentSessionId
|
|
357
|
+
"[CoordinatorClient] Polling session until capabilities are available..."
|
|
476
358
|
);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
method: "GET",
|
|
481
|
-
headers: this.getAuthHeaders(),
|
|
482
|
-
signal: this.signal
|
|
359
|
+
while (true) {
|
|
360
|
+
if (this.signal.aborted) {
|
|
361
|
+
throw new AbortError("Session polling aborted");
|
|
483
362
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
363
|
+
if (attempt >= maxAttempts) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Session polling exceeded maximum attempts (${maxAttempts}). The model may be unavailable or overloaded.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
attempt++;
|
|
369
|
+
const response = yield fetch(
|
|
370
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
371
|
+
{
|
|
372
|
+
method: "GET",
|
|
373
|
+
headers: this.getHeaders(),
|
|
374
|
+
signal: this.signal
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
yield this.checkVersionMismatch(response);
|
|
378
|
+
if (!response.ok) {
|
|
379
|
+
const errorText = yield response.text();
|
|
380
|
+
throw new Error(
|
|
381
|
+
`Failed to poll session: ${response.status} ${errorText}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const data = yield response.json();
|
|
385
|
+
const partial = SessionResponseSchema.parse(data);
|
|
386
|
+
const terminalStates = [
|
|
387
|
+
"CLOSED" /* CLOSED */,
|
|
388
|
+
"INACTIVE" /* INACTIVE */
|
|
389
|
+
];
|
|
390
|
+
if (terminalStates.includes(partial.state)) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Session entered terminal state "${partial.state}" while waiting for capabilities`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (partial.capabilities && partial.selected_transport) {
|
|
396
|
+
console.debug(
|
|
397
|
+
`[CoordinatorClient] Session ready after ${attempt} poll(s), transport: ${partial.selected_transport.protocol}, tracks: ${partial.capabilities.tracks.length}`
|
|
398
|
+
);
|
|
399
|
+
return partial;
|
|
400
|
+
}
|
|
401
|
+
console.debug(
|
|
402
|
+
`[CoordinatorClient] Session poll ${attempt}/${maxAttempts} \u2014 state: ${partial.state}, waiting ${backoffMs}ms...`
|
|
403
|
+
);
|
|
404
|
+
yield this.sleep(backoffMs);
|
|
405
|
+
backoffMs = Math.min(
|
|
406
|
+
backoffMs * SESSION_POLL_BACKOFF_MULTIPLIER,
|
|
407
|
+
SESSION_POLL_MAX_BACKOFF_MS
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Gets session details from the coordinator.
|
|
414
|
+
* Fields like selected_transport and capabilities are only present
|
|
415
|
+
* after the Runtime accepts the session.
|
|
497
416
|
*/
|
|
498
|
-
|
|
417
|
+
getSession() {
|
|
499
418
|
return __async(this, null, function* () {
|
|
500
419
|
if (!this.currentSessionId) {
|
|
501
|
-
|
|
420
|
+
throw new Error("No active session. Call createSession() first.");
|
|
502
421
|
}
|
|
503
|
-
console.debug(
|
|
504
|
-
"[CoordinatorClient] Terminating session:",
|
|
505
|
-
this.currentSessionId
|
|
506
|
-
);
|
|
507
422
|
const response = yield fetch(
|
|
508
423
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
509
424
|
{
|
|
510
|
-
method: "
|
|
511
|
-
headers: this.
|
|
425
|
+
method: "GET",
|
|
426
|
+
headers: this.getHeaders(),
|
|
512
427
|
signal: this.signal
|
|
513
428
|
}
|
|
514
429
|
);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (response.status === 404) {
|
|
520
|
-
console.debug(
|
|
521
|
-
"[CoordinatorClient] Session not found on server, clearing local state:",
|
|
522
|
-
this.currentSessionId
|
|
523
|
-
);
|
|
524
|
-
this.currentSessionId = void 0;
|
|
525
|
-
return;
|
|
430
|
+
yield this.checkVersionMismatch(response);
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
const errorText = yield response.text();
|
|
433
|
+
throw new Error(`Failed to get session: ${response.status} ${errorText}`);
|
|
526
434
|
}
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
`Failed to terminate session: ${response.status} ${errorText}`
|
|
530
|
-
);
|
|
435
|
+
const data = yield response.json();
|
|
436
|
+
return SessionResponseSchema.parse(data);
|
|
531
437
|
});
|
|
532
438
|
}
|
|
533
439
|
/**
|
|
534
|
-
*
|
|
440
|
+
* Gets lightweight session status (session_id, cluster, status).
|
|
535
441
|
*/
|
|
536
|
-
|
|
537
|
-
return this.currentSessionId;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Sends an SDP offer to the server for reconnection.
|
|
541
|
-
* @param sessionId - The session ID to connect to
|
|
542
|
-
* @param sdpOffer - The SDP offer from the local WebRTC peer connection
|
|
543
|
-
* @returns The SDP answer if ready (200), or null if pending (202)
|
|
544
|
-
*/
|
|
545
|
-
sendSdpOffer(sessionId, sdpOffer) {
|
|
442
|
+
getSessionInfo() {
|
|
546
443
|
return __async(this, null, function* () {
|
|
547
|
-
|
|
548
|
-
"
|
|
549
|
-
|
|
550
|
-
);
|
|
551
|
-
const requestBody = {
|
|
552
|
-
sdp_offer: sdpOffer,
|
|
553
|
-
extra_args: {}
|
|
554
|
-
};
|
|
444
|
+
if (!this.currentSessionId) {
|
|
445
|
+
throw new Error("No active session. Call createSession() first.");
|
|
446
|
+
}
|
|
555
447
|
const response = yield fetch(
|
|
556
|
-
`${this.baseUrl}/sessions/${
|
|
448
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}/info`,
|
|
557
449
|
{
|
|
558
|
-
method: "
|
|
559
|
-
headers:
|
|
560
|
-
"Content-Type": "application/json"
|
|
561
|
-
}),
|
|
562
|
-
body: JSON.stringify(requestBody),
|
|
450
|
+
method: "GET",
|
|
451
|
+
headers: this.getHeaders(),
|
|
563
452
|
signal: this.signal
|
|
564
453
|
}
|
|
565
454
|
);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (response.status === 202) {
|
|
572
|
-
console.debug(
|
|
573
|
-
"[CoordinatorClient] SDP offer accepted, answer pending (202)"
|
|
455
|
+
yield this.checkVersionMismatch(response);
|
|
456
|
+
if (!response.ok) {
|
|
457
|
+
const errorText = yield response.text();
|
|
458
|
+
throw new Error(
|
|
459
|
+
`Failed to get session info: ${response.status} ${errorText}`
|
|
574
460
|
);
|
|
575
|
-
return null;
|
|
576
461
|
}
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
`Failed to send SDP offer: ${response.status} ${errorText}`
|
|
580
|
-
);
|
|
462
|
+
const data = yield response.json();
|
|
463
|
+
return SessionInfoResponseSchema.parse(data);
|
|
581
464
|
});
|
|
582
465
|
}
|
|
583
466
|
/**
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* @param sessionId - The session ID to poll for
|
|
587
|
-
* @param maxAttempts - Optional maximum number of polling attempts before giving up
|
|
588
|
-
* @returns The SDP answer from the server
|
|
467
|
+
* Restarts an inactive session with a different compute unit.
|
|
468
|
+
* The session ID is preserved but a new transport must be established.
|
|
589
469
|
*/
|
|
590
|
-
|
|
591
|
-
return __async(this,
|
|
470
|
+
restartSession() {
|
|
471
|
+
return __async(this, null, function* () {
|
|
472
|
+
if (!this.currentSessionId) {
|
|
473
|
+
throw new Error("No active session. Call createSession() first.");
|
|
474
|
+
}
|
|
592
475
|
console.debug(
|
|
593
|
-
"[CoordinatorClient]
|
|
594
|
-
|
|
476
|
+
"[CoordinatorClient] Restarting session:",
|
|
477
|
+
this.currentSessionId
|
|
595
478
|
);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (attempt >= maxAttempts) {
|
|
603
|
-
throw new Error(
|
|
604
|
-
`SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
attempt++;
|
|
608
|
-
console.debug(
|
|
609
|
-
`[CoordinatorClient] SDP poll attempt ${attempt}/${maxAttempts} for session ${sessionId}`
|
|
610
|
-
);
|
|
611
|
-
const response = yield fetch(
|
|
612
|
-
`${this.baseUrl}/sessions/${sessionId}/sdp_params`,
|
|
613
|
-
{
|
|
614
|
-
method: "GET",
|
|
615
|
-
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
616
|
-
"Content-Type": "application/json"
|
|
617
|
-
}),
|
|
618
|
-
signal: this.signal
|
|
619
|
-
}
|
|
620
|
-
);
|
|
621
|
-
if (response.status === 200) {
|
|
622
|
-
const answerData = yield response.json();
|
|
623
|
-
console.debug("[CoordinatorClient] Received SDP answer via polling");
|
|
624
|
-
return { sdpAnswer: answerData.sdp_answer, attempts: attempt };
|
|
625
|
-
}
|
|
626
|
-
if (response.status === 202) {
|
|
627
|
-
console.warn(
|
|
628
|
-
`[CoordinatorClient] SDP answer pending (202), retrying in ${backoffMs}ms...`
|
|
629
|
-
);
|
|
630
|
-
yield this.sleep(backoffMs);
|
|
631
|
-
backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
632
|
-
continue;
|
|
479
|
+
const response = yield fetch(
|
|
480
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
481
|
+
{
|
|
482
|
+
method: "PUT",
|
|
483
|
+
headers: this.getHeaders(),
|
|
484
|
+
signal: this.signal
|
|
633
485
|
}
|
|
486
|
+
);
|
|
487
|
+
yield this.checkVersionMismatch(response);
|
|
488
|
+
if (!response.ok) {
|
|
634
489
|
const errorText = yield response.text();
|
|
635
490
|
throw new Error(
|
|
636
|
-
`Failed to
|
|
491
|
+
`Failed to restart session: ${response.status} ${errorText}`
|
|
637
492
|
);
|
|
638
493
|
}
|
|
639
494
|
});
|
|
640
495
|
}
|
|
641
496
|
/**
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* @param sessionId - The session ID to connect to
|
|
646
|
-
* @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
|
|
647
|
-
* @param maxAttempts - Optional maximum number of polling attempts before giving up
|
|
648
|
-
* @returns The SDP answer and the number of polling attempts made (0 if answered immediately via PUT)
|
|
497
|
+
* Terminates the current session by sending a DELETE request.
|
|
498
|
+
* No-op if no session has been created yet.
|
|
499
|
+
* @param reason Optional termination reason
|
|
649
500
|
*/
|
|
650
|
-
|
|
501
|
+
terminateSession(reason) {
|
|
651
502
|
return __async(this, null, function* () {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
|
|
655
|
-
if (answer !== null) {
|
|
656
|
-
return { sdpAnswer: answer, sdpPollingAttempts: 0 };
|
|
657
|
-
}
|
|
503
|
+
if (!this.currentSessionId) {
|
|
504
|
+
return;
|
|
658
505
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
506
|
+
console.debug(
|
|
507
|
+
"[CoordinatorClient] Terminating session:",
|
|
508
|
+
this.currentSessionId
|
|
509
|
+
);
|
|
510
|
+
const body = reason ? { reason } : void 0;
|
|
511
|
+
const response = yield fetch(
|
|
512
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
513
|
+
__spreadProps(__spreadValues({
|
|
514
|
+
method: "DELETE",
|
|
515
|
+
headers: __spreadValues(__spreadValues({}, this.getHeaders()), body ? { "Content-Type": "application/json" } : {})
|
|
516
|
+
}, body ? { body: JSON.stringify(body) } : {}), {
|
|
517
|
+
signal: this.signal
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
if (response.ok) {
|
|
521
|
+
this.currentSessionId = void 0;
|
|
672
522
|
return;
|
|
673
523
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
|
|
524
|
+
if (response.status === 404) {
|
|
525
|
+
console.debug(
|
|
526
|
+
"[CoordinatorClient] Session not found on server, clearing local state:",
|
|
527
|
+
this.currentSessionId
|
|
528
|
+
);
|
|
529
|
+
this.currentSessionId = void 0;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const errorText = yield response.text();
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Failed to terminate session: ${response.status} ${errorText}`
|
|
535
|
+
);
|
|
683
536
|
});
|
|
684
537
|
}
|
|
538
|
+
getSessionId() {
|
|
539
|
+
return this.currentSessionId;
|
|
540
|
+
}
|
|
685
541
|
};
|
|
686
542
|
|
|
687
543
|
// src/core/LocalCoordinatorClient.ts
|
|
688
544
|
var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
689
|
-
constructor(baseUrl) {
|
|
545
|
+
constructor(baseUrl, model) {
|
|
690
546
|
super({
|
|
691
547
|
baseUrl,
|
|
692
548
|
jwtToken: "local",
|
|
693
|
-
model
|
|
549
|
+
model
|
|
694
550
|
});
|
|
695
|
-
|
|
551
|
+
}
|
|
552
|
+
getHeaders() {
|
|
553
|
+
return {
|
|
554
|
+
[API_VERSION_HEADER]: String(REACTOR_API_VERSION),
|
|
555
|
+
[API_ACCEPT_VERSION_HEADER]: String(REACTOR_API_VERSION)
|
|
556
|
+
};
|
|
696
557
|
}
|
|
697
558
|
/**
|
|
698
|
-
*
|
|
699
|
-
*
|
|
559
|
+
* Starts a session on the local runtime.
|
|
560
|
+
*
|
|
561
|
+
* Unlike the production coordinator, the local runtime returns the full
|
|
562
|
+
* response (capabilities, selected_transport) immediately — no polling needed.
|
|
700
563
|
*/
|
|
701
|
-
|
|
564
|
+
createSession(extraArgs) {
|
|
702
565
|
return __async(this, null, function* () {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
566
|
+
var _a, _b;
|
|
567
|
+
console.debug("[LocalCoordinatorClient] Starting session...");
|
|
568
|
+
const response = yield fetch(`${this.baseUrl}/start_session`, {
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers: __spreadProps(__spreadValues({}, this.getHeaders()), {
|
|
571
|
+
"Content-Type": "application/json"
|
|
572
|
+
}),
|
|
573
|
+
body: JSON.stringify(__spreadValues({}, extraArgs ? { extra_args: extraArgs } : {})),
|
|
706
574
|
signal: this.signal
|
|
707
575
|
});
|
|
708
576
|
if (!response.ok) {
|
|
709
|
-
|
|
577
|
+
const errorText = yield response.text();
|
|
578
|
+
throw new Error(
|
|
579
|
+
`Failed to start session: ${response.status} ${errorText}`
|
|
580
|
+
);
|
|
710
581
|
}
|
|
711
582
|
const data = yield response.json();
|
|
712
|
-
const
|
|
713
|
-
|
|
583
|
+
const session = SessionResponseSchema.parse(data);
|
|
584
|
+
this.cachedSessionResponse = session;
|
|
585
|
+
this.currentSessionId = session.session_id;
|
|
714
586
|
console.debug(
|
|
715
|
-
"[LocalCoordinatorClient]
|
|
716
|
-
|
|
587
|
+
"[LocalCoordinatorClient] Session started:",
|
|
588
|
+
this.currentSessionId,
|
|
589
|
+
"transport:",
|
|
590
|
+
(_a = session.selected_transport) == null ? void 0 : _a.protocol,
|
|
591
|
+
"tracks:",
|
|
592
|
+
(_b = session.capabilities) == null ? void 0 : _b.tracks.length
|
|
717
593
|
);
|
|
718
|
-
return
|
|
594
|
+
return CreateSessionResponseSchema.parse(data);
|
|
719
595
|
});
|
|
720
596
|
}
|
|
721
597
|
/**
|
|
722
|
-
*
|
|
723
|
-
*
|
|
598
|
+
* Returns the cached full session response immediately.
|
|
599
|
+
* The local runtime already provided everything in start_session.
|
|
724
600
|
*/
|
|
725
|
-
|
|
601
|
+
pollSessionReady() {
|
|
726
602
|
return __async(this, null, function* () {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
signal: this.signal
|
|
732
|
-
});
|
|
733
|
-
if (!response.ok) {
|
|
734
|
-
throw new Error("Failed to send local start session command.");
|
|
603
|
+
if (!this.cachedSessionResponse) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
"No cached session response. Call createSession() first."
|
|
606
|
+
);
|
|
735
607
|
}
|
|
736
|
-
|
|
737
|
-
return "local";
|
|
608
|
+
return this.cachedSessionResponse;
|
|
738
609
|
});
|
|
739
610
|
}
|
|
740
611
|
/**
|
|
741
|
-
*
|
|
742
|
-
* Local connections are always immediate (no polling).
|
|
743
|
-
* @param sessionId - The session ID (ignored for local)
|
|
744
|
-
* @param sdpMessage - The SDP offer from the local WebRTC peer connection
|
|
745
|
-
* @returns The SDP answer and polling attempts (always 0 for local)
|
|
612
|
+
* Stops the session on the local runtime.
|
|
746
613
|
*/
|
|
747
|
-
|
|
614
|
+
terminateSession() {
|
|
748
615
|
return __async(this, null, function* () {
|
|
749
|
-
this.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
})
|
|
763
|
-
|
|
764
|
-
if (response.status === 409) {
|
|
765
|
-
throw new ConflictError("Connection superseded by newer request");
|
|
766
|
-
}
|
|
767
|
-
throw new Error("Failed to get SDP answer from local coordinator.");
|
|
616
|
+
if (!this.currentSessionId) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
console.debug(
|
|
620
|
+
"[LocalCoordinatorClient] Stopping session:",
|
|
621
|
+
this.currentSessionId
|
|
622
|
+
);
|
|
623
|
+
try {
|
|
624
|
+
yield fetch(`${this.baseUrl}/stop_session`, {
|
|
625
|
+
method: "POST",
|
|
626
|
+
headers: this.getHeaders(),
|
|
627
|
+
signal: this.signal
|
|
628
|
+
});
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error("[LocalCoordinatorClient] Error stopping session:", error);
|
|
768
631
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return { sdpAnswer: sdpAnswer.sdp, sdpPollingAttempts: 0 };
|
|
632
|
+
this.currentSessionId = void 0;
|
|
633
|
+
this.cachedSessionResponse = void 0;
|
|
772
634
|
});
|
|
773
635
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/utils/webrtc.ts
|
|
639
|
+
var DEFAULT_DATA_CHANNEL_LABEL = "data";
|
|
640
|
+
var FORCE_RELAY_MODE = false;
|
|
641
|
+
var DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
|
|
642
|
+
function createPeerConnection(config) {
|
|
643
|
+
return new RTCPeerConnection({
|
|
644
|
+
iceServers: config.iceServers,
|
|
645
|
+
iceTransportPolicy: FORCE_RELAY_MODE ? "relay" : "all"
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function createDataChannel(pc, label) {
|
|
649
|
+
return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
|
|
650
|
+
}
|
|
651
|
+
function createOffer(pc) {
|
|
652
|
+
return __async(this, null, function* () {
|
|
653
|
+
const offer = yield pc.createOffer();
|
|
654
|
+
yield pc.setLocalDescription(offer);
|
|
655
|
+
yield waitForIceGathering(pc);
|
|
656
|
+
const localDescription = pc.localDescription;
|
|
657
|
+
if (!localDescription) {
|
|
658
|
+
throw new Error("Failed to create local description");
|
|
659
|
+
}
|
|
660
|
+
return localDescription.sdp;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function setRemoteDescription(pc, sdp) {
|
|
664
|
+
return __async(this, null, function* () {
|
|
665
|
+
const sessionDescription = new RTCSessionDescription({
|
|
666
|
+
sdp,
|
|
667
|
+
type: "answer"
|
|
781
668
|
});
|
|
669
|
+
yield pc.setRemoteDescription(sessionDescription);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function transformIceServers(response) {
|
|
673
|
+
return response.ice_servers.map((server) => {
|
|
674
|
+
const rtcServer = {
|
|
675
|
+
urls: server.uris
|
|
676
|
+
};
|
|
677
|
+
if (server.credentials) {
|
|
678
|
+
rtcServer.username = server.credentials.username;
|
|
679
|
+
rtcServer.credential = server.credentials.password;
|
|
680
|
+
}
|
|
681
|
+
return rtcServer;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function waitForIceGathering(pc, timeoutMs = 5e3) {
|
|
685
|
+
return new Promise((resolve) => {
|
|
686
|
+
if (pc.iceGatheringState === "complete") {
|
|
687
|
+
resolve();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const onGatheringStateChange = () => {
|
|
691
|
+
if (pc.iceGatheringState === "complete") {
|
|
692
|
+
pc.removeEventListener(
|
|
693
|
+
"icegatheringstatechange",
|
|
694
|
+
onGatheringStateChange
|
|
695
|
+
);
|
|
696
|
+
resolve();
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
pc.addEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
700
|
+
setTimeout(() => {
|
|
701
|
+
pc.removeEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
702
|
+
resolve();
|
|
703
|
+
}, timeoutMs);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function sendMessage(channel, command, data, scope = "application", maxBytes = DEFAULT_MAX_MESSAGE_BYTES) {
|
|
707
|
+
if (channel.readyState !== "open") {
|
|
708
|
+
throw new Error(`Data channel not open: ${channel.readyState}`);
|
|
782
709
|
}
|
|
783
|
-
|
|
710
|
+
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
|
711
|
+
const inner = { type: command, data: jsonData };
|
|
712
|
+
const payload = { scope, data: inner };
|
|
713
|
+
const serialized = JSON.stringify(payload);
|
|
714
|
+
const byteLength = new TextEncoder().encode(serialized).byteLength;
|
|
715
|
+
if (byteLength > maxBytes) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Data channel message too large: ${byteLength} bytes exceeds limit of ${maxBytes} bytes (command: "${command}")`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
channel.send(serialized);
|
|
721
|
+
}
|
|
722
|
+
function parseMessage(data) {
|
|
723
|
+
if (typeof data === "string") {
|
|
724
|
+
try {
|
|
725
|
+
return JSON.parse(data);
|
|
726
|
+
} catch (e) {
|
|
727
|
+
return data;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return data;
|
|
731
|
+
}
|
|
732
|
+
function closePeerConnection(pc) {
|
|
733
|
+
pc.close();
|
|
734
|
+
}
|
|
735
|
+
function extractConnectionStats(report) {
|
|
736
|
+
let rtt;
|
|
737
|
+
let availableOutgoingBitrate;
|
|
738
|
+
let localCandidateId;
|
|
739
|
+
let framesPerSecond;
|
|
740
|
+
let jitter;
|
|
741
|
+
let packetLossRatio;
|
|
742
|
+
report.forEach((stat) => {
|
|
743
|
+
if (stat.type === "candidate-pair" && stat.state === "succeeded") {
|
|
744
|
+
if (stat.currentRoundTripTime !== void 0) {
|
|
745
|
+
rtt = stat.currentRoundTripTime * 1e3;
|
|
746
|
+
}
|
|
747
|
+
if (stat.availableOutgoingBitrate !== void 0) {
|
|
748
|
+
availableOutgoingBitrate = stat.availableOutgoingBitrate;
|
|
749
|
+
}
|
|
750
|
+
localCandidateId = stat.localCandidateId;
|
|
751
|
+
}
|
|
752
|
+
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
753
|
+
if (stat.framesPerSecond !== void 0) {
|
|
754
|
+
framesPerSecond = stat.framesPerSecond;
|
|
755
|
+
}
|
|
756
|
+
if (stat.jitter !== void 0) {
|
|
757
|
+
jitter = stat.jitter;
|
|
758
|
+
}
|
|
759
|
+
if (stat.packetsReceived !== void 0 && stat.packetsLost !== void 0 && stat.packetsReceived + stat.packetsLost > 0) {
|
|
760
|
+
packetLossRatio = stat.packetsLost / (stat.packetsReceived + stat.packetsLost);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
let candidateType;
|
|
765
|
+
if (localCandidateId) {
|
|
766
|
+
const localCandidate = report.get(localCandidateId);
|
|
767
|
+
if (localCandidate == null ? void 0 : localCandidate.candidateType) {
|
|
768
|
+
candidateType = localCandidate.candidateType;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
rtt,
|
|
773
|
+
candidateType,
|
|
774
|
+
availableOutgoingBitrate,
|
|
775
|
+
framesPerSecond,
|
|
776
|
+
packetLossRatio,
|
|
777
|
+
jitter,
|
|
778
|
+
timestamp: Date.now()
|
|
779
|
+
};
|
|
780
|
+
}
|
|
784
781
|
|
|
785
|
-
// src/core/
|
|
782
|
+
// src/core/WebRTCTransportClient.ts
|
|
786
783
|
var PING_INTERVAL_MS = 5e3;
|
|
787
784
|
var STATS_INTERVAL_MS = 2e3;
|
|
788
|
-
var
|
|
785
|
+
var INITIAL_BACKOFF_MS = 200;
|
|
786
|
+
var MAX_BACKOFF_MS = 15e3;
|
|
787
|
+
var BACKOFF_MULTIPLIER = 2;
|
|
788
|
+
var DEFAULT_MAX_POLL_ATTEMPTS = 6;
|
|
789
|
+
var WebRTCTransportClient = class {
|
|
789
790
|
constructor(config) {
|
|
790
791
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
791
792
|
this.status = "disconnected";
|
|
@@ -793,11 +794,17 @@ var GPUMachineClient = class {
|
|
|
793
794
|
this.publishedTracks = /* @__PURE__ */ new Map();
|
|
794
795
|
this.peerConnected = false;
|
|
795
796
|
this.dataChannelOpen = false;
|
|
796
|
-
|
|
797
|
+
var _a, _b;
|
|
798
|
+
this.baseUrl = config.baseUrl;
|
|
799
|
+
this.sessionId = config.sessionId;
|
|
800
|
+
this.jwtToken = config.jwtToken;
|
|
801
|
+
this.webrtcVersion = (_a = config.webrtcVersion) != null ? _a : REACTOR_WEBRTC_VERSION;
|
|
802
|
+
this.maxPollAttempts = (_b = config.maxPollAttempts) != null ? _b : DEFAULT_MAX_POLL_ATTEMPTS;
|
|
803
|
+
this.abortController = new AbortController();
|
|
797
804
|
}
|
|
798
|
-
//
|
|
799
|
-
// Event Emitter
|
|
800
|
-
//
|
|
805
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
806
|
+
// Event Emitter
|
|
807
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
801
808
|
on(event, handler) {
|
|
802
809
|
if (!this.eventListeners.has(event)) {
|
|
803
810
|
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
@@ -812,130 +819,241 @@ var GPUMachineClient = class {
|
|
|
812
819
|
var _a;
|
|
813
820
|
(_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
|
|
814
821
|
}
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
822
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
823
|
+
// HTTP Helpers
|
|
824
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
825
|
+
get signal() {
|
|
826
|
+
return this.abortController.signal;
|
|
827
|
+
}
|
|
828
|
+
get transportBaseUrl() {
|
|
829
|
+
return `${this.baseUrl}/sessions/${this.sessionId}/transport/webrtc`;
|
|
830
|
+
}
|
|
831
|
+
getHeaders() {
|
|
832
|
+
return {
|
|
833
|
+
Authorization: `Bearer ${this.jwtToken}`,
|
|
834
|
+
[WEBRTC_VERSION_HEADER]: this.webrtcVersion
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
checkVersionMismatch(response) {
|
|
838
|
+
return __async(this, null, function* () {
|
|
839
|
+
if (response.status === 426) {
|
|
840
|
+
const msg = `Client WebRTC version (${this.webrtcVersion}) is too old. Server requires a newer version. Please upgrade @reactor-team/js-sdk.`;
|
|
841
|
+
console.error(`[WebRTCTransport]`, msg);
|
|
842
|
+
throw new Error(`${VERSION_ERROR_CODES[426]}: ${msg}`);
|
|
843
|
+
}
|
|
844
|
+
if (response.status === 501) {
|
|
845
|
+
const msg = `Server does not support WebRTC version ${this.webrtcVersion}. The server may need to be updated.`;
|
|
846
|
+
console.error(`[WebRTCTransport]`, msg);
|
|
847
|
+
throw new Error(`${VERSION_ERROR_CODES[501]}: ${msg}`);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
sleep(ms) {
|
|
852
|
+
return new Promise((resolve, reject) => {
|
|
853
|
+
const { signal } = this;
|
|
854
|
+
if (signal.aborted) {
|
|
855
|
+
reject(new AbortError("Sleep aborted"));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const timer = setTimeout(() => {
|
|
859
|
+
signal.removeEventListener("abort", onAbort);
|
|
860
|
+
resolve();
|
|
861
|
+
}, ms);
|
|
862
|
+
const onAbort = () => {
|
|
863
|
+
clearTimeout(timer);
|
|
864
|
+
reject(new AbortError("Sleep aborted"));
|
|
865
|
+
};
|
|
866
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
870
|
+
// Transport Signaling (HTTP)
|
|
871
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
872
|
+
fetchIceServers() {
|
|
873
|
+
return __async(this, null, function* () {
|
|
874
|
+
console.debug("[WebRTCTransport] Fetching ICE servers...");
|
|
875
|
+
const response = yield fetch(`${this.transportBaseUrl}/ice_servers`, {
|
|
876
|
+
method: "GET",
|
|
877
|
+
headers: this.getHeaders(),
|
|
878
|
+
signal: this.signal
|
|
879
|
+
});
|
|
880
|
+
yield this.checkVersionMismatch(response);
|
|
881
|
+
if (!response.ok) {
|
|
882
|
+
throw new Error(`Failed to fetch ICE servers: ${response.status}`);
|
|
883
|
+
}
|
|
884
|
+
const data = yield response.json();
|
|
885
|
+
const parsed = IceServersResponseSchema.parse(data);
|
|
886
|
+
const iceServers = transformIceServers(parsed);
|
|
887
|
+
console.debug("[WebRTCTransport] Received ICE servers:", iceServers.length);
|
|
888
|
+
return iceServers;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
sendSdpOffer(sdpOffer, trackMapping, method = "POST") {
|
|
892
|
+
return __async(this, null, function* () {
|
|
893
|
+
console.debug(
|
|
894
|
+
`[WebRTCTransport] Sending SDP offer (${method}) for session:`,
|
|
895
|
+
this.sessionId
|
|
896
|
+
);
|
|
897
|
+
const requestBody = {
|
|
898
|
+
sdp_offer: sdpOffer,
|
|
899
|
+
client_info: {
|
|
900
|
+
sdk_version: REACTOR_SDK_VERSION,
|
|
901
|
+
sdk_type: REACTOR_SDK_TYPE
|
|
902
|
+
},
|
|
903
|
+
track_mapping: trackMapping
|
|
904
|
+
};
|
|
905
|
+
const response = yield fetch(`${this.transportBaseUrl}/sdp_params`, {
|
|
906
|
+
method,
|
|
907
|
+
headers: __spreadProps(__spreadValues({}, this.getHeaders()), {
|
|
908
|
+
"Content-Type": "application/json"
|
|
909
|
+
}),
|
|
910
|
+
body: JSON.stringify(requestBody),
|
|
911
|
+
signal: this.signal
|
|
912
|
+
});
|
|
913
|
+
yield this.checkVersionMismatch(response);
|
|
914
|
+
if (response.status !== 202) {
|
|
915
|
+
const errorText = yield response.text();
|
|
916
|
+
throw new Error(
|
|
917
|
+
`Failed to send SDP offer: ${response.status} ${errorText}`
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
console.debug("[WebRTCTransport] SDP offer accepted (202)");
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
pollSdpAnswer() {
|
|
924
|
+
return __async(this, null, function* () {
|
|
925
|
+
console.debug("[WebRTCTransport] Polling for SDP answer...");
|
|
926
|
+
const pollStart = performance.now();
|
|
927
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
928
|
+
let attempt = 0;
|
|
929
|
+
while (true) {
|
|
930
|
+
if (this.signal.aborted) {
|
|
931
|
+
throw new AbortError("SDP polling aborted");
|
|
932
|
+
}
|
|
933
|
+
if (attempt >= this.maxPollAttempts) {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`SDP polling exceeded maximum attempts (${this.maxPollAttempts})`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
attempt++;
|
|
939
|
+
console.debug(
|
|
940
|
+
`[WebRTCTransport] SDP poll attempt ${attempt}/${this.maxPollAttempts}`
|
|
941
|
+
);
|
|
942
|
+
const response = yield fetch(`${this.transportBaseUrl}/sdp_params`, {
|
|
943
|
+
method: "GET",
|
|
944
|
+
headers: this.getHeaders(),
|
|
945
|
+
signal: this.signal
|
|
946
|
+
});
|
|
947
|
+
yield this.checkVersionMismatch(response);
|
|
948
|
+
if (response.status === 200) {
|
|
949
|
+
const data = yield response.json();
|
|
950
|
+
const parsed = WebRTCSdpAnswerResponseSchema.parse(data);
|
|
951
|
+
this.sdpPollingMs = performance.now() - pollStart;
|
|
952
|
+
this.sdpPollingAttempts = attempt;
|
|
953
|
+
console.debug(
|
|
954
|
+
`[WebRTCTransport] Received SDP answer via polling (${attempt} attempt(s), ${this.sdpPollingMs.toFixed(0)}ms)`
|
|
955
|
+
);
|
|
956
|
+
return parsed;
|
|
957
|
+
}
|
|
958
|
+
if (response.status === 202) {
|
|
959
|
+
console.debug(
|
|
960
|
+
`[WebRTCTransport] SDP answer pending, retrying in ${backoffMs}ms...`
|
|
961
|
+
);
|
|
962
|
+
yield this.sleep(backoffMs);
|
|
963
|
+
backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const errorText = yield response.text();
|
|
967
|
+
throw new Error(
|
|
968
|
+
`Failed to poll SDP answer: ${response.status} ${errorText}`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
974
|
+
// Connection Lifecycle
|
|
975
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
976
|
+
connect(tracks) {
|
|
831
977
|
return __async(this, null, function* () {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
this.
|
|
837
|
-
|
|
838
|
-
this.config.dataChannelLabel
|
|
839
|
-
);
|
|
978
|
+
this.setStatus("connecting");
|
|
979
|
+
this.resetTransportTimings();
|
|
980
|
+
const iceServers = yield this.fetchIceServers();
|
|
981
|
+
this.peerConnection = createPeerConnection({ iceServers });
|
|
982
|
+
this.setupPeerConnectionHandlers();
|
|
983
|
+
this.dataChannel = createDataChannel(this.peerConnection);
|
|
840
984
|
this.setupDataChannelHandlers();
|
|
841
985
|
this.transceiverMap.clear();
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
986
|
+
for (const track of tracks) {
|
|
987
|
+
const transceiver = this.peerConnection.addTransceiver(track.kind, {
|
|
988
|
+
direction: track.direction
|
|
989
|
+
});
|
|
990
|
+
this.transceiverMap.set(track.name, {
|
|
991
|
+
name: track.name,
|
|
992
|
+
kind: track.kind,
|
|
993
|
+
direction: track.direction,
|
|
994
|
+
transceiver
|
|
846
995
|
});
|
|
847
|
-
entry.transceiver = transceiver;
|
|
848
|
-
this.transceiverMap.set(entry.name, entry);
|
|
849
996
|
console.debug(
|
|
850
|
-
`[
|
|
997
|
+
`[WebRTCTransport] Transceiver added: "${track.name}" (${track.kind}, ${track.direction})`
|
|
851
998
|
);
|
|
852
999
|
}
|
|
853
|
-
const
|
|
854
|
-
const
|
|
1000
|
+
const sdpOffer = yield createOffer(this.peerConnection);
|
|
1001
|
+
const trackMapping = this.buildTrackMapping(tracks);
|
|
1002
|
+
yield this.sendSdpOffer(sdpOffer, trackMapping, "POST");
|
|
1003
|
+
const answerResponse = yield this.pollSdpAnswer();
|
|
1004
|
+
this.iceStartTime = performance.now();
|
|
1005
|
+
yield setRemoteDescription(
|
|
855
1006
|
this.peerConnection,
|
|
856
|
-
|
|
857
|
-
);
|
|
858
|
-
if (needsAnswerRestore) {
|
|
859
|
-
this.midMapping = buildMidMapping(entries);
|
|
860
|
-
} else {
|
|
861
|
-
this.midMapping = void 0;
|
|
862
|
-
}
|
|
863
|
-
console.debug(
|
|
864
|
-
"[GPUMachineClient] Created SDP offer with MIDs:",
|
|
865
|
-
trackNames,
|
|
866
|
-
needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
|
|
1007
|
+
answerResponse.sdp_answer
|
|
867
1008
|
);
|
|
868
|
-
|
|
1009
|
+
console.debug("[WebRTCTransport] Remote description set");
|
|
869
1010
|
});
|
|
870
1011
|
}
|
|
871
|
-
|
|
872
|
-
* Builds an ordered list of transceiver entries from the receive/send arrays.
|
|
873
|
-
*
|
|
874
|
-
* Each track produces exactly one transceiver — `recvonly` for receive,
|
|
875
|
-
* `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
|
|
876
|
-
* supported; the same track name in both arrays is an error.
|
|
877
|
-
*/
|
|
878
|
-
buildTransceiverEntries(tracks) {
|
|
879
|
-
const map = /* @__PURE__ */ new Map();
|
|
880
|
-
for (const t of tracks.receive) {
|
|
881
|
-
if (map.has(t.name)) {
|
|
882
|
-
throw new Error(
|
|
883
|
-
`Duplicate receive track name "${t.name}". Track names must be unique.`
|
|
884
|
-
);
|
|
885
|
-
}
|
|
886
|
-
map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
|
|
887
|
-
}
|
|
888
|
-
for (const t of tracks.send) {
|
|
889
|
-
if (map.has(t.name)) {
|
|
890
|
-
throw new Error(
|
|
891
|
-
`Track name "${t.name}" appears in both receive and send. Bidirectional tracks are not supported \u2014 use distinct names for the inbound and outbound directions (e.g. "${t.name}_in" and "${t.name}_out").`
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
|
|
895
|
-
}
|
|
896
|
-
return Array.from(map.values());
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Connects to the GPU machine using the provided SDP answer.
|
|
900
|
-
* createOffer() must be called first.
|
|
901
|
-
* @param sdpAnswer The SDP answer from the GPU machine
|
|
902
|
-
*/
|
|
903
|
-
connect(sdpAnswer) {
|
|
1012
|
+
reconnect(tracks) {
|
|
904
1013
|
return __async(this, null, function* () {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1014
|
+
this.setStatus("connecting");
|
|
1015
|
+
this.stopPing();
|
|
1016
|
+
this.stopStatsPolling();
|
|
1017
|
+
if (this.dataChannel) {
|
|
1018
|
+
this.dataChannel.close();
|
|
1019
|
+
this.dataChannel = void 0;
|
|
909
1020
|
}
|
|
910
|
-
if (this.peerConnection
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
);
|
|
1021
|
+
if (this.peerConnection) {
|
|
1022
|
+
closePeerConnection(this.peerConnection);
|
|
1023
|
+
this.peerConnection = void 0;
|
|
914
1024
|
}
|
|
915
|
-
this.
|
|
916
|
-
this.
|
|
917
|
-
this.
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1025
|
+
this.peerConnected = false;
|
|
1026
|
+
this.dataChannelOpen = false;
|
|
1027
|
+
this.resetTransportTimings();
|
|
1028
|
+
const iceServers = yield this.fetchIceServers();
|
|
1029
|
+
this.peerConnection = createPeerConnection({ iceServers });
|
|
1030
|
+
this.setupPeerConnectionHandlers();
|
|
1031
|
+
this.dataChannel = createDataChannel(this.peerConnection);
|
|
1032
|
+
this.setupDataChannelHandlers();
|
|
1033
|
+
this.transceiverMap.clear();
|
|
1034
|
+
for (const track of tracks) {
|
|
1035
|
+
const transceiver = this.peerConnection.addTransceiver(track.kind, {
|
|
1036
|
+
direction: track.direction
|
|
1037
|
+
});
|
|
1038
|
+
this.transceiverMap.set(track.name, {
|
|
1039
|
+
name: track.name,
|
|
1040
|
+
kind: track.kind,
|
|
1041
|
+
direction: track.direction,
|
|
1042
|
+
transceiver
|
|
1043
|
+
});
|
|
933
1044
|
}
|
|
1045
|
+
const sdpOffer = yield createOffer(this.peerConnection);
|
|
1046
|
+
const trackMapping = this.buildTrackMapping(tracks);
|
|
1047
|
+
yield this.sendSdpOffer(sdpOffer, trackMapping, "PUT");
|
|
1048
|
+
const answerResponse = yield this.pollSdpAnswer();
|
|
1049
|
+
this.iceStartTime = performance.now();
|
|
1050
|
+
yield setRemoteDescription(
|
|
1051
|
+
this.peerConnection,
|
|
1052
|
+
answerResponse.sdp_answer
|
|
1053
|
+
);
|
|
1054
|
+
console.debug("[WebRTCTransport] Remote description set (reconnect)");
|
|
934
1055
|
});
|
|
935
1056
|
}
|
|
936
|
-
/**
|
|
937
|
-
* Disconnects from the GPU machine and cleans up resources.
|
|
938
|
-
*/
|
|
939
1057
|
disconnect() {
|
|
940
1058
|
return __async(this, null, function* () {
|
|
941
1059
|
this.stopPing();
|
|
@@ -952,51 +1070,56 @@ var GPUMachineClient = class {
|
|
|
952
1070
|
this.peerConnection = void 0;
|
|
953
1071
|
}
|
|
954
1072
|
this.transceiverMap.clear();
|
|
955
|
-
this.midMapping = void 0;
|
|
956
1073
|
this.peerConnected = false;
|
|
957
1074
|
this.dataChannelOpen = false;
|
|
958
|
-
this.
|
|
1075
|
+
this.resetTransportTimings();
|
|
959
1076
|
this.setStatus("disconnected");
|
|
960
|
-
console.debug("[
|
|
1077
|
+
console.debug("[WebRTCTransport] Disconnected");
|
|
961
1078
|
});
|
|
962
1079
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1080
|
+
abort() {
|
|
1081
|
+
this.abortController.abort();
|
|
1082
|
+
this.abortController = new AbortController();
|
|
1083
|
+
}
|
|
966
1084
|
getStatus() {
|
|
967
1085
|
return this.status;
|
|
968
1086
|
}
|
|
1087
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1088
|
+
// Track Mapping
|
|
1089
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
969
1090
|
/**
|
|
970
|
-
*
|
|
1091
|
+
* Builds the track_mapping array from capabilities + transceiver MIDs.
|
|
1092
|
+
* Must be called after createOffer + setLocalDescription so that
|
|
1093
|
+
* transceiver.mid is assigned.
|
|
971
1094
|
*/
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1095
|
+
buildTrackMapping(tracks) {
|
|
1096
|
+
return tracks.map((track) => {
|
|
1097
|
+
var _a;
|
|
1098
|
+
const entry = this.transceiverMap.get(track.name);
|
|
1099
|
+
const mid = (_a = entry == null ? void 0 : entry.transceiver) == null ? void 0 : _a.mid;
|
|
1100
|
+
if (mid == null) {
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`Cannot build track mapping: transceiver "${track.name}" has no MID. Was createOffer() called?`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
mid,
|
|
1107
|
+
name: track.name,
|
|
1108
|
+
kind: track.kind,
|
|
1109
|
+
direction: track.direction
|
|
1110
|
+
};
|
|
1111
|
+
});
|
|
979
1112
|
}
|
|
980
|
-
//
|
|
1113
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
981
1114
|
// Messaging
|
|
982
|
-
//
|
|
983
|
-
/**
|
|
984
|
-
* Returns the negotiated SCTP max message size (bytes) if available,
|
|
985
|
-
* otherwise `undefined` so `sendMessage` falls back to its built-in default.
|
|
986
|
-
*/
|
|
1115
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
987
1116
|
get maxMessageBytes() {
|
|
988
1117
|
var _a, _b, _c;
|
|
989
1118
|
return (_c = (_b = (_a = this.peerConnection) == null ? void 0 : _a.sctp) == null ? void 0 : _b.maxMessageSize) != null ? _c : void 0;
|
|
990
1119
|
}
|
|
991
|
-
/**
|
|
992
|
-
* Sends a command to the GPU machine via the data channel.
|
|
993
|
-
* @param command The command to send
|
|
994
|
-
* @param data The data to send with the command. These are the parameters for the command, matching the schema in the capabilities dictionary.
|
|
995
|
-
* @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
|
|
996
|
-
*/
|
|
997
1120
|
sendCommand(command, data, scope = "application") {
|
|
998
1121
|
if (!this.dataChannel) {
|
|
999
|
-
throw new Error("[
|
|
1122
|
+
throw new Error("[WebRTCTransport] Data channel not available");
|
|
1000
1123
|
}
|
|
1001
1124
|
try {
|
|
1002
1125
|
sendMessage(
|
|
@@ -1007,59 +1130,40 @@ var GPUMachineClient = class {
|
|
|
1007
1130
|
this.maxMessageBytes
|
|
1008
1131
|
);
|
|
1009
1132
|
} catch (error) {
|
|
1010
|
-
console.warn("[
|
|
1133
|
+
console.warn("[WebRTCTransport] Failed to send message:", error);
|
|
1011
1134
|
}
|
|
1012
1135
|
}
|
|
1013
|
-
//
|
|
1136
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1014
1137
|
// Track Publishing
|
|
1015
|
-
//
|
|
1016
|
-
/**
|
|
1017
|
-
* Publishes a MediaStreamTrack to the named send track.
|
|
1018
|
-
*
|
|
1019
|
-
* @param name The declared track name (must exist in transceiverMap with a sendable direction).
|
|
1020
|
-
* @param track The MediaStreamTrack to publish.
|
|
1021
|
-
*/
|
|
1138
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1022
1139
|
publishTrack(name, track) {
|
|
1023
1140
|
return __async(this, null, function* () {
|
|
1024
1141
|
if (!this.peerConnection) {
|
|
1025
1142
|
throw new Error(
|
|
1026
|
-
`[
|
|
1143
|
+
`[WebRTCTransport] Cannot publish track "${name}" - not initialized`
|
|
1027
1144
|
);
|
|
1028
1145
|
}
|
|
1029
1146
|
if (this.status !== "connected") {
|
|
1030
1147
|
throw new Error(
|
|
1031
|
-
`[
|
|
1148
|
+
`[WebRTCTransport] Cannot publish track "${name}" - not connected`
|
|
1032
1149
|
);
|
|
1033
1150
|
}
|
|
1034
1151
|
const entry = this.transceiverMap.get(name);
|
|
1035
1152
|
if (!entry || !entry.transceiver) {
|
|
1036
1153
|
throw new Error(
|
|
1037
|
-
`[
|
|
1154
|
+
`[WebRTCTransport] Cannot publish track "${name}" - no transceiver (was it declared in capabilities?)`
|
|
1038
1155
|
);
|
|
1039
1156
|
}
|
|
1040
1157
|
if (entry.direction === "recvonly") {
|
|
1041
1158
|
throw new Error(
|
|
1042
|
-
`[
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
|
-
try {
|
|
1046
|
-
yield entry.transceiver.sender.replaceTrack(track);
|
|
1047
|
-
this.publishedTracks.set(name, track);
|
|
1048
|
-
console.debug(
|
|
1049
|
-
`[GPUMachineClient] Track "${name}" published successfully`
|
|
1050
|
-
);
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
console.error(
|
|
1053
|
-
`[GPUMachineClient] Failed to publish track "${name}":`,
|
|
1054
|
-
error
|
|
1159
|
+
`[WebRTCTransport] Cannot publish track "${name}" - transceiver is recvonly`
|
|
1055
1160
|
);
|
|
1056
|
-
throw error;
|
|
1057
1161
|
}
|
|
1162
|
+
yield entry.transceiver.sender.replaceTrack(track);
|
|
1163
|
+
this.publishedTracks.set(name, track);
|
|
1164
|
+
console.debug(`[WebRTCTransport] Track "${name}" published successfully`);
|
|
1058
1165
|
});
|
|
1059
1166
|
}
|
|
1060
|
-
/**
|
|
1061
|
-
* Unpublishes the track with the given name.
|
|
1062
|
-
*/
|
|
1063
1167
|
unpublishTrack(name) {
|
|
1064
1168
|
return __async(this, null, function* () {
|
|
1065
1169
|
const entry = this.transceiverMap.get(name);
|
|
@@ -1067,11 +1171,11 @@ var GPUMachineClient = class {
|
|
|
1067
1171
|
try {
|
|
1068
1172
|
yield entry.transceiver.sender.replaceTrack(null);
|
|
1069
1173
|
console.debug(
|
|
1070
|
-
`[
|
|
1174
|
+
`[WebRTCTransport] Track "${name}" unpublished successfully`
|
|
1071
1175
|
);
|
|
1072
1176
|
} catch (error) {
|
|
1073
1177
|
console.error(
|
|
1074
|
-
`[
|
|
1178
|
+
`[WebRTCTransport] Failed to unpublish track "${name}":`,
|
|
1075
1179
|
error
|
|
1076
1180
|
);
|
|
1077
1181
|
throw error;
|
|
@@ -1080,76 +1184,31 @@ var GPUMachineClient = class {
|
|
|
1080
1184
|
}
|
|
1081
1185
|
});
|
|
1082
1186
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
getPublishedTrack(name) {
|
|
1087
|
-
return this.publishedTracks.get(name);
|
|
1088
|
-
}
|
|
1089
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1090
|
-
// Getters
|
|
1091
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1092
|
-
/**
|
|
1093
|
-
* Returns the remote media stream from the GPU machine.
|
|
1094
|
-
*/
|
|
1095
|
-
getRemoteStream() {
|
|
1096
|
-
if (!this.peerConnection) return void 0;
|
|
1097
|
-
const receivers = this.peerConnection.getReceivers();
|
|
1098
|
-
const tracks = receivers.map((r) => r.track).filter((t) => t !== null);
|
|
1099
|
-
if (tracks.length === 0) return void 0;
|
|
1100
|
-
return new MediaStream(tracks);
|
|
1101
|
-
}
|
|
1102
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1103
|
-
// Ping (Client Liveness)
|
|
1104
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1105
|
-
/**
|
|
1106
|
-
* Starts sending periodic "ping" messages on the runtime channel so the
|
|
1107
|
-
* server can detect stale connections quickly.
|
|
1108
|
-
*/
|
|
1109
|
-
startPing() {
|
|
1110
|
-
this.stopPing();
|
|
1111
|
-
this.pingInterval = setInterval(() => {
|
|
1112
|
-
var _a;
|
|
1113
|
-
if (((_a = this.dataChannel) == null ? void 0 : _a.readyState) === "open") {
|
|
1114
|
-
try {
|
|
1115
|
-
sendMessage(this.dataChannel, "ping", {}, "runtime");
|
|
1116
|
-
} catch (e) {
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}, PING_INTERVAL_MS);
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* Stops the periodic ping.
|
|
1123
|
-
*/
|
|
1124
|
-
stopPing() {
|
|
1125
|
-
if (this.pingInterval !== void 0) {
|
|
1126
|
-
clearInterval(this.pingInterval);
|
|
1127
|
-
this.pingInterval = void 0;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1131
|
-
// Stats Polling (RTT)
|
|
1132
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1187
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1188
|
+
// Stats
|
|
1189
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1133
1190
|
getStats() {
|
|
1134
1191
|
return this.stats;
|
|
1135
1192
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
* or undefined if no connection has completed yet.
|
|
1139
|
-
*/
|
|
1140
|
-
getConnectionTimings() {
|
|
1193
|
+
getTransportTimings() {
|
|
1194
|
+
var _a, _b;
|
|
1141
1195
|
if (this.iceNegotiationMs == null || this.dataChannelMs == null) {
|
|
1142
1196
|
return void 0;
|
|
1143
1197
|
}
|
|
1144
1198
|
return {
|
|
1199
|
+
protocol: "webrtc",
|
|
1200
|
+
sdpPollingMs: (_a = this.sdpPollingMs) != null ? _a : 0,
|
|
1201
|
+
sdpPollingAttempts: (_b = this.sdpPollingAttempts) != null ? _b : 0,
|
|
1145
1202
|
iceNegotiationMs: this.iceNegotiationMs,
|
|
1146
1203
|
dataChannelMs: this.dataChannelMs
|
|
1147
1204
|
};
|
|
1148
1205
|
}
|
|
1149
|
-
|
|
1206
|
+
resetTransportTimings() {
|
|
1150
1207
|
this.iceStartTime = void 0;
|
|
1151
1208
|
this.iceNegotiationMs = void 0;
|
|
1152
1209
|
this.dataChannelMs = void 0;
|
|
1210
|
+
this.sdpPollingMs = void 0;
|
|
1211
|
+
this.sdpPollingAttempts = void 0;
|
|
1153
1212
|
}
|
|
1154
1213
|
startStatsPolling() {
|
|
1155
1214
|
this.stopStatsPolling();
|
|
@@ -1170,9 +1229,30 @@ var GPUMachineClient = class {
|
|
|
1170
1229
|
}
|
|
1171
1230
|
this.stats = void 0;
|
|
1172
1231
|
}
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1232
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1233
|
+
// Ping (Client Liveness)
|
|
1234
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1235
|
+
startPing() {
|
|
1236
|
+
this.stopPing();
|
|
1237
|
+
this.pingInterval = setInterval(() => {
|
|
1238
|
+
var _a;
|
|
1239
|
+
if (((_a = this.dataChannel) == null ? void 0 : _a.readyState) === "open") {
|
|
1240
|
+
try {
|
|
1241
|
+
sendMessage(this.dataChannel, "ping", {}, "runtime");
|
|
1242
|
+
} catch (e) {
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}, PING_INTERVAL_MS);
|
|
1246
|
+
}
|
|
1247
|
+
stopPing() {
|
|
1248
|
+
if (this.pingInterval !== void 0) {
|
|
1249
|
+
clearInterval(this.pingInterval);
|
|
1250
|
+
this.pingInterval = void 0;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1254
|
+
// Internal Helpers
|
|
1255
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1176
1256
|
checkFullyConnected() {
|
|
1177
1257
|
if (this.peerConnected && this.dataChannelOpen) {
|
|
1178
1258
|
this.setStatus("connected");
|
|
@@ -1190,7 +1270,7 @@ var GPUMachineClient = class {
|
|
|
1190
1270
|
this.peerConnection.onconnectionstatechange = () => {
|
|
1191
1271
|
var _a;
|
|
1192
1272
|
const state = (_a = this.peerConnection) == null ? void 0 : _a.connectionState;
|
|
1193
|
-
console.debug("[
|
|
1273
|
+
console.debug("[WebRTCTransport] Connection state:", state);
|
|
1194
1274
|
if (state) {
|
|
1195
1275
|
switch (state) {
|
|
1196
1276
|
case "connected":
|
|
@@ -1223,21 +1303,21 @@ var GPUMachineClient = class {
|
|
|
1223
1303
|
}
|
|
1224
1304
|
trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
|
|
1225
1305
|
console.debug(
|
|
1226
|
-
`[
|
|
1306
|
+
`[WebRTCTransport] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
|
|
1227
1307
|
);
|
|
1228
1308
|
const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
|
|
1229
1309
|
this.emit("trackReceived", trackName, event.track, stream);
|
|
1230
1310
|
};
|
|
1231
1311
|
this.peerConnection.onicecandidate = (event) => {
|
|
1232
1312
|
if (event.candidate) {
|
|
1233
|
-
console.debug("[
|
|
1313
|
+
console.debug("[WebRTCTransport] ICE candidate:", event.candidate);
|
|
1234
1314
|
}
|
|
1235
1315
|
};
|
|
1236
1316
|
this.peerConnection.onicecandidateerror = (event) => {
|
|
1237
|
-
console.warn("[
|
|
1317
|
+
console.warn("[WebRTCTransport] ICE candidate error:", event);
|
|
1238
1318
|
};
|
|
1239
1319
|
this.peerConnection.ondatachannel = (event) => {
|
|
1240
|
-
console.debug("[
|
|
1320
|
+
console.debug("[WebRTCTransport] Data channel received from remote");
|
|
1241
1321
|
this.dataChannel = event.channel;
|
|
1242
1322
|
this.setupDataChannelHandlers();
|
|
1243
1323
|
};
|
|
@@ -1245,7 +1325,7 @@ var GPUMachineClient = class {
|
|
|
1245
1325
|
setupDataChannelHandlers() {
|
|
1246
1326
|
if (!this.dataChannel) return;
|
|
1247
1327
|
this.dataChannel.onopen = () => {
|
|
1248
|
-
console.debug("[
|
|
1328
|
+
console.debug("[WebRTCTransport] Data channel open");
|
|
1249
1329
|
if (this.iceStartTime != null && this.dataChannelMs == null) {
|
|
1250
1330
|
this.dataChannelMs = performance.now() - this.iceStartTime;
|
|
1251
1331
|
}
|
|
@@ -1254,16 +1334,16 @@ var GPUMachineClient = class {
|
|
|
1254
1334
|
this.checkFullyConnected();
|
|
1255
1335
|
};
|
|
1256
1336
|
this.dataChannel.onclose = () => {
|
|
1257
|
-
console.debug("[
|
|
1337
|
+
console.debug("[WebRTCTransport] Data channel closed");
|
|
1258
1338
|
this.dataChannelOpen = false;
|
|
1259
1339
|
this.stopPing();
|
|
1260
1340
|
};
|
|
1261
1341
|
this.dataChannel.onerror = (error) => {
|
|
1262
|
-
console.error("[
|
|
1342
|
+
console.error("[WebRTCTransport] Data channel error:", error);
|
|
1263
1343
|
};
|
|
1264
1344
|
this.dataChannel.onmessage = (event) => {
|
|
1265
1345
|
const rawData = parseMessage(event.data);
|
|
1266
|
-
console.debug("[
|
|
1346
|
+
console.debug("[WebRTCTransport] Received message:", rawData);
|
|
1267
1347
|
try {
|
|
1268
1348
|
if ((rawData == null ? void 0 : rawData.scope) === "application" && (rawData == null ? void 0 : rawData.data) !== void 0) {
|
|
1269
1349
|
this.emit("message", rawData.data, "application");
|
|
@@ -1271,13 +1351,13 @@ var GPUMachineClient = class {
|
|
|
1271
1351
|
this.emit("message", rawData.data, "runtime");
|
|
1272
1352
|
} else {
|
|
1273
1353
|
console.warn(
|
|
1274
|
-
"[
|
|
1354
|
+
"[WebRTCTransport] Received message without envelope, treating as application"
|
|
1275
1355
|
);
|
|
1276
1356
|
this.emit("message", rawData, "application");
|
|
1277
1357
|
}
|
|
1278
1358
|
} catch (error) {
|
|
1279
1359
|
console.error(
|
|
1280
|
-
"[
|
|
1360
|
+
"[WebRTCTransport] Failed to parse/validate message:",
|
|
1281
1361
|
error
|
|
1282
1362
|
);
|
|
1283
1363
|
}
|
|
@@ -1289,46 +1369,24 @@ var GPUMachineClient = class {
|
|
|
1289
1369
|
import { z as z2 } from "zod";
|
|
1290
1370
|
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
1291
1371
|
var DEFAULT_BASE_URL = "https://api.reactor.inc";
|
|
1292
|
-
var TrackConfigSchema = z2.object({
|
|
1293
|
-
name: z2.string(),
|
|
1294
|
-
kind: z2.enum(["audio", "video"])
|
|
1295
|
-
});
|
|
1296
1372
|
var OptionsSchema = z2.object({
|
|
1297
1373
|
apiUrl: z2.string().default(DEFAULT_BASE_URL),
|
|
1298
1374
|
modelName: z2.string(),
|
|
1299
|
-
local: z2.boolean().default(false)
|
|
1300
|
-
/**
|
|
1301
|
-
* Tracks the client **RECEIVES** from the model (model → client).
|
|
1302
|
-
* Each entry produces a `recvonly` transceiver.
|
|
1303
|
-
* Names must be unique across both `receive` and `send`.
|
|
1304
|
-
*
|
|
1305
|
-
* When omitted, defaults to a single video track named `"main_video"`.
|
|
1306
|
-
* Pass an explicit empty array to opt out of the default.
|
|
1307
|
-
*/
|
|
1308
|
-
receive: z2.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
|
|
1309
|
-
/**
|
|
1310
|
-
* Tracks the client **SENDS** to the model (client → model).
|
|
1311
|
-
* Each entry produces a `sendonly` transceiver.
|
|
1312
|
-
* Names must be unique across both `receive` and `send`.
|
|
1313
|
-
*/
|
|
1314
|
-
send: z2.array(TrackConfigSchema).default([])
|
|
1375
|
+
local: z2.boolean().default(false)
|
|
1315
1376
|
});
|
|
1316
1377
|
var Reactor = class {
|
|
1317
1378
|
constructor(options) {
|
|
1318
1379
|
this.status = "disconnected";
|
|
1319
|
-
|
|
1380
|
+
this.tracks = [];
|
|
1320
1381
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1321
1382
|
const validatedOptions = OptionsSchema.parse(options);
|
|
1322
1383
|
this.coordinatorUrl = validatedOptions.apiUrl;
|
|
1323
1384
|
this.model = validatedOptions.modelName;
|
|
1324
1385
|
this.local = validatedOptions.local;
|
|
1325
|
-
this.receive = validatedOptions.receive;
|
|
1326
|
-
this.send = validatedOptions.send;
|
|
1327
1386
|
if (this.local && options.apiUrl === void 0) {
|
|
1328
1387
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
1329
1388
|
}
|
|
1330
1389
|
}
|
|
1331
|
-
// Event Emitter API
|
|
1332
1390
|
on(event, handler) {
|
|
1333
1391
|
if (!this.eventListeners.has(event)) {
|
|
1334
1392
|
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
@@ -1345,10 +1403,6 @@ var Reactor = class {
|
|
|
1345
1403
|
}
|
|
1346
1404
|
/**
|
|
1347
1405
|
* Sends a command to the model via the data channel.
|
|
1348
|
-
*
|
|
1349
|
-
* @param command The command name.
|
|
1350
|
-
* @param data The command payload.
|
|
1351
|
-
* @param scope "application" (default) for model commands, "runtime" for platform messages.
|
|
1352
1406
|
*/
|
|
1353
1407
|
sendCommand(command, data, scope = "application") {
|
|
1354
1408
|
return __async(this, null, function* () {
|
|
@@ -1359,7 +1413,7 @@ var Reactor = class {
|
|
|
1359
1413
|
return;
|
|
1360
1414
|
}
|
|
1361
1415
|
try {
|
|
1362
|
-
(_a = this.
|
|
1416
|
+
(_a = this.transportClient) == null ? void 0 : _a.sendCommand(command, data, scope);
|
|
1363
1417
|
} catch (error) {
|
|
1364
1418
|
console.error("[Reactor] Failed to send message:", error);
|
|
1365
1419
|
this.createError(
|
|
@@ -1372,10 +1426,9 @@ var Reactor = class {
|
|
|
1372
1426
|
});
|
|
1373
1427
|
}
|
|
1374
1428
|
/**
|
|
1375
|
-
* Publishes a MediaStreamTrack to a named
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
* @param track The MediaStreamTrack to publish.
|
|
1429
|
+
* Publishes a MediaStreamTrack to a named sendonly track.
|
|
1430
|
+
* The transceiver is already set up from capabilities — this just
|
|
1431
|
+
* calls replaceTrack() on the sender.
|
|
1379
1432
|
*/
|
|
1380
1433
|
publishTrack(name, track) {
|
|
1381
1434
|
return __async(this, null, function* () {
|
|
@@ -1387,7 +1440,7 @@ var Reactor = class {
|
|
|
1387
1440
|
return;
|
|
1388
1441
|
}
|
|
1389
1442
|
try {
|
|
1390
|
-
yield (_a = this.
|
|
1443
|
+
yield (_a = this.transportClient) == null ? void 0 : _a.publishTrack(name, track);
|
|
1391
1444
|
} catch (error) {
|
|
1392
1445
|
console.error(`[Reactor] Failed to publish track "${name}":`, error);
|
|
1393
1446
|
this.createError(
|
|
@@ -1399,16 +1452,11 @@ var Reactor = class {
|
|
|
1399
1452
|
}
|
|
1400
1453
|
});
|
|
1401
1454
|
}
|
|
1402
|
-
/**
|
|
1403
|
-
* Unpublishes the track with the given name.
|
|
1404
|
-
*
|
|
1405
|
-
* @param name The declared send track name to unpublish.
|
|
1406
|
-
*/
|
|
1407
1455
|
unpublishTrack(name) {
|
|
1408
1456
|
return __async(this, null, function* () {
|
|
1409
1457
|
var _a;
|
|
1410
1458
|
try {
|
|
1411
|
-
yield (_a = this.
|
|
1459
|
+
yield (_a = this.transportClient) == null ? void 0 : _a.unpublishTrack(name);
|
|
1412
1460
|
} catch (error) {
|
|
1413
1461
|
console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
|
|
1414
1462
|
this.createError(
|
|
@@ -1421,8 +1469,7 @@ var Reactor = class {
|
|
|
1421
1469
|
});
|
|
1422
1470
|
}
|
|
1423
1471
|
/**
|
|
1424
|
-
*
|
|
1425
|
-
* @param options Optional connect options (e.g. maxAttempts for SDP polling)
|
|
1472
|
+
* Reconnects to an existing session with a fresh transport.
|
|
1426
1473
|
*/
|
|
1427
1474
|
reconnect(options) {
|
|
1428
1475
|
return __async(this, null, function* () {
|
|
@@ -1434,31 +1481,26 @@ var Reactor = class {
|
|
|
1434
1481
|
console.warn("[Reactor] Already connected, no need to reconnect.");
|
|
1435
1482
|
return;
|
|
1436
1483
|
}
|
|
1484
|
+
if (this.tracks.length === 0) {
|
|
1485
|
+
console.warn("[Reactor] No tracks available for reconnect.");
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1437
1488
|
this.setStatus("connecting");
|
|
1438
|
-
if (!this.machineClient) {
|
|
1439
|
-
const iceServers = yield this.coordinatorClient.getIceServers();
|
|
1440
|
-
this.machineClient = new GPUMachineClient({ iceServers });
|
|
1441
|
-
this.setupMachineClientHandlers();
|
|
1442
|
-
}
|
|
1443
|
-
const sdpOffer = yield this.machineClient.createOffer({
|
|
1444
|
-
send: this.send,
|
|
1445
|
-
receive: this.receive
|
|
1446
|
-
});
|
|
1447
1489
|
try {
|
|
1448
|
-
|
|
1449
|
-
this.
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1490
|
+
if (!this.transportClient) {
|
|
1491
|
+
this.transportClient = new WebRTCTransportClient({
|
|
1492
|
+
baseUrl: this.coordinatorUrl,
|
|
1493
|
+
sessionId: this.sessionId,
|
|
1494
|
+
jwtToken: this.local ? "local" : "",
|
|
1495
|
+
maxPollAttempts: options == null ? void 0 : options.maxAttempts
|
|
1496
|
+
});
|
|
1497
|
+
this.setupTransportHandlers();
|
|
1498
|
+
}
|
|
1499
|
+
yield this.transportClient.reconnect(this.tracks);
|
|
1454
1500
|
} catch (error) {
|
|
1455
1501
|
if (isAbortError(error)) return;
|
|
1456
|
-
let recoverable = false;
|
|
1457
|
-
if (error instanceof ConflictError) {
|
|
1458
|
-
recoverable = true;
|
|
1459
|
-
}
|
|
1460
1502
|
console.error("[Reactor] Failed to reconnect:", error);
|
|
1461
|
-
this.disconnect(
|
|
1503
|
+
this.disconnect(true);
|
|
1462
1504
|
this.createError(
|
|
1463
1505
|
"RECONNECTION_FAILED",
|
|
1464
1506
|
`Failed to reconnect: ${error}`,
|
|
@@ -1469,14 +1511,12 @@ var Reactor = class {
|
|
|
1469
1511
|
});
|
|
1470
1512
|
}
|
|
1471
1513
|
/**
|
|
1472
|
-
* Connects to the coordinator
|
|
1473
|
-
*
|
|
1474
|
-
* If no authentication is provided and not in local mode, an error is thrown.
|
|
1475
|
-
* @param jwtToken Optional JWT token for authentication
|
|
1476
|
-
* @param options Optional connect options (e.g. maxAttempts for SDP polling)
|
|
1514
|
+
* Connects to the coordinator, creates a session, then establishes
|
|
1515
|
+
* the transport using server-declared capabilities.
|
|
1477
1516
|
*/
|
|
1478
1517
|
connect(jwtToken, options) {
|
|
1479
1518
|
return __async(this, null, function* () {
|
|
1519
|
+
var _a, _b;
|
|
1480
1520
|
console.debug("[Reactor] Connecting, status:", this.status);
|
|
1481
1521
|
if (jwtToken == void 0 && !this.local) {
|
|
1482
1522
|
throw new Error("No authentication provided and not in local mode");
|
|
@@ -1487,42 +1527,55 @@ var Reactor = class {
|
|
|
1487
1527
|
this.setStatus("connecting");
|
|
1488
1528
|
this.connectStartTime = performance.now();
|
|
1489
1529
|
try {
|
|
1490
|
-
|
|
1491
|
-
"[Reactor] Connecting to coordinator with authenticated URL"
|
|
1492
|
-
);
|
|
1493
|
-
this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
|
|
1530
|
+
this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl, this.model) : new CoordinatorClient({
|
|
1494
1531
|
baseUrl: this.coordinatorUrl,
|
|
1495
1532
|
jwtToken,
|
|
1496
|
-
// Safe: validated above
|
|
1497
1533
|
model: this.model
|
|
1498
1534
|
});
|
|
1499
|
-
const iceServers = yield this.coordinatorClient.getIceServers();
|
|
1500
|
-
this.machineClient = new GPUMachineClient({ iceServers });
|
|
1501
|
-
this.setupMachineClientHandlers();
|
|
1502
|
-
const sdpOffer = yield this.machineClient.createOffer({
|
|
1503
|
-
send: this.send,
|
|
1504
|
-
receive: this.receive
|
|
1505
|
-
});
|
|
1506
1535
|
const tSession = performance.now();
|
|
1507
|
-
const
|
|
1536
|
+
const initialResponse = yield this.coordinatorClient.createSession();
|
|
1508
1537
|
const sessionCreationMs = performance.now() - tSession;
|
|
1509
|
-
this.setSessionId(
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1538
|
+
this.setSessionId(initialResponse.session_id);
|
|
1539
|
+
console.debug(
|
|
1540
|
+
"[Reactor] Session created:",
|
|
1541
|
+
initialResponse.session_id,
|
|
1542
|
+
"state:",
|
|
1543
|
+
initialResponse.state
|
|
1544
|
+
);
|
|
1545
|
+
this.setStatus("waiting");
|
|
1546
|
+
const tPoll = performance.now();
|
|
1547
|
+
const sessionResponse = yield this.coordinatorClient.pollSessionReady();
|
|
1548
|
+
const sessionPollingMs = performance.now() - tPoll;
|
|
1549
|
+
this.sessionResponse = sessionResponse;
|
|
1550
|
+
this.capabilities = sessionResponse.capabilities;
|
|
1551
|
+
this.tracks = sessionResponse.capabilities.tracks;
|
|
1552
|
+
this.emit("capabilitiesReceived", this.capabilities);
|
|
1553
|
+
console.debug(
|
|
1554
|
+
"[Reactor] Session ready, transport:",
|
|
1555
|
+
sessionResponse.selected_transport.protocol,
|
|
1556
|
+
"tracks:",
|
|
1557
|
+
this.tracks.length
|
|
1515
1558
|
);
|
|
1516
|
-
const
|
|
1559
|
+
const protocol = sessionResponse.selected_transport.protocol;
|
|
1560
|
+
if (protocol !== "webrtc") {
|
|
1561
|
+
throw new Error(`Unsupported transport protocol: ${protocol}`);
|
|
1562
|
+
}
|
|
1563
|
+
this.transportClient = new WebRTCTransportClient({
|
|
1564
|
+
baseUrl: this.coordinatorUrl,
|
|
1565
|
+
sessionId: sessionResponse.session_id,
|
|
1566
|
+
jwtToken: this.local ? "local" : jwtToken,
|
|
1567
|
+
webrtcVersion: (_b = (_a = sessionResponse.selected_transport) == null ? void 0 : _a.version) != null ? _b : REACTOR_WEBRTC_VERSION,
|
|
1568
|
+
maxPollAttempts: options == null ? void 0 : options.maxAttempts
|
|
1569
|
+
});
|
|
1570
|
+
this.setupTransportHandlers();
|
|
1571
|
+
const tTransport = performance.now();
|
|
1572
|
+
yield this.transportClient.connect(this.tracks);
|
|
1573
|
+
const transportConnectingMs = performance.now() - tTransport;
|
|
1517
1574
|
this.connectionTimings = {
|
|
1518
|
-
sessionCreationMs,
|
|
1519
|
-
|
|
1520
|
-
sdpPollingAttempts,
|
|
1521
|
-
iceNegotiationMs: 0,
|
|
1522
|
-
dataChannelMs: 0,
|
|
1575
|
+
sessionCreationMs: sessionCreationMs + sessionPollingMs,
|
|
1576
|
+
transportConnectingMs,
|
|
1523
1577
|
totalMs: 0
|
|
1524
1578
|
};
|
|
1525
|
-
yield this.machineClient.connect(sdpAnswer);
|
|
1526
1579
|
} catch (error) {
|
|
1527
1580
|
if (isAbortError(error)) return;
|
|
1528
1581
|
console.error("[Reactor] Connection failed:", error);
|
|
@@ -1545,18 +1598,14 @@ var Reactor = class {
|
|
|
1545
1598
|
});
|
|
1546
1599
|
}
|
|
1547
1600
|
/**
|
|
1548
|
-
* Sets up event handlers for the
|
|
1549
|
-
*
|
|
1550
|
-
* Each handler captures the client reference at registration time and
|
|
1551
|
-
* ignores events if this.machineClient has since changed (e.g. after
|
|
1552
|
-
* disconnect + reconnect), preventing stale WebRTC teardown events from
|
|
1553
|
-
* interfering with a new connection.
|
|
1601
|
+
* Sets up event handlers for the transport client.
|
|
1602
|
+
* Each handler captures the client reference to ignore stale events.
|
|
1554
1603
|
*/
|
|
1555
|
-
|
|
1556
|
-
if (!this.
|
|
1557
|
-
const client = this.
|
|
1604
|
+
setupTransportHandlers() {
|
|
1605
|
+
if (!this.transportClient) return;
|
|
1606
|
+
const client = this.transportClient;
|
|
1558
1607
|
client.on("message", (message, scope) => {
|
|
1559
|
-
if (this.
|
|
1608
|
+
if (this.transportClient !== client) return;
|
|
1560
1609
|
if (scope === "application") {
|
|
1561
1610
|
this.emit("message", message);
|
|
1562
1611
|
} else if (scope === "runtime") {
|
|
@@ -1564,10 +1613,10 @@ var Reactor = class {
|
|
|
1564
1613
|
}
|
|
1565
1614
|
});
|
|
1566
1615
|
client.on("statusChanged", (status) => {
|
|
1567
|
-
if (this.
|
|
1616
|
+
if (this.transportClient !== client) return;
|
|
1568
1617
|
switch (status) {
|
|
1569
1618
|
case "connected":
|
|
1570
|
-
this.finalizeConnectionTimings(
|
|
1619
|
+
this.finalizeConnectionTimings();
|
|
1571
1620
|
this.setStatus("ready");
|
|
1572
1621
|
break;
|
|
1573
1622
|
case "disconnected":
|
|
@@ -1576,7 +1625,7 @@ var Reactor = class {
|
|
|
1576
1625
|
case "error":
|
|
1577
1626
|
this.createError(
|
|
1578
1627
|
"GPU_CONNECTION_ERROR",
|
|
1579
|
-
"
|
|
1628
|
+
"Transport connection failed",
|
|
1580
1629
|
"gpu",
|
|
1581
1630
|
true
|
|
1582
1631
|
);
|
|
@@ -1587,29 +1636,29 @@ var Reactor = class {
|
|
|
1587
1636
|
client.on(
|
|
1588
1637
|
"trackReceived",
|
|
1589
1638
|
(name, track, stream) => {
|
|
1590
|
-
if (this.
|
|
1639
|
+
if (this.transportClient !== client) return;
|
|
1591
1640
|
this.emit("trackReceived", name, track, stream);
|
|
1592
1641
|
}
|
|
1593
1642
|
);
|
|
1594
1643
|
client.on("statsUpdate", (stats) => {
|
|
1595
|
-
if (this.
|
|
1644
|
+
if (this.transportClient !== client) return;
|
|
1596
1645
|
this.emit("statsUpdate", __spreadProps(__spreadValues({}, stats), {
|
|
1597
1646
|
connectionTimings: this.connectionTimings
|
|
1598
1647
|
}));
|
|
1599
1648
|
});
|
|
1600
1649
|
}
|
|
1601
1650
|
/**
|
|
1602
|
-
* Disconnects from the
|
|
1603
|
-
* Ensures cleanup completes even if individual disconnections fail.
|
|
1651
|
+
* Disconnects from both the transport and the coordinator.
|
|
1604
1652
|
*/
|
|
1605
1653
|
disconnect(recoverable = false) {
|
|
1606
1654
|
return __async(this, null, function* () {
|
|
1607
|
-
var _a;
|
|
1655
|
+
var _a, _b;
|
|
1608
1656
|
if (this.status === "disconnected" && !this.sessionId) {
|
|
1609
1657
|
console.warn("[Reactor] Already disconnected");
|
|
1610
1658
|
return;
|
|
1611
1659
|
}
|
|
1612
1660
|
(_a = this.coordinatorClient) == null ? void 0 : _a.abort();
|
|
1661
|
+
(_b = this.transportClient) == null ? void 0 : _b.abort();
|
|
1613
1662
|
if (this.coordinatorClient && !recoverable) {
|
|
1614
1663
|
try {
|
|
1615
1664
|
yield this.coordinatorClient.terminateSession();
|
|
@@ -1618,14 +1667,14 @@ var Reactor = class {
|
|
|
1618
1667
|
}
|
|
1619
1668
|
this.coordinatorClient = void 0;
|
|
1620
1669
|
}
|
|
1621
|
-
if (this.
|
|
1670
|
+
if (this.transportClient) {
|
|
1622
1671
|
try {
|
|
1623
|
-
yield this.
|
|
1672
|
+
yield this.transportClient.disconnect();
|
|
1624
1673
|
} catch (error) {
|
|
1625
|
-
console.error("[Reactor] Error disconnecting
|
|
1674
|
+
console.error("[Reactor] Error disconnecting transport:", error);
|
|
1626
1675
|
}
|
|
1627
1676
|
if (!recoverable) {
|
|
1628
|
-
this.
|
|
1677
|
+
this.transportClient = void 0;
|
|
1629
1678
|
}
|
|
1630
1679
|
}
|
|
1631
1680
|
this.setStatus("disconnected");
|
|
@@ -1633,9 +1682,45 @@ var Reactor = class {
|
|
|
1633
1682
|
if (!recoverable) {
|
|
1634
1683
|
this.setSessionExpiration(void 0);
|
|
1635
1684
|
this.setSessionId(void 0);
|
|
1685
|
+
this.capabilities = void 0;
|
|
1686
|
+
this.tracks = [];
|
|
1687
|
+
this.sessionResponse = void 0;
|
|
1636
1688
|
}
|
|
1637
1689
|
});
|
|
1638
1690
|
}
|
|
1691
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1692
|
+
// Getters
|
|
1693
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1694
|
+
getSessionId() {
|
|
1695
|
+
return this.sessionId;
|
|
1696
|
+
}
|
|
1697
|
+
getStatus() {
|
|
1698
|
+
return this.status;
|
|
1699
|
+
}
|
|
1700
|
+
getState() {
|
|
1701
|
+
return {
|
|
1702
|
+
status: this.status,
|
|
1703
|
+
lastError: this.lastError
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
getLastError() {
|
|
1707
|
+
return this.lastError;
|
|
1708
|
+
}
|
|
1709
|
+
getCapabilities() {
|
|
1710
|
+
return this.capabilities;
|
|
1711
|
+
}
|
|
1712
|
+
getSessionInfo() {
|
|
1713
|
+
return this.sessionResponse;
|
|
1714
|
+
}
|
|
1715
|
+
getStats() {
|
|
1716
|
+
var _a;
|
|
1717
|
+
const stats = (_a = this.transportClient) == null ? void 0 : _a.getStats();
|
|
1718
|
+
if (!stats) return void 0;
|
|
1719
|
+
return __spreadProps(__spreadValues({}, stats), { connectionTimings: this.connectionTimings });
|
|
1720
|
+
}
|
|
1721
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1722
|
+
// Private State Management
|
|
1723
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1639
1724
|
setSessionId(newSessionId) {
|
|
1640
1725
|
console.debug(
|
|
1641
1726
|
"[Reactor] Setting session ID:",
|
|
@@ -1648,9 +1733,6 @@ var Reactor = class {
|
|
|
1648
1733
|
this.emit("sessionIdChanged", newSessionId);
|
|
1649
1734
|
}
|
|
1650
1735
|
}
|
|
1651
|
-
getSessionId() {
|
|
1652
|
-
return this.sessionId;
|
|
1653
|
-
}
|
|
1654
1736
|
setStatus(newStatus) {
|
|
1655
1737
|
console.debug("[Reactor] Setting status:", newStatus, "from", this.status);
|
|
1656
1738
|
if (this.status !== newStatus) {
|
|
@@ -1658,13 +1740,6 @@ var Reactor = class {
|
|
|
1658
1740
|
this.emit("statusChanged", newStatus);
|
|
1659
1741
|
}
|
|
1660
1742
|
}
|
|
1661
|
-
getStatus() {
|
|
1662
|
-
return this.status;
|
|
1663
|
-
}
|
|
1664
|
-
/**
|
|
1665
|
-
* Set the session expiration time.
|
|
1666
|
-
* @param newSessionExpiration The new session expiration time in seconds.
|
|
1667
|
-
*/
|
|
1668
1743
|
setSessionExpiration(newSessionExpiration) {
|
|
1669
1744
|
console.debug(
|
|
1670
1745
|
"[Reactor] Setting session expiration:",
|
|
@@ -1675,44 +1750,16 @@ var Reactor = class {
|
|
|
1675
1750
|
this.emit("sessionExpirationChanged", newSessionExpiration);
|
|
1676
1751
|
}
|
|
1677
1752
|
}
|
|
1678
|
-
/**
|
|
1679
|
-
* Get the current state including status, error, and waiting info
|
|
1680
|
-
*/
|
|
1681
|
-
getState() {
|
|
1682
|
-
return {
|
|
1683
|
-
status: this.status,
|
|
1684
|
-
lastError: this.lastError
|
|
1685
|
-
};
|
|
1686
|
-
}
|
|
1687
|
-
/**
|
|
1688
|
-
* Get the last error that occurred
|
|
1689
|
-
*/
|
|
1690
|
-
getLastError() {
|
|
1691
|
-
return this.lastError;
|
|
1692
|
-
}
|
|
1693
|
-
getStats() {
|
|
1694
|
-
var _a;
|
|
1695
|
-
const stats = (_a = this.machineClient) == null ? void 0 : _a.getStats();
|
|
1696
|
-
if (!stats) return void 0;
|
|
1697
|
-
return __spreadProps(__spreadValues({}, stats), { connectionTimings: this.connectionTimings });
|
|
1698
|
-
}
|
|
1699
1753
|
resetConnectionTimings() {
|
|
1700
1754
|
this.connectStartTime = void 0;
|
|
1701
1755
|
this.connectionTimings = void 0;
|
|
1702
1756
|
}
|
|
1703
|
-
finalizeConnectionTimings(
|
|
1704
|
-
var _a, _b;
|
|
1757
|
+
finalizeConnectionTimings() {
|
|
1705
1758
|
if (!this.connectionTimings || this.connectStartTime == null) return;
|
|
1706
|
-
const webrtcTimings = client.getConnectionTimings();
|
|
1707
|
-
this.connectionTimings.iceNegotiationMs = (_a = webrtcTimings == null ? void 0 : webrtcTimings.iceNegotiationMs) != null ? _a : 0;
|
|
1708
|
-
this.connectionTimings.dataChannelMs = (_b = webrtcTimings == null ? void 0 : webrtcTimings.dataChannelMs) != null ? _b : 0;
|
|
1709
1759
|
this.connectionTimings.totalMs = performance.now() - this.connectStartTime;
|
|
1710
1760
|
this.connectStartTime = void 0;
|
|
1711
1761
|
console.debug("[Reactor] Connection timings:", this.connectionTimings);
|
|
1712
1762
|
}
|
|
1713
|
-
/**
|
|
1714
|
-
* Create and store an error
|
|
1715
|
-
*/
|
|
1716
1763
|
createError(code, message, component, recoverable, retryAfter) {
|
|
1717
1764
|
this.lastError = {
|
|
1718
1765
|
code,
|
|
@@ -1916,7 +1963,7 @@ function ReactorProvider(_a) {
|
|
|
1916
1963
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1917
1964
|
}
|
|
1918
1965
|
const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
|
|
1919
|
-
const { apiUrl, modelName, local
|
|
1966
|
+
const { apiUrl, modelName, local } = props;
|
|
1920
1967
|
const maxAttempts = pollingOptions.maxAttempts;
|
|
1921
1968
|
useEffect(() => {
|
|
1922
1969
|
const handleBeforeUnload = () => {
|
|
@@ -1972,8 +2019,6 @@ function ReactorProvider(_a) {
|
|
|
1972
2019
|
apiUrl,
|
|
1973
2020
|
modelName,
|
|
1974
2021
|
local,
|
|
1975
|
-
receive,
|
|
1976
|
-
send,
|
|
1977
2022
|
jwtToken
|
|
1978
2023
|
})
|
|
1979
2024
|
);
|
|
@@ -2000,16 +2045,7 @@ function ReactorProvider(_a) {
|
|
|
2000
2045
|
console.error("[ReactorProvider] Failed to disconnect:", error);
|
|
2001
2046
|
});
|
|
2002
2047
|
};
|
|
2003
|
-
}, [
|
|
2004
|
-
apiUrl,
|
|
2005
|
-
modelName,
|
|
2006
|
-
autoConnect,
|
|
2007
|
-
local,
|
|
2008
|
-
receive,
|
|
2009
|
-
send,
|
|
2010
|
-
jwtToken,
|
|
2011
|
-
maxAttempts
|
|
2012
|
-
]);
|
|
2048
|
+
}, [apiUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
|
|
2013
2049
|
return /* @__PURE__ */ jsx(ReactorContext.Provider, { value: storeRef.current, children });
|
|
2014
2050
|
}
|
|
2015
2051
|
function useReactorStore(selector) {
|
|
@@ -2223,31 +2259,37 @@ function ReactorController({
|
|
|
2223
2259
|
return () => clearInterval(interval);
|
|
2224
2260
|
}, [status, commands, requestCapabilities]);
|
|
2225
2261
|
useReactorInternalMessage((message) => {
|
|
2262
|
+
var _a;
|
|
2226
2263
|
if (message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
|
|
2227
2264
|
const commandsMessage = message.data;
|
|
2228
|
-
|
|
2265
|
+
const commandsRecord = {};
|
|
2266
|
+
for (const cmd of commandsMessage.commands) {
|
|
2267
|
+
commandsRecord[cmd.name] = {
|
|
2268
|
+
description: cmd.description,
|
|
2269
|
+
schema: (_a = cmd.schema) != null ? _a : {}
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
setCommands(commandsRecord);
|
|
2229
2273
|
const initialValues = {};
|
|
2230
2274
|
const initialExpanded = {};
|
|
2231
|
-
Object.entries(
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
initialValues[commandName][paramName] = (_b = paramSchema.minimum) != null ? _b : 0;
|
|
2246
|
-
}
|
|
2275
|
+
Object.entries(commandsRecord).forEach(([commandName, commandSchema]) => {
|
|
2276
|
+
initialValues[commandName] = {};
|
|
2277
|
+
initialExpanded[commandName] = false;
|
|
2278
|
+
Object.entries(commandSchema.schema).forEach(
|
|
2279
|
+
([paramName, paramSchema]) => {
|
|
2280
|
+
var _a2, _b;
|
|
2281
|
+
if (paramSchema.type === "number") {
|
|
2282
|
+
initialValues[commandName][paramName] = (_a2 = paramSchema.minimum) != null ? _a2 : 0;
|
|
2283
|
+
} else if (paramSchema.type === "string") {
|
|
2284
|
+
initialValues[commandName][paramName] = "";
|
|
2285
|
+
} else if (paramSchema.type === "boolean") {
|
|
2286
|
+
initialValues[commandName][paramName] = false;
|
|
2287
|
+
} else if (paramSchema.type === "integer") {
|
|
2288
|
+
initialValues[commandName][paramName] = (_b = paramSchema.minimum) != null ? _b : 0;
|
|
2247
2289
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
);
|
|
2290
|
+
}
|
|
2291
|
+
);
|
|
2292
|
+
});
|
|
2251
2293
|
setFormValues(initialValues);
|
|
2252
2294
|
setExpandedCommands(initialExpanded);
|
|
2253
2295
|
}
|
|
@@ -2828,27 +2870,6 @@ function WebcamStream({
|
|
|
2828
2870
|
}
|
|
2829
2871
|
);
|
|
2830
2872
|
}
|
|
2831
|
-
|
|
2832
|
-
// src/utils/tokens.ts
|
|
2833
|
-
function fetchInsecureToken(_0) {
|
|
2834
|
-
return __async(this, arguments, function* (apiKey, apiUrl = DEFAULT_BASE_URL) {
|
|
2835
|
-
console.warn(
|
|
2836
|
-
"[Reactor] \u26A0\uFE0F SECURITY WARNING: fetchInsecureToken() exposes your API key in client-side code. This should ONLY be used for local development or testing. In production, fetch tokens from your server instead."
|
|
2837
|
-
);
|
|
2838
|
-
const response = yield fetch(`${apiUrl}/tokens`, {
|
|
2839
|
-
method: "GET",
|
|
2840
|
-
headers: {
|
|
2841
|
-
"X-API-Key": apiKey
|
|
2842
|
-
}
|
|
2843
|
-
});
|
|
2844
|
-
if (!response.ok) {
|
|
2845
|
-
const error = yield response.text();
|
|
2846
|
-
throw new Error(`Failed to create token: ${response.status} ${error}`);
|
|
2847
|
-
}
|
|
2848
|
-
const { jwt } = yield response.json();
|
|
2849
|
-
return jwt;
|
|
2850
|
-
});
|
|
2851
|
-
}
|
|
2852
2873
|
export {
|
|
2853
2874
|
AbortError,
|
|
2854
2875
|
ConflictError,
|
|
@@ -2858,14 +2879,11 @@ export {
|
|
|
2858
2879
|
ReactorProvider,
|
|
2859
2880
|
ReactorView,
|
|
2860
2881
|
WebcamStream,
|
|
2861
|
-
audio,
|
|
2862
|
-
fetchInsecureToken,
|
|
2863
2882
|
isAbortError,
|
|
2864
2883
|
useReactor,
|
|
2865
2884
|
useReactorInternalMessage,
|
|
2866
2885
|
useReactorMessage,
|
|
2867
2886
|
useReactorStore,
|
|
2868
|
-
useStats
|
|
2869
|
-
video
|
|
2887
|
+
useStats
|
|
2870
2888
|
};
|
|
2871
2889
|
//# sourceMappingURL=index.mjs.map
|