@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/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
- import { z as z2 } from "zod";
126
- var OptionsSchema = z2.object({
127
- wsUrl: z2.string().nonempty(),
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.eventListeners = /* @__PURE__ */ new Map();
139
- const validatedOptions = OptionsSchema.parse(options);
140
- this.wsUrl = validatedOptions.wsUrl;
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
- off(event, handler) {
155
- var _a;
156
- (_a = this.eventListeners.get(event)) == null ? void 0 : _a.delete(handler);
63
+ /**
64
+ * Returns the authorization header with JWT Bearer token
65
+ */
66
+ getAuthHeaders() {
67
+ return {
68
+ Authorization: `Bearer ${this.jwtToken}`
69
+ };
157
70
  }
158
- emit(event, ...args) {
159
- var _a;
160
- (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
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
- sendMessage(message) {
163
- var _a;
164
- try {
165
- const messageStr = typeof message === "string" ? message : JSON.stringify(message);
166
- (_a = this.websocket) == null ? void 0 : _a.send(messageStr);
167
- } catch (error) {
168
- console.error("[CoordinatorClient] Failed to send message:", error);
169
- this.emit("statusChanged", "error");
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
- connect() {
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
- const url = new URL(this.wsUrl);
175
- if (this.jwtToken) {
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
- if (this.queueing) {
181
- url.searchParams.set("queueing", "true");
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
- console.debug("[CoordinatorClient] Connecting to", url.toString());
184
- this.websocket = new WebSocket(url.toString());
185
- this.websocket.onopen = () => {
186
- console.debug("[CoordinatorClient] WebSocket opened");
187
- this.emit("statusChanged", "connected");
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
- this.websocket.onmessage = (event) => {
190
- try {
191
- let parsedData;
192
- if (typeof event.data === "string") {
193
- parsedData = JSON.parse(event.data);
194
- } else {
195
- parsedData = event.data;
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
- console.debug(
198
- "[CoordinatorClient] Received message from coordinator:",
199
- parsedData
200
- );
201
- const validatedData = CoordinatorMessageSchema.parse(parsedData);
202
- this.emit(validatedData.type, validatedData.data);
203
- } catch (error) {
204
- console.error(
205
- "[CoordinatorClient] Failed to parse WebSocket message from coordinator:",
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.emit("statusChanged", "error");
258
+ yield this.sleep(backoffMs);
259
+ backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
260
+ continue;
211
261
  }
212
- };
213
- this.websocket.onclose = (event) => {
214
- console.debug("[CoordinatorClient] WebSocket closed", event);
215
- this.websocket = void 0;
216
- this.emit("statusChanged", "disconnected");
217
- };
218
- this.websocket.onerror = (error) => {
219
- console.error("[CoordinatorClient] WebSocket error:", error);
220
- this.websocket = void 0;
221
- this.emit("statusChanged", "error");
222
- };
223
- yield new Promise((resolve, reject) => {
224
- var _a, _b;
225
- const onOpen = () => {
226
- var _a2;
227
- (_a2 = this.websocket) == null ? void 0 : _a2.removeEventListener("error", onError);
228
- resolve();
229
- };
230
- const onError = (error) => {
231
- var _a2;
232
- (_a2 = this.websocket) == null ? void 0 : _a2.removeEventListener("open", onOpen);
233
- reject(error);
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
- console.debug("[CoordinatorClient] Setup session message sent");
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
- * Closes the WebSocket connection if it exists.
251
- * This will trigger the onclose event handler.
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
- disconnect() {
254
- if (this.websocket) {
255
- console.debug("[CoordinatorClient] Closing WebSocket connection");
256
- this.websocket.close();
257
- this.websocket = void 0;
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(token, liveKitUrl) {
451
+ constructor(config) {
270
452
  this.eventListeners = /* @__PURE__ */ new Map();
271
- this.token = token;
272
- this.liveKitUrl = liveKitUrl;
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
- sendMessage(message) {
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
- try {
292
- if (this.roomInstance) {
293
- const messageStr = JSON.stringify(message);
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
- connect() {
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.roomInstance = new Room({
311
- adaptiveStream: true,
312
- dynacast: true
313
- });
314
- this.roomInstance.on(RoomEvent.Connected, () => {
315
- console.debug("[GPUMachineClient] Connected to room");
316
- this.emit("statusChanged", "connected");
317
- });
318
- this.roomInstance.on(RoomEvent.Disconnected, () => {
319
- console.debug("[GPUMachineClient] Disconnected from room");
320
- this.emit("statusChanged", "disconnected");
321
- });
322
- this.roomInstance.on(
323
- RoomEvent.TrackSubscribed,
324
- (track, _publication, participant) => {
325
- console.debug(
326
- "[GPUMachineClient] Track subscribed:",
327
- track.kind,
328
- participant.identity
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
- * Closes the LiveKit connection if it exists.
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.publishedVideoTrack) {
381
- yield this.unpublishVideoTrack();
532
+ if (this.publishedTrack) {
533
+ yield this.unpublishTrack();
382
534
  }
383
- if (this.publishedAudioTrack) {
384
- yield this.unpublishAudioTrack();
535
+ if (this.dataChannel) {
536
+ this.dataChannel.close();
537
+ this.dataChannel = void 0;
385
538
  }
386
- if (this.roomInstance) {
387
- console.debug("[GPUMachineClient] Closing LiveKit connection");
388
- yield this.roomInstance.disconnect();
389
- this.roomInstance = void 0;
539
+ if (this.peerConnection) {
540
+ closePeerConnection(this.peerConnection);
541
+ this.peerConnection = void 0;
390
542
  }
391
- this.machineFPS = void 0;
543
+ this.videoTransceiver = void 0;
544
+ this.setStatus("disconnected");
545
+ console.debug("[GPUMachineClient] Disconnected");
392
546
  });
393
547
  }
394
548
  /**
395
- * Returns the current fps rate of the machine.
396
- * @returns The current fps rate of the machine.
549
+ * Returns the current connection status.
397
550
  */
398
- getFPS() {
399
- return this.machineFPS;
551
+ getStatus() {
552
+ return this.status;
400
553
  }
401
554
  /**
402
- * Returns the current video stream from the GPU machine.
403
- * @returns The current video stream or undefined if not available.
555
+ * Gets the current local SDP description.
404
556
  */
405
- getVideoStream() {
406
- return this.videoStream;
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
- * Publishes a video track from the provided MediaStream to the LiveKit room.
410
- * Only one video track can be published at a time. If a video track is already
411
- * published, it will be unpublished first.
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
- publishVideoTrack(mediaStream) {
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.roomInstance) {
594
+ if (!this.peerConnection) {
419
595
  throw new Error(
420
- "[GPUMachineClient] Cannot publish track - not connected to room"
596
+ "[GPUMachineClient] Cannot publish track - not initialized"
421
597
  );
422
598
  }
423
- const videoTracks = mediaStream.getVideoTracks();
424
- if (videoTracks.length === 0) {
425
- throw new Error("[GPUMachineClient] No video track found in MediaStream");
599
+ if (this.status !== "connected") {
600
+ throw new Error(
601
+ "[GPUMachineClient] Cannot publish track - not connected"
602
+ );
426
603
  }
427
- if (this.publishedVideoTrack) {
428
- yield this.unpublishVideoTrack();
604
+ if (!this.videoTransceiver) {
605
+ throw new Error(
606
+ "[GPUMachineClient] Cannot publish track - no video transceiver"
607
+ );
429
608
  }
430
609
  try {
431
- const videoTrack = videoTracks[0];
432
- const localVideoTrack = yield this.roomInstance.localParticipant.publishTrack(videoTrack, {
433
- name: "client-video",
434
- source: Track.Source.Camera
435
- });
436
- this.publishedVideoTrack = localVideoTrack.track;
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 video track:", error);
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 video track.
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
- unpublishVideoTrack() {
625
+ unpublishTrack() {
450
626
  return __async(this, null, function* () {
451
- if (!this.roomInstance || !this.publishedVideoTrack) {
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.roomInstance.localParticipant.unpublishTrack(
458
- publishedVideoTrack,
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
- * Publishes an audio track from the provided MediaStream to the LiveKit room.
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
- publishAudioTrack(mediaStream) {
480
- return __async(this, null, function* () {
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
- * Unpublishes the currently published audio track.
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
- unpublishAudioTrack() {
514
- return __async(this, null, function* () {
515
- if (!this.roomInstance || !this.publishedAudioTrack) {
516
- return;
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
- const publishedAudioTrack = this.publishedAudioTrack;
519
- this.publishedAudioTrack = void 0;
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
- yield this.roomInstance.localParticipant.unpublishTrack(
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 unpublish audio track:",
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 as z3 } from "zod";
540
- var LOCAL_COORDINATOR_URL = "ws://localhost:8080/ws";
541
- var LOCAL_INSECURE_API_KEY = "1234";
542
- var PROD_COORDINATOR_URL = "wss://api.reactor.inc/ws";
543
- var OptionsSchema2 = z3.object({
544
- coordinatorUrl: z3.string().default(PROD_COORDINATOR_URL),
545
- modelName: z3.string(),
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 = OptionsSchema2.parse(options);
748
+ const validatedOptions = OptionsSchema.parse(options);
556
749
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
557
- this.modelName = validatedOptions.modelName;
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
- sendMessage(message) {
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.error("[Reactor] Not ready, cannot send message");
592
- throw new Error(errorMessage);
782
+ console.warn("[Reactor]", errorMessage);
783
+ return;
593
784
  }
594
785
  try {
595
- const applicationMessage = {
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 video stream to the machine.
613
- * @param videoStream The video stream to send to the machine.
799
+ * Public method to publish a track to the machine.
800
+ * @param track The track to send to the machine.
614
801
  */
615
- publishVideoStream(videoStream) {
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 video stream, status is ${this.status}`;
620
- console.error("[Reactor] Not ready, cannot publish video stream");
621
- throw new Error(errorMessage);
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.publishVideoTrack(videoStream);
811
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
625
812
  } catch (error) {
626
- console.error("[Reactor] Failed to publish video:", error);
813
+ console.error("[Reactor] Failed to publish track:", error);
627
814
  this.createError(
628
- "VIDEO_PUBLISH_FAILED",
629
- `Failed to publish video: ${error}`,
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 video stream to the machine.
638
- * This unpublishes the video track that was previously sent.
824
+ * Public method to unpublish the currently published track.
639
825
  */
640
- unpublishVideoStream() {
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.unpublishVideoTrack();
830
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
645
831
  } catch (error) {
646
- console.error("[Reactor] Failed to unpublish video:", error);
832
+ console.error("[Reactor] Failed to unpublish track:", error);
647
833
  this.createError(
648
- "VIDEO_UNPUBLISH_FAILED",
649
- `Failed to unpublish video: ${error}`,
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
- * Connects to the machine via LiveKit and waits for the gpu machine to be ready.
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
- connectToGPUMachine(livekitJwtToken, livekitWsUrl) {
845
+ reconnect() {
663
846
  return __async(this, null, function* () {
664
- console.debug("[Reactor] Connecting to machine room...");
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
- this.machineClient = new GPUMachineClient(livekitJwtToken, livekitWsUrl);
667
- this.machineClient.on("application", (message) => {
668
- this.emit("newMessage", message);
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.on("fps", (fps) => {
693
- this.emit("fps", fps);
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
- throw error;
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 LiveKit.
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(auth) {
881
+ connect(jwtToken) {
711
882
  return __async(this, null, function* () {
712
883
  console.debug("[Reactor] Connecting, status:", this.status);
713
- if (auth == void 0) {
714
- if (!this.local) {
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
- wsUrl: this.coordinatorUrl,
730
- jwtToken: auth.jwtToken,
731
- insecureApiKey: auth.insecureApiKey,
732
- modelName: this.modelName,
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.coordinatorClient.on(
737
- "gpu-machine-assigned",
738
- (assignmentData) => __async(this, null, function* () {
739
- console.debug(
740
- "[Reactor] GPU machine assigned by coordinator:",
741
- assignmentData
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] Authentication failed:", error);
909
+ console.error("[Reactor] Connection failed:", error);
803
910
  this.createError(
804
- "AUTHENTICATION_FAILED",
805
- `Authentication failed: ${error}`,
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") return;
821
- if (this.coordinatorClient) {
822
- try {
823
- this.coordinatorClient.disconnect();
824
- } catch (error) {
825
- console.error("[Reactor] Error disconnecting from coordinator:", error);
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
- this.machineClient = void 0;
975
+ if (!recoverable) {
976
+ this.machineClient = void 0;
977
+ }
836
978
  }
837
979
  this.setStatus("disconnected");
838
- this.setSessionExpiration(void 0);
839
- this.setWaitingInfo(void 0);
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
- setWaitingInfo(newWaitingInfo) {
850
- console.debug(
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
- videoTrackSid: videoTrack == null ? void 0 : videoTrack.sid
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
- sendMessage: (mess) => __async(null, null, function* () {
1006
- console.debug("[ReactorStore] Sending message", { message: mess });
1134
+ sendCommand: (command, data) => __async(null, null, function* () {
1135
+ console.debug("[ReactorStore] Sending command", { command, data });
1007
1136
  try {
1008
- yield get().internal.reactor.sendMessage(mess);
1009
- console.debug("[ReactorStore] Message sent successfully");
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 message:", error);
1140
+ console.error("[ReactorStore] Failed to send command:", error);
1012
1141
  throw error;
1013
1142
  }
1014
1143
  }),
1015
- connect: (auth) => __async(null, null, function* () {
1016
- console.debug("auth", auth);
1017
- if (auth === void 0) {
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", { auth });
1148
+ console.debug("[ReactorStore] Connect called.");
1024
1149
  try {
1025
- yield get().internal.reactor.connect(auth);
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.publishVideoStream(stream);
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.unpublishVideoStream();
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, queueing, local } = props;
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" && (insecureApiKey || jwtToken)) {
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" && (insecureApiKey || jwtToken)) {
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.attach(videoRef.current);
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
- try {
1257
- videoTrack.detach(videoRef.current);
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 { sendMessage, status } = useReactor((state) => ({
1327
- sendMessage: state.sendMessage,
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
- sendMessage({
1343
- type: "requestCapabilities",
1344
- data: {}
1345
- });
1471
+ sendCommand("requestCapabilities", {});
1346
1472
  }
1347
- }, [status, sendMessage]);
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 sendMessage({
1439
- type: commandName,
1440
- data
1441
- });
1564
+ yield sendCommand(commandName, data);
1442
1565
  }),
1443
- [formValues, sendMessage, commands]
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,