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