@reactor-team/js-sdk 1.0.18 → 1.0.22
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/README.md +43 -0
- package/dist/index.d.mts +37 -64
- package/dist/index.d.ts +37 -64
- package/dist/index.js +1006 -666
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1005 -670
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -10
package/dist/index.js
CHANGED
|
@@ -79,6 +79,7 @@ var __async = (__this, __arguments, generator) => {
|
|
|
79
79
|
// src/index.ts
|
|
80
80
|
var index_exports = {};
|
|
81
81
|
__export(index_exports, {
|
|
82
|
+
PROD_COORDINATOR_URL: () => PROD_COORDINATOR_URL,
|
|
82
83
|
Reactor: () => Reactor,
|
|
83
84
|
ReactorController: () => ReactorController,
|
|
84
85
|
ReactorProvider: () => ReactorProvider,
|
|
@@ -90,218 +91,603 @@ __export(index_exports, {
|
|
|
90
91
|
});
|
|
91
92
|
module.exports = __toCommonJS(index_exports);
|
|
92
93
|
|
|
93
|
-
// src/
|
|
94
|
-
var
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
94
|
+
// src/generated/api/types/api_types.ts
|
|
95
|
+
var import_wire2 = require("@bufbuild/protobuf/wire");
|
|
96
|
+
|
|
97
|
+
// src/generated/api/types/base.ts
|
|
98
|
+
var import_wire = require("@bufbuild/protobuf/wire");
|
|
99
|
+
|
|
100
|
+
// src/generated/api/types/api_types.ts
|
|
101
|
+
function createBaseSDPParamsResponse() {
|
|
102
|
+
return { sdpAnswer: "", extraArgs: {} };
|
|
103
|
+
}
|
|
104
|
+
var SDPParamsResponse = {
|
|
105
|
+
encode(message, writer = new import_wire2.BinaryWriter()) {
|
|
106
|
+
if (message.sdpAnswer !== "") {
|
|
107
|
+
writer.uint32(10).string(message.sdpAnswer);
|
|
108
|
+
}
|
|
109
|
+
globalThis.Object.entries(message.extraArgs).forEach(
|
|
110
|
+
([key, value]) => {
|
|
111
|
+
SDPParamsResponse_ExtraArgsEntry.encode(
|
|
112
|
+
{ key, value },
|
|
113
|
+
writer.uint32(18).fork()
|
|
114
|
+
).join();
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
return writer;
|
|
118
|
+
},
|
|
119
|
+
decode(input, length) {
|
|
120
|
+
const reader = input instanceof import_wire2.BinaryReader ? input : new import_wire2.BinaryReader(input);
|
|
121
|
+
const end = length === void 0 ? reader.len : reader.pos + length;
|
|
122
|
+
const message = createBaseSDPParamsResponse();
|
|
123
|
+
while (reader.pos < end) {
|
|
124
|
+
const tag = reader.uint32();
|
|
125
|
+
switch (tag >>> 3) {
|
|
126
|
+
case 1: {
|
|
127
|
+
if (tag !== 10) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
message.sdpAnswer = reader.string();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
case 2: {
|
|
134
|
+
if (tag !== 18) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const entry2 = SDPParamsResponse_ExtraArgsEntry.decode(
|
|
138
|
+
reader,
|
|
139
|
+
reader.uint32()
|
|
140
|
+
);
|
|
141
|
+
if (entry2.value !== void 0) {
|
|
142
|
+
message.extraArgs[entry2.key] = entry2.value;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if ((tag & 7) === 4 || tag === 0) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
reader.skip(tag & 7);
|
|
151
|
+
}
|
|
152
|
+
return message;
|
|
153
|
+
},
|
|
154
|
+
fromJSON(object) {
|
|
155
|
+
return {
|
|
156
|
+
sdpAnswer: isSet(object.sdpAnswer) ? globalThis.String(object.sdpAnswer) : "",
|
|
157
|
+
extraArgs: isObject(object.extraArgs) ? globalThis.Object.entries(object.extraArgs).reduce(
|
|
158
|
+
(acc, [key, value]) => {
|
|
159
|
+
acc[key] = globalThis.String(value);
|
|
160
|
+
return acc;
|
|
161
|
+
},
|
|
162
|
+
{}
|
|
163
|
+
) : {}
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
toJSON(message) {
|
|
167
|
+
const obj = {};
|
|
168
|
+
if (message.sdpAnswer !== "") {
|
|
169
|
+
obj.sdpAnswer = message.sdpAnswer;
|
|
170
|
+
}
|
|
171
|
+
if (message.extraArgs) {
|
|
172
|
+
const entries = globalThis.Object.entries(message.extraArgs);
|
|
173
|
+
if (entries.length > 0) {
|
|
174
|
+
obj.extraArgs = {};
|
|
175
|
+
entries.forEach(([k, v]) => {
|
|
176
|
+
obj.extraArgs[k] = v;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return obj;
|
|
181
|
+
},
|
|
182
|
+
create(base) {
|
|
183
|
+
return SDPParamsResponse.fromPartial(base != null ? base : {});
|
|
184
|
+
},
|
|
185
|
+
fromPartial(object) {
|
|
186
|
+
var _a, _b;
|
|
187
|
+
const message = createBaseSDPParamsResponse();
|
|
188
|
+
message.sdpAnswer = (_a = object.sdpAnswer) != null ? _a : "";
|
|
189
|
+
message.extraArgs = globalThis.Object.entries((_b = object.extraArgs) != null ? _b : {}).reduce(
|
|
190
|
+
(acc, [key, value]) => {
|
|
191
|
+
if (value !== void 0) {
|
|
192
|
+
acc[key] = globalThis.String(value);
|
|
193
|
+
}
|
|
194
|
+
return acc;
|
|
195
|
+
},
|
|
196
|
+
{}
|
|
197
|
+
);
|
|
198
|
+
return message;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
function createBaseSDPParamsResponse_ExtraArgsEntry() {
|
|
202
|
+
return { key: "", value: "" };
|
|
203
|
+
}
|
|
204
|
+
var SDPParamsResponse_ExtraArgsEntry = {
|
|
205
|
+
encode(message, writer = new import_wire2.BinaryWriter()) {
|
|
206
|
+
if (message.key !== "") {
|
|
207
|
+
writer.uint32(10).string(message.key);
|
|
208
|
+
}
|
|
209
|
+
if (message.value !== "") {
|
|
210
|
+
writer.uint32(18).string(message.value);
|
|
211
|
+
}
|
|
212
|
+
return writer;
|
|
213
|
+
},
|
|
214
|
+
decode(input, length) {
|
|
215
|
+
const reader = input instanceof import_wire2.BinaryReader ? input : new import_wire2.BinaryReader(input);
|
|
216
|
+
const end = length === void 0 ? reader.len : reader.pos + length;
|
|
217
|
+
const message = createBaseSDPParamsResponse_ExtraArgsEntry();
|
|
218
|
+
while (reader.pos < end) {
|
|
219
|
+
const tag = reader.uint32();
|
|
220
|
+
switch (tag >>> 3) {
|
|
221
|
+
case 1: {
|
|
222
|
+
if (tag !== 10) {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
message.key = reader.string();
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
case 2: {
|
|
229
|
+
if (tag !== 18) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
message.value = reader.string();
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if ((tag & 7) === 4 || tag === 0) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
reader.skip(tag & 7);
|
|
240
|
+
}
|
|
241
|
+
return message;
|
|
242
|
+
},
|
|
243
|
+
fromJSON(object) {
|
|
244
|
+
return {
|
|
245
|
+
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
|
246
|
+
value: isSet(object.value) ? globalThis.String(object.value) : ""
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
toJSON(message) {
|
|
250
|
+
const obj = {};
|
|
251
|
+
if (message.key !== "") {
|
|
252
|
+
obj.key = message.key;
|
|
253
|
+
}
|
|
254
|
+
if (message.value !== "") {
|
|
255
|
+
obj.value = message.value;
|
|
256
|
+
}
|
|
257
|
+
return obj;
|
|
258
|
+
},
|
|
259
|
+
create(base) {
|
|
260
|
+
return SDPParamsResponse_ExtraArgsEntry.fromPartial(base != null ? base : {});
|
|
261
|
+
},
|
|
262
|
+
fromPartial(object) {
|
|
263
|
+
var _a, _b;
|
|
264
|
+
const message = createBaseSDPParamsResponse_ExtraArgsEntry();
|
|
265
|
+
message.key = (_a = object.key) != null ? _a : "";
|
|
266
|
+
message.value = (_b = object.value) != null ? _b : "";
|
|
267
|
+
return message;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function isObject(value) {
|
|
271
|
+
return typeof value === "object" && value !== null;
|
|
272
|
+
}
|
|
273
|
+
function isSet(value) {
|
|
274
|
+
return value !== null && value !== void 0;
|
|
275
|
+
}
|
|
157
276
|
|
|
158
277
|
// src/core/CoordinatorClient.ts
|
|
159
|
-
var
|
|
160
|
-
var
|
|
161
|
-
|
|
162
|
-
jwtToken: import_zod2.z.string().optional(),
|
|
163
|
-
insecureApiKey: import_zod2.z.string().optional(),
|
|
164
|
-
modelName: import_zod2.z.string(),
|
|
165
|
-
modelVersion: import_zod2.z.string().default("latest"),
|
|
166
|
-
queueing: import_zod2.z.boolean().default(false)
|
|
167
|
-
}).refine((data) => data.jwtToken || data.insecureApiKey, {
|
|
168
|
-
message: "At least one of jwtToken or insecureApiKey must be provided."
|
|
169
|
-
});
|
|
278
|
+
var INITIAL_BACKOFF_MS = 500;
|
|
279
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
280
|
+
var BACKOFF_MULTIPLIER = 2;
|
|
170
281
|
var CoordinatorClient = class {
|
|
171
282
|
constructor(options) {
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
this.
|
|
175
|
-
this.jwtToken = validatedOptions.jwtToken;
|
|
176
|
-
this.insecureApiKey = validatedOptions.insecureApiKey;
|
|
177
|
-
this.modelName = validatedOptions.modelName;
|
|
178
|
-
this.modelVersion = validatedOptions.modelVersion;
|
|
179
|
-
this.queueing = validatedOptions.queueing;
|
|
283
|
+
this.baseUrl = options.baseUrl;
|
|
284
|
+
this.jwtToken = options.jwtToken;
|
|
285
|
+
this.model = options.model;
|
|
180
286
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
off(event, handler) {
|
|
189
|
-
var _a;
|
|
190
|
-
(_a = this.eventListeners.get(event)) == null ? void 0 : _a.delete(handler);
|
|
287
|
+
/**
|
|
288
|
+
* Returns the authorization header with JWT Bearer token
|
|
289
|
+
*/
|
|
290
|
+
getAuthHeaders() {
|
|
291
|
+
return {
|
|
292
|
+
Authorization: `Bearer ${this.jwtToken}`
|
|
293
|
+
};
|
|
191
294
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Creates a new session with the coordinator.
|
|
297
|
+
* Expects a 200 response and stores the session ID.
|
|
298
|
+
* @returns The session ID
|
|
299
|
+
*/
|
|
300
|
+
createSession(sdp_offer) {
|
|
301
|
+
return __async(this, null, function* () {
|
|
302
|
+
console.debug("[CoordinatorClient] Creating session...");
|
|
303
|
+
const requestBody = {
|
|
304
|
+
model: this.model,
|
|
305
|
+
sdp_offer,
|
|
306
|
+
extra_args: {}
|
|
307
|
+
};
|
|
308
|
+
const response = yield fetch(`${this.baseUrl}/sessions`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
311
|
+
"Content-Type": "application/json"
|
|
312
|
+
}),
|
|
313
|
+
body: JSON.stringify(requestBody)
|
|
314
|
+
});
|
|
315
|
+
if (response.status !== 200) {
|
|
316
|
+
const errorText = yield response.text();
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Failed to create session: ${response.status} ${errorText}`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const data = yield response.json();
|
|
322
|
+
this.currentSessionId = data.session_id;
|
|
323
|
+
console.debug(
|
|
324
|
+
"[CoordinatorClient] Session created with ID:",
|
|
325
|
+
this.currentSessionId
|
|
326
|
+
);
|
|
327
|
+
return data.session_id;
|
|
328
|
+
});
|
|
195
329
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Gets the current session information from the coordinator.
|
|
332
|
+
* @returns The session data (untyped for now)
|
|
333
|
+
*/
|
|
334
|
+
getSession() {
|
|
335
|
+
return __async(this, null, function* () {
|
|
336
|
+
if (!this.currentSessionId) {
|
|
337
|
+
throw new Error("No active session. Call createSession() first.");
|
|
338
|
+
}
|
|
339
|
+
console.debug(
|
|
340
|
+
"[CoordinatorClient] Getting session info for:",
|
|
341
|
+
this.currentSessionId
|
|
342
|
+
);
|
|
343
|
+
const response = yield fetch(
|
|
344
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
345
|
+
{
|
|
346
|
+
method: "GET",
|
|
347
|
+
headers: this.getAuthHeaders()
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
if (response.status !== 200) {
|
|
351
|
+
const errorText = yield response.text();
|
|
352
|
+
throw new Error(`Failed to get session: ${response.status} ${errorText}`);
|
|
353
|
+
}
|
|
354
|
+
const data = yield response.json();
|
|
355
|
+
return data;
|
|
356
|
+
});
|
|
205
357
|
}
|
|
206
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Terminates the current session by sending a DELETE request to the coordinator.
|
|
360
|
+
* @throws Error if no active session exists or if the request fails (except for 404)
|
|
361
|
+
*/
|
|
362
|
+
terminateSession() {
|
|
207
363
|
return __async(this, null, function* () {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
url.searchParams.set("jwt_token", this.jwtToken);
|
|
211
|
-
} else if (this.insecureApiKey) {
|
|
212
|
-
url.searchParams.set("api_key", this.insecureApiKey);
|
|
364
|
+
if (!this.currentSessionId) {
|
|
365
|
+
throw new Error("No active session. Call createSession() first.");
|
|
213
366
|
}
|
|
214
|
-
|
|
215
|
-
|
|
367
|
+
console.debug(
|
|
368
|
+
"[CoordinatorClient] Terminating session:",
|
|
369
|
+
this.currentSessionId
|
|
370
|
+
);
|
|
371
|
+
const response = yield fetch(
|
|
372
|
+
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
373
|
+
{
|
|
374
|
+
method: "DELETE",
|
|
375
|
+
headers: this.getAuthHeaders()
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
if (response.status === 200 || response.status === 204) {
|
|
379
|
+
const data = yield response.json();
|
|
380
|
+
console.debug(
|
|
381
|
+
"[CoordinatorClient] Session terminated:",
|
|
382
|
+
data.session_id,
|
|
383
|
+
data.status
|
|
384
|
+
);
|
|
385
|
+
this.currentSessionId = void 0;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (response.status === 404) {
|
|
389
|
+
console.debug(
|
|
390
|
+
"[CoordinatorClient] Session not found on server, clearing local state:",
|
|
391
|
+
this.currentSessionId
|
|
392
|
+
);
|
|
393
|
+
this.currentSessionId = void 0;
|
|
394
|
+
return;
|
|
216
395
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
396
|
+
const errorText = yield response.text();
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Failed to terminate session: ${response.status} ${errorText}`
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Get the current session ID
|
|
404
|
+
*/
|
|
405
|
+
getSessionId() {
|
|
406
|
+
return this.currentSessionId;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Sends an SDP offer to the server for reconnection.
|
|
410
|
+
* @param sessionId - The session ID to connect to
|
|
411
|
+
* @param sdpOffer - The SDP offer from the local WebRTC peer connection
|
|
412
|
+
* @returns The SDP answer if ready (200), or null if pending (202)
|
|
413
|
+
*/
|
|
414
|
+
sendSdpOffer(sessionId, sdpOffer) {
|
|
415
|
+
return __async(this, null, function* () {
|
|
416
|
+
console.debug(
|
|
417
|
+
"[CoordinatorClient] Sending SDP offer for session:",
|
|
418
|
+
sessionId
|
|
419
|
+
);
|
|
420
|
+
const requestBody = {
|
|
421
|
+
sdp_offer: sdpOffer,
|
|
422
|
+
extra_args: {}
|
|
222
423
|
};
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
424
|
+
const response = yield fetch(
|
|
425
|
+
`${this.baseUrl}/sessions/${sessionId}/sdp_params`,
|
|
426
|
+
{
|
|
427
|
+
method: "PUT",
|
|
428
|
+
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
429
|
+
"Content-Type": "application/json"
|
|
430
|
+
}),
|
|
431
|
+
body: JSON.stringify(requestBody)
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
if (response.status === 200) {
|
|
435
|
+
const answerData = yield response.json();
|
|
436
|
+
console.debug("[CoordinatorClient] Received SDP answer immediately");
|
|
437
|
+
return answerData.sdp_answer;
|
|
438
|
+
}
|
|
439
|
+
if (response.status === 202) {
|
|
440
|
+
console.debug(
|
|
441
|
+
"[CoordinatorClient] SDP offer accepted, answer pending (202)"
|
|
442
|
+
);
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
const errorText = yield response.text();
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Failed to send SDP offer: ${response.status} ${errorText}`
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Polls for the SDP answer with geometric backoff.
|
|
453
|
+
* Used for async reconnection when the answer is not immediately available.
|
|
454
|
+
* @param sessionId - The session ID to poll for
|
|
455
|
+
* @returns The SDP answer from the server
|
|
456
|
+
*/
|
|
457
|
+
pollSdpAnswer(sessionId) {
|
|
458
|
+
return __async(this, null, function* () {
|
|
459
|
+
console.debug(
|
|
460
|
+
"[CoordinatorClient] Polling for SDP answer for session:",
|
|
461
|
+
sessionId
|
|
462
|
+
);
|
|
463
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
464
|
+
let attempt = 0;
|
|
465
|
+
while (true) {
|
|
466
|
+
attempt++;
|
|
467
|
+
console.debug(
|
|
468
|
+
`[CoordinatorClient] SDP poll attempt ${attempt} for session ${sessionId}`
|
|
469
|
+
);
|
|
470
|
+
const response = yield fetch(
|
|
471
|
+
`${this.baseUrl}/sessions/${sessionId}/sdp_params`,
|
|
472
|
+
{
|
|
473
|
+
method: "GET",
|
|
474
|
+
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
475
|
+
"Content-Type": "application/json"
|
|
476
|
+
})
|
|
230
477
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
478
|
+
);
|
|
479
|
+
if (response.status === 200) {
|
|
480
|
+
const answerData = SDPParamsResponse.fromJSON(
|
|
481
|
+
yield response.json()
|
|
234
482
|
);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"message",
|
|
242
|
-
event.data
|
|
483
|
+
console.debug("[CoordinatorClient] Received SDP answer via polling");
|
|
484
|
+
return answerData.sdp_answer;
|
|
485
|
+
}
|
|
486
|
+
if (response.status === 202) {
|
|
487
|
+
console.warn(
|
|
488
|
+
`[CoordinatorClient] SDP answer pending (202), retrying in ${backoffMs}ms...`
|
|
243
489
|
);
|
|
244
|
-
this.
|
|
490
|
+
yield this.sleep(backoffMs);
|
|
491
|
+
backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
|
|
492
|
+
continue;
|
|
245
493
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
269
|
-
(_a = this.websocket) == null ? void 0 : _a.addEventListener("open", onOpen);
|
|
270
|
-
(_b = this.websocket) == null ? void 0 : _b.addEventListener("error", onError);
|
|
271
|
-
});
|
|
272
|
-
console.log("[CoordinatorClient] WebSocket connected");
|
|
273
|
-
this.sendMessage({
|
|
274
|
-
type: "sessionSetup",
|
|
275
|
-
data: {
|
|
276
|
-
modelName: this.modelName,
|
|
277
|
-
modelVersion: this.modelVersion
|
|
494
|
+
const errorText = yield response.text();
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Failed to poll SDP answer: ${response.status} ${errorText}`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Connects to the session by sending an SDP offer and receiving an SDP answer.
|
|
503
|
+
* If sdpOffer is provided, sends it first. If the answer is pending (202),
|
|
504
|
+
* falls back to polling. If no sdpOffer is provided, goes directly to polling.
|
|
505
|
+
* @param sessionId - The session ID to connect to
|
|
506
|
+
* @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
|
|
507
|
+
* @returns The SDP answer from the server
|
|
508
|
+
*/
|
|
509
|
+
connect(sessionId, sdpOffer) {
|
|
510
|
+
return __async(this, null, function* () {
|
|
511
|
+
console.debug("[CoordinatorClient] Connecting to session:", sessionId);
|
|
512
|
+
if (sdpOffer) {
|
|
513
|
+
const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
|
|
514
|
+
if (answer !== null) {
|
|
515
|
+
return answer;
|
|
278
516
|
}
|
|
517
|
+
}
|
|
518
|
+
return this.pollSdpAnswer(sessionId);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Utility function to sleep for a given number of milliseconds
|
|
523
|
+
*/
|
|
524
|
+
sleep(ms) {
|
|
525
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// src/core/LocalCoordinatorClient.ts
|
|
530
|
+
var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
531
|
+
constructor(baseUrl) {
|
|
532
|
+
super({
|
|
533
|
+
baseUrl,
|
|
534
|
+
jwtToken: "local",
|
|
535
|
+
model: "local"
|
|
536
|
+
});
|
|
537
|
+
this.localBaseUrl = baseUrl;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Creates a local session by posting to /start_session.
|
|
541
|
+
* @returns always "local"
|
|
542
|
+
*/
|
|
543
|
+
createSession(sdpOffer) {
|
|
544
|
+
return __async(this, null, function* () {
|
|
545
|
+
console.debug("[LocalCoordinatorClient] Creating local session...");
|
|
546
|
+
this.sdpOffer = sdpOffer;
|
|
547
|
+
const response = yield fetch(`${this.localBaseUrl}/start_session`, {
|
|
548
|
+
method: "POST"
|
|
279
549
|
});
|
|
280
|
-
|
|
550
|
+
if (!response.ok) {
|
|
551
|
+
throw new Error("Failed to send local start session command.");
|
|
552
|
+
}
|
|
553
|
+
console.debug("[LocalCoordinatorClient] Local session created");
|
|
554
|
+
return "local";
|
|
281
555
|
});
|
|
282
556
|
}
|
|
283
557
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
558
|
+
* Connects to the local session by posting SDP params to /sdp_params.
|
|
559
|
+
* @param sessionId - The session ID (ignored for local)
|
|
560
|
+
* @param sdpMessage - The SDP offer from the local WebRTC peer connection
|
|
561
|
+
* @returns The SDP answer from the server
|
|
286
562
|
*/
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
563
|
+
connect(sessionId, sdpMessage) {
|
|
564
|
+
return __async(this, null, function* () {
|
|
565
|
+
this.sdpOffer = sdpMessage || this.sdpOffer;
|
|
566
|
+
console.debug("[LocalCoordinatorClient] Connecting to local session...");
|
|
567
|
+
const sdpBody = {
|
|
568
|
+
sdp: this.sdpOffer,
|
|
569
|
+
type: "offer"
|
|
570
|
+
};
|
|
571
|
+
const response = yield fetch(`${this.localBaseUrl}/sdp_params`, {
|
|
572
|
+
method: "POST",
|
|
573
|
+
headers: {
|
|
574
|
+
"Content-Type": "application/json"
|
|
575
|
+
},
|
|
576
|
+
body: JSON.stringify(sdpBody)
|
|
577
|
+
});
|
|
578
|
+
if (!response.ok) {
|
|
579
|
+
throw new Error("Failed to get SDP answer from local coordinator.");
|
|
580
|
+
}
|
|
581
|
+
const sdpAnswer = yield response.json();
|
|
582
|
+
console.debug("[LocalCoordinatorClient] Received SDP answer");
|
|
583
|
+
return sdpAnswer.sdp;
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
terminateSession() {
|
|
587
|
+
return __async(this, null, function* () {
|
|
588
|
+
console.debug("[LocalCoordinatorClient] Stopping local session...");
|
|
589
|
+
yield fetch(`${this.localBaseUrl}/stop_session`, {
|
|
590
|
+
method: "POST"
|
|
591
|
+
});
|
|
592
|
+
});
|
|
293
593
|
}
|
|
294
594
|
};
|
|
295
595
|
|
|
596
|
+
// src/utils/webrtc.ts
|
|
597
|
+
var DEFAULT_ICE_SERVERS = [
|
|
598
|
+
{ urls: "stun:stun.l.google.com:19302" },
|
|
599
|
+
{ urls: "stun:stun1.l.google.com:19302" }
|
|
600
|
+
];
|
|
601
|
+
var DEFAULT_DATA_CHANNEL_LABEL = "data";
|
|
602
|
+
function createPeerConnection(config) {
|
|
603
|
+
var _a;
|
|
604
|
+
return new RTCPeerConnection({
|
|
605
|
+
iceServers: (_a = config == null ? void 0 : config.iceServers) != null ? _a : DEFAULT_ICE_SERVERS
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function createDataChannel(pc, label) {
|
|
609
|
+
return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
|
|
610
|
+
}
|
|
611
|
+
function createOffer(pc) {
|
|
612
|
+
return __async(this, null, function* () {
|
|
613
|
+
const offer = yield pc.createOffer();
|
|
614
|
+
yield pc.setLocalDescription(offer);
|
|
615
|
+
yield waitForIceGathering(pc);
|
|
616
|
+
const localDescription = pc.localDescription;
|
|
617
|
+
if (!localDescription) {
|
|
618
|
+
throw new Error("Failed to create local description");
|
|
619
|
+
}
|
|
620
|
+
return localDescription.sdp;
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function setRemoteDescription(pc, sdp) {
|
|
624
|
+
return __async(this, null, function* () {
|
|
625
|
+
const sessionDescription = new RTCSessionDescription({
|
|
626
|
+
sdp,
|
|
627
|
+
type: "answer"
|
|
628
|
+
});
|
|
629
|
+
yield pc.setRemoteDescription(sessionDescription);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function getLocalDescription(pc) {
|
|
633
|
+
const desc = pc.localDescription;
|
|
634
|
+
if (!desc) return void 0;
|
|
635
|
+
return desc.sdp;
|
|
636
|
+
}
|
|
637
|
+
function waitForIceGathering(pc, timeoutMs = 5e3) {
|
|
638
|
+
return new Promise((resolve) => {
|
|
639
|
+
if (pc.iceGatheringState === "complete") {
|
|
640
|
+
resolve();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const onGatheringStateChange = () => {
|
|
644
|
+
if (pc.iceGatheringState === "complete") {
|
|
645
|
+
pc.removeEventListener(
|
|
646
|
+
"icegatheringstatechange",
|
|
647
|
+
onGatheringStateChange
|
|
648
|
+
);
|
|
649
|
+
resolve();
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
pc.addEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
653
|
+
setTimeout(() => {
|
|
654
|
+
pc.removeEventListener("icegatheringstatechange", onGatheringStateChange);
|
|
655
|
+
resolve();
|
|
656
|
+
}, timeoutMs);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function sendMessage(channel, command, data) {
|
|
660
|
+
if (channel.readyState !== "open") {
|
|
661
|
+
throw new Error(`Data channel not open: ${channel.readyState}`);
|
|
662
|
+
}
|
|
663
|
+
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
|
664
|
+
const payload = { type: command, data: jsonData };
|
|
665
|
+
channel.send(JSON.stringify(payload));
|
|
666
|
+
}
|
|
667
|
+
function parseMessage(data) {
|
|
668
|
+
if (typeof data === "string") {
|
|
669
|
+
try {
|
|
670
|
+
return JSON.parse(data);
|
|
671
|
+
} catch (e) {
|
|
672
|
+
return data;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return data;
|
|
676
|
+
}
|
|
677
|
+
function closePeerConnection(pc) {
|
|
678
|
+
pc.close();
|
|
679
|
+
}
|
|
680
|
+
|
|
296
681
|
// src/core/GPUMachineClient.ts
|
|
297
|
-
var import_livekit_client = require("livekit-client");
|
|
298
682
|
var GPUMachineClient = class {
|
|
299
|
-
constructor(
|
|
683
|
+
constructor(config) {
|
|
300
684
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
301
|
-
this.
|
|
302
|
-
this.
|
|
685
|
+
this.status = "disconnected";
|
|
686
|
+
this.config = config != null ? config : {};
|
|
303
687
|
}
|
|
688
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
304
689
|
// Event Emitter API
|
|
690
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
305
691
|
on(event, handler) {
|
|
306
692
|
if (!this.eventListeners.has(event)) {
|
|
307
693
|
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
@@ -316,294 +702,287 @@ var GPUMachineClient = class {
|
|
|
316
702
|
var _a;
|
|
317
703
|
(_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
|
|
318
704
|
}
|
|
319
|
-
|
|
705
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
706
|
+
// SDP & Connection
|
|
707
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
708
|
+
/**
|
|
709
|
+
* Creates an SDP offer for initiating a connection.
|
|
710
|
+
* Must be called before connect().
|
|
711
|
+
*/
|
|
712
|
+
createOffer() {
|
|
320
713
|
return __async(this, null, function* () {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
yield this.roomInstance.localParticipant.sendText(messageStr, {
|
|
325
|
-
topic: "application"
|
|
326
|
-
});
|
|
327
|
-
} else {
|
|
328
|
-
console.warn(
|
|
329
|
-
"[GPUMachineClient] Cannot send message - not connected to room"
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
} catch (error) {
|
|
333
|
-
console.error("[GPUMachineClient] Failed to send message:", error);
|
|
334
|
-
this.emit("statusChanged", "error");
|
|
714
|
+
if (!this.peerConnection) {
|
|
715
|
+
this.peerConnection = createPeerConnection(this.config);
|
|
716
|
+
this.setupPeerConnectionHandlers();
|
|
335
717
|
}
|
|
718
|
+
this.dataChannel = createDataChannel(
|
|
719
|
+
this.peerConnection,
|
|
720
|
+
this.config.dataChannelLabel
|
|
721
|
+
);
|
|
722
|
+
this.setupDataChannelHandlers();
|
|
723
|
+
this.videoTransceiver = this.peerConnection.addTransceiver("video", {
|
|
724
|
+
direction: "sendrecv"
|
|
725
|
+
});
|
|
726
|
+
const offer = yield createOffer(this.peerConnection);
|
|
727
|
+
console.debug("[GPUMachineClient] Created SDP offer");
|
|
728
|
+
return offer;
|
|
336
729
|
});
|
|
337
730
|
}
|
|
338
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Connects to the GPU machine using the provided SDP answer.
|
|
733
|
+
* createOffer() must be called first.
|
|
734
|
+
* @param sdpAnswer The SDP answer from the GPU machine
|
|
735
|
+
*/
|
|
736
|
+
connect(sdpAnswer) {
|
|
339
737
|
return __async(this, null, function* () {
|
|
340
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
if (track.kind === import_livekit_client.Track.Kind.Video) {
|
|
361
|
-
const videoTrack = track;
|
|
362
|
-
this.emit("streamChanged", videoTrack);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
);
|
|
366
|
-
this.roomInstance.on(
|
|
367
|
-
import_livekit_client.RoomEvent.TrackUnsubscribed,
|
|
368
|
-
(track, _publication, participant) => {
|
|
369
|
-
console.debug(
|
|
370
|
-
"[GPUMachineClient] Track unsubscribed:",
|
|
371
|
-
track.kind,
|
|
372
|
-
participant.identity
|
|
373
|
-
);
|
|
374
|
-
if (track.kind === import_livekit_client.Track.Kind.Video) {
|
|
375
|
-
this.emit("streamChanged", null);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
);
|
|
379
|
-
this.roomInstance.registerTextStreamHandler(
|
|
380
|
-
"application",
|
|
381
|
-
(reader, participant) => __async(this, null, function* () {
|
|
382
|
-
const text = yield reader.readAll();
|
|
383
|
-
console.log("[GPUMachineClient] Received message:", text);
|
|
384
|
-
try {
|
|
385
|
-
const parsedData = JSON.parse(text);
|
|
386
|
-
const validatedMessage = GPUMachineReceiveMessageSchema.parse(parsedData);
|
|
387
|
-
if (validatedMessage.type === "fps") {
|
|
388
|
-
this.machineFPS = validatedMessage.data;
|
|
389
|
-
}
|
|
390
|
-
this.emit(validatedMessage.type, validatedMessage.data);
|
|
391
|
-
} catch (error) {
|
|
392
|
-
console.error(
|
|
393
|
-
"[GPUMachineClient] Failed to parse/validate message:",
|
|
394
|
-
error
|
|
395
|
-
);
|
|
396
|
-
this.emit("statusChanged", "error");
|
|
397
|
-
}
|
|
398
|
-
})
|
|
399
|
-
);
|
|
400
|
-
yield this.roomInstance.connect(this.liveKitUrl, this.token);
|
|
401
|
-
console.log("[GPUMachineClient] Room connected");
|
|
738
|
+
if (!this.peerConnection) {
|
|
739
|
+
throw new Error(
|
|
740
|
+
"[GPUMachineClient] Cannot connect - call createOffer() first"
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (this.peerConnection.signalingState !== "have-local-offer") {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`[GPUMachineClient] Invalid signaling state: ${this.peerConnection.signalingState}`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
this.setStatus("connecting");
|
|
749
|
+
try {
|
|
750
|
+
yield setRemoteDescription(this.peerConnection, sdpAnswer);
|
|
751
|
+
console.debug("[GPUMachineClient] Remote description set");
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error("[GPUMachineClient] Failed to connect:", error);
|
|
754
|
+
this.setStatus("error");
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
402
757
|
});
|
|
403
758
|
}
|
|
404
759
|
/**
|
|
405
|
-
*
|
|
406
|
-
* This will trigger the onclose event handler.
|
|
760
|
+
* Disconnects from the GPU machine and cleans up resources.
|
|
407
761
|
*/
|
|
408
762
|
disconnect() {
|
|
409
763
|
return __async(this, null, function* () {
|
|
410
|
-
if (this.
|
|
411
|
-
yield this.
|
|
764
|
+
if (this.publishedTrack) {
|
|
765
|
+
yield this.unpublishTrack();
|
|
412
766
|
}
|
|
413
|
-
if (this.
|
|
414
|
-
|
|
767
|
+
if (this.dataChannel) {
|
|
768
|
+
this.dataChannel.close();
|
|
769
|
+
this.dataChannel = void 0;
|
|
415
770
|
}
|
|
416
|
-
if (this.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.roomInstance = void 0;
|
|
771
|
+
if (this.peerConnection) {
|
|
772
|
+
closePeerConnection(this.peerConnection);
|
|
773
|
+
this.peerConnection = void 0;
|
|
420
774
|
}
|
|
421
|
-
this.
|
|
775
|
+
this.videoTransceiver = void 0;
|
|
776
|
+
this.setStatus("disconnected");
|
|
777
|
+
console.debug("[GPUMachineClient] Disconnected");
|
|
422
778
|
});
|
|
423
779
|
}
|
|
424
780
|
/**
|
|
425
|
-
* Returns the current
|
|
426
|
-
|
|
781
|
+
* Returns the current connection status.
|
|
782
|
+
*/
|
|
783
|
+
getStatus() {
|
|
784
|
+
return this.status;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Gets the current local SDP description.
|
|
427
788
|
*/
|
|
428
|
-
|
|
429
|
-
|
|
789
|
+
getLocalSDP() {
|
|
790
|
+
if (!this.peerConnection) return void 0;
|
|
791
|
+
return getLocalDescription(this.peerConnection);
|
|
430
792
|
}
|
|
793
|
+
isOfferStillValid() {
|
|
794
|
+
if (!this.peerConnection) return false;
|
|
795
|
+
return this.peerConnection.signalingState === "have-local-offer";
|
|
796
|
+
}
|
|
797
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
798
|
+
// Messaging
|
|
799
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
431
800
|
/**
|
|
432
|
-
*
|
|
433
|
-
* @
|
|
801
|
+
* Sends a command to the GPU machine via the data channel.
|
|
802
|
+
* @param command The command to send
|
|
803
|
+
* @param data The data to send with the command. These are the parameters for the command, matching the scheme in the capabilities dictionary.
|
|
434
804
|
*/
|
|
435
|
-
|
|
436
|
-
|
|
805
|
+
sendCommand(command, data) {
|
|
806
|
+
if (!this.dataChannel) {
|
|
807
|
+
throw new Error("[GPUMachineClient] Data channel not available");
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
sendMessage(this.dataChannel, command, data);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
console.warn("[GPUMachineClient] Failed to send message:", error);
|
|
813
|
+
}
|
|
437
814
|
}
|
|
815
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
816
|
+
// Track Publishing
|
|
817
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
438
818
|
/**
|
|
439
|
-
* Publishes a
|
|
440
|
-
* Only one
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* @param mediaStream The MediaStream containing the video track to publish
|
|
444
|
-
* @throws Error if no video track is found in the MediaStream or room is not connected
|
|
819
|
+
* Publishes a track to the GPU machine.
|
|
820
|
+
* Only one track can be published at a time.
|
|
821
|
+
* Uses the existing transceiver's sender to replace the track.
|
|
822
|
+
* @param track The MediaStreamTrack to publish
|
|
445
823
|
*/
|
|
446
|
-
|
|
824
|
+
publishTrack(track) {
|
|
447
825
|
return __async(this, null, function* () {
|
|
448
|
-
if (!this.
|
|
826
|
+
if (!this.peerConnection) {
|
|
449
827
|
throw new Error(
|
|
450
|
-
"[GPUMachineClient] Cannot publish track - not
|
|
828
|
+
"[GPUMachineClient] Cannot publish track - not initialized"
|
|
451
829
|
);
|
|
452
830
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
831
|
+
if (this.status !== "connected") {
|
|
832
|
+
throw new Error(
|
|
833
|
+
"[GPUMachineClient] Cannot publish track - not connected"
|
|
834
|
+
);
|
|
456
835
|
}
|
|
457
|
-
if (this.
|
|
458
|
-
|
|
836
|
+
if (!this.videoTransceiver) {
|
|
837
|
+
throw new Error(
|
|
838
|
+
"[GPUMachineClient] Cannot publish track - no video transceiver"
|
|
839
|
+
);
|
|
459
840
|
}
|
|
460
841
|
try {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
console.debug("[GPUMachineClient] Video track published successfully");
|
|
842
|
+
yield this.videoTransceiver.sender.replaceTrack(track);
|
|
843
|
+
this.publishedTrack = track;
|
|
844
|
+
console.debug(
|
|
845
|
+
"[GPUMachineClient] Track published successfully:",
|
|
846
|
+
track.kind
|
|
847
|
+
);
|
|
468
848
|
} catch (error) {
|
|
469
|
-
console.error("[GPUMachineClient] Failed to publish
|
|
849
|
+
console.error("[GPUMachineClient] Failed to publish track:", error);
|
|
470
850
|
throw error;
|
|
471
851
|
}
|
|
472
852
|
});
|
|
473
853
|
}
|
|
474
854
|
/**
|
|
475
|
-
* Unpublishes the currently published
|
|
476
|
-
* Note: We pass false to unpublishTrack to prevent LiveKit from stopping
|
|
477
|
-
* the source MediaStreamTrack, as it's owned by the component that created it.
|
|
855
|
+
* Unpublishes the currently published track.
|
|
478
856
|
*/
|
|
479
|
-
|
|
857
|
+
unpublishTrack() {
|
|
480
858
|
return __async(this, null, function* () {
|
|
481
|
-
if (!this.
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
const publishedVideoTrack = this.publishedVideoTrack;
|
|
485
|
-
this.publishedVideoTrack = void 0;
|
|
859
|
+
if (!this.videoTransceiver || !this.publishedTrack) return;
|
|
486
860
|
try {
|
|
487
|
-
yield this.
|
|
488
|
-
|
|
489
|
-
false
|
|
490
|
-
// Don't stop the source track - it's managed externally
|
|
491
|
-
);
|
|
492
|
-
console.debug("[GPUMachineClient] Video track unpublished successfully");
|
|
861
|
+
yield this.videoTransceiver.sender.replaceTrack(null);
|
|
862
|
+
console.debug("[GPUMachineClient] Track unpublished successfully");
|
|
493
863
|
} catch (error) {
|
|
494
|
-
console.error(
|
|
495
|
-
"[GPUMachineClient] Failed to unpublish video track:",
|
|
496
|
-
error
|
|
497
|
-
);
|
|
864
|
+
console.error("[GPUMachineClient] Failed to unpublish track:", error);
|
|
498
865
|
throw error;
|
|
866
|
+
} finally {
|
|
867
|
+
this.publishedTrack = void 0;
|
|
499
868
|
}
|
|
500
869
|
});
|
|
501
870
|
}
|
|
502
871
|
/**
|
|
503
|
-
*
|
|
504
|
-
* This is an internal method. Only one audio track can be published at a time.
|
|
505
|
-
*
|
|
506
|
-
* @param mediaStream The MediaStream containing the audio track to publish
|
|
507
|
-
* @private
|
|
872
|
+
* Returns the currently published track.
|
|
508
873
|
*/
|
|
509
|
-
|
|
510
|
-
return
|
|
511
|
-
if (!this.roomInstance) {
|
|
512
|
-
throw new Error(
|
|
513
|
-
"[GPUMachineClient] Cannot publish track - not connected to room"
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
const audioTracks = mediaStream.getAudioTracks();
|
|
517
|
-
if (audioTracks.length === 0) {
|
|
518
|
-
throw new Error("[GPUMachineClient] No audio track found in MediaStream");
|
|
519
|
-
}
|
|
520
|
-
if (this.publishedAudioTrack) {
|
|
521
|
-
yield this.unpublishAudioTrack();
|
|
522
|
-
}
|
|
523
|
-
try {
|
|
524
|
-
const audioTrack = audioTracks[0];
|
|
525
|
-
const localAudioTrack = yield this.roomInstance.localParticipant.publishTrack(audioTrack, {
|
|
526
|
-
name: "client-audio",
|
|
527
|
-
source: import_livekit_client.Track.Source.Microphone
|
|
528
|
-
});
|
|
529
|
-
this.publishedAudioTrack = localAudioTrack.track;
|
|
530
|
-
console.debug("[GPUMachineClient] Audio track published successfully");
|
|
531
|
-
} catch (error) {
|
|
532
|
-
console.error("[GPUMachineClient] Failed to publish audio track:", error);
|
|
533
|
-
throw error;
|
|
534
|
-
}
|
|
535
|
-
});
|
|
874
|
+
getPublishedTrack() {
|
|
875
|
+
return this.publishedTrack;
|
|
536
876
|
}
|
|
877
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
878
|
+
// Getters
|
|
879
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
537
880
|
/**
|
|
538
|
-
*
|
|
539
|
-
* Note: We pass false to unpublishTrack to prevent LiveKit from stopping
|
|
540
|
-
* the source MediaStreamTrack, as it's owned by the component that created it.
|
|
541
|
-
* @private
|
|
881
|
+
* Returns the remote media stream from the GPU machine.
|
|
542
882
|
*/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
883
|
+
getRemoteStream() {
|
|
884
|
+
if (!this.peerConnection) return void 0;
|
|
885
|
+
const receivers = this.peerConnection.getReceivers();
|
|
886
|
+
const tracks = receivers.map((r) => r.track).filter((t) => t !== null);
|
|
887
|
+
if (tracks.length === 0) return void 0;
|
|
888
|
+
return new MediaStream(tracks);
|
|
889
|
+
}
|
|
890
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
891
|
+
// Private Helpers
|
|
892
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
893
|
+
setStatus(newStatus) {
|
|
894
|
+
if (this.status !== newStatus) {
|
|
895
|
+
this.status = newStatus;
|
|
896
|
+
this.emit("statusChanged", newStatus);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
setupPeerConnectionHandlers() {
|
|
900
|
+
if (!this.peerConnection) return;
|
|
901
|
+
this.peerConnection.onconnectionstatechange = () => {
|
|
902
|
+
var _a;
|
|
903
|
+
const state = (_a = this.peerConnection) == null ? void 0 : _a.connectionState;
|
|
904
|
+
console.debug("[GPUMachineClient] Connection state:", state);
|
|
905
|
+
if (state) {
|
|
906
|
+
switch (state) {
|
|
907
|
+
case "connected":
|
|
908
|
+
this.setStatus("connected");
|
|
909
|
+
break;
|
|
910
|
+
case "disconnected":
|
|
911
|
+
case "closed":
|
|
912
|
+
this.setStatus("disconnected");
|
|
913
|
+
break;
|
|
914
|
+
case "failed":
|
|
915
|
+
this.setStatus("error");
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
547
918
|
}
|
|
548
|
-
|
|
549
|
-
|
|
919
|
+
};
|
|
920
|
+
this.peerConnection.ontrack = (event) => {
|
|
921
|
+
var _a;
|
|
922
|
+
console.debug("[GPUMachineClient] Track received:", event.track.kind);
|
|
923
|
+
const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
|
|
924
|
+
this.emit("trackReceived", event.track, stream);
|
|
925
|
+
};
|
|
926
|
+
this.peerConnection.onicecandidate = (event) => {
|
|
927
|
+
if (event.candidate) {
|
|
928
|
+
console.debug("[GPUMachineClient] ICE candidate:", event.candidate);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
this.peerConnection.onicecandidateerror = (event) => {
|
|
932
|
+
console.warn("[GPUMachineClient] ICE candidate error:", event);
|
|
933
|
+
};
|
|
934
|
+
this.peerConnection.ondatachannel = (event) => {
|
|
935
|
+
console.debug("[GPUMachineClient] Data channel received from remote");
|
|
936
|
+
this.dataChannel = event.channel;
|
|
937
|
+
this.setupDataChannelHandlers();
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
setupDataChannelHandlers() {
|
|
941
|
+
if (!this.dataChannel) return;
|
|
942
|
+
this.dataChannel.onopen = () => {
|
|
943
|
+
console.debug("[GPUMachineClient] Data channel open");
|
|
944
|
+
};
|
|
945
|
+
this.dataChannel.onclose = () => {
|
|
946
|
+
console.debug("[GPUMachineClient] Data channel closed");
|
|
947
|
+
};
|
|
948
|
+
this.dataChannel.onerror = (error) => {
|
|
949
|
+
console.error("[GPUMachineClient] Data channel error:", error);
|
|
950
|
+
};
|
|
951
|
+
this.dataChannel.onmessage = (event) => {
|
|
952
|
+
const data = parseMessage(event.data);
|
|
953
|
+
console.debug("[GPUMachineClient] Received message:", data);
|
|
550
954
|
try {
|
|
551
|
-
|
|
552
|
-
publishedAudioTrack,
|
|
553
|
-
false
|
|
554
|
-
// Don't stop the source track - it's managed externally
|
|
555
|
-
);
|
|
556
|
-
console.debug("[GPUMachineClient] Audio track unpublished successfully");
|
|
955
|
+
this.emit("application", data);
|
|
557
956
|
} catch (error) {
|
|
558
957
|
console.error(
|
|
559
|
-
"[GPUMachineClient] Failed to
|
|
958
|
+
"[GPUMachineClient] Failed to parse/validate message:",
|
|
560
959
|
error
|
|
561
960
|
);
|
|
562
|
-
throw error;
|
|
563
961
|
}
|
|
564
|
-
}
|
|
962
|
+
};
|
|
565
963
|
}
|
|
566
964
|
};
|
|
567
965
|
|
|
568
966
|
// src/core/Reactor.ts
|
|
569
|
-
var
|
|
570
|
-
var LOCAL_COORDINATOR_URL = "
|
|
571
|
-
var
|
|
572
|
-
var
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}).optional(),
|
|
578
|
-
insecureApiKey: import_zod3.z.string().optional(),
|
|
579
|
-
jwtToken: import_zod3.z.string().optional(),
|
|
580
|
-
coordinatorUrl: import_zod3.z.string().default(PROD_COORDINATOR_URL),
|
|
581
|
-
modelName: import_zod3.z.string(),
|
|
582
|
-
queueing: import_zod3.z.boolean().default(false),
|
|
583
|
-
local: import_zod3.z.boolean().default(false)
|
|
584
|
-
}).refine(
|
|
585
|
-
(data) => data.directConnection || data.insecureApiKey || data.jwtToken || data.local,
|
|
586
|
-
{
|
|
587
|
-
message: "At least one of directConnection, insecureApiKey, or jwtToken or local must be provided."
|
|
588
|
-
}
|
|
589
|
-
);
|
|
967
|
+
var import_zod = require("zod");
|
|
968
|
+
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
969
|
+
var PROD_COORDINATOR_URL = "https://api.reactor.inc";
|
|
970
|
+
var OptionsSchema = import_zod.z.object({
|
|
971
|
+
coordinatorUrl: import_zod.z.string().default(PROD_COORDINATOR_URL),
|
|
972
|
+
modelName: import_zod.z.string(),
|
|
973
|
+
local: import_zod.z.boolean().default(false)
|
|
974
|
+
});
|
|
590
975
|
var Reactor = class {
|
|
591
976
|
constructor(options) {
|
|
592
|
-
//client for the machine instance
|
|
593
977
|
this.status = "disconnected";
|
|
594
978
|
// Generic event map
|
|
595
979
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
596
|
-
const validatedOptions =
|
|
980
|
+
const validatedOptions = OptionsSchema.parse(options);
|
|
597
981
|
this.coordinatorUrl = validatedOptions.coordinatorUrl;
|
|
598
|
-
this.
|
|
599
|
-
this.
|
|
600
|
-
this.
|
|
601
|
-
this.modelName = validatedOptions.modelName;
|
|
602
|
-
this.queueing = validatedOptions.queueing;
|
|
603
|
-
this.modelVersion = "1.0.0";
|
|
604
|
-
if (validatedOptions.local) {
|
|
982
|
+
this.model = validatedOptions.modelName;
|
|
983
|
+
this.local = validatedOptions.local;
|
|
984
|
+
if (this.local) {
|
|
605
985
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
606
|
-
this.insecureApiKey = LOCAL_INSECURE_API_KEY;
|
|
607
986
|
}
|
|
608
987
|
}
|
|
609
988
|
// Event Emitter API
|
|
@@ -627,20 +1006,16 @@ var Reactor = class {
|
|
|
627
1006
|
* @param message The message to send to the machine.
|
|
628
1007
|
* @throws Error if not in ready state
|
|
629
1008
|
*/
|
|
630
|
-
|
|
1009
|
+
sendCommand(command, data) {
|
|
631
1010
|
return __async(this, null, function* () {
|
|
632
1011
|
var _a;
|
|
633
1012
|
if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
|
|
634
1013
|
const errorMessage = `Cannot send message, status is ${this.status}`;
|
|
635
|
-
console.
|
|
636
|
-
|
|
1014
|
+
console.warn("[Reactor]", errorMessage);
|
|
1015
|
+
return;
|
|
637
1016
|
}
|
|
638
1017
|
try {
|
|
639
|
-
|
|
640
|
-
type: "application",
|
|
641
|
-
data: message
|
|
642
|
-
};
|
|
643
|
-
yield (_a = this.machineClient) == null ? void 0 : _a.sendMessage(applicationMessage);
|
|
1018
|
+
(_a = this.machineClient) == null ? void 0 : _a.sendCommand(command, data);
|
|
644
1019
|
} catch (error) {
|
|
645
1020
|
console.error("[Reactor] Failed to send message:", error);
|
|
646
1021
|
this.createError(
|
|
@@ -653,24 +1028,24 @@ var Reactor = class {
|
|
|
653
1028
|
});
|
|
654
1029
|
}
|
|
655
1030
|
/**
|
|
656
|
-
* Public method to publish a
|
|
657
|
-
* @param
|
|
1031
|
+
* Public method to publish a track to the machine.
|
|
1032
|
+
* @param track The track to send to the machine.
|
|
658
1033
|
*/
|
|
659
|
-
|
|
1034
|
+
publishTrack(track) {
|
|
660
1035
|
return __async(this, null, function* () {
|
|
661
1036
|
var _a;
|
|
662
1037
|
if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
|
|
663
|
-
const errorMessage = `Cannot publish
|
|
664
|
-
console.
|
|
665
|
-
|
|
1038
|
+
const errorMessage = `Cannot publish track, status is ${this.status}`;
|
|
1039
|
+
console.warn("[Reactor]", errorMessage);
|
|
1040
|
+
return;
|
|
666
1041
|
}
|
|
667
1042
|
try {
|
|
668
|
-
yield (_a = this.machineClient) == null ? void 0 : _a.
|
|
1043
|
+
yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
|
|
669
1044
|
} catch (error) {
|
|
670
|
-
console.error("[Reactor] Failed to publish
|
|
1045
|
+
console.error("[Reactor] Failed to publish track:", error);
|
|
671
1046
|
this.createError(
|
|
672
|
-
"
|
|
673
|
-
`Failed to publish
|
|
1047
|
+
"TRACK_PUBLISH_FAILED",
|
|
1048
|
+
`Failed to publish track: ${error}`,
|
|
674
1049
|
"gpu",
|
|
675
1050
|
true
|
|
676
1051
|
);
|
|
@@ -678,19 +1053,18 @@ var Reactor = class {
|
|
|
678
1053
|
});
|
|
679
1054
|
}
|
|
680
1055
|
/**
|
|
681
|
-
* Public method to unpublish
|
|
682
|
-
* This unpublishes the video track that was previously sent.
|
|
1056
|
+
* Public method to unpublish the currently published track.
|
|
683
1057
|
*/
|
|
684
|
-
|
|
1058
|
+
unpublishTrack() {
|
|
685
1059
|
return __async(this, null, function* () {
|
|
686
1060
|
var _a;
|
|
687
1061
|
try {
|
|
688
|
-
yield (_a = this.machineClient) == null ? void 0 : _a.
|
|
1062
|
+
yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
|
|
689
1063
|
} catch (error) {
|
|
690
|
-
console.error("[Reactor] Failed to unpublish
|
|
1064
|
+
console.error("[Reactor] Failed to unpublish track:", error);
|
|
691
1065
|
this.createError(
|
|
692
|
-
"
|
|
693
|
-
`Failed to unpublish
|
|
1066
|
+
"TRACK_UNPUBLISH_FAILED",
|
|
1067
|
+
`Failed to unpublish track: ${error}`,
|
|
694
1068
|
"gpu",
|
|
695
1069
|
true
|
|
696
1070
|
);
|
|
@@ -698,152 +1072,76 @@ var Reactor = class {
|
|
|
698
1072
|
});
|
|
699
1073
|
}
|
|
700
1074
|
/**
|
|
701
|
-
*
|
|
702
|
-
* Once the machine is ready, the Reactor will establish the LiveKit connection.
|
|
703
|
-
* @param livekitJwtToken The JWT token for LiveKit authentication
|
|
704
|
-
* @param livekitWsUrl The WebSocket URL for LiveKit connection
|
|
1075
|
+
* Public method for reconnecting to an existing session, that may have been interrupted but can be recovered.
|
|
705
1076
|
*/
|
|
706
|
-
|
|
1077
|
+
reconnect() {
|
|
707
1078
|
return __async(this, null, function* () {
|
|
708
|
-
|
|
1079
|
+
if (!this.sessionId || !this.coordinatorClient) {
|
|
1080
|
+
console.warn("[Reactor] No active session to reconnect to.");
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
this.setStatus("connecting");
|
|
1084
|
+
if (!this.machineClient) {
|
|
1085
|
+
this.machineClient = new GPUMachineClient();
|
|
1086
|
+
this.setupMachineClientHandlers();
|
|
1087
|
+
}
|
|
1088
|
+
const sdpOffer = yield this.machineClient.createOffer();
|
|
709
1089
|
try {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
});
|
|
714
|
-
this.machineClient.on(
|
|
715
|
-
"statusChanged",
|
|
716
|
-
(status) => {
|
|
717
|
-
switch (status) {
|
|
718
|
-
case "connected":
|
|
719
|
-
this.setStatus("ready");
|
|
720
|
-
break;
|
|
721
|
-
case "disconnected":
|
|
722
|
-
this.disconnect();
|
|
723
|
-
break;
|
|
724
|
-
case "error":
|
|
725
|
-
this.createError(
|
|
726
|
-
"GPU_CONNECTION_ERROR",
|
|
727
|
-
"GPU machine connection failed",
|
|
728
|
-
"gpu",
|
|
729
|
-
true
|
|
730
|
-
);
|
|
731
|
-
this.disconnect();
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
1090
|
+
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
1091
|
+
this.sessionId,
|
|
1092
|
+
sdpOffer
|
|
735
1093
|
);
|
|
736
|
-
this.machineClient.
|
|
737
|
-
|
|
738
|
-
});
|
|
739
|
-
this.machineClient.on("streamChanged", (videoTrack) => {
|
|
740
|
-
this.emit("streamChanged", videoTrack);
|
|
741
|
-
});
|
|
742
|
-
console.debug("[Reactor] About to connect to machine");
|
|
743
|
-
yield this.machineClient.connect();
|
|
1094
|
+
yield this.machineClient.connect(sdpAnswer);
|
|
1095
|
+
this.setStatus("ready");
|
|
744
1096
|
} catch (error) {
|
|
745
|
-
|
|
1097
|
+
console.error("[Reactor] Failed to reconnect:", error);
|
|
1098
|
+
this.disconnect(false);
|
|
1099
|
+
this.createError(
|
|
1100
|
+
"RECONNECTION_FAILED",
|
|
1101
|
+
`Failed to reconnect: ${error}`,
|
|
1102
|
+
"coordinator",
|
|
1103
|
+
true
|
|
1104
|
+
);
|
|
746
1105
|
}
|
|
747
1106
|
});
|
|
748
1107
|
}
|
|
749
1108
|
/**
|
|
750
1109
|
* Connects to the coordinator and waits for a GPU to be assigned.
|
|
751
|
-
* Once a GPU is assigned, the Reactor will connect to the gpu machine via
|
|
1110
|
+
* Once a GPU is assigned, the Reactor will connect to the gpu machine via WebRTC.
|
|
1111
|
+
* If no authentication is provided and not in local mode, an error is thrown.
|
|
752
1112
|
*/
|
|
753
|
-
connect() {
|
|
1113
|
+
connect(jwtToken) {
|
|
754
1114
|
return __async(this, null, function* () {
|
|
755
1115
|
console.debug("[Reactor] Connecting, status:", this.status);
|
|
756
|
-
if (this.
|
|
1116
|
+
if (jwtToken == void 0 && !this.local) {
|
|
1117
|
+
throw new Error("No authentication provided and not in local mode");
|
|
1118
|
+
}
|
|
1119
|
+
if (this.status !== "disconnected") {
|
|
757
1120
|
throw new Error("Already connected or connecting");
|
|
758
|
-
if (this.directConnection) {
|
|
759
|
-
return this.connectToGPUMachine(
|
|
760
|
-
this.directConnection.livekitJwtToken,
|
|
761
|
-
this.directConnection.livekitWsUrl
|
|
762
|
-
);
|
|
763
1121
|
}
|
|
764
1122
|
this.setStatus("connecting");
|
|
765
1123
|
try {
|
|
766
1124
|
console.debug(
|
|
767
1125
|
"[Reactor] Connecting to coordinator with authenticated URL"
|
|
768
1126
|
);
|
|
769
|
-
this.coordinatorClient = new CoordinatorClient({
|
|
770
|
-
|
|
771
|
-
jwtToken
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
modelVersion: this.modelVersion,
|
|
775
|
-
queueing: this.queueing
|
|
1127
|
+
this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
|
|
1128
|
+
baseUrl: this.coordinatorUrl,
|
|
1129
|
+
jwtToken,
|
|
1130
|
+
// Safe: validated on line 186-188
|
|
1131
|
+
model: this.model
|
|
776
1132
|
});
|
|
777
|
-
this.
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
try {
|
|
785
|
-
yield this.connectToGPUMachine(
|
|
786
|
-
assignmentData.livekitJwtToken,
|
|
787
|
-
assignmentData.livekitWsUrl
|
|
788
|
-
);
|
|
789
|
-
} catch (error) {
|
|
790
|
-
console.error("[Reactor] Failed to connect to GPU machine:", error);
|
|
791
|
-
this.createError(
|
|
792
|
-
"GPU_CONNECTION_FAILED",
|
|
793
|
-
`Failed to connect to GPU machine: ${error}`,
|
|
794
|
-
"gpu",
|
|
795
|
-
true
|
|
796
|
-
);
|
|
797
|
-
this.disconnect();
|
|
798
|
-
}
|
|
799
|
-
})
|
|
800
|
-
);
|
|
801
|
-
this.coordinatorClient.on(
|
|
802
|
-
"waiting-info",
|
|
803
|
-
(waitingData) => {
|
|
804
|
-
console.debug("[Reactor] Waiting info update received:", waitingData);
|
|
805
|
-
this.setWaitingInfo(__spreadValues(__spreadValues({}, this.waitingInfo), waitingData));
|
|
806
|
-
}
|
|
807
|
-
);
|
|
808
|
-
this.coordinatorClient.on(
|
|
809
|
-
"session-expiration",
|
|
810
|
-
(sessionExpirationData) => {
|
|
811
|
-
this.setSessionExpiration(sessionExpirationData.expire);
|
|
812
|
-
}
|
|
813
|
-
);
|
|
814
|
-
this.coordinatorClient.on(
|
|
815
|
-
"statusChanged",
|
|
816
|
-
(newStatus) => {
|
|
817
|
-
switch (newStatus) {
|
|
818
|
-
case "connected":
|
|
819
|
-
this.setStatus("waiting");
|
|
820
|
-
this.setWaitingInfo({
|
|
821
|
-
position: void 0,
|
|
822
|
-
estimatedWaitTime: void 0,
|
|
823
|
-
averageWaitTime: void 0
|
|
824
|
-
});
|
|
825
|
-
break;
|
|
826
|
-
case "disconnected":
|
|
827
|
-
this.disconnect();
|
|
828
|
-
break;
|
|
829
|
-
case "error":
|
|
830
|
-
this.createError(
|
|
831
|
-
"COORDINATOR_CONNECTION_ERROR",
|
|
832
|
-
"Coordinator connection failed",
|
|
833
|
-
"coordinator",
|
|
834
|
-
true
|
|
835
|
-
);
|
|
836
|
-
this.disconnect();
|
|
837
|
-
break;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
);
|
|
841
|
-
yield this.coordinatorClient.connect();
|
|
1133
|
+
this.machineClient = new GPUMachineClient();
|
|
1134
|
+
this.setupMachineClientHandlers();
|
|
1135
|
+
const sdpOffer = yield this.machineClient.createOffer();
|
|
1136
|
+
const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
|
|
1137
|
+
this.setSessionId(sessionId);
|
|
1138
|
+
const sdpAnswer = yield this.coordinatorClient.connect(sessionId);
|
|
1139
|
+
yield this.machineClient.connect(sdpAnswer);
|
|
842
1140
|
} catch (error) {
|
|
843
|
-
console.error("[Reactor]
|
|
1141
|
+
console.error("[Reactor] Connection failed:", error);
|
|
844
1142
|
this.createError(
|
|
845
|
-
"
|
|
846
|
-
`
|
|
1143
|
+
"CONNECTION_FAILED",
|
|
1144
|
+
`Connection failed: ${error}`,
|
|
847
1145
|
"coordinator",
|
|
848
1146
|
true
|
|
849
1147
|
);
|
|
@@ -852,19 +1150,52 @@ var Reactor = class {
|
|
|
852
1150
|
}
|
|
853
1151
|
});
|
|
854
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Sets up event handlers for the machine client.
|
|
1155
|
+
*/
|
|
1156
|
+
setupMachineClientHandlers() {
|
|
1157
|
+
if (!this.machineClient) return;
|
|
1158
|
+
this.machineClient.on("application", (message) => {
|
|
1159
|
+
this.emit("newMessage", message);
|
|
1160
|
+
});
|
|
1161
|
+
this.machineClient.on("statusChanged", (status) => {
|
|
1162
|
+
switch (status) {
|
|
1163
|
+
case "connected":
|
|
1164
|
+
this.setStatus("ready");
|
|
1165
|
+
break;
|
|
1166
|
+
case "disconnected":
|
|
1167
|
+
this.disconnect(true);
|
|
1168
|
+
break;
|
|
1169
|
+
case "error":
|
|
1170
|
+
this.createError(
|
|
1171
|
+
"GPU_CONNECTION_ERROR",
|
|
1172
|
+
"GPU machine connection failed",
|
|
1173
|
+
"gpu",
|
|
1174
|
+
true
|
|
1175
|
+
);
|
|
1176
|
+
this.disconnect();
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
this.machineClient.on(
|
|
1181
|
+
"trackReceived",
|
|
1182
|
+
(track, stream) => {
|
|
1183
|
+
this.emit("streamChanged", track, stream);
|
|
1184
|
+
}
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
855
1187
|
/**
|
|
856
1188
|
* Disconnects from the coordinator and the gpu machine.
|
|
857
1189
|
* Ensures cleanup completes even if individual disconnections fail.
|
|
858
1190
|
*/
|
|
859
|
-
disconnect() {
|
|
1191
|
+
disconnect(recoverable = false) {
|
|
860
1192
|
return __async(this, null, function* () {
|
|
861
|
-
if (this.status === "disconnected")
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
1193
|
+
if (this.status === "disconnected" && !this.sessionId) {
|
|
1194
|
+
console.warn("[Reactor] Already disconnected");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (this.coordinatorClient && !recoverable) {
|
|
1198
|
+
yield this.coordinatorClient.terminateSession();
|
|
868
1199
|
this.coordinatorClient = void 0;
|
|
869
1200
|
}
|
|
870
1201
|
if (this.machineClient) {
|
|
@@ -873,13 +1204,32 @@ var Reactor = class {
|
|
|
873
1204
|
} catch (error) {
|
|
874
1205
|
console.error("[Reactor] Error disconnecting from GPU machine:", error);
|
|
875
1206
|
}
|
|
876
|
-
|
|
1207
|
+
if (!recoverable) {
|
|
1208
|
+
this.machineClient = void 0;
|
|
1209
|
+
}
|
|
877
1210
|
}
|
|
878
1211
|
this.setStatus("disconnected");
|
|
879
|
-
|
|
880
|
-
|
|
1212
|
+
if (!recoverable) {
|
|
1213
|
+
this.setSessionExpiration(void 0);
|
|
1214
|
+
this.setSessionId(void 0);
|
|
1215
|
+
}
|
|
881
1216
|
});
|
|
882
1217
|
}
|
|
1218
|
+
setSessionId(newSessionId) {
|
|
1219
|
+
console.debug(
|
|
1220
|
+
"[Reactor] Setting session ID:",
|
|
1221
|
+
newSessionId,
|
|
1222
|
+
"from",
|
|
1223
|
+
this.sessionId
|
|
1224
|
+
);
|
|
1225
|
+
if (this.sessionId !== newSessionId) {
|
|
1226
|
+
this.sessionId = newSessionId;
|
|
1227
|
+
this.emit("sessionIdChanged", newSessionId);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
getSessionId() {
|
|
1231
|
+
return this.sessionId;
|
|
1232
|
+
}
|
|
883
1233
|
setStatus(newStatus) {
|
|
884
1234
|
console.debug("[Reactor] Setting status:", newStatus, "from", this.status);
|
|
885
1235
|
if (this.status !== newStatus) {
|
|
@@ -887,17 +1237,8 @@ var Reactor = class {
|
|
|
887
1237
|
this.emit("statusChanged", newStatus);
|
|
888
1238
|
}
|
|
889
1239
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
"[Reactor] Setting waiting info:",
|
|
893
|
-
newWaitingInfo,
|
|
894
|
-
"from",
|
|
895
|
-
this.waitingInfo
|
|
896
|
-
);
|
|
897
|
-
if (this.waitingInfo !== newWaitingInfo) {
|
|
898
|
-
this.waitingInfo = newWaitingInfo;
|
|
899
|
-
this.emit("waitingInfoChanged", newWaitingInfo);
|
|
900
|
-
}
|
|
1240
|
+
getStatus() {
|
|
1241
|
+
return this.status;
|
|
901
1242
|
}
|
|
902
1243
|
/**
|
|
903
1244
|
* Set the session expiration time.
|
|
@@ -913,25 +1254,15 @@ var Reactor = class {
|
|
|
913
1254
|
this.emit("sessionExpirationChanged", newSessionExpiration);
|
|
914
1255
|
}
|
|
915
1256
|
}
|
|
916
|
-
getStatus() {
|
|
917
|
-
return this.status;
|
|
918
|
-
}
|
|
919
1257
|
/**
|
|
920
1258
|
* Get the current state including status, error, and waiting info
|
|
921
1259
|
*/
|
|
922
1260
|
getState() {
|
|
923
1261
|
return {
|
|
924
1262
|
status: this.status,
|
|
925
|
-
waitingInfo: this.waitingInfo,
|
|
926
1263
|
lastError: this.lastError
|
|
927
1264
|
};
|
|
928
1265
|
}
|
|
929
|
-
/**
|
|
930
|
-
* Get waiting information when status is 'waiting'
|
|
931
|
-
*/
|
|
932
|
-
getWaitingInfo() {
|
|
933
|
-
return this.waitingInfo;
|
|
934
|
-
}
|
|
935
1266
|
/**
|
|
936
1267
|
* Get the last error that occurred
|
|
937
1268
|
*/
|
|
@@ -966,10 +1297,11 @@ var ReactorContext = (0, import_react2.createContext)(
|
|
|
966
1297
|
var defaultInitState = {
|
|
967
1298
|
status: "disconnected",
|
|
968
1299
|
videoTrack: null,
|
|
969
|
-
fps: void 0,
|
|
970
|
-
waitingInfo: void 0,
|
|
971
1300
|
lastError: void 0,
|
|
972
|
-
sessionExpiration: void 0
|
|
1301
|
+
sessionExpiration: void 0,
|
|
1302
|
+
insecureApiKey: void 0,
|
|
1303
|
+
jwtToken: void 0,
|
|
1304
|
+
sessionId: void 0
|
|
973
1305
|
};
|
|
974
1306
|
var initReactorStore = (props) => {
|
|
975
1307
|
return __spreadValues(__spreadValues({}, defaultInitState), props);
|
|
@@ -977,6 +1309,7 @@ var initReactorStore = (props) => {
|
|
|
977
1309
|
var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
978
1310
|
console.debug("[ReactorStore] Creating store", {
|
|
979
1311
|
coordinatorUrl: initProps.coordinatorUrl,
|
|
1312
|
+
jwtToken: initProps.jwtToken,
|
|
980
1313
|
initialState: publicState
|
|
981
1314
|
});
|
|
982
1315
|
return (0, import_react.create)()((set, get) => {
|
|
@@ -989,37 +1322,37 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
989
1322
|
});
|
|
990
1323
|
set({ status: newStatus });
|
|
991
1324
|
});
|
|
992
|
-
reactor.on(
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
newSessionExpiration
|
|
1003
|
-
});
|
|
1004
|
-
set({ sessionExpiration: newSessionExpiration });
|
|
1005
|
-
});
|
|
1325
|
+
reactor.on(
|
|
1326
|
+
"sessionExpirationChanged",
|
|
1327
|
+
(newSessionExpiration) => {
|
|
1328
|
+
console.debug("[ReactorStore] Session expiration changed", {
|
|
1329
|
+
oldSessionExpiration: get().sessionExpiration,
|
|
1330
|
+
newSessionExpiration
|
|
1331
|
+
});
|
|
1332
|
+
set({ sessionExpiration: newSessionExpiration });
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1006
1335
|
reactor.on("streamChanged", (videoTrack) => {
|
|
1007
1336
|
console.debug("[ReactorStore] Stream changed", {
|
|
1008
1337
|
hasVideoTrack: !!videoTrack,
|
|
1009
1338
|
videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind,
|
|
1010
|
-
|
|
1339
|
+
videoTrackId: videoTrack == null ? void 0 : videoTrack.id
|
|
1011
1340
|
});
|
|
1012
1341
|
set({ videoTrack });
|
|
1013
1342
|
});
|
|
1014
|
-
reactor.on("fps", (fps) => {
|
|
1015
|
-
console.debug("[ReactorStore] FPS updated", { fps });
|
|
1016
|
-
set({ fps });
|
|
1017
|
-
});
|
|
1018
1343
|
reactor.on("error", (error) => {
|
|
1019
1344
|
console.debug("[ReactorStore] Error occurred", error);
|
|
1020
1345
|
set({ lastError: error });
|
|
1021
1346
|
});
|
|
1347
|
+
reactor.on("sessionIdChanged", (newSessionId) => {
|
|
1348
|
+
console.debug("[ReactorStore] Session ID changed", {
|
|
1349
|
+
oldSessionId: get().sessionId,
|
|
1350
|
+
newSessionId
|
|
1351
|
+
});
|
|
1352
|
+
set({ sessionId: newSessionId });
|
|
1353
|
+
});
|
|
1022
1354
|
return __spreadProps(__spreadValues({}, publicState), {
|
|
1355
|
+
jwtToken: initProps.jwtToken,
|
|
1023
1356
|
internal: { reactor },
|
|
1024
1357
|
// actions
|
|
1025
1358
|
onMessage: (handler) => {
|
|
@@ -1030,32 +1363,35 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1030
1363
|
get().internal.reactor.off("newMessage", handler);
|
|
1031
1364
|
};
|
|
1032
1365
|
},
|
|
1033
|
-
|
|
1034
|
-
console.debug("[ReactorStore] Sending
|
|
1366
|
+
sendCommand: (command, data) => __async(null, null, function* () {
|
|
1367
|
+
console.debug("[ReactorStore] Sending command", { command, data });
|
|
1035
1368
|
try {
|
|
1036
|
-
yield get().internal.reactor.
|
|
1037
|
-
console.debug("[ReactorStore]
|
|
1369
|
+
yield get().internal.reactor.sendCommand(command, data);
|
|
1370
|
+
console.debug("[ReactorStore] Command sent successfully");
|
|
1038
1371
|
} catch (error) {
|
|
1039
|
-
console.error("[ReactorStore] Failed to send
|
|
1372
|
+
console.error("[ReactorStore] Failed to send command:", error);
|
|
1040
1373
|
throw error;
|
|
1041
1374
|
}
|
|
1042
1375
|
}),
|
|
1043
|
-
connect: () => __async(null, null, function* () {
|
|
1044
|
-
|
|
1376
|
+
connect: (jwtToken) => __async(null, null, function* () {
|
|
1377
|
+
if (jwtToken === void 0) {
|
|
1378
|
+
jwtToken = get().jwtToken;
|
|
1379
|
+
}
|
|
1380
|
+
console.debug("[ReactorStore] Connect called.");
|
|
1045
1381
|
try {
|
|
1046
|
-
yield get().internal.reactor.connect();
|
|
1382
|
+
yield get().internal.reactor.connect(jwtToken);
|
|
1047
1383
|
console.debug("[ReactorStore] Connect completed successfully");
|
|
1048
1384
|
} catch (error) {
|
|
1049
1385
|
console.error("[ReactorStore] Connect failed:", error);
|
|
1050
1386
|
throw error;
|
|
1051
1387
|
}
|
|
1052
1388
|
}),
|
|
1053
|
-
disconnect: () => __async(null, null, function* () {
|
|
1389
|
+
disconnect: (recoverable = false) => __async(null, null, function* () {
|
|
1054
1390
|
console.debug("[ReactorStore] Disconnect called", {
|
|
1055
1391
|
currentStatus: get().status
|
|
1056
1392
|
});
|
|
1057
1393
|
try {
|
|
1058
|
-
yield get().internal.reactor.disconnect();
|
|
1394
|
+
yield get().internal.reactor.disconnect(recoverable);
|
|
1059
1395
|
console.debug("[ReactorStore] Disconnect completed successfully");
|
|
1060
1396
|
} catch (error) {
|
|
1061
1397
|
console.error("[ReactorStore] Disconnect failed:", error);
|
|
@@ -1065,7 +1401,7 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1065
1401
|
publishVideoStream: (stream) => __async(null, null, function* () {
|
|
1066
1402
|
console.debug("[ReactorStore] Publishing video stream");
|
|
1067
1403
|
try {
|
|
1068
|
-
yield get().internal.reactor.
|
|
1404
|
+
yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
|
|
1069
1405
|
console.debug("[ReactorStore] Video stream published successfully");
|
|
1070
1406
|
} catch (error) {
|
|
1071
1407
|
console.error(
|
|
@@ -1078,7 +1414,7 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1078
1414
|
unpublishVideoStream: () => __async(null, null, function* () {
|
|
1079
1415
|
console.debug("[ReactorStore] Unpublishing video stream");
|
|
1080
1416
|
try {
|
|
1081
|
-
yield get().internal.reactor.
|
|
1417
|
+
yield get().internal.reactor.unpublishTrack();
|
|
1082
1418
|
console.debug("[ReactorStore] Video stream unpublished successfully");
|
|
1083
1419
|
} catch (error) {
|
|
1084
1420
|
console.error(
|
|
@@ -1087,6 +1423,16 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1087
1423
|
);
|
|
1088
1424
|
throw error;
|
|
1089
1425
|
}
|
|
1426
|
+
}),
|
|
1427
|
+
reconnect: () => __async(null, null, function* () {
|
|
1428
|
+
console.debug("[ReactorStore] Reconnecting");
|
|
1429
|
+
try {
|
|
1430
|
+
yield get().internal.reactor.reconnect();
|
|
1431
|
+
console.debug("[ReactorStore] Reconnect completed successfully");
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
console.error("[ReactorStore] Failed to reconnect:", error);
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1090
1436
|
})
|
|
1091
1437
|
});
|
|
1092
1438
|
});
|
|
@@ -1098,37 +1444,48 @@ var import_jsx_runtime = require("react/jsx-runtime");
|
|
|
1098
1444
|
function ReactorProvider(_a) {
|
|
1099
1445
|
var _b = _a, {
|
|
1100
1446
|
children,
|
|
1101
|
-
autoConnect = true
|
|
1447
|
+
autoConnect = true,
|
|
1448
|
+
jwtToken
|
|
1102
1449
|
} = _b, props = __objRest(_b, [
|
|
1103
1450
|
"children",
|
|
1104
|
-
"autoConnect"
|
|
1451
|
+
"autoConnect",
|
|
1452
|
+
"jwtToken"
|
|
1105
1453
|
]);
|
|
1106
1454
|
const storeRef = (0, import_react3.useRef)(void 0);
|
|
1107
1455
|
const firstRender = (0, import_react3.useRef)(true);
|
|
1108
1456
|
const [_storeVersion, setStoreVersion] = (0, import_react3.useState)(0);
|
|
1109
1457
|
if (storeRef.current === void 0) {
|
|
1110
1458
|
console.debug("[ReactorProvider] Creating new reactor store");
|
|
1111
|
-
storeRef.current = createReactorStore(
|
|
1459
|
+
storeRef.current = createReactorStore(
|
|
1460
|
+
initReactorStore(__spreadProps(__spreadValues({}, props), {
|
|
1461
|
+
jwtToken
|
|
1462
|
+
}))
|
|
1463
|
+
);
|
|
1112
1464
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1113
1465
|
}
|
|
1114
|
-
const {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1466
|
+
const { coordinatorUrl, modelName, local } = props;
|
|
1467
|
+
(0, import_react3.useEffect)(() => {
|
|
1468
|
+
const handleBeforeUnload = () => {
|
|
1469
|
+
var _a2;
|
|
1470
|
+
console.debug(
|
|
1471
|
+
"[ReactorProvider] Page unloading, performing non-recoverable disconnect"
|
|
1472
|
+
);
|
|
1473
|
+
(_a2 = storeRef.current) == null ? void 0 : _a2.getState().internal.reactor.disconnect(false);
|
|
1474
|
+
};
|
|
1475
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
1476
|
+
return () => {
|
|
1477
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
1478
|
+
};
|
|
1479
|
+
}, []);
|
|
1123
1480
|
(0, import_react3.useEffect)(() => {
|
|
1124
1481
|
if (firstRender.current) {
|
|
1125
1482
|
firstRender.current = false;
|
|
1126
1483
|
const current2 = storeRef.current;
|
|
1127
|
-
if (autoConnect && current2.getState().status === "disconnected") {
|
|
1484
|
+
if (autoConnect && current2.getState().status === "disconnected" && jwtToken) {
|
|
1128
1485
|
console.debug(
|
|
1129
1486
|
"[ReactorProvider] Starting autoconnect in first render..."
|
|
1130
1487
|
);
|
|
1131
|
-
current2.getState().connect().then(() => {
|
|
1488
|
+
current2.getState().connect(jwtToken).then(() => {
|
|
1132
1489
|
console.debug(
|
|
1133
1490
|
"[ReactorProvider] Autoconnect successful in first render"
|
|
1134
1491
|
);
|
|
@@ -1160,11 +1517,8 @@ function ReactorProvider(_a) {
|
|
|
1160
1517
|
initReactorStore({
|
|
1161
1518
|
coordinatorUrl,
|
|
1162
1519
|
modelName,
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
directConnection,
|
|
1166
|
-
queueing,
|
|
1167
|
-
local
|
|
1520
|
+
local,
|
|
1521
|
+
jwtToken
|
|
1168
1522
|
})
|
|
1169
1523
|
);
|
|
1170
1524
|
const current = storeRef.current;
|
|
@@ -1172,9 +1526,9 @@ function ReactorProvider(_a) {
|
|
|
1172
1526
|
console.debug(
|
|
1173
1527
|
"[ReactorProvider] Reactor store updated successfully, and increased version"
|
|
1174
1528
|
);
|
|
1175
|
-
if (autoConnect && current.getState().status === "disconnected") {
|
|
1529
|
+
if (autoConnect && current.getState().status === "disconnected" && jwtToken) {
|
|
1176
1530
|
console.debug("[ReactorProvider] Starting autoconnect...");
|
|
1177
|
-
current.getState().connect().then(() => {
|
|
1531
|
+
current.getState().connect(jwtToken).then(() => {
|
|
1178
1532
|
console.debug("[ReactorProvider] Autoconnect successful");
|
|
1179
1533
|
}).catch((error) => {
|
|
1180
1534
|
console.error("[ReactorProvider] Failed to autoconnect:", error);
|
|
@@ -1190,16 +1544,7 @@ function ReactorProvider(_a) {
|
|
|
1190
1544
|
console.error("[ReactorProvider] Failed to disconnect:", error);
|
|
1191
1545
|
});
|
|
1192
1546
|
};
|
|
1193
|
-
}, [
|
|
1194
|
-
coordinatorUrl,
|
|
1195
|
-
modelName,
|
|
1196
|
-
jwtToken,
|
|
1197
|
-
insecureApiKey,
|
|
1198
|
-
directConnection,
|
|
1199
|
-
queueing,
|
|
1200
|
-
autoConnect,
|
|
1201
|
-
local
|
|
1202
|
-
]);
|
|
1547
|
+
}, [coordinatorUrl, modelName, autoConnect, local, jwtToken]);
|
|
1203
1548
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
|
|
1204
1549
|
}
|
|
1205
1550
|
function useReactorStore(selector) {
|
|
@@ -1261,7 +1606,11 @@ function ReactorView({
|
|
|
1261
1606
|
if (videoRef.current && videoTrack) {
|
|
1262
1607
|
console.debug("[ReactorView] Attaching video track to element");
|
|
1263
1608
|
try {
|
|
1264
|
-
videoTrack
|
|
1609
|
+
const stream = new MediaStream([videoTrack]);
|
|
1610
|
+
videoRef.current.srcObject = stream;
|
|
1611
|
+
videoRef.current.play().catch((e) => {
|
|
1612
|
+
console.warn("[ReactorView] Auto-play failed:", e);
|
|
1613
|
+
});
|
|
1265
1614
|
console.debug("[ReactorView] Video track attached successfully");
|
|
1266
1615
|
} catch (error) {
|
|
1267
1616
|
console.error("[ReactorView] Failed to attach video track:", error);
|
|
@@ -1269,12 +1618,8 @@ function ReactorView({
|
|
|
1269
1618
|
return () => {
|
|
1270
1619
|
console.debug("[ReactorView] Detaching video track from element");
|
|
1271
1620
|
if (videoRef.current) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
console.debug("[ReactorView] Video track detached successfully");
|
|
1275
|
-
} catch (error) {
|
|
1276
|
-
console.error("[ReactorView] Failed to detach video track:", error);
|
|
1277
|
-
}
|
|
1621
|
+
videoRef.current.srcObject = null;
|
|
1622
|
+
console.debug("[ReactorView] Video track detached successfully");
|
|
1278
1623
|
}
|
|
1279
1624
|
};
|
|
1280
1625
|
} else {
|
|
@@ -1339,8 +1684,8 @@ function ReactorController({
|
|
|
1339
1684
|
className,
|
|
1340
1685
|
style
|
|
1341
1686
|
}) {
|
|
1342
|
-
const {
|
|
1343
|
-
|
|
1687
|
+
const { sendCommand, status } = useReactor((state) => ({
|
|
1688
|
+
sendCommand: state.sendCommand,
|
|
1344
1689
|
status: state.status
|
|
1345
1690
|
}));
|
|
1346
1691
|
const [commands, setCommands] = (0, import_react6.useState)({});
|
|
@@ -1355,12 +1700,9 @@ function ReactorController({
|
|
|
1355
1700
|
}, [status]);
|
|
1356
1701
|
const requestCapabilities = (0, import_react6.useCallback)(() => {
|
|
1357
1702
|
if (status === "ready") {
|
|
1358
|
-
|
|
1359
|
-
type: "requestCapabilities",
|
|
1360
|
-
data: {}
|
|
1361
|
-
});
|
|
1703
|
+
sendCommand("requestCapabilities", {});
|
|
1362
1704
|
}
|
|
1363
|
-
}, [status,
|
|
1705
|
+
}, [status, sendCommand]);
|
|
1364
1706
|
import_react6.default.useEffect(() => {
|
|
1365
1707
|
if (status === "ready") {
|
|
1366
1708
|
requestCapabilities();
|
|
@@ -1451,12 +1793,9 @@ function ReactorController({
|
|
|
1451
1793
|
}
|
|
1452
1794
|
});
|
|
1453
1795
|
console.log(`Executing command: ${commandName}`, data);
|
|
1454
|
-
yield
|
|
1455
|
-
type: commandName,
|
|
1456
|
-
data
|
|
1457
|
-
});
|
|
1796
|
+
yield sendCommand(commandName, data);
|
|
1458
1797
|
}),
|
|
1459
|
-
[formValues,
|
|
1798
|
+
[formValues, sendCommand, commands]
|
|
1460
1799
|
);
|
|
1461
1800
|
const renderInput = (commandName, paramName, paramSchema) => {
|
|
1462
1801
|
var _a, _b;
|
|
@@ -1982,6 +2321,7 @@ function WebcamStream({
|
|
|
1982
2321
|
}
|
|
1983
2322
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1984
2323
|
0 && (module.exports = {
|
|
2324
|
+
PROD_COORDINATOR_URL,
|
|
1985
2325
|
Reactor,
|
|
1986
2326
|
ReactorController,
|
|
1987
2327
|
ReactorProvider,
|