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