@reactor-team/js-sdk 2.4.0 → 2.5.1

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.js CHANGED
@@ -79,6 +79,7 @@ var __async = (__this, __arguments, generator) => {
79
79
  // src/index.ts
80
80
  var index_exports = {};
81
81
  __export(index_exports, {
82
+ AbortError: () => AbortError,
82
83
  ConflictError: () => ConflictError,
83
84
  PROD_COORDINATOR_URL: () => PROD_COORDINATOR_URL,
84
85
  Reactor: () => Reactor,
@@ -86,21 +87,38 @@ __export(index_exports, {
86
87
  ReactorProvider: () => ReactorProvider,
87
88
  ReactorView: () => ReactorView,
88
89
  WebcamStream: () => WebcamStream,
90
+ audio: () => audio,
89
91
  fetchInsecureJwtToken: () => fetchInsecureJwtToken,
92
+ isAbortError: () => isAbortError,
90
93
  useReactor: () => useReactor,
91
94
  useReactorInternalMessage: () => useReactorInternalMessage,
92
95
  useReactorMessage: () => useReactorMessage,
93
96
  useReactorStore: () => useReactorStore,
94
- useStats: () => useStats
97
+ useStats: () => useStats,
98
+ video: () => video
95
99
  });
96
100
  module.exports = __toCommonJS(index_exports);
97
101
 
98
102
  // src/types.ts
103
+ function video(name, _options) {
104
+ return { name, kind: "video" };
105
+ }
106
+ function audio(name, _options) {
107
+ return { name, kind: "audio" };
108
+ }
99
109
  var ConflictError = class extends Error {
100
110
  constructor(message) {
101
111
  super(message);
102
112
  }
103
113
  };
114
+ var AbortError = class extends Error {
115
+ constructor(message) {
116
+ super(message);
117
+ }
118
+ };
119
+ function isAbortError(error) {
120
+ return error instanceof AbortError || error instanceof Error && error.name === "AbortError";
121
+ }
104
122
 
105
123
  // src/core/types.ts
106
124
  var import_zod = require("zod");
@@ -169,18 +187,105 @@ function createPeerConnection(config) {
169
187
  function createDataChannel(pc, label) {
170
188
  return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
171
189
  }
172
- function createOffer(pc) {
190
+ function rewriteMids(sdp, trackNames) {
191
+ const lines = sdp.split("\r\n");
192
+ let mediaIdx = 0;
193
+ const replacements = /* @__PURE__ */ new Map();
194
+ let inApplication = false;
195
+ for (let i = 0; i < lines.length; i++) {
196
+ if (lines[i].startsWith("m=")) {
197
+ inApplication = lines[i].startsWith("m=application");
198
+ }
199
+ if (!inApplication && lines[i].startsWith("a=mid:")) {
200
+ const oldMid = lines[i].substring("a=mid:".length);
201
+ if (mediaIdx < trackNames.length) {
202
+ const newMid = trackNames[mediaIdx];
203
+ replacements.set(oldMid, newMid);
204
+ lines[i] = `a=mid:${newMid}`;
205
+ mediaIdx++;
206
+ }
207
+ }
208
+ }
209
+ for (let i = 0; i < lines.length; i++) {
210
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
211
+ const parts = lines[i].split(" ");
212
+ for (let j = 1; j < parts.length; j++) {
213
+ const replacement = replacements.get(parts[j]);
214
+ if (replacement !== void 0) {
215
+ parts[j] = replacement;
216
+ }
217
+ }
218
+ lines[i] = parts.join(" ");
219
+ break;
220
+ }
221
+ }
222
+ return lines.join("\r\n");
223
+ }
224
+ function createOffer(pc, trackNames) {
173
225
  return __async(this, null, function* () {
174
226
  const offer = yield pc.createOffer();
175
- yield pc.setLocalDescription(offer);
227
+ let needsAnswerRestore = false;
228
+ if (trackNames && trackNames.length > 0 && offer.sdp) {
229
+ const munged = rewriteMids(offer.sdp, trackNames);
230
+ try {
231
+ yield pc.setLocalDescription(
232
+ new RTCSessionDescription({ type: "offer", sdp: munged })
233
+ );
234
+ } catch (e) {
235
+ yield pc.setLocalDescription(offer);
236
+ needsAnswerRestore = true;
237
+ }
238
+ } else {
239
+ yield pc.setLocalDescription(offer);
240
+ }
176
241
  yield waitForIceGathering(pc);
177
242
  const localDescription = pc.localDescription;
178
243
  if (!localDescription) {
179
244
  throw new Error("Failed to create local description");
180
245
  }
181
- return localDescription.sdp;
246
+ let sdp = localDescription.sdp;
247
+ if (needsAnswerRestore && trackNames && trackNames.length > 0) {
248
+ sdp = rewriteMids(sdp, trackNames);
249
+ }
250
+ return { sdp, needsAnswerRestore };
182
251
  });
183
252
  }
253
+ function buildMidMapping(transceivers) {
254
+ var _a;
255
+ const localToRemote = /* @__PURE__ */ new Map();
256
+ const remoteToLocal = /* @__PURE__ */ new Map();
257
+ for (const entry of transceivers) {
258
+ const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
259
+ if (mid) {
260
+ localToRemote.set(mid, entry.name);
261
+ remoteToLocal.set(entry.name, mid);
262
+ }
263
+ }
264
+ return { localToRemote, remoteToLocal };
265
+ }
266
+ function restoreAnswerMids(sdp, remoteToLocal) {
267
+ const lines = sdp.split("\r\n");
268
+ for (let i = 0; i < lines.length; i++) {
269
+ if (lines[i].startsWith("a=mid:")) {
270
+ const remoteMid = lines[i].substring("a=mid:".length);
271
+ const localMid = remoteToLocal.get(remoteMid);
272
+ if (localMid !== void 0) {
273
+ lines[i] = `a=mid:${localMid}`;
274
+ }
275
+ }
276
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
277
+ const parts = lines[i].split(" ");
278
+ for (let j = 1; j < parts.length; j++) {
279
+ const localMid = remoteToLocal.get(parts[j]);
280
+ if (localMid !== void 0) {
281
+ parts[j] = localMid;
282
+ }
283
+ }
284
+ lines[i] = parts.join(" ");
285
+ }
286
+ }
287
+ return lines.join("\r\n");
288
+ }
184
289
  function setRemoteDescription(pc, sdp) {
185
290
  return __async(this, null, function* () {
186
291
  const sessionDescription = new RTCSessionDescription({
@@ -308,6 +413,22 @@ var CoordinatorClient = class {
308
413
  this.baseUrl = options.baseUrl;
309
414
  this.jwtToken = options.jwtToken;
310
415
  this.model = options.model;
416
+ this.abortController = new AbortController();
417
+ }
418
+ /**
419
+ * Aborts any in-flight HTTP requests and polling loops.
420
+ * A fresh AbortController is created so the client remains reusable.
421
+ */
422
+ abort() {
423
+ this.abortController.abort();
424
+ this.abortController = new AbortController();
425
+ }
426
+ /**
427
+ * The current abort signal, passed to every fetch() and sleep() call.
428
+ * Protected so subclasses can forward it to their own fetch calls.
429
+ */
430
+ get signal() {
431
+ return this.abortController.signal;
311
432
  }
312
433
  /**
313
434
  * Returns the authorization header with JWT Bearer token
@@ -328,7 +449,8 @@ var CoordinatorClient = class {
328
449
  `${this.baseUrl}/ice_servers?model=${this.model}`,
329
450
  {
330
451
  method: "GET",
331
- headers: this.getAuthHeaders()
452
+ headers: this.getAuthHeaders(),
453
+ signal: this.signal
332
454
  }
333
455
  );
334
456
  if (!response.ok) {
@@ -362,7 +484,8 @@ var CoordinatorClient = class {
362
484
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
363
485
  "Content-Type": "application/json"
364
486
  }),
365
- body: JSON.stringify(requestBody)
487
+ body: JSON.stringify(requestBody),
488
+ signal: this.signal
366
489
  });
367
490
  if (!response.ok) {
368
491
  const errorText = yield response.text();
@@ -396,7 +519,8 @@ var CoordinatorClient = class {
396
519
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
397
520
  {
398
521
  method: "GET",
399
- headers: this.getAuthHeaders()
522
+ headers: this.getAuthHeaders(),
523
+ signal: this.signal
400
524
  }
401
525
  );
402
526
  if (!response.ok) {
@@ -409,12 +533,13 @@ var CoordinatorClient = class {
409
533
  }
410
534
  /**
411
535
  * Terminates the current session by sending a DELETE request to the coordinator.
412
- * @throws Error if no active session exists or if the request fails (except for 404)
536
+ * No-op if no session has been created yet.
537
+ * @throws Error if the request fails (except for 404, which clears local state)
413
538
  */
414
539
  terminateSession() {
415
540
  return __async(this, null, function* () {
416
541
  if (!this.currentSessionId) {
417
- throw new Error("No active session. Call createSession() first.");
542
+ return;
418
543
  }
419
544
  console.debug(
420
545
  "[CoordinatorClient] Terminating session:",
@@ -424,7 +549,8 @@ var CoordinatorClient = class {
424
549
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
425
550
  {
426
551
  method: "DELETE",
427
- headers: this.getAuthHeaders()
552
+ headers: this.getAuthHeaders(),
553
+ signal: this.signal
428
554
  }
429
555
  );
430
556
  if (response.ok) {
@@ -474,7 +600,8 @@ var CoordinatorClient = class {
474
600
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
475
601
  "Content-Type": "application/json"
476
602
  }),
477
- body: JSON.stringify(requestBody)
603
+ body: JSON.stringify(requestBody),
604
+ signal: this.signal
478
605
  }
479
606
  );
480
607
  if (response.status === 200) {
@@ -510,6 +637,9 @@ var CoordinatorClient = class {
510
637
  let backoffMs = INITIAL_BACKOFF_MS;
511
638
  let attempt = 0;
512
639
  while (true) {
640
+ if (this.signal.aborted) {
641
+ throw new AbortError("SDP polling aborted");
642
+ }
513
643
  if (attempt >= maxAttempts) {
514
644
  throw new Error(
515
645
  `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
@@ -525,7 +655,8 @@ var CoordinatorClient = class {
525
655
  method: "GET",
526
656
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
527
657
  "Content-Type": "application/json"
528
- })
658
+ }),
659
+ signal: this.signal
529
660
  }
530
661
  );
531
662
  if (response.status === 200) {
@@ -570,10 +701,26 @@ var CoordinatorClient = class {
570
701
  });
571
702
  }
572
703
  /**
573
- * Utility function to sleep for a given number of milliseconds
704
+ * Abort-aware sleep. Resolves after `ms` milliseconds unless the
705
+ * abort signal fires first, in which case it rejects with AbortError.
574
706
  */
575
707
  sleep(ms) {
576
- return new Promise((resolve) => setTimeout(resolve, ms));
708
+ return new Promise((resolve, reject) => {
709
+ const { signal } = this;
710
+ if (signal.aborted) {
711
+ reject(new AbortError("Sleep aborted"));
712
+ return;
713
+ }
714
+ const timer = setTimeout(() => {
715
+ signal.removeEventListener("abort", onAbort);
716
+ resolve();
717
+ }, ms);
718
+ const onAbort = () => {
719
+ clearTimeout(timer);
720
+ reject(new AbortError("Sleep aborted"));
721
+ };
722
+ signal.addEventListener("abort", onAbort, { once: true });
723
+ });
577
724
  }
578
725
  };
579
726
 
@@ -595,7 +742,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
595
742
  return __async(this, null, function* () {
596
743
  console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
597
744
  const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
598
- method: "GET"
745
+ method: "GET",
746
+ signal: this.signal
599
747
  });
600
748
  if (!response.ok) {
601
749
  throw new Error("Failed to get ICE servers from local coordinator.");
@@ -619,7 +767,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
619
767
  console.debug("[LocalCoordinatorClient] Creating local session...");
620
768
  this.sdpOffer = sdpOffer;
621
769
  const response = yield fetch(`${this.localBaseUrl}/start_session`, {
622
- method: "POST"
770
+ method: "POST",
771
+ signal: this.signal
623
772
  });
624
773
  if (!response.ok) {
625
774
  throw new Error("Failed to send local start session command.");
@@ -647,7 +796,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
647
796
  headers: {
648
797
  "Content-Type": "application/json"
649
798
  },
650
- body: JSON.stringify(sdpBody)
799
+ body: JSON.stringify(sdpBody),
800
+ signal: this.signal
651
801
  });
652
802
  if (!response.ok) {
653
803
  if (response.status === 409) {
@@ -664,7 +814,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
664
814
  return __async(this, null, function* () {
665
815
  console.debug("[LocalCoordinatorClient] Stopping local session...");
666
816
  yield fetch(`${this.localBaseUrl}/stop_session`, {
667
- method: "POST"
817
+ method: "POST",
818
+ signal: this.signal
668
819
  });
669
820
  });
670
821
  }
@@ -677,6 +828,10 @@ var GPUMachineClient = class {
677
828
  constructor(config) {
678
829
  this.eventListeners = /* @__PURE__ */ new Map();
679
830
  this.status = "disconnected";
831
+ this.transceiverMap = /* @__PURE__ */ new Map();
832
+ this.publishedTracks = /* @__PURE__ */ new Map();
833
+ this.peerConnected = false;
834
+ this.dataChannelOpen = false;
680
835
  this.config = config;
681
836
  }
682
837
  // ─────────────────────────────────────────────────────────────────────────────
@@ -700,10 +855,18 @@ var GPUMachineClient = class {
700
855
  // SDP & Connection
701
856
  // ─────────────────────────────────────────────────────────────────────────────
702
857
  /**
703
- * Creates an SDP offer for initiating a connection.
858
+ * Creates an SDP offer based on the declared tracks.
859
+ *
860
+ * **RECEIVE** = client receives from the model (model → client) → `recvonly`
861
+ * **SEND** = client sends to the model (client → model) → `sendonly`
862
+ *
863
+ * Track names must be unique across both arrays. A name that appears in
864
+ * both `receive` and `send` will throw — use distinct names instead.
865
+ *
866
+ * The data channel is always created first (before transceivers).
704
867
  * Must be called before connect().
705
868
  */
706
- createOffer() {
869
+ createOffer(tracks) {
707
870
  return __async(this, null, function* () {
708
871
  if (!this.peerConnection) {
709
872
  this.peerConnection = createPeerConnection(this.config);
@@ -714,14 +877,63 @@ var GPUMachineClient = class {
714
877
  this.config.dataChannelLabel
715
878
  );
716
879
  this.setupDataChannelHandlers();
717
- this.videoTransceiver = this.peerConnection.addTransceiver("video", {
718
- direction: "sendrecv"
719
- });
720
- const offer = yield createOffer(this.peerConnection);
721
- console.debug("[GPUMachineClient] Created SDP offer");
722
- return offer;
880
+ this.transceiverMap.clear();
881
+ const entries = this.buildTransceiverEntries(tracks);
882
+ for (const entry of entries) {
883
+ const transceiver = this.peerConnection.addTransceiver(entry.kind, {
884
+ direction: entry.direction
885
+ });
886
+ entry.transceiver = transceiver;
887
+ this.transceiverMap.set(entry.name, entry);
888
+ console.debug(
889
+ `[GPUMachineClient] Transceiver added: "${entry.name}" (${entry.kind}, ${entry.direction})`
890
+ );
891
+ }
892
+ const trackNames = entries.map((e) => e.name);
893
+ const { sdp, needsAnswerRestore } = yield createOffer(
894
+ this.peerConnection,
895
+ trackNames
896
+ );
897
+ if (needsAnswerRestore) {
898
+ this.midMapping = buildMidMapping(entries);
899
+ } else {
900
+ this.midMapping = void 0;
901
+ }
902
+ console.debug(
903
+ "[GPUMachineClient] Created SDP offer with MIDs:",
904
+ trackNames,
905
+ needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
906
+ );
907
+ return sdp;
723
908
  });
724
909
  }
910
+ /**
911
+ * Builds an ordered list of transceiver entries from the receive/send arrays.
912
+ *
913
+ * Each track produces exactly one transceiver — `recvonly` for receive,
914
+ * `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
915
+ * supported; the same track name in both arrays is an error.
916
+ */
917
+ buildTransceiverEntries(tracks) {
918
+ const map = /* @__PURE__ */ new Map();
919
+ for (const t of tracks.receive) {
920
+ if (map.has(t.name)) {
921
+ throw new Error(
922
+ `Duplicate receive track name "${t.name}". Track names must be unique.`
923
+ );
924
+ }
925
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
926
+ }
927
+ for (const t of tracks.send) {
928
+ if (map.has(t.name)) {
929
+ throw new Error(
930
+ `Track name "${t.name}" appears in both receive and send. Bidirectional tracks are not supported \u2014 use distinct names for the inbound and outbound directions (e.g. "${t.name}_in" and "${t.name}_out").`
931
+ );
932
+ }
933
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
934
+ }
935
+ return Array.from(map.values());
936
+ }
725
937
  /**
726
938
  * Connects to the GPU machine using the provided SDP answer.
727
939
  * createOffer() must be called first.
@@ -741,7 +953,14 @@ var GPUMachineClient = class {
741
953
  }
742
954
  this.setStatus("connecting");
743
955
  try {
744
- yield setRemoteDescription(this.peerConnection, sdpAnswer);
956
+ let answer = sdpAnswer;
957
+ if (this.midMapping) {
958
+ answer = restoreAnswerMids(
959
+ answer,
960
+ this.midMapping.remoteToLocal
961
+ );
962
+ }
963
+ yield setRemoteDescription(this.peerConnection, answer);
745
964
  console.debug("[GPUMachineClient] Remote description set");
746
965
  } catch (error) {
747
966
  console.error("[GPUMachineClient] Failed to connect:", error);
@@ -757,8 +976,8 @@ var GPUMachineClient = class {
757
976
  return __async(this, null, function* () {
758
977
  this.stopPing();
759
978
  this.stopStatsPolling();
760
- if (this.publishedTrack) {
761
- yield this.unpublishTrack();
979
+ for (const name of Array.from(this.publishedTracks.keys())) {
980
+ yield this.unpublishTrack(name);
762
981
  }
763
982
  if (this.dataChannel) {
764
983
  this.dataChannel.close();
@@ -768,7 +987,10 @@ var GPUMachineClient = class {
768
987
  closePeerConnection(this.peerConnection);
769
988
  this.peerConnection = void 0;
770
989
  }
771
- this.videoTransceiver = void 0;
990
+ this.transceiverMap.clear();
991
+ this.midMapping = void 0;
992
+ this.peerConnected = false;
993
+ this.dataChannelOpen = false;
772
994
  this.setStatus("disconnected");
773
995
  console.debug("[GPUMachineClient] Disconnected");
774
996
  });
@@ -796,7 +1018,7 @@ var GPUMachineClient = class {
796
1018
  /**
797
1019
  * Sends a command to the GPU machine via the data channel.
798
1020
  * @param command The command to send
799
- * @param data The data to send with the command. These are the parameters for the command, matching the scheme in the capabilities dictionary.
1021
+ * @param data The data to send with the command. These are the parameters for the command, matching the schema in the capabilities dictionary.
800
1022
  * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
801
1023
  */
802
1024
  sendCommand(command, data, scope = "application") {
@@ -813,63 +1035,77 @@ var GPUMachineClient = class {
813
1035
  // Track Publishing
814
1036
  // ─────────────────────────────────────────────────────────────────────────────
815
1037
  /**
816
- * Publishes a track to the GPU machine.
817
- * Only one track can be published at a time.
818
- * Uses the existing transceiver's sender to replace the track.
819
- * @param track The MediaStreamTrack to publish
1038
+ * Publishes a MediaStreamTrack to the named send track.
1039
+ *
1040
+ * @param name The declared track name (must exist in transceiverMap with a sendable direction).
1041
+ * @param track The MediaStreamTrack to publish.
820
1042
  */
821
- publishTrack(track) {
1043
+ publishTrack(name, track) {
822
1044
  return __async(this, null, function* () {
823
1045
  if (!this.peerConnection) {
824
1046
  throw new Error(
825
- "[GPUMachineClient] Cannot publish track - not initialized"
1047
+ `[GPUMachineClient] Cannot publish track "${name}" - not initialized`
826
1048
  );
827
1049
  }
828
1050
  if (this.status !== "connected") {
829
1051
  throw new Error(
830
- "[GPUMachineClient] Cannot publish track - not connected"
1052
+ `[GPUMachineClient] Cannot publish track "${name}" - not connected`
1053
+ );
1054
+ }
1055
+ const entry = this.transceiverMap.get(name);
1056
+ if (!entry || !entry.transceiver) {
1057
+ throw new Error(
1058
+ `[GPUMachineClient] Cannot publish track "${name}" - no transceiver (was it declared in tracks.send?)`
831
1059
  );
832
1060
  }
833
- if (!this.videoTransceiver) {
1061
+ if (entry.direction === "recvonly") {
834
1062
  throw new Error(
835
- "[GPUMachineClient] Cannot publish track - no video transceiver"
1063
+ `[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
836
1064
  );
837
1065
  }
838
1066
  try {
839
- yield this.videoTransceiver.sender.replaceTrack(track);
840
- this.publishedTrack = track;
1067
+ yield entry.transceiver.sender.replaceTrack(track);
1068
+ this.publishedTracks.set(name, track);
841
1069
  console.debug(
842
- "[GPUMachineClient] Track published successfully:",
843
- track.kind
1070
+ `[GPUMachineClient] Track "${name}" published successfully`
844
1071
  );
845
1072
  } catch (error) {
846
- console.error("[GPUMachineClient] Failed to publish track:", error);
1073
+ console.error(
1074
+ `[GPUMachineClient] Failed to publish track "${name}":`,
1075
+ error
1076
+ );
847
1077
  throw error;
848
1078
  }
849
1079
  });
850
1080
  }
851
1081
  /**
852
- * Unpublishes the currently published track.
1082
+ * Unpublishes the track with the given name.
853
1083
  */
854
- unpublishTrack() {
1084
+ unpublishTrack(name) {
855
1085
  return __async(this, null, function* () {
856
- if (!this.videoTransceiver || !this.publishedTrack) return;
1086
+ const entry = this.transceiverMap.get(name);
1087
+ if (!(entry == null ? void 0 : entry.transceiver) || !this.publishedTracks.has(name)) return;
857
1088
  try {
858
- yield this.videoTransceiver.sender.replaceTrack(null);
859
- console.debug("[GPUMachineClient] Track unpublished successfully");
1089
+ yield entry.transceiver.sender.replaceTrack(null);
1090
+ console.debug(
1091
+ `[GPUMachineClient] Track "${name}" unpublished successfully`
1092
+ );
860
1093
  } catch (error) {
861
- console.error("[GPUMachineClient] Failed to unpublish track:", error);
1094
+ console.error(
1095
+ `[GPUMachineClient] Failed to unpublish track "${name}":`,
1096
+ error
1097
+ );
862
1098
  throw error;
863
1099
  } finally {
864
- this.publishedTrack = void 0;
1100
+ this.publishedTracks.delete(name);
865
1101
  }
866
1102
  });
867
1103
  }
868
1104
  /**
869
- * Returns the currently published track.
1105
+ * Returns the currently published track for the given name.
870
1106
  */
871
- getPublishedTrack() {
872
- return this.publishedTrack;
1107
+ getPublishedTrack(name) {
1108
+ return this.publishedTracks.get(name);
873
1109
  }
874
1110
  // ─────────────────────────────────────────────────────────────────────────────
875
1111
  // Getters
@@ -940,6 +1176,12 @@ var GPUMachineClient = class {
940
1176
  // ─────────────────────────────────────────────────────────────────────────────
941
1177
  // Private Helpers
942
1178
  // ─────────────────────────────────────────────────────────────────────────────
1179
+ checkFullyConnected() {
1180
+ if (this.peerConnected && this.dataChannelOpen) {
1181
+ this.setStatus("connected");
1182
+ this.startStatsPolling();
1183
+ }
1184
+ }
943
1185
  setStatus(newStatus) {
944
1186
  if (this.status !== newStatus) {
945
1187
  this.status = newStatus;
@@ -955,24 +1197,36 @@ var GPUMachineClient = class {
955
1197
  if (state) {
956
1198
  switch (state) {
957
1199
  case "connected":
958
- this.setStatus("connected");
959
- this.startStatsPolling();
1200
+ this.peerConnected = true;
1201
+ this.checkFullyConnected();
960
1202
  break;
961
1203
  case "disconnected":
962
1204
  case "closed":
1205
+ this.peerConnected = false;
963
1206
  this.setStatus("disconnected");
964
1207
  break;
965
1208
  case "failed":
1209
+ this.peerConnected = false;
966
1210
  this.setStatus("error");
967
1211
  break;
968
1212
  }
969
1213
  }
970
1214
  };
971
1215
  this.peerConnection.ontrack = (event) => {
972
- var _a;
973
- console.debug("[GPUMachineClient] Track received:", event.track.kind);
974
- const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
975
- this.emit("trackReceived", event.track, stream);
1216
+ var _a, _b;
1217
+ let trackName;
1218
+ for (const [name, entry] of this.transceiverMap) {
1219
+ if (entry.transceiver === event.transceiver) {
1220
+ trackName = name;
1221
+ break;
1222
+ }
1223
+ }
1224
+ trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
1225
+ console.debug(
1226
+ `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
1227
+ );
1228
+ const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
1229
+ this.emit("trackReceived", trackName, event.track, stream);
976
1230
  };
977
1231
  this.peerConnection.onicecandidate = (event) => {
978
1232
  if (event.candidate) {
@@ -992,10 +1246,13 @@ var GPUMachineClient = class {
992
1246
  if (!this.dataChannel) return;
993
1247
  this.dataChannel.onopen = () => {
994
1248
  console.debug("[GPUMachineClient] Data channel open");
1249
+ this.dataChannelOpen = true;
995
1250
  this.startPing();
1251
+ this.checkFullyConnected();
996
1252
  };
997
1253
  this.dataChannel.onclose = () => {
998
1254
  console.debug("[GPUMachineClient] Data channel closed");
1255
+ this.dataChannelOpen = false;
999
1256
  this.stopPing();
1000
1257
  };
1001
1258
  this.dataChannel.onerror = (error) => {
@@ -1029,10 +1286,29 @@ var GPUMachineClient = class {
1029
1286
  var import_zod2 = require("zod");
1030
1287
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
1031
1288
  var PROD_COORDINATOR_URL = "https://api.reactor.inc";
1289
+ var TrackConfigSchema = import_zod2.z.object({
1290
+ name: import_zod2.z.string(),
1291
+ kind: import_zod2.z.enum(["audio", "video"])
1292
+ });
1032
1293
  var OptionsSchema = import_zod2.z.object({
1033
1294
  coordinatorUrl: import_zod2.z.string().default(PROD_COORDINATOR_URL),
1034
1295
  modelName: import_zod2.z.string(),
1035
- local: import_zod2.z.boolean().default(false)
1296
+ local: import_zod2.z.boolean().default(false),
1297
+ /**
1298
+ * Tracks the client **RECEIVES** from the model (model → client).
1299
+ * Each entry produces a `recvonly` transceiver.
1300
+ * Names must be unique across both `receive` and `send`.
1301
+ *
1302
+ * When omitted, defaults to a single video track named `"main_video"`.
1303
+ * Pass an explicit empty array to opt out of the default.
1304
+ */
1305
+ receive: import_zod2.z.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
1306
+ /**
1307
+ * Tracks the client **SENDS** to the model (client → model).
1308
+ * Each entry produces a `sendonly` transceiver.
1309
+ * Names must be unique across both `receive` and `send`.
1310
+ */
1311
+ send: import_zod2.z.array(TrackConfigSchema).default([])
1036
1312
  });
1037
1313
  var Reactor = class {
1038
1314
  constructor(options) {
@@ -1043,7 +1319,9 @@ var Reactor = class {
1043
1319
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
1044
1320
  this.model = validatedOptions.modelName;
1045
1321
  this.local = validatedOptions.local;
1046
- if (this.local) {
1322
+ this.receive = validatedOptions.receive;
1323
+ this.send = validatedOptions.send;
1324
+ if (this.local && options.coordinatorUrl === void 0) {
1047
1325
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
1048
1326
  }
1049
1327
  }
@@ -1063,13 +1341,11 @@ var Reactor = class {
1063
1341
  (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
1064
1342
  }
1065
1343
  /**
1066
- * Public method to send a message to the machine.
1067
- * Wraps the message in the specified channel envelope (defaults to "application").
1068
- * @param command The command name to send.
1344
+ * Sends a command to the model via the data channel.
1345
+ *
1346
+ * @param command The command name.
1069
1347
  * @param data The command payload.
1070
- * @param scope The envelope scope – "application" (default) for model commands,
1071
- * "runtime" for platform-level messages (e.g. requestCapabilities).
1072
- * @throws Error if not in ready state
1348
+ * @param scope "application" (default) for model commands, "runtime" for platform messages.
1073
1349
  */
1074
1350
  sendCommand(command, data, scope = "application") {
1075
1351
  return __async(this, null, function* () {
@@ -1093,24 +1369,27 @@ var Reactor = class {
1093
1369
  });
1094
1370
  }
1095
1371
  /**
1096
- * Public method to publish a track to the machine.
1097
- * @param track The track to send to the machine.
1372
+ * Publishes a MediaStreamTrack to a named send track.
1373
+ *
1374
+ * @param name The declared send track name (e.g. "webcam").
1375
+ * @param track The MediaStreamTrack to publish.
1098
1376
  */
1099
- publishTrack(track) {
1377
+ publishTrack(name, track) {
1100
1378
  return __async(this, null, function* () {
1101
1379
  var _a;
1102
1380
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
1103
- const errorMessage = `Cannot publish track, status is ${this.status}`;
1104
- console.warn("[Reactor]", errorMessage);
1381
+ console.warn(
1382
+ `[Reactor] Cannot publish track "${name}", status is ${this.status}`
1383
+ );
1105
1384
  return;
1106
1385
  }
1107
1386
  try {
1108
- yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
1387
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
1109
1388
  } catch (error) {
1110
- console.error("[Reactor] Failed to publish track:", error);
1389
+ console.error(`[Reactor] Failed to publish track "${name}":`, error);
1111
1390
  this.createError(
1112
1391
  "TRACK_PUBLISH_FAILED",
1113
- `Failed to publish track: ${error}`,
1392
+ `Failed to publish track "${name}": ${error}`,
1114
1393
  "gpu",
1115
1394
  true
1116
1395
  );
@@ -1118,18 +1397,20 @@ var Reactor = class {
1118
1397
  });
1119
1398
  }
1120
1399
  /**
1121
- * Public method to unpublish the currently published track.
1400
+ * Unpublishes the track with the given name.
1401
+ *
1402
+ * @param name The declared send track name to unpublish.
1122
1403
  */
1123
- unpublishTrack() {
1404
+ unpublishTrack(name) {
1124
1405
  return __async(this, null, function* () {
1125
1406
  var _a;
1126
1407
  try {
1127
- yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
1408
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
1128
1409
  } catch (error) {
1129
- console.error("[Reactor] Failed to unpublish track:", error);
1410
+ console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
1130
1411
  this.createError(
1131
1412
  "TRACK_UNPUBLISH_FAILED",
1132
- `Failed to unpublish track: ${error}`,
1413
+ `Failed to unpublish track "${name}": ${error}`,
1133
1414
  "gpu",
1134
1415
  true
1135
1416
  );
@@ -1156,7 +1437,10 @@ var Reactor = class {
1156
1437
  this.machineClient = new GPUMachineClient({ iceServers });
1157
1438
  this.setupMachineClientHandlers();
1158
1439
  }
1159
- const sdpOffer = yield this.machineClient.createOffer();
1440
+ const sdpOffer = yield this.machineClient.createOffer({
1441
+ send: this.send,
1442
+ receive: this.receive
1443
+ });
1160
1444
  try {
1161
1445
  const sdpAnswer = yield this.coordinatorClient.connect(
1162
1446
  this.sessionId,
@@ -1164,8 +1448,8 @@ var Reactor = class {
1164
1448
  options == null ? void 0 : options.maxAttempts
1165
1449
  );
1166
1450
  yield this.machineClient.connect(sdpAnswer);
1167
- this.setStatus("ready");
1168
1451
  } catch (error) {
1452
+ if (isAbortError(error)) return;
1169
1453
  let recoverable = false;
1170
1454
  if (error instanceof ConflictError) {
1171
1455
  recoverable = true;
@@ -1211,7 +1495,10 @@ var Reactor = class {
1211
1495
  const iceServers = yield this.coordinatorClient.getIceServers();
1212
1496
  this.machineClient = new GPUMachineClient({ iceServers });
1213
1497
  this.setupMachineClientHandlers();
1214
- const sdpOffer = yield this.machineClient.createOffer();
1498
+ const sdpOffer = yield this.machineClient.createOffer({
1499
+ send: this.send,
1500
+ receive: this.receive
1501
+ });
1215
1502
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1216
1503
  this.setSessionId(sessionId);
1217
1504
  const sdpAnswer = yield this.coordinatorClient.connect(
@@ -1221,6 +1508,7 @@ var Reactor = class {
1221
1508
  );
1222
1509
  yield this.machineClient.connect(sdpAnswer);
1223
1510
  } catch (error) {
1511
+ if (isAbortError(error)) return;
1224
1512
  console.error("[Reactor] Connection failed:", error);
1225
1513
  this.createError(
1226
1514
  "CONNECTION_FAILED",
@@ -1242,17 +1530,25 @@ var Reactor = class {
1242
1530
  }
1243
1531
  /**
1244
1532
  * Sets up event handlers for the machine client.
1533
+ *
1534
+ * Each handler captures the client reference at registration time and
1535
+ * ignores events if this.machineClient has since changed (e.g. after
1536
+ * disconnect + reconnect), preventing stale WebRTC teardown events from
1537
+ * interfering with a new connection.
1245
1538
  */
1246
1539
  setupMachineClientHandlers() {
1247
1540
  if (!this.machineClient) return;
1248
- this.machineClient.on("message", (message, scope) => {
1541
+ const client = this.machineClient;
1542
+ client.on("message", (message, scope) => {
1543
+ if (this.machineClient !== client) return;
1249
1544
  if (scope === "application") {
1250
1545
  this.emit("message", message);
1251
1546
  } else if (scope === "runtime") {
1252
1547
  this.emit("runtimeMessage", message);
1253
1548
  }
1254
1549
  });
1255
- this.machineClient.on("statusChanged", (status) => {
1550
+ client.on("statusChanged", (status) => {
1551
+ if (this.machineClient !== client) return;
1256
1552
  switch (status) {
1257
1553
  case "connected":
1258
1554
  this.setStatus("ready");
@@ -1271,13 +1567,15 @@ var Reactor = class {
1271
1567
  break;
1272
1568
  }
1273
1569
  });
1274
- this.machineClient.on(
1570
+ client.on(
1275
1571
  "trackReceived",
1276
- (track, stream) => {
1277
- this.emit("streamChanged", track, stream);
1572
+ (name, track, stream) => {
1573
+ if (this.machineClient !== client) return;
1574
+ this.emit("trackReceived", name, track, stream);
1278
1575
  }
1279
1576
  );
1280
- this.machineClient.on("statsUpdate", (stats) => {
1577
+ client.on("statsUpdate", (stats) => {
1578
+ if (this.machineClient !== client) return;
1281
1579
  this.emit("statsUpdate", stats);
1282
1580
  });
1283
1581
  }
@@ -1287,12 +1585,18 @@ var Reactor = class {
1287
1585
  */
1288
1586
  disconnect(recoverable = false) {
1289
1587
  return __async(this, null, function* () {
1588
+ var _a;
1290
1589
  if (this.status === "disconnected" && !this.sessionId) {
1291
1590
  console.warn("[Reactor] Already disconnected");
1292
1591
  return;
1293
1592
  }
1593
+ (_a = this.coordinatorClient) == null ? void 0 : _a.abort();
1294
1594
  if (this.coordinatorClient && !recoverable) {
1295
- yield this.coordinatorClient.terminateSession();
1595
+ try {
1596
+ yield this.coordinatorClient.terminateSession();
1597
+ } catch (error) {
1598
+ console.error("[Reactor] Error terminating session:", error);
1599
+ }
1296
1600
  this.coordinatorClient = void 0;
1297
1601
  }
1298
1602
  if (this.machineClient) {
@@ -1397,10 +1701,9 @@ var ReactorContext = (0, import_react2.createContext)(
1397
1701
  );
1398
1702
  var defaultInitState = {
1399
1703
  status: "disconnected",
1400
- videoTrack: null,
1704
+ tracks: {},
1401
1705
  lastError: void 0,
1402
1706
  sessionExpiration: void 0,
1403
- insecureApiKey: void 0,
1404
1707
  jwtToken: void 0,
1405
1708
  sessionId: void 0
1406
1709
  };
@@ -1421,7 +1724,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1421
1724
  oldStatus: get().status,
1422
1725
  newStatus
1423
1726
  });
1424
- set({ status: newStatus });
1727
+ if (newStatus === "disconnected") {
1728
+ set({ status: newStatus, tracks: {} });
1729
+ } else {
1730
+ set({ status: newStatus });
1731
+ }
1425
1732
  });
1426
1733
  reactor.on(
1427
1734
  "sessionExpirationChanged",
@@ -1433,13 +1740,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1433
1740
  set({ sessionExpiration: newSessionExpiration });
1434
1741
  }
1435
1742
  );
1436
- reactor.on("streamChanged", (videoTrack) => {
1437
- console.debug("[ReactorStore] Stream changed", {
1438
- hasVideoTrack: !!videoTrack,
1439
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind,
1440
- videoTrackId: videoTrack == null ? void 0 : videoTrack.id
1743
+ reactor.on("trackReceived", (name, track) => {
1744
+ console.debug("[ReactorStore] Track received", {
1745
+ name,
1746
+ kind: track.kind,
1747
+ id: track.id
1441
1748
  });
1442
- set({ videoTrack });
1749
+ set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
1443
1750
  });
1444
1751
  reactor.on("error", (error) => {
1445
1752
  console.debug("[ReactorStore] Error occurred", error);
@@ -1503,27 +1810,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1503
1810
  throw error;
1504
1811
  }
1505
1812
  }),
1506
- publishVideoStream: (stream) => __async(null, null, function* () {
1507
- console.debug("[ReactorStore] Publishing video stream");
1813
+ publish: (name, track) => __async(null, null, function* () {
1814
+ console.debug(`[ReactorStore] Publishing track "${name}"`);
1508
1815
  try {
1509
- yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
1510
- console.debug("[ReactorStore] Video stream published successfully");
1816
+ yield get().internal.reactor.publishTrack(name, track);
1817
+ console.debug(
1818
+ `[ReactorStore] Track "${name}" published successfully`
1819
+ );
1511
1820
  } catch (error) {
1512
1821
  console.error(
1513
- "[ReactorStore] Failed to publish video stream:",
1822
+ `[ReactorStore] Failed to publish track "${name}":`,
1514
1823
  error
1515
1824
  );
1516
1825
  throw error;
1517
1826
  }
1518
1827
  }),
1519
- unpublishVideoStream: () => __async(null, null, function* () {
1520
- console.debug("[ReactorStore] Unpublishing video stream");
1828
+ unpublish: (name) => __async(null, null, function* () {
1829
+ console.debug(`[ReactorStore] Unpublishing track "${name}"`);
1521
1830
  try {
1522
- yield get().internal.reactor.unpublishTrack();
1523
- console.debug("[ReactorStore] Video stream unpublished successfully");
1831
+ yield get().internal.reactor.unpublishTrack(name);
1832
+ console.debug(
1833
+ `[ReactorStore] Track "${name}" unpublished successfully`
1834
+ );
1524
1835
  } catch (error) {
1525
1836
  console.error(
1526
- "[ReactorStore] Failed to unpublish video stream:",
1837
+ `[ReactorStore] Failed to unpublish track "${name}":`,
1527
1838
  error
1528
1839
  );
1529
1840
  throw error;
@@ -1569,7 +1880,7 @@ function ReactorProvider(_a) {
1569
1880
  console.debug("[ReactorProvider] Reactor store created successfully");
1570
1881
  }
1571
1882
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1572
- const { coordinatorUrl, modelName, local } = props;
1883
+ const { coordinatorUrl, modelName, local, receive, send } = props;
1573
1884
  const maxAttempts = pollingOptions.maxAttempts;
1574
1885
  (0, import_react3.useEffect)(() => {
1575
1886
  const handleBeforeUnload = () => {
@@ -1625,6 +1936,8 @@ function ReactorProvider(_a) {
1625
1936
  coordinatorUrl,
1626
1937
  modelName,
1627
1938
  local,
1939
+ receive,
1940
+ send,
1628
1941
  jwtToken
1629
1942
  })
1630
1943
  );
@@ -1651,7 +1964,16 @@ function ReactorProvider(_a) {
1651
1964
  console.error("[ReactorProvider] Failed to disconnect:", error);
1652
1965
  });
1653
1966
  };
1654
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1967
+ }, [
1968
+ coordinatorUrl,
1969
+ modelName,
1970
+ autoConnect,
1971
+ local,
1972
+ receive,
1973
+ send,
1974
+ jwtToken,
1975
+ maxAttempts
1976
+ ]);
1655
1977
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
1656
1978
  }
1657
1979
  function useReactorStore(selector) {
@@ -1720,47 +2042,60 @@ function useStats() {
1720
2042
  var import_react5 = require("react");
1721
2043
  var import_jsx_runtime2 = require("react/jsx-runtime");
1722
2044
  function ReactorView({
2045
+ track = "main_video",
2046
+ audioTrack,
1723
2047
  width,
1724
2048
  height,
1725
2049
  className,
1726
2050
  style,
1727
- videoObjectFit = "contain"
2051
+ videoObjectFit = "contain",
2052
+ muted = true
1728
2053
  }) {
1729
- const { videoTrack, status } = useReactor((state) => ({
1730
- videoTrack: state.videoTrack,
1731
- status: state.status
1732
- }));
2054
+ const { videoMediaTrack, audioMediaTrack, status } = useReactor((state) => {
2055
+ var _a, _b;
2056
+ return {
2057
+ videoMediaTrack: (_a = state.tracks[track]) != null ? _a : null,
2058
+ audioMediaTrack: audioTrack ? (_b = state.tracks[audioTrack]) != null ? _b : null : null,
2059
+ status: state.status
2060
+ };
2061
+ });
1733
2062
  const videoRef = (0, import_react5.useRef)(null);
2063
+ const mediaStream = (0, import_react5.useMemo)(() => {
2064
+ const tracks = [];
2065
+ if (videoMediaTrack) tracks.push(videoMediaTrack);
2066
+ if (audioMediaTrack) tracks.push(audioMediaTrack);
2067
+ if (tracks.length === 0) return null;
2068
+ return new MediaStream(tracks);
2069
+ }, [videoMediaTrack, audioMediaTrack]);
1734
2070
  (0, import_react5.useEffect)(() => {
1735
- console.debug("[ReactorView] Video track effect triggered", {
2071
+ console.debug("[ReactorView] Media track effect triggered", {
2072
+ track,
1736
2073
  hasVideoElement: !!videoRef.current,
1737
- hasVideoTrack: !!videoTrack,
1738
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind
2074
+ hasVideoTrack: !!videoMediaTrack,
2075
+ hasAudioTrack: !!audioMediaTrack
1739
2076
  });
1740
- if (videoRef.current && videoTrack) {
1741
- console.debug("[ReactorView] Attaching video track to element");
2077
+ if (videoRef.current && mediaStream) {
2078
+ console.debug("[ReactorView] Attaching media stream to element");
1742
2079
  try {
1743
- const stream = new MediaStream([videoTrack]);
1744
- videoRef.current.srcObject = stream;
2080
+ videoRef.current.srcObject = mediaStream;
1745
2081
  videoRef.current.play().catch((e) => {
1746
2082
  console.warn("[ReactorView] Auto-play failed:", e);
1747
2083
  });
1748
- console.debug("[ReactorView] Video track attached successfully");
2084
+ console.debug("[ReactorView] Media stream attached successfully");
1749
2085
  } catch (error) {
1750
- console.error("[ReactorView] Failed to attach video track:", error);
2086
+ console.error("[ReactorView] Failed to attach media stream:", error);
1751
2087
  }
1752
2088
  return () => {
1753
- console.debug("[ReactorView] Detaching video track from element");
2089
+ console.debug("[ReactorView] Detaching media stream from element");
1754
2090
  if (videoRef.current) {
1755
2091
  videoRef.current.srcObject = null;
1756
- console.debug("[ReactorView] Video track detached successfully");
1757
2092
  }
1758
2093
  };
1759
2094
  } else {
1760
- console.debug("[ReactorView] No video track or element to attach");
2095
+ console.debug("[ReactorView] No tracks or element to attach");
1761
2096
  }
1762
- }, [videoTrack]);
1763
- const showPlaceholder = !videoTrack;
2097
+ }, [mediaStream]);
2098
+ const showPlaceholder = !videoMediaTrack;
1764
2099
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1765
2100
  "div",
1766
2101
  {
@@ -1780,7 +2115,7 @@ function ReactorView({
1780
2115
  objectFit: videoObjectFit,
1781
2116
  display: showPlaceholder ? "none" : "block"
1782
2117
  },
1783
- muted: true,
2118
+ muted,
1784
2119
  playsInline: true
1785
2120
  }
1786
2121
  ),
@@ -2268,6 +2603,7 @@ function ReactorController({
2268
2603
  var import_react7 = require("react");
2269
2604
  var import_jsx_runtime4 = require("react/jsx-runtime");
2270
2605
  function WebcamStream({
2606
+ track,
2271
2607
  className,
2272
2608
  style,
2273
2609
  videoConstraints = {
@@ -2280,10 +2616,10 @@ function WebcamStream({
2280
2616
  const [stream, setStream] = (0, import_react7.useState)(null);
2281
2617
  const [isPublishing, setIsPublishing] = (0, import_react7.useState)(false);
2282
2618
  const [permissionDenied, setPermissionDenied] = (0, import_react7.useState)(false);
2283
- const { status, publishVideoStream, unpublishVideoStream, reactor } = useReactor((state) => ({
2619
+ const { status, publish, unpublish, reactor } = useReactor((state) => ({
2284
2620
  status: state.status,
2285
- publishVideoStream: state.publishVideoStream,
2286
- unpublishVideoStream: state.unpublishVideoStream,
2621
+ publish: state.publish,
2622
+ unpublish: state.unpublish,
2287
2623
  reactor: state.internal.reactor
2288
2624
  }));
2289
2625
  const videoRef = (0, import_react7.useRef)(null);
@@ -2308,15 +2644,15 @@ function WebcamStream({
2308
2644
  const stopWebcam = () => __async(null, null, function* () {
2309
2645
  console.debug("[WebcamPublisher] Stopping webcam");
2310
2646
  try {
2311
- yield unpublishVideoStream();
2647
+ yield unpublish(track);
2312
2648
  console.debug("[WebcamPublisher] Unpublished before stopping");
2313
2649
  } catch (err) {
2314
2650
  console.error("[WebcamPublisher] Error unpublishing before stop:", err);
2315
2651
  }
2316
2652
  setIsPublishing(false);
2317
- stream == null ? void 0 : stream.getTracks().forEach((track) => {
2318
- track.stop();
2319
- console.debug("[WebcamPublisher] Stopped track:", track.kind);
2653
+ stream == null ? void 0 : stream.getTracks().forEach((t) => {
2654
+ t.stop();
2655
+ console.debug("[WebcamPublisher] Stopped track:", t.kind);
2320
2656
  });
2321
2657
  setStream(null);
2322
2658
  console.debug("[WebcamPublisher] Webcam stopped");
@@ -2346,28 +2682,31 @@ function WebcamStream({
2346
2682
  console.debug(
2347
2683
  "[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
2348
2684
  );
2349
- publishVideoStream(stream).then(() => {
2350
- console.debug("[WebcamPublisher] Auto-publish successful");
2351
- setIsPublishing(true);
2352
- }).catch((err) => {
2353
- console.error("[WebcamPublisher] Auto-publish failed:", err);
2354
- });
2685
+ const videoTrack = stream.getVideoTracks()[0];
2686
+ if (videoTrack) {
2687
+ publish(track, videoTrack).then(() => {
2688
+ console.debug("[WebcamPublisher] Auto-publish successful");
2689
+ setIsPublishing(true);
2690
+ }).catch((err) => {
2691
+ console.error("[WebcamPublisher] Auto-publish failed:", err);
2692
+ });
2693
+ }
2355
2694
  } else if (status !== "ready" && isPublishing) {
2356
2695
  console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
2357
- unpublishVideoStream().then(() => {
2696
+ unpublish(track).then(() => {
2358
2697
  console.debug("[WebcamPublisher] Auto-unpublish successful");
2359
2698
  setIsPublishing(false);
2360
2699
  }).catch((err) => {
2361
2700
  console.error("[WebcamPublisher] Auto-unpublish failed:", err);
2362
2701
  });
2363
2702
  }
2364
- }, [status, stream, isPublishing, publishVideoStream, unpublishVideoStream]);
2703
+ }, [status, stream, isPublishing, publish, unpublish, track]);
2365
2704
  (0, import_react7.useEffect)(() => {
2366
2705
  const handleError = (error) => {
2367
2706
  console.debug("[WebcamPublisher] Received error event:", error);
2368
- if (error.code === "VIDEO_PUBLISH_FAILED") {
2707
+ if (error.code === "TRACK_PUBLISH_FAILED") {
2369
2708
  console.debug(
2370
- "[WebcamPublisher] Video publish failed, resetting isPublishing state"
2709
+ "[WebcamPublisher] Track publish failed, resetting isPublishing state"
2371
2710
  );
2372
2711
  setIsPublishing(false);
2373
2712
  }
@@ -2476,6 +2815,7 @@ function fetchInsecureJwtToken(_0) {
2476
2815
  }
2477
2816
  // Annotate the CommonJS export names for ESM import in node:
2478
2817
  0 && (module.exports = {
2818
+ AbortError,
2479
2819
  ConflictError,
2480
2820
  PROD_COORDINATOR_URL,
2481
2821
  Reactor,
@@ -2483,11 +2823,14 @@ function fetchInsecureJwtToken(_0) {
2483
2823
  ReactorProvider,
2484
2824
  ReactorView,
2485
2825
  WebcamStream,
2826
+ audio,
2486
2827
  fetchInsecureJwtToken,
2828
+ isAbortError,
2487
2829
  useReactor,
2488
2830
  useReactorInternalMessage,
2489
2831
  useReactorMessage,
2490
2832
  useReactorStore,
2491
- useStats
2833
+ useStats,
2834
+ video
2492
2835
  });
2493
2836
  //# sourceMappingURL=index.js.map