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