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