@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.mjs CHANGED
@@ -51,11 +51,25 @@ var __async = (__this, __arguments, generator) => {
51
51
  };
52
52
 
53
53
  // src/types.ts
54
+ function video(name, _options) {
55
+ return { name, kind: "video" };
56
+ }
57
+ function audio(name, _options) {
58
+ return { name, kind: "audio" };
59
+ }
54
60
  var ConflictError = class extends Error {
55
61
  constructor(message) {
56
62
  super(message);
57
63
  }
58
64
  };
65
+ var AbortError = class extends Error {
66
+ constructor(message) {
67
+ super(message);
68
+ }
69
+ };
70
+ function isAbortError(error) {
71
+ return error instanceof AbortError || error instanceof Error && error.name === "AbortError";
72
+ }
59
73
 
60
74
  // src/core/types.ts
61
75
  import { z } from "zod";
@@ -124,18 +138,105 @@ function createPeerConnection(config) {
124
138
  function createDataChannel(pc, label) {
125
139
  return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
126
140
  }
127
- function createOffer(pc) {
141
+ function rewriteMids(sdp, trackNames) {
142
+ const lines = sdp.split("\r\n");
143
+ let mediaIdx = 0;
144
+ const replacements = /* @__PURE__ */ new Map();
145
+ let inApplication = false;
146
+ for (let i = 0; i < lines.length; i++) {
147
+ if (lines[i].startsWith("m=")) {
148
+ inApplication = lines[i].startsWith("m=application");
149
+ }
150
+ if (!inApplication && lines[i].startsWith("a=mid:")) {
151
+ const oldMid = lines[i].substring("a=mid:".length);
152
+ if (mediaIdx < trackNames.length) {
153
+ const newMid = trackNames[mediaIdx];
154
+ replacements.set(oldMid, newMid);
155
+ lines[i] = `a=mid:${newMid}`;
156
+ mediaIdx++;
157
+ }
158
+ }
159
+ }
160
+ for (let i = 0; i < lines.length; i++) {
161
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
162
+ const parts = lines[i].split(" ");
163
+ for (let j = 1; j < parts.length; j++) {
164
+ const replacement = replacements.get(parts[j]);
165
+ if (replacement !== void 0) {
166
+ parts[j] = replacement;
167
+ }
168
+ }
169
+ lines[i] = parts.join(" ");
170
+ break;
171
+ }
172
+ }
173
+ return lines.join("\r\n");
174
+ }
175
+ function createOffer(pc, trackNames) {
128
176
  return __async(this, null, function* () {
129
177
  const offer = yield pc.createOffer();
130
- yield pc.setLocalDescription(offer);
178
+ let needsAnswerRestore = false;
179
+ if (trackNames && trackNames.length > 0 && offer.sdp) {
180
+ const munged = rewriteMids(offer.sdp, trackNames);
181
+ try {
182
+ yield pc.setLocalDescription(
183
+ new RTCSessionDescription({ type: "offer", sdp: munged })
184
+ );
185
+ } catch (e) {
186
+ yield pc.setLocalDescription(offer);
187
+ needsAnswerRestore = true;
188
+ }
189
+ } else {
190
+ yield pc.setLocalDescription(offer);
191
+ }
131
192
  yield waitForIceGathering(pc);
132
193
  const localDescription = pc.localDescription;
133
194
  if (!localDescription) {
134
195
  throw new Error("Failed to create local description");
135
196
  }
136
- return localDescription.sdp;
197
+ let sdp = localDescription.sdp;
198
+ if (needsAnswerRestore && trackNames && trackNames.length > 0) {
199
+ sdp = rewriteMids(sdp, trackNames);
200
+ }
201
+ return { sdp, needsAnswerRestore };
137
202
  });
138
203
  }
204
+ function buildMidMapping(transceivers) {
205
+ var _a;
206
+ const localToRemote = /* @__PURE__ */ new Map();
207
+ const remoteToLocal = /* @__PURE__ */ new Map();
208
+ for (const entry of transceivers) {
209
+ const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
210
+ if (mid) {
211
+ localToRemote.set(mid, entry.name);
212
+ remoteToLocal.set(entry.name, mid);
213
+ }
214
+ }
215
+ return { localToRemote, remoteToLocal };
216
+ }
217
+ function restoreAnswerMids(sdp, remoteToLocal) {
218
+ const lines = sdp.split("\r\n");
219
+ for (let i = 0; i < lines.length; i++) {
220
+ if (lines[i].startsWith("a=mid:")) {
221
+ const remoteMid = lines[i].substring("a=mid:".length);
222
+ const localMid = remoteToLocal.get(remoteMid);
223
+ if (localMid !== void 0) {
224
+ lines[i] = `a=mid:${localMid}`;
225
+ }
226
+ }
227
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
228
+ const parts = lines[i].split(" ");
229
+ for (let j = 1; j < parts.length; j++) {
230
+ const localMid = remoteToLocal.get(parts[j]);
231
+ if (localMid !== void 0) {
232
+ parts[j] = localMid;
233
+ }
234
+ }
235
+ lines[i] = parts.join(" ");
236
+ }
237
+ }
238
+ return lines.join("\r\n");
239
+ }
139
240
  function setRemoteDescription(pc, sdp) {
140
241
  return __async(this, null, function* () {
141
242
  const sessionDescription = new RTCSessionDescription({
@@ -263,6 +364,22 @@ var CoordinatorClient = class {
263
364
  this.baseUrl = options.baseUrl;
264
365
  this.jwtToken = options.jwtToken;
265
366
  this.model = options.model;
367
+ this.abortController = new AbortController();
368
+ }
369
+ /**
370
+ * Aborts any in-flight HTTP requests and polling loops.
371
+ * A fresh AbortController is created so the client remains reusable.
372
+ */
373
+ abort() {
374
+ this.abortController.abort();
375
+ this.abortController = new AbortController();
376
+ }
377
+ /**
378
+ * The current abort signal, passed to every fetch() and sleep() call.
379
+ * Protected so subclasses can forward it to their own fetch calls.
380
+ */
381
+ get signal() {
382
+ return this.abortController.signal;
266
383
  }
267
384
  /**
268
385
  * Returns the authorization header with JWT Bearer token
@@ -283,7 +400,8 @@ var CoordinatorClient = class {
283
400
  `${this.baseUrl}/ice_servers?model=${this.model}`,
284
401
  {
285
402
  method: "GET",
286
- headers: this.getAuthHeaders()
403
+ headers: this.getAuthHeaders(),
404
+ signal: this.signal
287
405
  }
288
406
  );
289
407
  if (!response.ok) {
@@ -317,7 +435,8 @@ var CoordinatorClient = class {
317
435
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
318
436
  "Content-Type": "application/json"
319
437
  }),
320
- body: JSON.stringify(requestBody)
438
+ body: JSON.stringify(requestBody),
439
+ signal: this.signal
321
440
  });
322
441
  if (!response.ok) {
323
442
  const errorText = yield response.text();
@@ -351,7 +470,8 @@ var CoordinatorClient = class {
351
470
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
352
471
  {
353
472
  method: "GET",
354
- headers: this.getAuthHeaders()
473
+ headers: this.getAuthHeaders(),
474
+ signal: this.signal
355
475
  }
356
476
  );
357
477
  if (!response.ok) {
@@ -364,12 +484,13 @@ var CoordinatorClient = class {
364
484
  }
365
485
  /**
366
486
  * Terminates the current session by sending a DELETE request to the coordinator.
367
- * @throws Error if no active session exists or if the request fails (except for 404)
487
+ * No-op if no session has been created yet.
488
+ * @throws Error if the request fails (except for 404, which clears local state)
368
489
  */
369
490
  terminateSession() {
370
491
  return __async(this, null, function* () {
371
492
  if (!this.currentSessionId) {
372
- throw new Error("No active session. Call createSession() first.");
493
+ return;
373
494
  }
374
495
  console.debug(
375
496
  "[CoordinatorClient] Terminating session:",
@@ -379,7 +500,8 @@ var CoordinatorClient = class {
379
500
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
380
501
  {
381
502
  method: "DELETE",
382
- headers: this.getAuthHeaders()
503
+ headers: this.getAuthHeaders(),
504
+ signal: this.signal
383
505
  }
384
506
  );
385
507
  if (response.ok) {
@@ -429,7 +551,8 @@ var CoordinatorClient = class {
429
551
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
430
552
  "Content-Type": "application/json"
431
553
  }),
432
- body: JSON.stringify(requestBody)
554
+ body: JSON.stringify(requestBody),
555
+ signal: this.signal
433
556
  }
434
557
  );
435
558
  if (response.status === 200) {
@@ -465,6 +588,9 @@ var CoordinatorClient = class {
465
588
  let backoffMs = INITIAL_BACKOFF_MS;
466
589
  let attempt = 0;
467
590
  while (true) {
591
+ if (this.signal.aborted) {
592
+ throw new AbortError("SDP polling aborted");
593
+ }
468
594
  if (attempt >= maxAttempts) {
469
595
  throw new Error(
470
596
  `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
@@ -480,7 +606,8 @@ var CoordinatorClient = class {
480
606
  method: "GET",
481
607
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
482
608
  "Content-Type": "application/json"
483
- })
609
+ }),
610
+ signal: this.signal
484
611
  }
485
612
  );
486
613
  if (response.status === 200) {
@@ -525,10 +652,26 @@ var CoordinatorClient = class {
525
652
  });
526
653
  }
527
654
  /**
528
- * Utility function to sleep for a given number of milliseconds
655
+ * Abort-aware sleep. Resolves after `ms` milliseconds unless the
656
+ * abort signal fires first, in which case it rejects with AbortError.
529
657
  */
530
658
  sleep(ms) {
531
- return new Promise((resolve) => setTimeout(resolve, ms));
659
+ return new Promise((resolve, reject) => {
660
+ const { signal } = this;
661
+ if (signal.aborted) {
662
+ reject(new AbortError("Sleep aborted"));
663
+ return;
664
+ }
665
+ const timer = setTimeout(() => {
666
+ signal.removeEventListener("abort", onAbort);
667
+ resolve();
668
+ }, ms);
669
+ const onAbort = () => {
670
+ clearTimeout(timer);
671
+ reject(new AbortError("Sleep aborted"));
672
+ };
673
+ signal.addEventListener("abort", onAbort, { once: true });
674
+ });
532
675
  }
533
676
  };
534
677
 
@@ -550,7 +693,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
550
693
  return __async(this, null, function* () {
551
694
  console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
552
695
  const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
553
- method: "GET"
696
+ method: "GET",
697
+ signal: this.signal
554
698
  });
555
699
  if (!response.ok) {
556
700
  throw new Error("Failed to get ICE servers from local coordinator.");
@@ -574,7 +718,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
574
718
  console.debug("[LocalCoordinatorClient] Creating local session...");
575
719
  this.sdpOffer = sdpOffer;
576
720
  const response = yield fetch(`${this.localBaseUrl}/start_session`, {
577
- method: "POST"
721
+ method: "POST",
722
+ signal: this.signal
578
723
  });
579
724
  if (!response.ok) {
580
725
  throw new Error("Failed to send local start session command.");
@@ -602,7 +747,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
602
747
  headers: {
603
748
  "Content-Type": "application/json"
604
749
  },
605
- body: JSON.stringify(sdpBody)
750
+ body: JSON.stringify(sdpBody),
751
+ signal: this.signal
606
752
  });
607
753
  if (!response.ok) {
608
754
  if (response.status === 409) {
@@ -619,7 +765,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
619
765
  return __async(this, null, function* () {
620
766
  console.debug("[LocalCoordinatorClient] Stopping local session...");
621
767
  yield fetch(`${this.localBaseUrl}/stop_session`, {
622
- method: "POST"
768
+ method: "POST",
769
+ signal: this.signal
623
770
  });
624
771
  });
625
772
  }
@@ -632,6 +779,10 @@ var GPUMachineClient = class {
632
779
  constructor(config) {
633
780
  this.eventListeners = /* @__PURE__ */ new Map();
634
781
  this.status = "disconnected";
782
+ this.transceiverMap = /* @__PURE__ */ new Map();
783
+ this.publishedTracks = /* @__PURE__ */ new Map();
784
+ this.peerConnected = false;
785
+ this.dataChannelOpen = false;
635
786
  this.config = config;
636
787
  }
637
788
  // ─────────────────────────────────────────────────────────────────────────────
@@ -655,10 +806,18 @@ var GPUMachineClient = class {
655
806
  // SDP & Connection
656
807
  // ─────────────────────────────────────────────────────────────────────────────
657
808
  /**
658
- * Creates an SDP offer for initiating a connection.
809
+ * Creates an SDP offer based on the declared tracks.
810
+ *
811
+ * **RECEIVE** = client receives from the model (model → client) → `recvonly`
812
+ * **SEND** = client sends to the model (client → model) → `sendonly`
813
+ *
814
+ * Track names must be unique across both arrays. A name that appears in
815
+ * both `receive` and `send` will throw — use distinct names instead.
816
+ *
817
+ * The data channel is always created first (before transceivers).
659
818
  * Must be called before connect().
660
819
  */
661
- createOffer() {
820
+ createOffer(tracks) {
662
821
  return __async(this, null, function* () {
663
822
  if (!this.peerConnection) {
664
823
  this.peerConnection = createPeerConnection(this.config);
@@ -669,14 +828,63 @@ var GPUMachineClient = class {
669
828
  this.config.dataChannelLabel
670
829
  );
671
830
  this.setupDataChannelHandlers();
672
- this.videoTransceiver = this.peerConnection.addTransceiver("video", {
673
- direction: "sendrecv"
674
- });
675
- const offer = yield createOffer(this.peerConnection);
676
- console.debug("[GPUMachineClient] Created SDP offer");
677
- return offer;
831
+ this.transceiverMap.clear();
832
+ const entries = this.buildTransceiverEntries(tracks);
833
+ for (const entry of entries) {
834
+ const transceiver = this.peerConnection.addTransceiver(entry.kind, {
835
+ direction: entry.direction
836
+ });
837
+ entry.transceiver = transceiver;
838
+ this.transceiverMap.set(entry.name, entry);
839
+ console.debug(
840
+ `[GPUMachineClient] Transceiver added: "${entry.name}" (${entry.kind}, ${entry.direction})`
841
+ );
842
+ }
843
+ const trackNames = entries.map((e) => e.name);
844
+ const { sdp, needsAnswerRestore } = yield createOffer(
845
+ this.peerConnection,
846
+ trackNames
847
+ );
848
+ if (needsAnswerRestore) {
849
+ this.midMapping = buildMidMapping(entries);
850
+ } else {
851
+ this.midMapping = void 0;
852
+ }
853
+ console.debug(
854
+ "[GPUMachineClient] Created SDP offer with MIDs:",
855
+ trackNames,
856
+ needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
857
+ );
858
+ return sdp;
678
859
  });
679
860
  }
861
+ /**
862
+ * Builds an ordered list of transceiver entries from the receive/send arrays.
863
+ *
864
+ * Each track produces exactly one transceiver — `recvonly` for receive,
865
+ * `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
866
+ * supported; the same track name in both arrays is an error.
867
+ */
868
+ buildTransceiverEntries(tracks) {
869
+ const map = /* @__PURE__ */ new Map();
870
+ for (const t of tracks.receive) {
871
+ if (map.has(t.name)) {
872
+ throw new Error(
873
+ `Duplicate receive track name "${t.name}". Track names must be unique.`
874
+ );
875
+ }
876
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
877
+ }
878
+ for (const t of tracks.send) {
879
+ if (map.has(t.name)) {
880
+ throw new Error(
881
+ `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").`
882
+ );
883
+ }
884
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
885
+ }
886
+ return Array.from(map.values());
887
+ }
680
888
  /**
681
889
  * Connects to the GPU machine using the provided SDP answer.
682
890
  * createOffer() must be called first.
@@ -696,7 +904,14 @@ var GPUMachineClient = class {
696
904
  }
697
905
  this.setStatus("connecting");
698
906
  try {
699
- yield setRemoteDescription(this.peerConnection, sdpAnswer);
907
+ let answer = sdpAnswer;
908
+ if (this.midMapping) {
909
+ answer = restoreAnswerMids(
910
+ answer,
911
+ this.midMapping.remoteToLocal
912
+ );
913
+ }
914
+ yield setRemoteDescription(this.peerConnection, answer);
700
915
  console.debug("[GPUMachineClient] Remote description set");
701
916
  } catch (error) {
702
917
  console.error("[GPUMachineClient] Failed to connect:", error);
@@ -712,8 +927,8 @@ var GPUMachineClient = class {
712
927
  return __async(this, null, function* () {
713
928
  this.stopPing();
714
929
  this.stopStatsPolling();
715
- if (this.publishedTrack) {
716
- yield this.unpublishTrack();
930
+ for (const name of Array.from(this.publishedTracks.keys())) {
931
+ yield this.unpublishTrack(name);
717
932
  }
718
933
  if (this.dataChannel) {
719
934
  this.dataChannel.close();
@@ -723,7 +938,10 @@ var GPUMachineClient = class {
723
938
  closePeerConnection(this.peerConnection);
724
939
  this.peerConnection = void 0;
725
940
  }
726
- this.videoTransceiver = void 0;
941
+ this.transceiverMap.clear();
942
+ this.midMapping = void 0;
943
+ this.peerConnected = false;
944
+ this.dataChannelOpen = false;
727
945
  this.setStatus("disconnected");
728
946
  console.debug("[GPUMachineClient] Disconnected");
729
947
  });
@@ -751,7 +969,7 @@ var GPUMachineClient = class {
751
969
  /**
752
970
  * Sends a command to the GPU machine via the data channel.
753
971
  * @param command The command to send
754
- * @param data The data to send with the command. These are the parameters for the command, matching the scheme in the capabilities dictionary.
972
+ * @param data The data to send with the command. These are the parameters for the command, matching the schema in the capabilities dictionary.
755
973
  * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
756
974
  */
757
975
  sendCommand(command, data, scope = "application") {
@@ -768,63 +986,77 @@ var GPUMachineClient = class {
768
986
  // Track Publishing
769
987
  // ─────────────────────────────────────────────────────────────────────────────
770
988
  /**
771
- * Publishes a track to the GPU machine.
772
- * Only one track can be published at a time.
773
- * Uses the existing transceiver's sender to replace the track.
774
- * @param track The MediaStreamTrack to publish
989
+ * Publishes a MediaStreamTrack to the named send track.
990
+ *
991
+ * @param name The declared track name (must exist in transceiverMap with a sendable direction).
992
+ * @param track The MediaStreamTrack to publish.
775
993
  */
776
- publishTrack(track) {
994
+ publishTrack(name, track) {
777
995
  return __async(this, null, function* () {
778
996
  if (!this.peerConnection) {
779
997
  throw new Error(
780
- "[GPUMachineClient] Cannot publish track - not initialized"
998
+ `[GPUMachineClient] Cannot publish track "${name}" - not initialized`
781
999
  );
782
1000
  }
783
1001
  if (this.status !== "connected") {
784
1002
  throw new Error(
785
- "[GPUMachineClient] Cannot publish track - not connected"
1003
+ `[GPUMachineClient] Cannot publish track "${name}" - not connected`
1004
+ );
1005
+ }
1006
+ const entry = this.transceiverMap.get(name);
1007
+ if (!entry || !entry.transceiver) {
1008
+ throw new Error(
1009
+ `[GPUMachineClient] Cannot publish track "${name}" - no transceiver (was it declared in tracks.send?)`
786
1010
  );
787
1011
  }
788
- if (!this.videoTransceiver) {
1012
+ if (entry.direction === "recvonly") {
789
1013
  throw new Error(
790
- "[GPUMachineClient] Cannot publish track - no video transceiver"
1014
+ `[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
791
1015
  );
792
1016
  }
793
1017
  try {
794
- yield this.videoTransceiver.sender.replaceTrack(track);
795
- this.publishedTrack = track;
1018
+ yield entry.transceiver.sender.replaceTrack(track);
1019
+ this.publishedTracks.set(name, track);
796
1020
  console.debug(
797
- "[GPUMachineClient] Track published successfully:",
798
- track.kind
1021
+ `[GPUMachineClient] Track "${name}" published successfully`
799
1022
  );
800
1023
  } catch (error) {
801
- console.error("[GPUMachineClient] Failed to publish track:", error);
1024
+ console.error(
1025
+ `[GPUMachineClient] Failed to publish track "${name}":`,
1026
+ error
1027
+ );
802
1028
  throw error;
803
1029
  }
804
1030
  });
805
1031
  }
806
1032
  /**
807
- * Unpublishes the currently published track.
1033
+ * Unpublishes the track with the given name.
808
1034
  */
809
- unpublishTrack() {
1035
+ unpublishTrack(name) {
810
1036
  return __async(this, null, function* () {
811
- if (!this.videoTransceiver || !this.publishedTrack) return;
1037
+ const entry = this.transceiverMap.get(name);
1038
+ if (!(entry == null ? void 0 : entry.transceiver) || !this.publishedTracks.has(name)) return;
812
1039
  try {
813
- yield this.videoTransceiver.sender.replaceTrack(null);
814
- console.debug("[GPUMachineClient] Track unpublished successfully");
1040
+ yield entry.transceiver.sender.replaceTrack(null);
1041
+ console.debug(
1042
+ `[GPUMachineClient] Track "${name}" unpublished successfully`
1043
+ );
815
1044
  } catch (error) {
816
- console.error("[GPUMachineClient] Failed to unpublish track:", error);
1045
+ console.error(
1046
+ `[GPUMachineClient] Failed to unpublish track "${name}":`,
1047
+ error
1048
+ );
817
1049
  throw error;
818
1050
  } finally {
819
- this.publishedTrack = void 0;
1051
+ this.publishedTracks.delete(name);
820
1052
  }
821
1053
  });
822
1054
  }
823
1055
  /**
824
- * Returns the currently published track.
1056
+ * Returns the currently published track for the given name.
825
1057
  */
826
- getPublishedTrack() {
827
- return this.publishedTrack;
1058
+ getPublishedTrack(name) {
1059
+ return this.publishedTracks.get(name);
828
1060
  }
829
1061
  // ─────────────────────────────────────────────────────────────────────────────
830
1062
  // Getters
@@ -895,6 +1127,12 @@ var GPUMachineClient = class {
895
1127
  // ─────────────────────────────────────────────────────────────────────────────
896
1128
  // Private Helpers
897
1129
  // ─────────────────────────────────────────────────────────────────────────────
1130
+ checkFullyConnected() {
1131
+ if (this.peerConnected && this.dataChannelOpen) {
1132
+ this.setStatus("connected");
1133
+ this.startStatsPolling();
1134
+ }
1135
+ }
898
1136
  setStatus(newStatus) {
899
1137
  if (this.status !== newStatus) {
900
1138
  this.status = newStatus;
@@ -910,24 +1148,36 @@ var GPUMachineClient = class {
910
1148
  if (state) {
911
1149
  switch (state) {
912
1150
  case "connected":
913
- this.setStatus("connected");
914
- this.startStatsPolling();
1151
+ this.peerConnected = true;
1152
+ this.checkFullyConnected();
915
1153
  break;
916
1154
  case "disconnected":
917
1155
  case "closed":
1156
+ this.peerConnected = false;
918
1157
  this.setStatus("disconnected");
919
1158
  break;
920
1159
  case "failed":
1160
+ this.peerConnected = false;
921
1161
  this.setStatus("error");
922
1162
  break;
923
1163
  }
924
1164
  }
925
1165
  };
926
1166
  this.peerConnection.ontrack = (event) => {
927
- var _a;
928
- console.debug("[GPUMachineClient] Track received:", event.track.kind);
929
- const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
930
- this.emit("trackReceived", event.track, stream);
1167
+ var _a, _b;
1168
+ let trackName;
1169
+ for (const [name, entry] of this.transceiverMap) {
1170
+ if (entry.transceiver === event.transceiver) {
1171
+ trackName = name;
1172
+ break;
1173
+ }
1174
+ }
1175
+ trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
1176
+ console.debug(
1177
+ `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
1178
+ );
1179
+ const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
1180
+ this.emit("trackReceived", trackName, event.track, stream);
931
1181
  };
932
1182
  this.peerConnection.onicecandidate = (event) => {
933
1183
  if (event.candidate) {
@@ -947,10 +1197,13 @@ var GPUMachineClient = class {
947
1197
  if (!this.dataChannel) return;
948
1198
  this.dataChannel.onopen = () => {
949
1199
  console.debug("[GPUMachineClient] Data channel open");
1200
+ this.dataChannelOpen = true;
950
1201
  this.startPing();
1202
+ this.checkFullyConnected();
951
1203
  };
952
1204
  this.dataChannel.onclose = () => {
953
1205
  console.debug("[GPUMachineClient] Data channel closed");
1206
+ this.dataChannelOpen = false;
954
1207
  this.stopPing();
955
1208
  };
956
1209
  this.dataChannel.onerror = (error) => {
@@ -984,10 +1237,29 @@ var GPUMachineClient = class {
984
1237
  import { z as z2 } from "zod";
985
1238
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
986
1239
  var PROD_COORDINATOR_URL = "https://api.reactor.inc";
1240
+ var TrackConfigSchema = z2.object({
1241
+ name: z2.string(),
1242
+ kind: z2.enum(["audio", "video"])
1243
+ });
987
1244
  var OptionsSchema = z2.object({
988
1245
  coordinatorUrl: z2.string().default(PROD_COORDINATOR_URL),
989
1246
  modelName: z2.string(),
990
- local: z2.boolean().default(false)
1247
+ local: z2.boolean().default(false),
1248
+ /**
1249
+ * Tracks the client **RECEIVES** from the model (model → client).
1250
+ * Each entry produces a `recvonly` transceiver.
1251
+ * Names must be unique across both `receive` and `send`.
1252
+ *
1253
+ * When omitted, defaults to a single video track named `"main_video"`.
1254
+ * Pass an explicit empty array to opt out of the default.
1255
+ */
1256
+ receive: z2.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
1257
+ /**
1258
+ * Tracks the client **SENDS** to the model (client → model).
1259
+ * Each entry produces a `sendonly` transceiver.
1260
+ * Names must be unique across both `receive` and `send`.
1261
+ */
1262
+ send: z2.array(TrackConfigSchema).default([])
991
1263
  });
992
1264
  var Reactor = class {
993
1265
  constructor(options) {
@@ -998,7 +1270,9 @@ var Reactor = class {
998
1270
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
999
1271
  this.model = validatedOptions.modelName;
1000
1272
  this.local = validatedOptions.local;
1001
- if (this.local) {
1273
+ this.receive = validatedOptions.receive;
1274
+ this.send = validatedOptions.send;
1275
+ if (this.local && options.coordinatorUrl === void 0) {
1002
1276
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
1003
1277
  }
1004
1278
  }
@@ -1018,13 +1292,11 @@ var Reactor = class {
1018
1292
  (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
1019
1293
  }
1020
1294
  /**
1021
- * Public method to send a message to the machine.
1022
- * Wraps the message in the specified channel envelope (defaults to "application").
1023
- * @param command The command name to send.
1295
+ * Sends a command to the model via the data channel.
1296
+ *
1297
+ * @param command The command name.
1024
1298
  * @param data The command payload.
1025
- * @param scope The envelope scope – "application" (default) for model commands,
1026
- * "runtime" for platform-level messages (e.g. requestCapabilities).
1027
- * @throws Error if not in ready state
1299
+ * @param scope "application" (default) for model commands, "runtime" for platform messages.
1028
1300
  */
1029
1301
  sendCommand(command, data, scope = "application") {
1030
1302
  return __async(this, null, function* () {
@@ -1048,24 +1320,27 @@ var Reactor = class {
1048
1320
  });
1049
1321
  }
1050
1322
  /**
1051
- * Public method to publish a track to the machine.
1052
- * @param track The track to send to the machine.
1323
+ * Publishes a MediaStreamTrack to a named send track.
1324
+ *
1325
+ * @param name The declared send track name (e.g. "webcam").
1326
+ * @param track The MediaStreamTrack to publish.
1053
1327
  */
1054
- publishTrack(track) {
1328
+ publishTrack(name, track) {
1055
1329
  return __async(this, null, function* () {
1056
1330
  var _a;
1057
1331
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
1058
- const errorMessage = `Cannot publish track, status is ${this.status}`;
1059
- console.warn("[Reactor]", errorMessage);
1332
+ console.warn(
1333
+ `[Reactor] Cannot publish track "${name}", status is ${this.status}`
1334
+ );
1060
1335
  return;
1061
1336
  }
1062
1337
  try {
1063
- yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
1338
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
1064
1339
  } catch (error) {
1065
- console.error("[Reactor] Failed to publish track:", error);
1340
+ console.error(`[Reactor] Failed to publish track "${name}":`, error);
1066
1341
  this.createError(
1067
1342
  "TRACK_PUBLISH_FAILED",
1068
- `Failed to publish track: ${error}`,
1343
+ `Failed to publish track "${name}": ${error}`,
1069
1344
  "gpu",
1070
1345
  true
1071
1346
  );
@@ -1073,18 +1348,20 @@ var Reactor = class {
1073
1348
  });
1074
1349
  }
1075
1350
  /**
1076
- * Public method to unpublish the currently published track.
1351
+ * Unpublishes the track with the given name.
1352
+ *
1353
+ * @param name The declared send track name to unpublish.
1077
1354
  */
1078
- unpublishTrack() {
1355
+ unpublishTrack(name) {
1079
1356
  return __async(this, null, function* () {
1080
1357
  var _a;
1081
1358
  try {
1082
- yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
1359
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
1083
1360
  } catch (error) {
1084
- console.error("[Reactor] Failed to unpublish track:", error);
1361
+ console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
1085
1362
  this.createError(
1086
1363
  "TRACK_UNPUBLISH_FAILED",
1087
- `Failed to unpublish track: ${error}`,
1364
+ `Failed to unpublish track "${name}": ${error}`,
1088
1365
  "gpu",
1089
1366
  true
1090
1367
  );
@@ -1111,7 +1388,10 @@ var Reactor = class {
1111
1388
  this.machineClient = new GPUMachineClient({ iceServers });
1112
1389
  this.setupMachineClientHandlers();
1113
1390
  }
1114
- const sdpOffer = yield this.machineClient.createOffer();
1391
+ const sdpOffer = yield this.machineClient.createOffer({
1392
+ send: this.send,
1393
+ receive: this.receive
1394
+ });
1115
1395
  try {
1116
1396
  const sdpAnswer = yield this.coordinatorClient.connect(
1117
1397
  this.sessionId,
@@ -1119,8 +1399,8 @@ var Reactor = class {
1119
1399
  options == null ? void 0 : options.maxAttempts
1120
1400
  );
1121
1401
  yield this.machineClient.connect(sdpAnswer);
1122
- this.setStatus("ready");
1123
1402
  } catch (error) {
1403
+ if (isAbortError(error)) return;
1124
1404
  let recoverable = false;
1125
1405
  if (error instanceof ConflictError) {
1126
1406
  recoverable = true;
@@ -1166,7 +1446,10 @@ var Reactor = class {
1166
1446
  const iceServers = yield this.coordinatorClient.getIceServers();
1167
1447
  this.machineClient = new GPUMachineClient({ iceServers });
1168
1448
  this.setupMachineClientHandlers();
1169
- const sdpOffer = yield this.machineClient.createOffer();
1449
+ const sdpOffer = yield this.machineClient.createOffer({
1450
+ send: this.send,
1451
+ receive: this.receive
1452
+ });
1170
1453
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1171
1454
  this.setSessionId(sessionId);
1172
1455
  const sdpAnswer = yield this.coordinatorClient.connect(
@@ -1176,6 +1459,7 @@ var Reactor = class {
1176
1459
  );
1177
1460
  yield this.machineClient.connect(sdpAnswer);
1178
1461
  } catch (error) {
1462
+ if (isAbortError(error)) return;
1179
1463
  console.error("[Reactor] Connection failed:", error);
1180
1464
  this.createError(
1181
1465
  "CONNECTION_FAILED",
@@ -1197,17 +1481,25 @@ var Reactor = class {
1197
1481
  }
1198
1482
  /**
1199
1483
  * Sets up event handlers for the machine client.
1484
+ *
1485
+ * Each handler captures the client reference at registration time and
1486
+ * ignores events if this.machineClient has since changed (e.g. after
1487
+ * disconnect + reconnect), preventing stale WebRTC teardown events from
1488
+ * interfering with a new connection.
1200
1489
  */
1201
1490
  setupMachineClientHandlers() {
1202
1491
  if (!this.machineClient) return;
1203
- this.machineClient.on("message", (message, scope) => {
1492
+ const client = this.machineClient;
1493
+ client.on("message", (message, scope) => {
1494
+ if (this.machineClient !== client) return;
1204
1495
  if (scope === "application") {
1205
1496
  this.emit("message", message);
1206
1497
  } else if (scope === "runtime") {
1207
1498
  this.emit("runtimeMessage", message);
1208
1499
  }
1209
1500
  });
1210
- this.machineClient.on("statusChanged", (status) => {
1501
+ client.on("statusChanged", (status) => {
1502
+ if (this.machineClient !== client) return;
1211
1503
  switch (status) {
1212
1504
  case "connected":
1213
1505
  this.setStatus("ready");
@@ -1226,13 +1518,15 @@ var Reactor = class {
1226
1518
  break;
1227
1519
  }
1228
1520
  });
1229
- this.machineClient.on(
1521
+ client.on(
1230
1522
  "trackReceived",
1231
- (track, stream) => {
1232
- this.emit("streamChanged", track, stream);
1523
+ (name, track, stream) => {
1524
+ if (this.machineClient !== client) return;
1525
+ this.emit("trackReceived", name, track, stream);
1233
1526
  }
1234
1527
  );
1235
- this.machineClient.on("statsUpdate", (stats) => {
1528
+ client.on("statsUpdate", (stats) => {
1529
+ if (this.machineClient !== client) return;
1236
1530
  this.emit("statsUpdate", stats);
1237
1531
  });
1238
1532
  }
@@ -1242,12 +1536,18 @@ var Reactor = class {
1242
1536
  */
1243
1537
  disconnect(recoverable = false) {
1244
1538
  return __async(this, null, function* () {
1539
+ var _a;
1245
1540
  if (this.status === "disconnected" && !this.sessionId) {
1246
1541
  console.warn("[Reactor] Already disconnected");
1247
1542
  return;
1248
1543
  }
1544
+ (_a = this.coordinatorClient) == null ? void 0 : _a.abort();
1249
1545
  if (this.coordinatorClient && !recoverable) {
1250
- yield this.coordinatorClient.terminateSession();
1546
+ try {
1547
+ yield this.coordinatorClient.terminateSession();
1548
+ } catch (error) {
1549
+ console.error("[Reactor] Error terminating session:", error);
1550
+ }
1251
1551
  this.coordinatorClient = void 0;
1252
1552
  }
1253
1553
  if (this.machineClient) {
@@ -1352,10 +1652,9 @@ var ReactorContext = createContext(
1352
1652
  );
1353
1653
  var defaultInitState = {
1354
1654
  status: "disconnected",
1355
- videoTrack: null,
1655
+ tracks: {},
1356
1656
  lastError: void 0,
1357
1657
  sessionExpiration: void 0,
1358
- insecureApiKey: void 0,
1359
1658
  jwtToken: void 0,
1360
1659
  sessionId: void 0
1361
1660
  };
@@ -1376,7 +1675,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1376
1675
  oldStatus: get().status,
1377
1676
  newStatus
1378
1677
  });
1379
- set({ status: newStatus });
1678
+ if (newStatus === "disconnected") {
1679
+ set({ status: newStatus, tracks: {} });
1680
+ } else {
1681
+ set({ status: newStatus });
1682
+ }
1380
1683
  });
1381
1684
  reactor.on(
1382
1685
  "sessionExpirationChanged",
@@ -1388,13 +1691,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1388
1691
  set({ sessionExpiration: newSessionExpiration });
1389
1692
  }
1390
1693
  );
1391
- reactor.on("streamChanged", (videoTrack) => {
1392
- console.debug("[ReactorStore] Stream changed", {
1393
- hasVideoTrack: !!videoTrack,
1394
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind,
1395
- videoTrackId: videoTrack == null ? void 0 : videoTrack.id
1694
+ reactor.on("trackReceived", (name, track) => {
1695
+ console.debug("[ReactorStore] Track received", {
1696
+ name,
1697
+ kind: track.kind,
1698
+ id: track.id
1396
1699
  });
1397
- set({ videoTrack });
1700
+ set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
1398
1701
  });
1399
1702
  reactor.on("error", (error) => {
1400
1703
  console.debug("[ReactorStore] Error occurred", error);
@@ -1458,27 +1761,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1458
1761
  throw error;
1459
1762
  }
1460
1763
  }),
1461
- publishVideoStream: (stream) => __async(null, null, function* () {
1462
- console.debug("[ReactorStore] Publishing video stream");
1764
+ publish: (name, track) => __async(null, null, function* () {
1765
+ console.debug(`[ReactorStore] Publishing track "${name}"`);
1463
1766
  try {
1464
- yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
1465
- console.debug("[ReactorStore] Video stream published successfully");
1767
+ yield get().internal.reactor.publishTrack(name, track);
1768
+ console.debug(
1769
+ `[ReactorStore] Track "${name}" published successfully`
1770
+ );
1466
1771
  } catch (error) {
1467
1772
  console.error(
1468
- "[ReactorStore] Failed to publish video stream:",
1773
+ `[ReactorStore] Failed to publish track "${name}":`,
1469
1774
  error
1470
1775
  );
1471
1776
  throw error;
1472
1777
  }
1473
1778
  }),
1474
- unpublishVideoStream: () => __async(null, null, function* () {
1475
- console.debug("[ReactorStore] Unpublishing video stream");
1779
+ unpublish: (name) => __async(null, null, function* () {
1780
+ console.debug(`[ReactorStore] Unpublishing track "${name}"`);
1476
1781
  try {
1477
- yield get().internal.reactor.unpublishTrack();
1478
- console.debug("[ReactorStore] Video stream unpublished successfully");
1782
+ yield get().internal.reactor.unpublishTrack(name);
1783
+ console.debug(
1784
+ `[ReactorStore] Track "${name}" unpublished successfully`
1785
+ );
1479
1786
  } catch (error) {
1480
1787
  console.error(
1481
- "[ReactorStore] Failed to unpublish video stream:",
1788
+ `[ReactorStore] Failed to unpublish track "${name}":`,
1482
1789
  error
1483
1790
  );
1484
1791
  throw error;
@@ -1524,7 +1831,7 @@ function ReactorProvider(_a) {
1524
1831
  console.debug("[ReactorProvider] Reactor store created successfully");
1525
1832
  }
1526
1833
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1527
- const { coordinatorUrl, modelName, local } = props;
1834
+ const { coordinatorUrl, modelName, local, receive, send } = props;
1528
1835
  const maxAttempts = pollingOptions.maxAttempts;
1529
1836
  useEffect(() => {
1530
1837
  const handleBeforeUnload = () => {
@@ -1580,6 +1887,8 @@ function ReactorProvider(_a) {
1580
1887
  coordinatorUrl,
1581
1888
  modelName,
1582
1889
  local,
1890
+ receive,
1891
+ send,
1583
1892
  jwtToken
1584
1893
  })
1585
1894
  );
@@ -1606,7 +1915,16 @@ function ReactorProvider(_a) {
1606
1915
  console.error("[ReactorProvider] Failed to disconnect:", error);
1607
1916
  });
1608
1917
  };
1609
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1918
+ }, [
1919
+ coordinatorUrl,
1920
+ modelName,
1921
+ autoConnect,
1922
+ local,
1923
+ receive,
1924
+ send,
1925
+ jwtToken,
1926
+ maxAttempts
1927
+ ]);
1610
1928
  return /* @__PURE__ */ jsx(ReactorContext.Provider, { value: storeRef.current, children });
1611
1929
  }
1612
1930
  function useReactorStore(selector) {
@@ -1672,50 +1990,63 @@ function useStats() {
1672
1990
  }
1673
1991
 
1674
1992
  // src/react/ReactorView.tsx
1675
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
1993
+ import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
1676
1994
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1677
1995
  function ReactorView({
1996
+ track = "main_video",
1997
+ audioTrack,
1678
1998
  width,
1679
1999
  height,
1680
2000
  className,
1681
2001
  style,
1682
- videoObjectFit = "contain"
2002
+ videoObjectFit = "contain",
2003
+ muted = true
1683
2004
  }) {
1684
- const { videoTrack, status } = useReactor((state) => ({
1685
- videoTrack: state.videoTrack,
1686
- status: state.status
1687
- }));
2005
+ const { videoMediaTrack, audioMediaTrack, status } = useReactor((state) => {
2006
+ var _a, _b;
2007
+ return {
2008
+ videoMediaTrack: (_a = state.tracks[track]) != null ? _a : null,
2009
+ audioMediaTrack: audioTrack ? (_b = state.tracks[audioTrack]) != null ? _b : null : null,
2010
+ status: state.status
2011
+ };
2012
+ });
1688
2013
  const videoRef = useRef3(null);
2014
+ const mediaStream = useMemo(() => {
2015
+ const tracks = [];
2016
+ if (videoMediaTrack) tracks.push(videoMediaTrack);
2017
+ if (audioMediaTrack) tracks.push(audioMediaTrack);
2018
+ if (tracks.length === 0) return null;
2019
+ return new MediaStream(tracks);
2020
+ }, [videoMediaTrack, audioMediaTrack]);
1689
2021
  useEffect3(() => {
1690
- console.debug("[ReactorView] Video track effect triggered", {
2022
+ console.debug("[ReactorView] Media track effect triggered", {
2023
+ track,
1691
2024
  hasVideoElement: !!videoRef.current,
1692
- hasVideoTrack: !!videoTrack,
1693
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind
2025
+ hasVideoTrack: !!videoMediaTrack,
2026
+ hasAudioTrack: !!audioMediaTrack
1694
2027
  });
1695
- if (videoRef.current && videoTrack) {
1696
- console.debug("[ReactorView] Attaching video track to element");
2028
+ if (videoRef.current && mediaStream) {
2029
+ console.debug("[ReactorView] Attaching media stream to element");
1697
2030
  try {
1698
- const stream = new MediaStream([videoTrack]);
1699
- videoRef.current.srcObject = stream;
2031
+ videoRef.current.srcObject = mediaStream;
1700
2032
  videoRef.current.play().catch((e) => {
1701
2033
  console.warn("[ReactorView] Auto-play failed:", e);
1702
2034
  });
1703
- console.debug("[ReactorView] Video track attached successfully");
2035
+ console.debug("[ReactorView] Media stream attached successfully");
1704
2036
  } catch (error) {
1705
- console.error("[ReactorView] Failed to attach video track:", error);
2037
+ console.error("[ReactorView] Failed to attach media stream:", error);
1706
2038
  }
1707
2039
  return () => {
1708
- console.debug("[ReactorView] Detaching video track from element");
2040
+ console.debug("[ReactorView] Detaching media stream from element");
1709
2041
  if (videoRef.current) {
1710
2042
  videoRef.current.srcObject = null;
1711
- console.debug("[ReactorView] Video track detached successfully");
1712
2043
  }
1713
2044
  };
1714
2045
  } else {
1715
- console.debug("[ReactorView] No video track or element to attach");
2046
+ console.debug("[ReactorView] No tracks or element to attach");
1716
2047
  }
1717
- }, [videoTrack]);
1718
- const showPlaceholder = !videoTrack;
2048
+ }, [mediaStream]);
2049
+ const showPlaceholder = !videoMediaTrack;
1719
2050
  return /* @__PURE__ */ jsxs(
1720
2051
  "div",
1721
2052
  {
@@ -1735,7 +2066,7 @@ function ReactorView({
1735
2066
  objectFit: videoObjectFit,
1736
2067
  display: showPlaceholder ? "none" : "block"
1737
2068
  },
1738
- muted: true,
2069
+ muted,
1739
2070
  playsInline: true
1740
2071
  }
1741
2072
  ),
@@ -2223,6 +2554,7 @@ function ReactorController({
2223
2554
  import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
2224
2555
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2225
2556
  function WebcamStream({
2557
+ track,
2226
2558
  className,
2227
2559
  style,
2228
2560
  videoConstraints = {
@@ -2235,10 +2567,10 @@ function WebcamStream({
2235
2567
  const [stream, setStream] = useState4(null);
2236
2568
  const [isPublishing, setIsPublishing] = useState4(false);
2237
2569
  const [permissionDenied, setPermissionDenied] = useState4(false);
2238
- const { status, publishVideoStream, unpublishVideoStream, reactor } = useReactor((state) => ({
2570
+ const { status, publish, unpublish, reactor } = useReactor((state) => ({
2239
2571
  status: state.status,
2240
- publishVideoStream: state.publishVideoStream,
2241
- unpublishVideoStream: state.unpublishVideoStream,
2572
+ publish: state.publish,
2573
+ unpublish: state.unpublish,
2242
2574
  reactor: state.internal.reactor
2243
2575
  }));
2244
2576
  const videoRef = useRef4(null);
@@ -2263,15 +2595,15 @@ function WebcamStream({
2263
2595
  const stopWebcam = () => __async(null, null, function* () {
2264
2596
  console.debug("[WebcamPublisher] Stopping webcam");
2265
2597
  try {
2266
- yield unpublishVideoStream();
2598
+ yield unpublish(track);
2267
2599
  console.debug("[WebcamPublisher] Unpublished before stopping");
2268
2600
  } catch (err) {
2269
2601
  console.error("[WebcamPublisher] Error unpublishing before stop:", err);
2270
2602
  }
2271
2603
  setIsPublishing(false);
2272
- stream == null ? void 0 : stream.getTracks().forEach((track) => {
2273
- track.stop();
2274
- console.debug("[WebcamPublisher] Stopped track:", track.kind);
2604
+ stream == null ? void 0 : stream.getTracks().forEach((t) => {
2605
+ t.stop();
2606
+ console.debug("[WebcamPublisher] Stopped track:", t.kind);
2275
2607
  });
2276
2608
  setStream(null);
2277
2609
  console.debug("[WebcamPublisher] Webcam stopped");
@@ -2301,28 +2633,31 @@ function WebcamStream({
2301
2633
  console.debug(
2302
2634
  "[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
2303
2635
  );
2304
- publishVideoStream(stream).then(() => {
2305
- console.debug("[WebcamPublisher] Auto-publish successful");
2306
- setIsPublishing(true);
2307
- }).catch((err) => {
2308
- console.error("[WebcamPublisher] Auto-publish failed:", err);
2309
- });
2636
+ const videoTrack = stream.getVideoTracks()[0];
2637
+ if (videoTrack) {
2638
+ publish(track, videoTrack).then(() => {
2639
+ console.debug("[WebcamPublisher] Auto-publish successful");
2640
+ setIsPublishing(true);
2641
+ }).catch((err) => {
2642
+ console.error("[WebcamPublisher] Auto-publish failed:", err);
2643
+ });
2644
+ }
2310
2645
  } else if (status !== "ready" && isPublishing) {
2311
2646
  console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
2312
- unpublishVideoStream().then(() => {
2647
+ unpublish(track).then(() => {
2313
2648
  console.debug("[WebcamPublisher] Auto-unpublish successful");
2314
2649
  setIsPublishing(false);
2315
2650
  }).catch((err) => {
2316
2651
  console.error("[WebcamPublisher] Auto-unpublish failed:", err);
2317
2652
  });
2318
2653
  }
2319
- }, [status, stream, isPublishing, publishVideoStream, unpublishVideoStream]);
2654
+ }, [status, stream, isPublishing, publish, unpublish, track]);
2320
2655
  useEffect4(() => {
2321
2656
  const handleError = (error) => {
2322
2657
  console.debug("[WebcamPublisher] Received error event:", error);
2323
- if (error.code === "VIDEO_PUBLISH_FAILED") {
2658
+ if (error.code === "TRACK_PUBLISH_FAILED") {
2324
2659
  console.debug(
2325
- "[WebcamPublisher] Video publish failed, resetting isPublishing state"
2660
+ "[WebcamPublisher] Track publish failed, resetting isPublishing state"
2326
2661
  );
2327
2662
  setIsPublishing(false);
2328
2663
  }
@@ -2430,6 +2765,7 @@ function fetchInsecureJwtToken(_0) {
2430
2765
  });
2431
2766
  }
2432
2767
  export {
2768
+ AbortError,
2433
2769
  ConflictError,
2434
2770
  PROD_COORDINATOR_URL,
2435
2771
  Reactor,
@@ -2437,11 +2773,14 @@ export {
2437
2773
  ReactorProvider,
2438
2774
  ReactorView,
2439
2775
  WebcamStream,
2776
+ audio,
2440
2777
  fetchInsecureJwtToken,
2778
+ isAbortError,
2441
2779
  useReactor,
2442
2780
  useReactorInternalMessage,
2443
2781
  useReactorMessage,
2444
2782
  useReactorStore,
2445
- useStats
2783
+ useStats,
2784
+ video
2446
2785
  };
2447
2786
  //# sourceMappingURL=index.mjs.map