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