@reactor-team/js-sdk 2.5.1 → 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 +323 -151
- package/dist/index.d.ts +323 -151
- package/dist/index.js +1109 -974
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1105 -969
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -2
package/dist/index.js
CHANGED
|
@@ -81,31 +81,24 @@ var index_exports = {};
|
|
|
81
81
|
__export(index_exports, {
|
|
82
82
|
AbortError: () => AbortError,
|
|
83
83
|
ConflictError: () => ConflictError,
|
|
84
|
-
|
|
84
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
85
85
|
Reactor: () => Reactor,
|
|
86
86
|
ReactorController: () => ReactorController,
|
|
87
87
|
ReactorProvider: () => ReactorProvider,
|
|
88
88
|
ReactorView: () => ReactorView,
|
|
89
|
+
WebRTCTransportClient: () => WebRTCTransportClient,
|
|
89
90
|
WebcamStream: () => WebcamStream,
|
|
90
|
-
|
|
91
|
-
fetchInsecureJwtToken: () => fetchInsecureJwtToken,
|
|
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,292 +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
|
-
function createPeerConnection(config) {
|
|
182
|
-
return new RTCPeerConnection({
|
|
183
|
-
iceServers: config.iceServers,
|
|
184
|
-
iceTransportPolicy: FORCE_RELAY_MODE ? "relay" : "all"
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
function createDataChannel(pc, label) {
|
|
188
|
-
return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
|
|
189
|
-
}
|
|
190
|
-
function rewriteMids(sdp, trackNames) {
|
|
191
|
-
const lines = sdp.split("\r\n");
|
|
192
|
-
let mediaIdx = 0;
|
|
193
|
-
const replacements = /* @__PURE__ */ new Map();
|
|
194
|
-
let inApplication = false;
|
|
195
|
-
for (let i = 0; i < lines.length; i++) {
|
|
196
|
-
if (lines[i].startsWith("m=")) {
|
|
197
|
-
inApplication = lines[i].startsWith("m=application");
|
|
198
|
-
}
|
|
199
|
-
if (!inApplication && lines[i].startsWith("a=mid:")) {
|
|
200
|
-
const oldMid = lines[i].substring("a=mid:".length);
|
|
201
|
-
if (mediaIdx < trackNames.length) {
|
|
202
|
-
const newMid = trackNames[mediaIdx];
|
|
203
|
-
replacements.set(oldMid, newMid);
|
|
204
|
-
lines[i] = `a=mid:${newMid}`;
|
|
205
|
-
mediaIdx++;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
for (let i = 0; i < lines.length; i++) {
|
|
210
|
-
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
211
|
-
const parts = lines[i].split(" ");
|
|
212
|
-
for (let j = 1; j < parts.length; j++) {
|
|
213
|
-
const replacement = replacements.get(parts[j]);
|
|
214
|
-
if (replacement !== void 0) {
|
|
215
|
-
parts[j] = replacement;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
lines[i] = parts.join(" ");
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return lines.join("\r\n");
|
|
223
|
-
}
|
|
224
|
-
function createOffer(pc, trackNames) {
|
|
225
|
-
return __async(this, null, function* () {
|
|
226
|
-
const offer = yield pc.createOffer();
|
|
227
|
-
let needsAnswerRestore = false;
|
|
228
|
-
if (trackNames && trackNames.length > 0 && offer.sdp) {
|
|
229
|
-
const munged = rewriteMids(offer.sdp, trackNames);
|
|
230
|
-
try {
|
|
231
|
-
yield pc.setLocalDescription(
|
|
232
|
-
new RTCSessionDescription({ type: "offer", sdp: munged })
|
|
233
|
-
);
|
|
234
|
-
} catch (e) {
|
|
235
|
-
yield pc.setLocalDescription(offer);
|
|
236
|
-
needsAnswerRestore = true;
|
|
237
|
-
}
|
|
238
|
-
} else {
|
|
239
|
-
yield pc.setLocalDescription(offer);
|
|
240
|
-
}
|
|
241
|
-
yield waitForIceGathering(pc);
|
|
242
|
-
const localDescription = pc.localDescription;
|
|
243
|
-
if (!localDescription) {
|
|
244
|
-
throw new Error("Failed to create local description");
|
|
245
|
-
}
|
|
246
|
-
let sdp = localDescription.sdp;
|
|
247
|
-
if (needsAnswerRestore && trackNames && trackNames.length > 0) {
|
|
248
|
-
sdp = rewriteMids(sdp, trackNames);
|
|
249
|
-
}
|
|
250
|
-
return { sdp, needsAnswerRestore };
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
function buildMidMapping(transceivers) {
|
|
254
|
-
var _a;
|
|
255
|
-
const localToRemote = /* @__PURE__ */ new Map();
|
|
256
|
-
const remoteToLocal = /* @__PURE__ */ new Map();
|
|
257
|
-
for (const entry of transceivers) {
|
|
258
|
-
const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
|
|
259
|
-
if (mid) {
|
|
260
|
-
localToRemote.set(mid, entry.name);
|
|
261
|
-
remoteToLocal.set(entry.name, mid);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return { localToRemote, remoteToLocal };
|
|
265
|
-
}
|
|
266
|
-
function restoreAnswerMids(sdp, remoteToLocal) {
|
|
267
|
-
const lines = sdp.split("\r\n");
|
|
268
|
-
for (let i = 0; i < lines.length; i++) {
|
|
269
|
-
if (lines[i].startsWith("a=mid:")) {
|
|
270
|
-
const remoteMid = lines[i].substring("a=mid:".length);
|
|
271
|
-
const localMid = remoteToLocal.get(remoteMid);
|
|
272
|
-
if (localMid !== void 0) {
|
|
273
|
-
lines[i] = `a=mid:${localMid}`;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
277
|
-
const parts = lines[i].split(" ");
|
|
278
|
-
for (let j = 1; j < parts.length; j++) {
|
|
279
|
-
const localMid = remoteToLocal.get(parts[j]);
|
|
280
|
-
if (localMid !== void 0) {
|
|
281
|
-
parts[j] = localMid;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
lines[i] = parts.join(" ");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return lines.join("\r\n");
|
|
288
|
-
}
|
|
289
|
-
function setRemoteDescription(pc, sdp) {
|
|
290
|
-
return __async(this, null, function* () {
|
|
291
|
-
const sessionDescription = new RTCSessionDescription({
|
|
292
|
-
sdp,
|
|
293
|
-
type: "answer"
|
|
294
|
-
});
|
|
295
|
-
yield pc.setRemoteDescription(sessionDescription);
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
function getLocalDescription(pc) {
|
|
299
|
-
const desc = pc.localDescription;
|
|
300
|
-
if (!desc) return void 0;
|
|
301
|
-
return desc.sdp;
|
|
302
|
-
}
|
|
303
|
-
function transformIceServers(response) {
|
|
304
|
-
return response.ice_servers.map((server) => {
|
|
305
|
-
const rtcServer = {
|
|
306
|
-
urls: server.uris
|
|
307
|
-
};
|
|
308
|
-
if (server.credentials) {
|
|
309
|
-
rtcServer.username = server.credentials.username;
|
|
310
|
-
rtcServer.credential = server.credentials.password;
|
|
311
|
-
}
|
|
312
|
-
return rtcServer;
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
function waitForIceGathering(pc, timeoutMs = 5e3) {
|
|
316
|
-
return new Promise((resolve) => {
|
|
317
|
-
if (pc.iceGatheringState === "complete") {
|
|
318
|
-
resolve();
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const onGatheringStateChange = () => {
|
|
322
|
-
if (pc.iceGatheringState === "complete") {
|
|
323
|
-
pc.removeEventListener(
|
|
324
|
-
"icegatheringstatechange",
|
|
325
|
-
onGatheringStateChange
|
|
326
|
-
);
|
|
327
|
-
resolve();
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
pc.addEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
331
|
-
setTimeout(() => {
|
|
332
|
-
pc.removeEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
333
|
-
resolve();
|
|
334
|
-
}, timeoutMs);
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
function sendMessage(channel, command, data, scope = "application") {
|
|
338
|
-
if (channel.readyState !== "open") {
|
|
339
|
-
throw new Error(`Data channel not open: ${channel.readyState}`);
|
|
340
|
-
}
|
|
341
|
-
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
|
342
|
-
const inner = { type: command, data: jsonData };
|
|
343
|
-
const payload = { scope, data: inner };
|
|
344
|
-
channel.send(JSON.stringify(payload));
|
|
345
|
-
}
|
|
346
|
-
function parseMessage(data) {
|
|
347
|
-
if (typeof data === "string") {
|
|
348
|
-
try {
|
|
349
|
-
return JSON.parse(data);
|
|
350
|
-
} catch (e) {
|
|
351
|
-
return data;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return data;
|
|
355
|
-
}
|
|
356
|
-
function closePeerConnection(pc) {
|
|
357
|
-
pc.close();
|
|
358
|
-
}
|
|
359
|
-
function extractConnectionStats(report) {
|
|
360
|
-
let rtt;
|
|
361
|
-
let availableOutgoingBitrate;
|
|
362
|
-
let localCandidateId;
|
|
363
|
-
let framesPerSecond;
|
|
364
|
-
let jitter;
|
|
365
|
-
let packetLossRatio;
|
|
366
|
-
report.forEach((stat) => {
|
|
367
|
-
if (stat.type === "candidate-pair" && stat.state === "succeeded") {
|
|
368
|
-
if (stat.currentRoundTripTime !== void 0) {
|
|
369
|
-
rtt = stat.currentRoundTripTime * 1e3;
|
|
370
|
-
}
|
|
371
|
-
if (stat.availableOutgoingBitrate !== void 0) {
|
|
372
|
-
availableOutgoingBitrate = stat.availableOutgoingBitrate;
|
|
373
|
-
}
|
|
374
|
-
localCandidateId = stat.localCandidateId;
|
|
375
|
-
}
|
|
376
|
-
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
377
|
-
if (stat.framesPerSecond !== void 0) {
|
|
378
|
-
framesPerSecond = stat.framesPerSecond;
|
|
379
|
-
}
|
|
380
|
-
if (stat.jitter !== void 0) {
|
|
381
|
-
jitter = stat.jitter;
|
|
382
|
-
}
|
|
383
|
-
if (stat.packetsReceived !== void 0 && stat.packetsLost !== void 0 && stat.packetsReceived + stat.packetsLost > 0) {
|
|
384
|
-
packetLossRatio = stat.packetsLost / (stat.packetsReceived + stat.packetsLost);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
let candidateType;
|
|
389
|
-
if (localCandidateId) {
|
|
390
|
-
const localCandidate = report.get(localCandidateId);
|
|
391
|
-
if (localCandidate == null ? void 0 : localCandidate.candidateType) {
|
|
392
|
-
candidateType = localCandidate.candidateType;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return {
|
|
396
|
-
rtt,
|
|
397
|
-
candidateType,
|
|
398
|
-
availableOutgoingBitrate,
|
|
399
|
-
framesPerSecond,
|
|
400
|
-
packetLossRatio,
|
|
401
|
-
jitter,
|
|
402
|
-
timestamp: Date.now()
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
281
|
|
|
406
282
|
// src/core/CoordinatorClient.ts
|
|
407
|
-
var
|
|
408
|
-
var
|
|
409
|
-
var
|
|
410
|
-
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;
|
|
411
287
|
var CoordinatorClient = class {
|
|
412
288
|
constructor(options) {
|
|
413
289
|
this.baseUrl = options.baseUrl;
|
|
@@ -416,7 +292,7 @@ var CoordinatorClient = class {
|
|
|
416
292
|
this.abortController = new AbortController();
|
|
417
293
|
}
|
|
418
294
|
/**
|
|
419
|
-
* Aborts any in-flight HTTP requests
|
|
295
|
+
* Aborts any in-flight HTTP requests.
|
|
420
296
|
* A fresh AbortController is created so the client remains reusable.
|
|
421
297
|
*/
|
|
422
298
|
abort() {
|
|
@@ -424,69 +300,88 @@ var CoordinatorClient = class {
|
|
|
424
300
|
this.abortController = new AbortController();
|
|
425
301
|
}
|
|
426
302
|
/**
|
|
427
|
-
* The current abort signal, passed to every fetch()
|
|
303
|
+
* The current abort signal, passed to every fetch() call.
|
|
428
304
|
* Protected so subclasses can forward it to their own fetch calls.
|
|
429
305
|
*/
|
|
430
306
|
get signal() {
|
|
431
307
|
return this.abortController.signal;
|
|
432
308
|
}
|
|
433
309
|
/**
|
|
434
|
-
* Returns
|
|
310
|
+
* Returns authorization + versioning headers for all coordinator requests.
|
|
435
311
|
*/
|
|
436
|
-
|
|
312
|
+
getHeaders() {
|
|
437
313
|
return {
|
|
438
|
-
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)
|
|
439
317
|
};
|
|
440
318
|
}
|
|
441
319
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
320
|
+
* Checks an HTTP response for version mismatch errors (426, 501).
|
|
321
|
+
* Logs a clear message and throws with a descriptive error code.
|
|
444
322
|
*/
|
|
445
|
-
|
|
323
|
+
checkVersionMismatch(response) {
|
|
446
324
|
return __async(this, null, function* () {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
{
|
|
451
|
-
method: "GET",
|
|
452
|
-
headers: this.getAuthHeaders(),
|
|
453
|
-
signal: this.signal
|
|
454
|
-
}
|
|
455
|
-
);
|
|
456
|
-
if (!response.ok) {
|
|
457
|
-
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}`);
|
|
458
329
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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 });
|
|
467
353
|
});
|
|
468
354
|
}
|
|
469
355
|
/**
|
|
470
356
|
* Creates a new session with the coordinator.
|
|
471
|
-
*
|
|
472
|
-
*
|
|
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.
|
|
473
362
|
*/
|
|
474
|
-
createSession(
|
|
363
|
+
createSession(extraArgs) {
|
|
475
364
|
return __async(this, null, function* () {
|
|
476
365
|
console.debug("[CoordinatorClient] Creating session...");
|
|
477
|
-
const requestBody = {
|
|
366
|
+
const requestBody = __spreadValues({
|
|
478
367
|
model: { name: this.model },
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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 } : {});
|
|
482
376
|
const response = yield fetch(`${this.baseUrl}/sessions`, {
|
|
483
377
|
method: "POST",
|
|
484
|
-
headers: __spreadProps(__spreadValues({}, this.
|
|
378
|
+
headers: __spreadProps(__spreadValues({}, this.getHeaders()), {
|
|
485
379
|
"Content-Type": "application/json"
|
|
486
380
|
}),
|
|
487
381
|
body: JSON.stringify(requestBody),
|
|
488
382
|
signal: this.signal
|
|
489
383
|
});
|
|
384
|
+
yield this.checkVersionMismatch(response);
|
|
490
385
|
if (!response.ok) {
|
|
491
386
|
const errorText = yield response.text();
|
|
492
387
|
throw new Error(
|
|
@@ -494,337 +389,462 @@ var CoordinatorClient = class {
|
|
|
494
389
|
);
|
|
495
390
|
}
|
|
496
391
|
const data = yield response.json();
|
|
497
|
-
|
|
392
|
+
const parsed = InitialSessionResponseSchema.parse(data);
|
|
393
|
+
this.currentSessionId = parsed.session_id;
|
|
498
394
|
console.debug(
|
|
499
|
-
"[CoordinatorClient] Session created
|
|
500
|
-
this.currentSessionId
|
|
395
|
+
"[CoordinatorClient] Session created:",
|
|
396
|
+
this.currentSessionId,
|
|
397
|
+
"state:",
|
|
398
|
+
parsed.state
|
|
501
399
|
);
|
|
502
|
-
return
|
|
400
|
+
return parsed;
|
|
503
401
|
});
|
|
504
402
|
}
|
|
505
403
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
404
|
+
* Polls GET /sessions/{id} until the Runtime has accepted the session
|
|
405
|
+
* and populated capabilities and selected_transport.
|
|
508
406
|
*/
|
|
509
|
-
|
|
407
|
+
pollSessionReady(opts) {
|
|
510
408
|
return __async(this, null, function* () {
|
|
409
|
+
var _a;
|
|
511
410
|
if (!this.currentSessionId) {
|
|
512
411
|
throw new Error("No active session. Call createSession() first.");
|
|
513
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;
|
|
514
416
|
console.debug(
|
|
515
|
-
"[CoordinatorClient]
|
|
516
|
-
this.currentSessionId
|
|
417
|
+
"[CoordinatorClient] Polling session until capabilities are available..."
|
|
517
418
|
);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
method: "GET",
|
|
522
|
-
headers: this.getAuthHeaders(),
|
|
523
|
-
signal: this.signal
|
|
419
|
+
while (true) {
|
|
420
|
+
if (this.signal.aborted) {
|
|
421
|
+
throw new AbortError("Session polling aborted");
|
|
524
422
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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;
|
|
554
461
|
}
|
|
555
|
-
);
|
|
556
|
-
if (response.ok) {
|
|
557
|
-
this.currentSessionId = void 0;
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (response.status === 404) {
|
|
561
462
|
console.debug(
|
|
562
|
-
|
|
563
|
-
|
|
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
|
|
564
469
|
);
|
|
565
|
-
this.currentSessionId = void 0;
|
|
566
|
-
return;
|
|
567
470
|
}
|
|
568
|
-
const errorText = yield response.text();
|
|
569
|
-
throw new Error(
|
|
570
|
-
`Failed to terminate session: ${response.status} ${errorText}`
|
|
571
|
-
);
|
|
572
471
|
});
|
|
573
472
|
}
|
|
574
473
|
/**
|
|
575
|
-
*
|
|
474
|
+
* Gets full session details from the coordinator.
|
|
475
|
+
* Returns the same shape as the creation response but with updated state.
|
|
576
476
|
*/
|
|
577
|
-
|
|
578
|
-
return this
|
|
477
|
+
getSession() {
|
|
478
|
+
return __async(this, null, function* () {
|
|
479
|
+
if (!this.currentSessionId) {
|
|
480
|
+
throw new Error("No active session. Call createSession() first.");
|
|
481
|
+
}
|
|
482
|
+
const response = yield fetch(
|
|
483
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
484
|
+
{
|
|
485
|
+
method: "GET",
|
|
486
|
+
headers: this.getHeaders(),
|
|
487
|
+
signal: this.signal
|
|
488
|
+
}
|
|
489
|
+
);
|
|
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}`);
|
|
494
|
+
}
|
|
495
|
+
const data = yield response.json();
|
|
496
|
+
return CreateSessionResponseSchema.parse(data);
|
|
497
|
+
});
|
|
579
498
|
}
|
|
580
499
|
/**
|
|
581
|
-
*
|
|
582
|
-
* @param sessionId - The session ID to connect to
|
|
583
|
-
* @param sdpOffer - The SDP offer from the local WebRTC peer connection
|
|
584
|
-
* @returns The SDP answer if ready (200), or null if pending (202)
|
|
500
|
+
* Gets lightweight session status (session_id, cluster, status).
|
|
585
501
|
*/
|
|
586
|
-
|
|
502
|
+
getSessionInfo() {
|
|
587
503
|
return __async(this, null, function* () {
|
|
588
|
-
|
|
589
|
-
"
|
|
590
|
-
|
|
591
|
-
);
|
|
592
|
-
const requestBody = {
|
|
593
|
-
sdp_offer: sdpOffer,
|
|
594
|
-
extra_args: {}
|
|
595
|
-
};
|
|
504
|
+
if (!this.currentSessionId) {
|
|
505
|
+
throw new Error("No active session. Call createSession() first.");
|
|
506
|
+
}
|
|
596
507
|
const response = yield fetch(
|
|
597
|
-
`${this.baseUrl}/sessions/${
|
|
508
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}/info`,
|
|
598
509
|
{
|
|
599
|
-
method: "
|
|
600
|
-
headers:
|
|
601
|
-
"Content-Type": "application/json"
|
|
602
|
-
}),
|
|
603
|
-
body: JSON.stringify(requestBody),
|
|
510
|
+
method: "GET",
|
|
511
|
+
headers: this.getHeaders(),
|
|
604
512
|
signal: this.signal
|
|
605
513
|
}
|
|
606
514
|
);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (response.status === 202) {
|
|
613
|
-
console.debug(
|
|
614
|
-
"[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}`
|
|
615
520
|
);
|
|
616
|
-
return null;
|
|
617
521
|
}
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
`Failed to send SDP offer: ${response.status} ${errorText}`
|
|
621
|
-
);
|
|
522
|
+
const data = yield response.json();
|
|
523
|
+
return SessionInfoResponseSchema.parse(data);
|
|
622
524
|
});
|
|
623
525
|
}
|
|
624
526
|
/**
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* @param sessionId - The session ID to poll for
|
|
628
|
-
* @param maxAttempts - Optional maximum number of polling attempts before giving up
|
|
629
|
-
* @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.
|
|
630
529
|
*/
|
|
631
|
-
|
|
632
|
-
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
|
+
}
|
|
633
535
|
console.debug(
|
|
634
|
-
"[CoordinatorClient]
|
|
635
|
-
|
|
536
|
+
"[CoordinatorClient] Restarting session:",
|
|
537
|
+
this.currentSessionId
|
|
636
538
|
);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (attempt >= maxAttempts) {
|
|
644
|
-
throw new Error(
|
|
645
|
-
`SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
attempt++;
|
|
649
|
-
console.debug(
|
|
650
|
-
`[CoordinatorClient] SDP poll attempt ${attempt}/${maxAttempts} for session ${sessionId}`
|
|
651
|
-
);
|
|
652
|
-
const response = yield fetch(
|
|
653
|
-
`${this.baseUrl}/sessions/${sessionId}/sdp_params`,
|
|
654
|
-
{
|
|
655
|
-
method: "GET",
|
|
656
|
-
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
657
|
-
"Content-Type": "application/json"
|
|
658
|
-
}),
|
|
659
|
-
signal: this.signal
|
|
660
|
-
}
|
|
661
|
-
);
|
|
662
|
-
if (response.status === 200) {
|
|
663
|
-
const answerData = yield response.json();
|
|
664
|
-
console.debug("[CoordinatorClient] Received SDP answer via polling");
|
|
665
|
-
return answerData.sdp_answer;
|
|
666
|
-
}
|
|
667
|
-
if (response.status === 202) {
|
|
668
|
-
console.warn(
|
|
669
|
-
`[CoordinatorClient] SDP answer pending (202), retrying in ${backoffMs}ms...`
|
|
670
|
-
);
|
|
671
|
-
yield this.sleep(backoffMs);
|
|
672
|
-
backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
673
|
-
continue;
|
|
539
|
+
const response = yield fetch(
|
|
540
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
541
|
+
{
|
|
542
|
+
method: "PUT",
|
|
543
|
+
headers: this.getHeaders(),
|
|
544
|
+
signal: this.signal
|
|
674
545
|
}
|
|
546
|
+
);
|
|
547
|
+
yield this.checkVersionMismatch(response);
|
|
548
|
+
if (!response.ok) {
|
|
675
549
|
const errorText = yield response.text();
|
|
676
550
|
throw new Error(
|
|
677
|
-
`Failed to
|
|
551
|
+
`Failed to restart session: ${response.status} ${errorText}`
|
|
678
552
|
);
|
|
679
553
|
}
|
|
680
554
|
});
|
|
681
555
|
}
|
|
682
556
|
/**
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
* @param sessionId - The session ID to connect to
|
|
687
|
-
* @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
|
|
688
|
-
* @param maxAttempts - Optional maximum number of polling attempts before giving up
|
|
689
|
-
* @returns The SDP answer from the server
|
|
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
|
|
690
560
|
*/
|
|
691
|
-
|
|
561
|
+
terminateSession(reason) {
|
|
692
562
|
return __async(this, null, function* () {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
|
|
696
|
-
if (answer !== null) {
|
|
697
|
-
return answer;
|
|
698
|
-
}
|
|
563
|
+
if (!this.currentSessionId) {
|
|
564
|
+
return;
|
|
699
565
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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;
|
|
712
582
|
return;
|
|
713
583
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
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
|
+
);
|
|
723
596
|
});
|
|
724
597
|
}
|
|
598
|
+
getSessionId() {
|
|
599
|
+
return this.currentSessionId;
|
|
600
|
+
}
|
|
725
601
|
};
|
|
726
602
|
|
|
727
603
|
// src/core/LocalCoordinatorClient.ts
|
|
728
604
|
var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
729
|
-
constructor(baseUrl) {
|
|
605
|
+
constructor(baseUrl, model) {
|
|
730
606
|
super({
|
|
731
607
|
baseUrl,
|
|
732
608
|
jwtToken: "local",
|
|
733
|
-
model
|
|
609
|
+
model
|
|
734
610
|
});
|
|
735
|
-
|
|
611
|
+
}
|
|
612
|
+
getHeaders() {
|
|
613
|
+
return {
|
|
614
|
+
[API_VERSION_HEADER]: String(REACTOR_API_VERSION),
|
|
615
|
+
[API_ACCEPT_VERSION_HEADER]: String(REACTOR_API_VERSION)
|
|
616
|
+
};
|
|
736
617
|
}
|
|
737
618
|
/**
|
|
738
|
-
*
|
|
739
|
-
*
|
|
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.
|
|
740
623
|
*/
|
|
741
|
-
|
|
624
|
+
createSession(extraArgs) {
|
|
742
625
|
return __async(this, null, function* () {
|
|
743
|
-
console.debug("[LocalCoordinatorClient]
|
|
744
|
-
const response = yield fetch(`${this.
|
|
745
|
-
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 } : {})),
|
|
746
633
|
signal: this.signal
|
|
747
634
|
});
|
|
748
635
|
if (!response.ok) {
|
|
749
|
-
|
|
636
|
+
const errorText = yield response.text();
|
|
637
|
+
throw new Error(
|
|
638
|
+
`Failed to start session: ${response.status} ${errorText}`
|
|
639
|
+
);
|
|
750
640
|
}
|
|
751
641
|
const data = yield response.json();
|
|
752
|
-
|
|
753
|
-
|
|
642
|
+
this.cachedSessionResponse = CreateSessionResponseSchema.parse(data);
|
|
643
|
+
this.currentSessionId = this.cachedSessionResponse.session_id;
|
|
754
644
|
console.debug(
|
|
755
|
-
"[LocalCoordinatorClient]
|
|
756
|
-
|
|
645
|
+
"[LocalCoordinatorClient] Session started:",
|
|
646
|
+
this.currentSessionId,
|
|
647
|
+
"transport:",
|
|
648
|
+
this.cachedSessionResponse.selected_transport.protocol,
|
|
649
|
+
"tracks:",
|
|
650
|
+
this.cachedSessionResponse.capabilities.tracks.length
|
|
757
651
|
);
|
|
758
|
-
return
|
|
652
|
+
return InitialSessionResponseSchema.parse(data);
|
|
759
653
|
});
|
|
760
654
|
}
|
|
761
655
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
656
|
+
* Returns the cached full session response immediately.
|
|
657
|
+
* The local runtime already provided everything in start_session.
|
|
764
658
|
*/
|
|
765
|
-
|
|
659
|
+
pollSessionReady() {
|
|
766
660
|
return __async(this, null, function* () {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
signal: this.signal
|
|
772
|
-
});
|
|
773
|
-
if (!response.ok) {
|
|
774
|
-
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
|
+
);
|
|
775
665
|
}
|
|
776
|
-
|
|
777
|
-
return "local";
|
|
666
|
+
return this.cachedSessionResponse;
|
|
778
667
|
});
|
|
779
668
|
}
|
|
780
669
|
/**
|
|
781
|
-
*
|
|
782
|
-
* @param sessionId - The session ID (ignored for local)
|
|
783
|
-
* @param sdpMessage - The SDP offer from the local WebRTC peer connection
|
|
784
|
-
* @returns The SDP answer from the server
|
|
670
|
+
* Stops the session on the local runtime.
|
|
785
671
|
*/
|
|
786
|
-
|
|
672
|
+
terminateSession() {
|
|
787
673
|
return __async(this, null, function* () {
|
|
788
|
-
this.
|
|
789
|
-
|
|
790
|
-
const sdpBody = {
|
|
791
|
-
sdp: this.sdpOffer,
|
|
792
|
-
type: "offer"
|
|
793
|
-
};
|
|
794
|
-
const response = yield fetch(`${this.localBaseUrl}/sdp_params`, {
|
|
795
|
-
method: "POST",
|
|
796
|
-
headers: {
|
|
797
|
-
"Content-Type": "application/json"
|
|
798
|
-
},
|
|
799
|
-
body: JSON.stringify(sdpBody),
|
|
800
|
-
signal: this.signal
|
|
801
|
-
});
|
|
802
|
-
if (!response.ok) {
|
|
803
|
-
if (response.status === 409) {
|
|
804
|
-
throw new ConflictError("Connection superseded by newer request");
|
|
805
|
-
}
|
|
806
|
-
throw new Error("Failed to get SDP answer from local coordinator.");
|
|
674
|
+
if (!this.currentSessionId) {
|
|
675
|
+
return;
|
|
807
676
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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;
|
|
811
692
|
});
|
|
812
693
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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"
|
|
820
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}`);
|
|
821
767
|
}
|
|
822
|
-
|
|
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
|
+
}
|
|
823
839
|
|
|
824
|
-
// src/core/
|
|
840
|
+
// src/core/WebRTCTransportClient.ts
|
|
825
841
|
var PING_INTERVAL_MS = 5e3;
|
|
826
842
|
var STATS_INTERVAL_MS = 2e3;
|
|
827
|
-
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 {
|
|
828
848
|
constructor(config) {
|
|
829
849
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
830
850
|
this.status = "disconnected";
|
|
@@ -832,11 +852,17 @@ var GPUMachineClient = class {
|
|
|
832
852
|
this.publishedTracks = /* @__PURE__ */ new Map();
|
|
833
853
|
this.peerConnected = false;
|
|
834
854
|
this.dataChannelOpen = false;
|
|
835
|
-
|
|
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();
|
|
836
862
|
}
|
|
837
|
-
//
|
|
838
|
-
// Event Emitter
|
|
839
|
-
//
|
|
863
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
864
|
+
// Event Emitter
|
|
865
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
840
866
|
on(event, handler) {
|
|
841
867
|
if (!this.eventListeners.has(event)) {
|
|
842
868
|
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
@@ -851,127 +877,241 @@ var GPUMachineClient = class {
|
|
|
851
877
|
var _a;
|
|
852
878
|
(_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
|
|
853
879
|
}
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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) {
|
|
870
1035
|
return __async(this, null, function* () {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
this.
|
|
876
|
-
|
|
877
|
-
this.config.dataChannelLabel
|
|
878
|
-
);
|
|
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);
|
|
879
1042
|
this.setupDataChannelHandlers();
|
|
880
1043
|
this.transceiverMap.clear();
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
|
885
1053
|
});
|
|
886
|
-
entry.transceiver = transceiver;
|
|
887
|
-
this.transceiverMap.set(entry.name, entry);
|
|
888
1054
|
console.debug(
|
|
889
|
-
`[
|
|
1055
|
+
`[WebRTCTransport] Transceiver added: "${track.name}" (${track.kind}, ${track.direction})`
|
|
890
1056
|
);
|
|
891
1057
|
}
|
|
892
|
-
const
|
|
893
|
-
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(
|
|
894
1064
|
this.peerConnection,
|
|
895
|
-
|
|
1065
|
+
answerResponse.sdp_answer
|
|
896
1066
|
);
|
|
897
|
-
|
|
898
|
-
this.midMapping = buildMidMapping(entries);
|
|
899
|
-
} else {
|
|
900
|
-
this.midMapping = void 0;
|
|
901
|
-
}
|
|
902
|
-
console.debug(
|
|
903
|
-
"[GPUMachineClient] Created SDP offer with MIDs:",
|
|
904
|
-
trackNames,
|
|
905
|
-
needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
|
|
906
|
-
);
|
|
907
|
-
return sdp;
|
|
1067
|
+
console.debug("[WebRTCTransport] Remote description set");
|
|
908
1068
|
});
|
|
909
1069
|
}
|
|
910
|
-
|
|
911
|
-
* Builds an ordered list of transceiver entries from the receive/send arrays.
|
|
912
|
-
*
|
|
913
|
-
* Each track produces exactly one transceiver — `recvonly` for receive,
|
|
914
|
-
* `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
|
|
915
|
-
* supported; the same track name in both arrays is an error.
|
|
916
|
-
*/
|
|
917
|
-
buildTransceiverEntries(tracks) {
|
|
918
|
-
const map = /* @__PURE__ */ new Map();
|
|
919
|
-
for (const t of tracks.receive) {
|
|
920
|
-
if (map.has(t.name)) {
|
|
921
|
-
throw new Error(
|
|
922
|
-
`Duplicate receive track name "${t.name}". Track names must be unique.`
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
|
|
926
|
-
}
|
|
927
|
-
for (const t of tracks.send) {
|
|
928
|
-
if (map.has(t.name)) {
|
|
929
|
-
throw new Error(
|
|
930
|
-
`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").`
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
|
|
934
|
-
}
|
|
935
|
-
return Array.from(map.values());
|
|
936
|
-
}
|
|
937
|
-
/**
|
|
938
|
-
* Connects to the GPU machine using the provided SDP answer.
|
|
939
|
-
* createOffer() must be called first.
|
|
940
|
-
* @param sdpAnswer The SDP answer from the GPU machine
|
|
941
|
-
*/
|
|
942
|
-
connect(sdpAnswer) {
|
|
1070
|
+
reconnect(tracks) {
|
|
943
1071
|
return __async(this, null, function* () {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1072
|
+
this.setStatus("connecting");
|
|
1073
|
+
this.stopPing();
|
|
1074
|
+
this.stopStatsPolling();
|
|
1075
|
+
if (this.dataChannel) {
|
|
1076
|
+
this.dataChannel.close();
|
|
1077
|
+
this.dataChannel = void 0;
|
|
948
1078
|
}
|
|
949
|
-
if (this.peerConnection
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
);
|
|
1079
|
+
if (this.peerConnection) {
|
|
1080
|
+
closePeerConnection(this.peerConnection);
|
|
1081
|
+
this.peerConnection = void 0;
|
|
953
1082
|
}
|
|
954
|
-
this.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
this.
|
|
968
|
-
|
|
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
|
+
});
|
|
969
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)");
|
|
970
1113
|
});
|
|
971
1114
|
}
|
|
972
|
-
/**
|
|
973
|
-
* Disconnects from the GPU machine and cleans up resources.
|
|
974
|
-
*/
|
|
975
1115
|
disconnect() {
|
|
976
1116
|
return __async(this, null, function* () {
|
|
977
1117
|
this.stopPing();
|
|
@@ -988,99 +1128,100 @@ var GPUMachineClient = class {
|
|
|
988
1128
|
this.peerConnection = void 0;
|
|
989
1129
|
}
|
|
990
1130
|
this.transceiverMap.clear();
|
|
991
|
-
this.midMapping = void 0;
|
|
992
1131
|
this.peerConnected = false;
|
|
993
1132
|
this.dataChannelOpen = false;
|
|
1133
|
+
this.resetTransportTimings();
|
|
994
1134
|
this.setStatus("disconnected");
|
|
995
|
-
console.debug("[
|
|
1135
|
+
console.debug("[WebRTCTransport] Disconnected");
|
|
996
1136
|
});
|
|
997
1137
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1138
|
+
abort() {
|
|
1139
|
+
this.abortController.abort();
|
|
1140
|
+
this.abortController = new AbortController();
|
|
1141
|
+
}
|
|
1001
1142
|
getStatus() {
|
|
1002
1143
|
return this.status;
|
|
1003
1144
|
}
|
|
1145
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1146
|
+
// Track Mapping
|
|
1147
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1004
1148
|
/**
|
|
1005
|
-
*
|
|
1149
|
+
* Builds the track_mapping array from capabilities + transceiver MIDs.
|
|
1150
|
+
* Must be called after createOffer + setLocalDescription so that
|
|
1151
|
+
* transceiver.mid is assigned.
|
|
1006
1152
|
*/
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
+
});
|
|
1014
1170
|
}
|
|
1015
|
-
//
|
|
1171
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1016
1172
|
// Messaging
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
* @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
|
|
1023
|
-
*/
|
|
1173
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1174
|
+
get maxMessageBytes() {
|
|
1175
|
+
var _a, _b, _c;
|
|
1176
|
+
return (_c = (_b = (_a = this.peerConnection) == null ? void 0 : _a.sctp) == null ? void 0 : _b.maxMessageSize) != null ? _c : void 0;
|
|
1177
|
+
}
|
|
1024
1178
|
sendCommand(command, data, scope = "application") {
|
|
1025
1179
|
if (!this.dataChannel) {
|
|
1026
|
-
throw new Error("[
|
|
1180
|
+
throw new Error("[WebRTCTransport] Data channel not available");
|
|
1027
1181
|
}
|
|
1028
1182
|
try {
|
|
1029
|
-
sendMessage(
|
|
1183
|
+
sendMessage(
|
|
1184
|
+
this.dataChannel,
|
|
1185
|
+
command,
|
|
1186
|
+
data,
|
|
1187
|
+
scope,
|
|
1188
|
+
this.maxMessageBytes
|
|
1189
|
+
);
|
|
1030
1190
|
} catch (error) {
|
|
1031
|
-
console.warn("[
|
|
1191
|
+
console.warn("[WebRTCTransport] Failed to send message:", error);
|
|
1032
1192
|
}
|
|
1033
1193
|
}
|
|
1034
|
-
//
|
|
1194
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1035
1195
|
// Track Publishing
|
|
1036
|
-
//
|
|
1037
|
-
/**
|
|
1038
|
-
* Publishes a MediaStreamTrack to the named send track.
|
|
1039
|
-
*
|
|
1040
|
-
* @param name The declared track name (must exist in transceiverMap with a sendable direction).
|
|
1041
|
-
* @param track The MediaStreamTrack to publish.
|
|
1042
|
-
*/
|
|
1196
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1043
1197
|
publishTrack(name, track) {
|
|
1044
1198
|
return __async(this, null, function* () {
|
|
1045
1199
|
if (!this.peerConnection) {
|
|
1046
1200
|
throw new Error(
|
|
1047
|
-
`[
|
|
1201
|
+
`[WebRTCTransport] Cannot publish track "${name}" - not initialized`
|
|
1048
1202
|
);
|
|
1049
1203
|
}
|
|
1050
1204
|
if (this.status !== "connected") {
|
|
1051
1205
|
throw new Error(
|
|
1052
|
-
`[
|
|
1206
|
+
`[WebRTCTransport] Cannot publish track "${name}" - not connected`
|
|
1053
1207
|
);
|
|
1054
1208
|
}
|
|
1055
1209
|
const entry = this.transceiverMap.get(name);
|
|
1056
1210
|
if (!entry || !entry.transceiver) {
|
|
1057
1211
|
throw new Error(
|
|
1058
|
-
`[
|
|
1212
|
+
`[WebRTCTransport] Cannot publish track "${name}" - no transceiver (was it declared in capabilities?)`
|
|
1059
1213
|
);
|
|
1060
1214
|
}
|
|
1061
1215
|
if (entry.direction === "recvonly") {
|
|
1062
1216
|
throw new Error(
|
|
1063
|
-
`[
|
|
1064
|
-
);
|
|
1065
|
-
}
|
|
1066
|
-
try {
|
|
1067
|
-
yield entry.transceiver.sender.replaceTrack(track);
|
|
1068
|
-
this.publishedTracks.set(name, track);
|
|
1069
|
-
console.debug(
|
|
1070
|
-
`[GPUMachineClient] Track "${name}" published successfully`
|
|
1071
|
-
);
|
|
1072
|
-
} catch (error) {
|
|
1073
|
-
console.error(
|
|
1074
|
-
`[GPUMachineClient] Failed to publish track "${name}":`,
|
|
1075
|
-
error
|
|
1217
|
+
`[WebRTCTransport] Cannot publish track "${name}" - transceiver is recvonly`
|
|
1076
1218
|
);
|
|
1077
|
-
throw error;
|
|
1078
1219
|
}
|
|
1220
|
+
yield entry.transceiver.sender.replaceTrack(track);
|
|
1221
|
+
this.publishedTracks.set(name, track);
|
|
1222
|
+
console.debug(`[WebRTCTransport] Track "${name}" published successfully`);
|
|
1079
1223
|
});
|
|
1080
1224
|
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Unpublishes the track with the given name.
|
|
1083
|
-
*/
|
|
1084
1225
|
unpublishTrack(name) {
|
|
1085
1226
|
return __async(this, null, function* () {
|
|
1086
1227
|
const entry = this.transceiverMap.get(name);
|
|
@@ -1088,11 +1229,11 @@ var GPUMachineClient = class {
|
|
|
1088
1229
|
try {
|
|
1089
1230
|
yield entry.transceiver.sender.replaceTrack(null);
|
|
1090
1231
|
console.debug(
|
|
1091
|
-
`[
|
|
1232
|
+
`[WebRTCTransport] Track "${name}" unpublished successfully`
|
|
1092
1233
|
);
|
|
1093
1234
|
} catch (error) {
|
|
1094
1235
|
console.error(
|
|
1095
|
-
`[
|
|
1236
|
+
`[WebRTCTransport] Failed to unpublish track "${name}":`,
|
|
1096
1237
|
error
|
|
1097
1238
|
);
|
|
1098
1239
|
throw error;
|
|
@@ -1101,58 +1242,31 @@ var GPUMachineClient = class {
|
|
|
1101
1242
|
}
|
|
1102
1243
|
});
|
|
1103
1244
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
return this.
|
|
1109
|
-
}
|
|
1110
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1111
|
-
// Getters
|
|
1112
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1113
|
-
/**
|
|
1114
|
-
* Returns the remote media stream from the GPU machine.
|
|
1115
|
-
*/
|
|
1116
|
-
getRemoteStream() {
|
|
1117
|
-
if (!this.peerConnection) return void 0;
|
|
1118
|
-
const receivers = this.peerConnection.getReceivers();
|
|
1119
|
-
const tracks = receivers.map((r) => r.track).filter((t) => t !== null);
|
|
1120
|
-
if (tracks.length === 0) return void 0;
|
|
1121
|
-
return new MediaStream(tracks);
|
|
1122
|
-
}
|
|
1123
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1124
|
-
// Ping (Client Liveness)
|
|
1125
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1126
|
-
/**
|
|
1127
|
-
* Starts sending periodic "ping" messages on the runtime channel so the
|
|
1128
|
-
* server can detect stale connections quickly.
|
|
1129
|
-
*/
|
|
1130
|
-
startPing() {
|
|
1131
|
-
this.stopPing();
|
|
1132
|
-
this.pingInterval = setInterval(() => {
|
|
1133
|
-
var _a;
|
|
1134
|
-
if (((_a = this.dataChannel) == null ? void 0 : _a.readyState) === "open") {
|
|
1135
|
-
try {
|
|
1136
|
-
sendMessage(this.dataChannel, "ping", {}, "runtime");
|
|
1137
|
-
} catch (e) {
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}, PING_INTERVAL_MS);
|
|
1245
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1246
|
+
// Stats
|
|
1247
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1248
|
+
getStats() {
|
|
1249
|
+
return this.stats;
|
|
1141
1250
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (this.pingInterval !== void 0) {
|
|
1147
|
-
clearInterval(this.pingInterval);
|
|
1148
|
-
this.pingInterval = void 0;
|
|
1251
|
+
getTransportTimings() {
|
|
1252
|
+
var _a, _b;
|
|
1253
|
+
if (this.iceNegotiationMs == null || this.dataChannelMs == null) {
|
|
1254
|
+
return void 0;
|
|
1149
1255
|
}
|
|
1256
|
+
return {
|
|
1257
|
+
protocol: "webrtc",
|
|
1258
|
+
sdpPollingMs: (_a = this.sdpPollingMs) != null ? _a : 0,
|
|
1259
|
+
sdpPollingAttempts: (_b = this.sdpPollingAttempts) != null ? _b : 0,
|
|
1260
|
+
iceNegotiationMs: this.iceNegotiationMs,
|
|
1261
|
+
dataChannelMs: this.dataChannelMs
|
|
1262
|
+
};
|
|
1150
1263
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1264
|
+
resetTransportTimings() {
|
|
1265
|
+
this.iceStartTime = void 0;
|
|
1266
|
+
this.iceNegotiationMs = void 0;
|
|
1267
|
+
this.dataChannelMs = void 0;
|
|
1268
|
+
this.sdpPollingMs = void 0;
|
|
1269
|
+
this.sdpPollingAttempts = void 0;
|
|
1156
1270
|
}
|
|
1157
1271
|
startStatsPolling() {
|
|
1158
1272
|
this.stopStatsPolling();
|
|
@@ -1173,9 +1287,30 @@ var GPUMachineClient = class {
|
|
|
1173
1287
|
}
|
|
1174
1288
|
this.stats = void 0;
|
|
1175
1289
|
}
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
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
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1179
1314
|
checkFullyConnected() {
|
|
1180
1315
|
if (this.peerConnected && this.dataChannelOpen) {
|
|
1181
1316
|
this.setStatus("connected");
|
|
@@ -1193,10 +1328,13 @@ var GPUMachineClient = class {
|
|
|
1193
1328
|
this.peerConnection.onconnectionstatechange = () => {
|
|
1194
1329
|
var _a;
|
|
1195
1330
|
const state = (_a = this.peerConnection) == null ? void 0 : _a.connectionState;
|
|
1196
|
-
console.debug("[
|
|
1331
|
+
console.debug("[WebRTCTransport] Connection state:", state);
|
|
1197
1332
|
if (state) {
|
|
1198
1333
|
switch (state) {
|
|
1199
1334
|
case "connected":
|
|
1335
|
+
if (this.iceStartTime != null && this.iceNegotiationMs == null) {
|
|
1336
|
+
this.iceNegotiationMs = performance.now() - this.iceStartTime;
|
|
1337
|
+
}
|
|
1200
1338
|
this.peerConnected = true;
|
|
1201
1339
|
this.checkFullyConnected();
|
|
1202
1340
|
break;
|
|
@@ -1223,21 +1361,21 @@ var GPUMachineClient = class {
|
|
|
1223
1361
|
}
|
|
1224
1362
|
trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
|
|
1225
1363
|
console.debug(
|
|
1226
|
-
`[
|
|
1364
|
+
`[WebRTCTransport] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
|
|
1227
1365
|
);
|
|
1228
1366
|
const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
|
|
1229
1367
|
this.emit("trackReceived", trackName, event.track, stream);
|
|
1230
1368
|
};
|
|
1231
1369
|
this.peerConnection.onicecandidate = (event) => {
|
|
1232
1370
|
if (event.candidate) {
|
|
1233
|
-
console.debug("[
|
|
1371
|
+
console.debug("[WebRTCTransport] ICE candidate:", event.candidate);
|
|
1234
1372
|
}
|
|
1235
1373
|
};
|
|
1236
1374
|
this.peerConnection.onicecandidateerror = (event) => {
|
|
1237
|
-
console.warn("[
|
|
1375
|
+
console.warn("[WebRTCTransport] ICE candidate error:", event);
|
|
1238
1376
|
};
|
|
1239
1377
|
this.peerConnection.ondatachannel = (event) => {
|
|
1240
|
-
console.debug("[
|
|
1378
|
+
console.debug("[WebRTCTransport] Data channel received from remote");
|
|
1241
1379
|
this.dataChannel = event.channel;
|
|
1242
1380
|
this.setupDataChannelHandlers();
|
|
1243
1381
|
};
|
|
@@ -1245,22 +1383,25 @@ var GPUMachineClient = class {
|
|
|
1245
1383
|
setupDataChannelHandlers() {
|
|
1246
1384
|
if (!this.dataChannel) return;
|
|
1247
1385
|
this.dataChannel.onopen = () => {
|
|
1248
|
-
console.debug("[
|
|
1386
|
+
console.debug("[WebRTCTransport] Data channel open");
|
|
1387
|
+
if (this.iceStartTime != null && this.dataChannelMs == null) {
|
|
1388
|
+
this.dataChannelMs = performance.now() - this.iceStartTime;
|
|
1389
|
+
}
|
|
1249
1390
|
this.dataChannelOpen = true;
|
|
1250
1391
|
this.startPing();
|
|
1251
1392
|
this.checkFullyConnected();
|
|
1252
1393
|
};
|
|
1253
1394
|
this.dataChannel.onclose = () => {
|
|
1254
|
-
console.debug("[
|
|
1395
|
+
console.debug("[WebRTCTransport] Data channel closed");
|
|
1255
1396
|
this.dataChannelOpen = false;
|
|
1256
1397
|
this.stopPing();
|
|
1257
1398
|
};
|
|
1258
1399
|
this.dataChannel.onerror = (error) => {
|
|
1259
|
-
console.error("[
|
|
1400
|
+
console.error("[WebRTCTransport] Data channel error:", error);
|
|
1260
1401
|
};
|
|
1261
1402
|
this.dataChannel.onmessage = (event) => {
|
|
1262
1403
|
const rawData = parseMessage(event.data);
|
|
1263
|
-
console.debug("[
|
|
1404
|
+
console.debug("[WebRTCTransport] Received message:", rawData);
|
|
1264
1405
|
try {
|
|
1265
1406
|
if ((rawData == null ? void 0 : rawData.scope) === "application" && (rawData == null ? void 0 : rawData.data) !== void 0) {
|
|
1266
1407
|
this.emit("message", rawData.data, "application");
|
|
@@ -1268,13 +1409,13 @@ var GPUMachineClient = class {
|
|
|
1268
1409
|
this.emit("message", rawData.data, "runtime");
|
|
1269
1410
|
} else {
|
|
1270
1411
|
console.warn(
|
|
1271
|
-
"[
|
|
1412
|
+
"[WebRTCTransport] Received message without envelope, treating as application"
|
|
1272
1413
|
);
|
|
1273
1414
|
this.emit("message", rawData, "application");
|
|
1274
1415
|
}
|
|
1275
1416
|
} catch (error) {
|
|
1276
1417
|
console.error(
|
|
1277
|
-
"[
|
|
1418
|
+
"[WebRTCTransport] Failed to parse/validate message:",
|
|
1278
1419
|
error
|
|
1279
1420
|
);
|
|
1280
1421
|
}
|
|
@@ -1285,47 +1426,25 @@ var GPUMachineClient = class {
|
|
|
1285
1426
|
// src/core/Reactor.ts
|
|
1286
1427
|
var import_zod2 = require("zod");
|
|
1287
1428
|
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
1288
|
-
var
|
|
1289
|
-
var TrackConfigSchema = import_zod2.z.object({
|
|
1290
|
-
name: import_zod2.z.string(),
|
|
1291
|
-
kind: import_zod2.z.enum(["audio", "video"])
|
|
1292
|
-
});
|
|
1429
|
+
var DEFAULT_BASE_URL = "https://api.reactor.inc";
|
|
1293
1430
|
var OptionsSchema = import_zod2.z.object({
|
|
1294
|
-
|
|
1431
|
+
apiUrl: import_zod2.z.string().default(DEFAULT_BASE_URL),
|
|
1295
1432
|
modelName: import_zod2.z.string(),
|
|
1296
|
-
local: import_zod2.z.boolean().default(false)
|
|
1297
|
-
/**
|
|
1298
|
-
* Tracks the client **RECEIVES** from the model (model → client).
|
|
1299
|
-
* Each entry produces a `recvonly` transceiver.
|
|
1300
|
-
* Names must be unique across both `receive` and `send`.
|
|
1301
|
-
*
|
|
1302
|
-
* When omitted, defaults to a single video track named `"main_video"`.
|
|
1303
|
-
* Pass an explicit empty array to opt out of the default.
|
|
1304
|
-
*/
|
|
1305
|
-
receive: import_zod2.z.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
|
|
1306
|
-
/**
|
|
1307
|
-
* Tracks the client **SENDS** to the model (client → model).
|
|
1308
|
-
* Each entry produces a `sendonly` transceiver.
|
|
1309
|
-
* Names must be unique across both `receive` and `send`.
|
|
1310
|
-
*/
|
|
1311
|
-
send: import_zod2.z.array(TrackConfigSchema).default([])
|
|
1433
|
+
local: import_zod2.z.boolean().default(false)
|
|
1312
1434
|
});
|
|
1313
1435
|
var Reactor = class {
|
|
1314
1436
|
constructor(options) {
|
|
1315
1437
|
this.status = "disconnected";
|
|
1316
|
-
|
|
1438
|
+
this.tracks = [];
|
|
1317
1439
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1318
1440
|
const validatedOptions = OptionsSchema.parse(options);
|
|
1319
|
-
this.coordinatorUrl = validatedOptions.
|
|
1441
|
+
this.coordinatorUrl = validatedOptions.apiUrl;
|
|
1320
1442
|
this.model = validatedOptions.modelName;
|
|
1321
1443
|
this.local = validatedOptions.local;
|
|
1322
|
-
this.
|
|
1323
|
-
this.send = validatedOptions.send;
|
|
1324
|
-
if (this.local && options.coordinatorUrl === void 0) {
|
|
1444
|
+
if (this.local && options.apiUrl === void 0) {
|
|
1325
1445
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
1326
1446
|
}
|
|
1327
1447
|
}
|
|
1328
|
-
// Event Emitter API
|
|
1329
1448
|
on(event, handler) {
|
|
1330
1449
|
if (!this.eventListeners.has(event)) {
|
|
1331
1450
|
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
@@ -1342,10 +1461,6 @@ var Reactor = class {
|
|
|
1342
1461
|
}
|
|
1343
1462
|
/**
|
|
1344
1463
|
* Sends a command to the model via the data channel.
|
|
1345
|
-
*
|
|
1346
|
-
* @param command The command name.
|
|
1347
|
-
* @param data The command payload.
|
|
1348
|
-
* @param scope "application" (default) for model commands, "runtime" for platform messages.
|
|
1349
1464
|
*/
|
|
1350
1465
|
sendCommand(command, data, scope = "application") {
|
|
1351
1466
|
return __async(this, null, function* () {
|
|
@@ -1356,7 +1471,7 @@ var Reactor = class {
|
|
|
1356
1471
|
return;
|
|
1357
1472
|
}
|
|
1358
1473
|
try {
|
|
1359
|
-
(_a = this.
|
|
1474
|
+
(_a = this.transportClient) == null ? void 0 : _a.sendCommand(command, data, scope);
|
|
1360
1475
|
} catch (error) {
|
|
1361
1476
|
console.error("[Reactor] Failed to send message:", error);
|
|
1362
1477
|
this.createError(
|
|
@@ -1369,10 +1484,9 @@ var Reactor = class {
|
|
|
1369
1484
|
});
|
|
1370
1485
|
}
|
|
1371
1486
|
/**
|
|
1372
|
-
* Publishes a MediaStreamTrack to a named
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
* @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.
|
|
1376
1490
|
*/
|
|
1377
1491
|
publishTrack(name, track) {
|
|
1378
1492
|
return __async(this, null, function* () {
|
|
@@ -1384,7 +1498,7 @@ var Reactor = class {
|
|
|
1384
1498
|
return;
|
|
1385
1499
|
}
|
|
1386
1500
|
try {
|
|
1387
|
-
yield (_a = this.
|
|
1501
|
+
yield (_a = this.transportClient) == null ? void 0 : _a.publishTrack(name, track);
|
|
1388
1502
|
} catch (error) {
|
|
1389
1503
|
console.error(`[Reactor] Failed to publish track "${name}":`, error);
|
|
1390
1504
|
this.createError(
|
|
@@ -1396,16 +1510,11 @@ var Reactor = class {
|
|
|
1396
1510
|
}
|
|
1397
1511
|
});
|
|
1398
1512
|
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Unpublishes the track with the given name.
|
|
1401
|
-
*
|
|
1402
|
-
* @param name The declared send track name to unpublish.
|
|
1403
|
-
*/
|
|
1404
1513
|
unpublishTrack(name) {
|
|
1405
1514
|
return __async(this, null, function* () {
|
|
1406
1515
|
var _a;
|
|
1407
1516
|
try {
|
|
1408
|
-
yield (_a = this.
|
|
1517
|
+
yield (_a = this.transportClient) == null ? void 0 : _a.unpublishTrack(name);
|
|
1409
1518
|
} catch (error) {
|
|
1410
1519
|
console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
|
|
1411
1520
|
this.createError(
|
|
@@ -1418,8 +1527,7 @@ var Reactor = class {
|
|
|
1418
1527
|
});
|
|
1419
1528
|
}
|
|
1420
1529
|
/**
|
|
1421
|
-
*
|
|
1422
|
-
* @param options Optional connect options (e.g. maxAttempts for SDP polling)
|
|
1530
|
+
* Reconnects to an existing session with a fresh transport.
|
|
1423
1531
|
*/
|
|
1424
1532
|
reconnect(options) {
|
|
1425
1533
|
return __async(this, null, function* () {
|
|
@@ -1431,49 +1539,42 @@ var Reactor = class {
|
|
|
1431
1539
|
console.warn("[Reactor] Already connected, no need to reconnect.");
|
|
1432
1540
|
return;
|
|
1433
1541
|
}
|
|
1542
|
+
if (this.tracks.length === 0) {
|
|
1543
|
+
console.warn("[Reactor] No tracks available for reconnect.");
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1434
1546
|
this.setStatus("connecting");
|
|
1435
|
-
if (!this.machineClient) {
|
|
1436
|
-
const iceServers = yield this.coordinatorClient.getIceServers();
|
|
1437
|
-
this.machineClient = new GPUMachineClient({ iceServers });
|
|
1438
|
-
this.setupMachineClientHandlers();
|
|
1439
|
-
}
|
|
1440
|
-
const sdpOffer = yield this.machineClient.createOffer({
|
|
1441
|
-
send: this.send,
|
|
1442
|
-
receive: this.receive
|
|
1443
|
-
});
|
|
1444
1547
|
try {
|
|
1445
|
-
|
|
1446
|
-
this.
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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);
|
|
1451
1558
|
} catch (error) {
|
|
1452
1559
|
if (isAbortError(error)) return;
|
|
1453
|
-
let recoverable = false;
|
|
1454
|
-
if (error instanceof ConflictError) {
|
|
1455
|
-
recoverable = true;
|
|
1456
|
-
}
|
|
1457
1560
|
console.error("[Reactor] Failed to reconnect:", error);
|
|
1458
|
-
this.disconnect(
|
|
1561
|
+
this.disconnect(true);
|
|
1459
1562
|
this.createError(
|
|
1460
1563
|
"RECONNECTION_FAILED",
|
|
1461
1564
|
`Failed to reconnect: ${error}`,
|
|
1462
|
-
"
|
|
1565
|
+
"api",
|
|
1463
1566
|
true
|
|
1464
1567
|
);
|
|
1465
1568
|
}
|
|
1466
1569
|
});
|
|
1467
1570
|
}
|
|
1468
1571
|
/**
|
|
1469
|
-
* Connects to the coordinator
|
|
1470
|
-
*
|
|
1471
|
-
* If no authentication is provided and not in local mode, an error is thrown.
|
|
1472
|
-
* @param jwtToken Optional JWT token for authentication
|
|
1473
|
-
* @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.
|
|
1474
1574
|
*/
|
|
1475
1575
|
connect(jwtToken, options) {
|
|
1476
1576
|
return __async(this, null, function* () {
|
|
1577
|
+
var _a;
|
|
1477
1578
|
console.debug("[Reactor] Connecting, status:", this.status);
|
|
1478
1579
|
if (jwtToken == void 0 && !this.local) {
|
|
1479
1580
|
throw new Error("No authentication provided and not in local mode");
|
|
@@ -1482,38 +1583,64 @@ var Reactor = class {
|
|
|
1482
1583
|
throw new Error("Already connected or connecting");
|
|
1483
1584
|
}
|
|
1484
1585
|
this.setStatus("connecting");
|
|
1586
|
+
this.connectStartTime = performance.now();
|
|
1485
1587
|
try {
|
|
1486
|
-
|
|
1487
|
-
"[Reactor] Connecting to coordinator with authenticated URL"
|
|
1488
|
-
);
|
|
1489
|
-
this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
|
|
1588
|
+
this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl, this.model) : new CoordinatorClient({
|
|
1490
1589
|
baseUrl: this.coordinatorUrl,
|
|
1491
1590
|
jwtToken,
|
|
1492
|
-
// Safe: validated above
|
|
1493
1591
|
model: this.model
|
|
1494
1592
|
});
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1593
|
+
const tSession = performance.now();
|
|
1594
|
+
const initialResponse = yield this.coordinatorClient.createSession();
|
|
1595
|
+
const sessionCreationMs = performance.now() - tSession;
|
|
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
|
|
1508
1616
|
);
|
|
1509
|
-
|
|
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;
|
|
1632
|
+
this.connectionTimings = {
|
|
1633
|
+
sessionCreationMs: sessionCreationMs + sessionPollingMs,
|
|
1634
|
+
transportConnectingMs,
|
|
1635
|
+
totalMs: 0
|
|
1636
|
+
};
|
|
1510
1637
|
} catch (error) {
|
|
1511
1638
|
if (isAbortError(error)) return;
|
|
1512
1639
|
console.error("[Reactor] Connection failed:", error);
|
|
1513
1640
|
this.createError(
|
|
1514
1641
|
"CONNECTION_FAILED",
|
|
1515
1642
|
`Connection failed: ${error}`,
|
|
1516
|
-
"
|
|
1643
|
+
"api",
|
|
1517
1644
|
true
|
|
1518
1645
|
);
|
|
1519
1646
|
try {
|
|
@@ -1529,18 +1656,14 @@ var Reactor = class {
|
|
|
1529
1656
|
});
|
|
1530
1657
|
}
|
|
1531
1658
|
/**
|
|
1532
|
-
* Sets up event handlers for the
|
|
1533
|
-
*
|
|
1534
|
-
* Each handler captures the client reference at registration time and
|
|
1535
|
-
* ignores events if this.machineClient has since changed (e.g. after
|
|
1536
|
-
* disconnect + reconnect), preventing stale WebRTC teardown events from
|
|
1537
|
-
* 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.
|
|
1538
1661
|
*/
|
|
1539
|
-
|
|
1540
|
-
if (!this.
|
|
1541
|
-
const client = this.
|
|
1662
|
+
setupTransportHandlers() {
|
|
1663
|
+
if (!this.transportClient) return;
|
|
1664
|
+
const client = this.transportClient;
|
|
1542
1665
|
client.on("message", (message, scope) => {
|
|
1543
|
-
if (this.
|
|
1666
|
+
if (this.transportClient !== client) return;
|
|
1544
1667
|
if (scope === "application") {
|
|
1545
1668
|
this.emit("message", message);
|
|
1546
1669
|
} else if (scope === "runtime") {
|
|
@@ -1548,9 +1671,10 @@ var Reactor = class {
|
|
|
1548
1671
|
}
|
|
1549
1672
|
});
|
|
1550
1673
|
client.on("statusChanged", (status) => {
|
|
1551
|
-
if (this.
|
|
1674
|
+
if (this.transportClient !== client) return;
|
|
1552
1675
|
switch (status) {
|
|
1553
1676
|
case "connected":
|
|
1677
|
+
this.finalizeConnectionTimings();
|
|
1554
1678
|
this.setStatus("ready");
|
|
1555
1679
|
break;
|
|
1556
1680
|
case "disconnected":
|
|
@@ -1559,7 +1683,7 @@ var Reactor = class {
|
|
|
1559
1683
|
case "error":
|
|
1560
1684
|
this.createError(
|
|
1561
1685
|
"GPU_CONNECTION_ERROR",
|
|
1562
|
-
"
|
|
1686
|
+
"Transport connection failed",
|
|
1563
1687
|
"gpu",
|
|
1564
1688
|
true
|
|
1565
1689
|
);
|
|
@@ -1570,27 +1694,29 @@ var Reactor = class {
|
|
|
1570
1694
|
client.on(
|
|
1571
1695
|
"trackReceived",
|
|
1572
1696
|
(name, track, stream) => {
|
|
1573
|
-
if (this.
|
|
1697
|
+
if (this.transportClient !== client) return;
|
|
1574
1698
|
this.emit("trackReceived", name, track, stream);
|
|
1575
1699
|
}
|
|
1576
1700
|
);
|
|
1577
1701
|
client.on("statsUpdate", (stats) => {
|
|
1578
|
-
if (this.
|
|
1579
|
-
this.emit("statsUpdate", stats)
|
|
1702
|
+
if (this.transportClient !== client) return;
|
|
1703
|
+
this.emit("statsUpdate", __spreadProps(__spreadValues({}, stats), {
|
|
1704
|
+
connectionTimings: this.connectionTimings
|
|
1705
|
+
}));
|
|
1580
1706
|
});
|
|
1581
1707
|
}
|
|
1582
1708
|
/**
|
|
1583
|
-
* Disconnects from the
|
|
1584
|
-
* Ensures cleanup completes even if individual disconnections fail.
|
|
1709
|
+
* Disconnects from both the transport and the coordinator.
|
|
1585
1710
|
*/
|
|
1586
1711
|
disconnect(recoverable = false) {
|
|
1587
1712
|
return __async(this, null, function* () {
|
|
1588
|
-
var _a;
|
|
1713
|
+
var _a, _b;
|
|
1589
1714
|
if (this.status === "disconnected" && !this.sessionId) {
|
|
1590
1715
|
console.warn("[Reactor] Already disconnected");
|
|
1591
1716
|
return;
|
|
1592
1717
|
}
|
|
1593
1718
|
(_a = this.coordinatorClient) == null ? void 0 : _a.abort();
|
|
1719
|
+
(_b = this.transportClient) == null ? void 0 : _b.abort();
|
|
1594
1720
|
if (this.coordinatorClient && !recoverable) {
|
|
1595
1721
|
try {
|
|
1596
1722
|
yield this.coordinatorClient.terminateSession();
|
|
@@ -1599,23 +1725,60 @@ var Reactor = class {
|
|
|
1599
1725
|
}
|
|
1600
1726
|
this.coordinatorClient = void 0;
|
|
1601
1727
|
}
|
|
1602
|
-
if (this.
|
|
1728
|
+
if (this.transportClient) {
|
|
1603
1729
|
try {
|
|
1604
|
-
yield this.
|
|
1730
|
+
yield this.transportClient.disconnect();
|
|
1605
1731
|
} catch (error) {
|
|
1606
|
-
console.error("[Reactor] Error disconnecting
|
|
1732
|
+
console.error("[Reactor] Error disconnecting transport:", error);
|
|
1607
1733
|
}
|
|
1608
1734
|
if (!recoverable) {
|
|
1609
|
-
this.
|
|
1735
|
+
this.transportClient = void 0;
|
|
1610
1736
|
}
|
|
1611
1737
|
}
|
|
1612
1738
|
this.setStatus("disconnected");
|
|
1739
|
+
this.resetConnectionTimings();
|
|
1613
1740
|
if (!recoverable) {
|
|
1614
1741
|
this.setSessionExpiration(void 0);
|
|
1615
1742
|
this.setSessionId(void 0);
|
|
1743
|
+
this.capabilities = void 0;
|
|
1744
|
+
this.tracks = [];
|
|
1745
|
+
this.sessionResponse = void 0;
|
|
1616
1746
|
}
|
|
1617
1747
|
});
|
|
1618
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
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1619
1782
|
setSessionId(newSessionId) {
|
|
1620
1783
|
console.debug(
|
|
1621
1784
|
"[Reactor] Setting session ID:",
|
|
@@ -1628,9 +1791,6 @@ var Reactor = class {
|
|
|
1628
1791
|
this.emit("sessionIdChanged", newSessionId);
|
|
1629
1792
|
}
|
|
1630
1793
|
}
|
|
1631
|
-
getSessionId() {
|
|
1632
|
-
return this.sessionId;
|
|
1633
|
-
}
|
|
1634
1794
|
setStatus(newStatus) {
|
|
1635
1795
|
console.debug("[Reactor] Setting status:", newStatus, "from", this.status);
|
|
1636
1796
|
if (this.status !== newStatus) {
|
|
@@ -1638,13 +1798,6 @@ var Reactor = class {
|
|
|
1638
1798
|
this.emit("statusChanged", newStatus);
|
|
1639
1799
|
}
|
|
1640
1800
|
}
|
|
1641
|
-
getStatus() {
|
|
1642
|
-
return this.status;
|
|
1643
|
-
}
|
|
1644
|
-
/**
|
|
1645
|
-
* Set the session expiration time.
|
|
1646
|
-
* @param newSessionExpiration The new session expiration time in seconds.
|
|
1647
|
-
*/
|
|
1648
1801
|
setSessionExpiration(newSessionExpiration) {
|
|
1649
1802
|
console.debug(
|
|
1650
1803
|
"[Reactor] Setting session expiration:",
|
|
@@ -1655,28 +1808,16 @@ var Reactor = class {
|
|
|
1655
1808
|
this.emit("sessionExpirationChanged", newSessionExpiration);
|
|
1656
1809
|
}
|
|
1657
1810
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
getState() {
|
|
1662
|
-
return {
|
|
1663
|
-
status: this.status,
|
|
1664
|
-
lastError: this.lastError
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
1667
|
-
/**
|
|
1668
|
-
* Get the last error that occurred
|
|
1669
|
-
*/
|
|
1670
|
-
getLastError() {
|
|
1671
|
-
return this.lastError;
|
|
1811
|
+
resetConnectionTimings() {
|
|
1812
|
+
this.connectStartTime = void 0;
|
|
1813
|
+
this.connectionTimings = void 0;
|
|
1672
1814
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1815
|
+
finalizeConnectionTimings() {
|
|
1816
|
+
if (!this.connectionTimings || this.connectStartTime == null) return;
|
|
1817
|
+
this.connectionTimings.totalMs = performance.now() - this.connectStartTime;
|
|
1818
|
+
this.connectStartTime = void 0;
|
|
1819
|
+
console.debug("[Reactor] Connection timings:", this.connectionTimings);
|
|
1676
1820
|
}
|
|
1677
|
-
/**
|
|
1678
|
-
* Create and store an error
|
|
1679
|
-
*/
|
|
1680
1821
|
createError(code, message, component, recoverable, retryAfter) {
|
|
1681
1822
|
this.lastError = {
|
|
1682
1823
|
code,
|
|
@@ -1712,7 +1853,7 @@ var initReactorStore = (props) => {
|
|
|
1712
1853
|
};
|
|
1713
1854
|
var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
1714
1855
|
console.debug("[ReactorStore] Creating store", {
|
|
1715
|
-
|
|
1856
|
+
apiUrl: initProps.apiUrl,
|
|
1716
1857
|
jwtToken: initProps.jwtToken,
|
|
1717
1858
|
initialState: publicState
|
|
1718
1859
|
});
|
|
@@ -1880,7 +2021,7 @@ function ReactorProvider(_a) {
|
|
|
1880
2021
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1881
2022
|
}
|
|
1882
2023
|
const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
|
|
1883
|
-
const {
|
|
2024
|
+
const { apiUrl, modelName, local } = props;
|
|
1884
2025
|
const maxAttempts = pollingOptions.maxAttempts;
|
|
1885
2026
|
(0, import_react3.useEffect)(() => {
|
|
1886
2027
|
const handleBeforeUnload = () => {
|
|
@@ -1933,11 +2074,9 @@ function ReactorProvider(_a) {
|
|
|
1933
2074
|
console.debug("[ReactorProvider] Updating reactor store");
|
|
1934
2075
|
storeRef.current = createReactorStore(
|
|
1935
2076
|
initReactorStore({
|
|
1936
|
-
|
|
2077
|
+
apiUrl,
|
|
1937
2078
|
modelName,
|
|
1938
2079
|
local,
|
|
1939
|
-
receive,
|
|
1940
|
-
send,
|
|
1941
2080
|
jwtToken
|
|
1942
2081
|
})
|
|
1943
2082
|
);
|
|
@@ -1964,16 +2103,7 @@ function ReactorProvider(_a) {
|
|
|
1964
2103
|
console.error("[ReactorProvider] Failed to disconnect:", error);
|
|
1965
2104
|
});
|
|
1966
2105
|
};
|
|
1967
|
-
}, [
|
|
1968
|
-
coordinatorUrl,
|
|
1969
|
-
modelName,
|
|
1970
|
-
autoConnect,
|
|
1971
|
-
local,
|
|
1972
|
-
receive,
|
|
1973
|
-
send,
|
|
1974
|
-
jwtToken,
|
|
1975
|
-
maxAttempts
|
|
1976
|
-
]);
|
|
2106
|
+
}, [apiUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
|
|
1977
2107
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
|
|
1978
2108
|
}
|
|
1979
2109
|
function useReactorStore(selector) {
|
|
@@ -2187,31 +2317,37 @@ function ReactorController({
|
|
|
2187
2317
|
return () => clearInterval(interval);
|
|
2188
2318
|
}, [status, commands, requestCapabilities]);
|
|
2189
2319
|
useReactorInternalMessage((message) => {
|
|
2320
|
+
var _a;
|
|
2190
2321
|
if (message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
|
|
2191
2322
|
const commandsMessage = message.data;
|
|
2192
|
-
|
|
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);
|
|
2193
2331
|
const initialValues = {};
|
|
2194
2332
|
const initialExpanded = {};
|
|
2195
|
-
Object.entries(
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
initialValues[commandName][paramName] = (_b = paramSchema.minimum) != null ? _b : 0;
|
|
2210
|
-
}
|
|
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;
|
|
2211
2347
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
);
|
|
2348
|
+
}
|
|
2349
|
+
);
|
|
2350
|
+
});
|
|
2215
2351
|
setFormValues(initialValues);
|
|
2216
2352
|
setExpandedCommands(initialExpanded);
|
|
2217
2353
|
}
|
|
@@ -2794,15 +2930,15 @@ function WebcamStream({
|
|
|
2794
2930
|
}
|
|
2795
2931
|
|
|
2796
2932
|
// src/utils/tokens.ts
|
|
2797
|
-
function
|
|
2798
|
-
return __async(this, arguments, function* (apiKey,
|
|
2933
|
+
function fetchInsecureToken(_0) {
|
|
2934
|
+
return __async(this, arguments, function* (apiKey, apiUrl = DEFAULT_BASE_URL) {
|
|
2799
2935
|
console.warn(
|
|
2800
|
-
"[Reactor] \u26A0\uFE0F SECURITY WARNING:
|
|
2936
|
+
"[Reactor] \u26A0\uFE0F SECURITY WARNING: fetchInsecureToken() exposes your API key in client-side code. This should ONLY be used for local development or testing. In production, fetch tokens from your server instead."
|
|
2801
2937
|
);
|
|
2802
|
-
const response = yield fetch(`${
|
|
2938
|
+
const response = yield fetch(`${apiUrl}/tokens`, {
|
|
2803
2939
|
method: "GET",
|
|
2804
2940
|
headers: {
|
|
2805
|
-
"
|
|
2941
|
+
"Reactor-API-Key": apiKey
|
|
2806
2942
|
}
|
|
2807
2943
|
});
|
|
2808
2944
|
if (!response.ok) {
|
|
@@ -2817,20 +2953,19 @@ function fetchInsecureJwtToken(_0) {
|
|
|
2817
2953
|
0 && (module.exports = {
|
|
2818
2954
|
AbortError,
|
|
2819
2955
|
ConflictError,
|
|
2820
|
-
|
|
2956
|
+
DEFAULT_BASE_URL,
|
|
2821
2957
|
Reactor,
|
|
2822
2958
|
ReactorController,
|
|
2823
2959
|
ReactorProvider,
|
|
2824
2960
|
ReactorView,
|
|
2961
|
+
WebRTCTransportClient,
|
|
2825
2962
|
WebcamStream,
|
|
2826
|
-
|
|
2827
|
-
fetchInsecureJwtToken,
|
|
2963
|
+
fetchInsecureToken,
|
|
2828
2964
|
isAbortError,
|
|
2829
2965
|
useReactor,
|
|
2830
2966
|
useReactorInternalMessage,
|
|
2831
2967
|
useReactorMessage,
|
|
2832
2968
|
useReactorStore,
|
|
2833
|
-
useStats
|
|
2834
|
-
video
|
|
2969
|
+
useStats
|
|
2835
2970
|
});
|
|
2836
2971
|
//# sourceMappingURL=index.js.map
|