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