@reactor-team/js-sdk 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -51,6 +51,12 @@ 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);
@@ -124,10 +130,53 @@ function createPeerConnection(config) {
124
130
  function createDataChannel(pc, label) {
125
131
  return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
126
132
  }
127
- function createOffer(pc) {
133
+ function rewriteMids(sdp, trackNames) {
134
+ const lines = sdp.split("\r\n");
135
+ let mediaIdx = 0;
136
+ const replacements = /* @__PURE__ */ new Map();
137
+ let inApplication = false;
138
+ for (let i = 0; i < lines.length; i++) {
139
+ if (lines[i].startsWith("m=")) {
140
+ inApplication = lines[i].startsWith("m=application");
141
+ }
142
+ if (!inApplication && lines[i].startsWith("a=mid:")) {
143
+ const oldMid = lines[i].substring("a=mid:".length);
144
+ if (mediaIdx < trackNames.length) {
145
+ const newMid = trackNames[mediaIdx];
146
+ replacements.set(oldMid, newMid);
147
+ lines[i] = `a=mid:${newMid}`;
148
+ mediaIdx++;
149
+ }
150
+ }
151
+ }
152
+ for (let i = 0; i < lines.length; i++) {
153
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
154
+ const parts = lines[i].split(" ");
155
+ for (let j = 1; j < parts.length; j++) {
156
+ const replacement = replacements.get(parts[j]);
157
+ if (replacement !== void 0) {
158
+ parts[j] = replacement;
159
+ }
160
+ }
161
+ lines[i] = parts.join(" ");
162
+ break;
163
+ }
164
+ }
165
+ return lines.join("\r\n");
166
+ }
167
+ function createOffer(pc, trackNames) {
128
168
  return __async(this, null, function* () {
129
169
  const offer = yield pc.createOffer();
130
- yield pc.setLocalDescription(offer);
170
+ if (trackNames && trackNames.length > 0 && offer.sdp) {
171
+ const munged = rewriteMids(offer.sdp, trackNames);
172
+ const mungedOffer = new RTCSessionDescription({
173
+ type: "offer",
174
+ sdp: munged
175
+ });
176
+ yield pc.setLocalDescription(mungedOffer);
177
+ } else {
178
+ yield pc.setLocalDescription(offer);
179
+ }
131
180
  yield waitForIceGathering(pc);
132
181
  const localDescription = pc.localDescription;
133
182
  if (!localDescription) {
@@ -632,6 +681,10 @@ var GPUMachineClient = class {
632
681
  constructor(config) {
633
682
  this.eventListeners = /* @__PURE__ */ new Map();
634
683
  this.status = "disconnected";
684
+ this.transceiverMap = /* @__PURE__ */ new Map();
685
+ this.publishedTracks = /* @__PURE__ */ new Map();
686
+ this.peerConnected = false;
687
+ this.dataChannelOpen = false;
635
688
  this.config = config;
636
689
  }
637
690
  // ─────────────────────────────────────────────────────────────────────────────
@@ -655,10 +708,18 @@ var GPUMachineClient = class {
655
708
  // SDP & Connection
656
709
  // ─────────────────────────────────────────────────────────────────────────────
657
710
  /**
658
- * Creates an SDP offer for initiating a connection.
711
+ * Creates an SDP offer based on the declared tracks.
712
+ *
713
+ * **RECEIVE** = client receives from the model (model → client) → `recvonly`
714
+ * **SEND** = client sends to the model (client → model) → `sendonly`
715
+ *
716
+ * Track names must be unique across both arrays. A name that appears in
717
+ * both `receive` and `send` will throw — use distinct names instead.
718
+ *
719
+ * The data channel is always created first (before transceivers).
659
720
  * Must be called before connect().
660
721
  */
661
- createOffer() {
722
+ createOffer(tracks) {
662
723
  return __async(this, null, function* () {
663
724
  if (!this.peerConnection) {
664
725
  this.peerConnection = createPeerConnection(this.config);
@@ -669,14 +730,54 @@ var GPUMachineClient = class {
669
730
  this.config.dataChannelLabel
670
731
  );
671
732
  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");
733
+ this.transceiverMap.clear();
734
+ const entries = this.buildTransceiverEntries(tracks);
735
+ for (const entry of entries) {
736
+ const transceiver = this.peerConnection.addTransceiver(entry.kind, {
737
+ direction: entry.direction
738
+ });
739
+ entry.transceiver = transceiver;
740
+ this.transceiverMap.set(entry.name, entry);
741
+ console.debug(
742
+ `[GPUMachineClient] Transceiver added: "${entry.name}" (${entry.kind}, ${entry.direction})`
743
+ );
744
+ }
745
+ const trackNames = entries.map((e) => e.name);
746
+ const offer = yield createOffer(this.peerConnection, trackNames);
747
+ console.debug(
748
+ "[GPUMachineClient] Created SDP offer with MIDs:",
749
+ trackNames
750
+ );
677
751
  return offer;
678
752
  });
679
753
  }
754
+ /**
755
+ * Builds an ordered list of transceiver entries from the receive/send arrays.
756
+ *
757
+ * Each track produces exactly one transceiver — `recvonly` for receive,
758
+ * `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
759
+ * supported; the same track name in both arrays is an error.
760
+ */
761
+ buildTransceiverEntries(tracks) {
762
+ const map = /* @__PURE__ */ new Map();
763
+ for (const t of tracks.receive) {
764
+ if (map.has(t.name)) {
765
+ throw new Error(
766
+ `Duplicate receive track name "${t.name}". Track names must be unique.`
767
+ );
768
+ }
769
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
770
+ }
771
+ for (const t of tracks.send) {
772
+ if (map.has(t.name)) {
773
+ throw new Error(
774
+ `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").`
775
+ );
776
+ }
777
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
778
+ }
779
+ return Array.from(map.values());
780
+ }
680
781
  /**
681
782
  * Connects to the GPU machine using the provided SDP answer.
682
783
  * createOffer() must be called first.
@@ -712,8 +813,8 @@ var GPUMachineClient = class {
712
813
  return __async(this, null, function* () {
713
814
  this.stopPing();
714
815
  this.stopStatsPolling();
715
- if (this.publishedTrack) {
716
- yield this.unpublishTrack();
816
+ for (const name of Array.from(this.publishedTracks.keys())) {
817
+ yield this.unpublishTrack(name);
717
818
  }
718
819
  if (this.dataChannel) {
719
820
  this.dataChannel.close();
@@ -723,7 +824,9 @@ var GPUMachineClient = class {
723
824
  closePeerConnection(this.peerConnection);
724
825
  this.peerConnection = void 0;
725
826
  }
726
- this.videoTransceiver = void 0;
827
+ this.transceiverMap.clear();
828
+ this.peerConnected = false;
829
+ this.dataChannelOpen = false;
727
830
  this.setStatus("disconnected");
728
831
  console.debug("[GPUMachineClient] Disconnected");
729
832
  });
@@ -751,7 +854,7 @@ var GPUMachineClient = class {
751
854
  /**
752
855
  * Sends a command to the GPU machine via the data channel.
753
856
  * @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.
857
+ * @param data The data to send with the command. These are the parameters for the command, matching the schema in the capabilities dictionary.
755
858
  * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
756
859
  */
757
860
  sendCommand(command, data, scope = "application") {
@@ -768,63 +871,77 @@ var GPUMachineClient = class {
768
871
  // Track Publishing
769
872
  // ─────────────────────────────────────────────────────────────────────────────
770
873
  /**
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
874
+ * Publishes a MediaStreamTrack to the named send track.
875
+ *
876
+ * @param name The declared track name (must exist in transceiverMap with a sendable direction).
877
+ * @param track The MediaStreamTrack to publish.
775
878
  */
776
- publishTrack(track) {
879
+ publishTrack(name, track) {
777
880
  return __async(this, null, function* () {
778
881
  if (!this.peerConnection) {
779
882
  throw new Error(
780
- "[GPUMachineClient] Cannot publish track - not initialized"
883
+ `[GPUMachineClient] Cannot publish track "${name}" - not initialized`
781
884
  );
782
885
  }
783
886
  if (this.status !== "connected") {
784
887
  throw new Error(
785
- "[GPUMachineClient] Cannot publish track - not connected"
888
+ `[GPUMachineClient] Cannot publish track "${name}" - not connected`
889
+ );
890
+ }
891
+ const entry = this.transceiverMap.get(name);
892
+ if (!entry || !entry.transceiver) {
893
+ throw new Error(
894
+ `[GPUMachineClient] Cannot publish track "${name}" - no transceiver (was it declared in tracks.send?)`
786
895
  );
787
896
  }
788
- if (!this.videoTransceiver) {
897
+ if (entry.direction === "recvonly") {
789
898
  throw new Error(
790
- "[GPUMachineClient] Cannot publish track - no video transceiver"
899
+ `[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
791
900
  );
792
901
  }
793
902
  try {
794
- yield this.videoTransceiver.sender.replaceTrack(track);
795
- this.publishedTrack = track;
903
+ yield entry.transceiver.sender.replaceTrack(track);
904
+ this.publishedTracks.set(name, track);
796
905
  console.debug(
797
- "[GPUMachineClient] Track published successfully:",
798
- track.kind
906
+ `[GPUMachineClient] Track "${name}" published successfully`
799
907
  );
800
908
  } catch (error) {
801
- console.error("[GPUMachineClient] Failed to publish track:", error);
909
+ console.error(
910
+ `[GPUMachineClient] Failed to publish track "${name}":`,
911
+ error
912
+ );
802
913
  throw error;
803
914
  }
804
915
  });
805
916
  }
806
917
  /**
807
- * Unpublishes the currently published track.
918
+ * Unpublishes the track with the given name.
808
919
  */
809
- unpublishTrack() {
920
+ unpublishTrack(name) {
810
921
  return __async(this, null, function* () {
811
- if (!this.videoTransceiver || !this.publishedTrack) return;
922
+ const entry = this.transceiverMap.get(name);
923
+ if (!(entry == null ? void 0 : entry.transceiver) || !this.publishedTracks.has(name)) return;
812
924
  try {
813
- yield this.videoTransceiver.sender.replaceTrack(null);
814
- console.debug("[GPUMachineClient] Track unpublished successfully");
925
+ yield entry.transceiver.sender.replaceTrack(null);
926
+ console.debug(
927
+ `[GPUMachineClient] Track "${name}" unpublished successfully`
928
+ );
815
929
  } catch (error) {
816
- console.error("[GPUMachineClient] Failed to unpublish track:", error);
930
+ console.error(
931
+ `[GPUMachineClient] Failed to unpublish track "${name}":`,
932
+ error
933
+ );
817
934
  throw error;
818
935
  } finally {
819
- this.publishedTrack = void 0;
936
+ this.publishedTracks.delete(name);
820
937
  }
821
938
  });
822
939
  }
823
940
  /**
824
- * Returns the currently published track.
941
+ * Returns the currently published track for the given name.
825
942
  */
826
- getPublishedTrack() {
827
- return this.publishedTrack;
943
+ getPublishedTrack(name) {
944
+ return this.publishedTracks.get(name);
828
945
  }
829
946
  // ─────────────────────────────────────────────────────────────────────────────
830
947
  // Getters
@@ -895,6 +1012,12 @@ var GPUMachineClient = class {
895
1012
  // ─────────────────────────────────────────────────────────────────────────────
896
1013
  // Private Helpers
897
1014
  // ─────────────────────────────────────────────────────────────────────────────
1015
+ checkFullyConnected() {
1016
+ if (this.peerConnected && this.dataChannelOpen) {
1017
+ this.setStatus("connected");
1018
+ this.startStatsPolling();
1019
+ }
1020
+ }
898
1021
  setStatus(newStatus) {
899
1022
  if (this.status !== newStatus) {
900
1023
  this.status = newStatus;
@@ -910,14 +1033,16 @@ var GPUMachineClient = class {
910
1033
  if (state) {
911
1034
  switch (state) {
912
1035
  case "connected":
913
- this.setStatus("connected");
914
- this.startStatsPolling();
1036
+ this.peerConnected = true;
1037
+ this.checkFullyConnected();
915
1038
  break;
916
1039
  case "disconnected":
917
1040
  case "closed":
1041
+ this.peerConnected = false;
918
1042
  this.setStatus("disconnected");
919
1043
  break;
920
1044
  case "failed":
1045
+ this.peerConnected = false;
921
1046
  this.setStatus("error");
922
1047
  break;
923
1048
  }
@@ -925,9 +1050,13 @@ var GPUMachineClient = class {
925
1050
  };
926
1051
  this.peerConnection.ontrack = (event) => {
927
1052
  var _a;
928
- console.debug("[GPUMachineClient] Track received:", event.track.kind);
1053
+ const mid = event.transceiver.mid;
1054
+ const trackName = mid != null ? mid : `unknown-${event.track.id}`;
1055
+ console.debug(
1056
+ `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${mid})`
1057
+ );
929
1058
  const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
930
- this.emit("trackReceived", event.track, stream);
1059
+ this.emit("trackReceived", trackName, event.track, stream);
931
1060
  };
932
1061
  this.peerConnection.onicecandidate = (event) => {
933
1062
  if (event.candidate) {
@@ -947,10 +1076,13 @@ var GPUMachineClient = class {
947
1076
  if (!this.dataChannel) return;
948
1077
  this.dataChannel.onopen = () => {
949
1078
  console.debug("[GPUMachineClient] Data channel open");
1079
+ this.dataChannelOpen = true;
950
1080
  this.startPing();
1081
+ this.checkFullyConnected();
951
1082
  };
952
1083
  this.dataChannel.onclose = () => {
953
1084
  console.debug("[GPUMachineClient] Data channel closed");
1085
+ this.dataChannelOpen = false;
954
1086
  this.stopPing();
955
1087
  };
956
1088
  this.dataChannel.onerror = (error) => {
@@ -984,10 +1116,29 @@ var GPUMachineClient = class {
984
1116
  import { z as z2 } from "zod";
985
1117
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
986
1118
  var PROD_COORDINATOR_URL = "https://api.reactor.inc";
1119
+ var TrackConfigSchema = z2.object({
1120
+ name: z2.string(),
1121
+ kind: z2.enum(["audio", "video"])
1122
+ });
987
1123
  var OptionsSchema = z2.object({
988
1124
  coordinatorUrl: z2.string().default(PROD_COORDINATOR_URL),
989
1125
  modelName: z2.string(),
990
- local: z2.boolean().default(false)
1126
+ local: z2.boolean().default(false),
1127
+ /**
1128
+ * Tracks the client **RECEIVES** from the model (model → client).
1129
+ * Each entry produces a `recvonly` transceiver.
1130
+ * Names must be unique across both `receive` and `send`.
1131
+ *
1132
+ * When omitted, defaults to a single video track named `"main_video"`.
1133
+ * Pass an explicit empty array to opt out of the default.
1134
+ */
1135
+ receive: z2.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
1136
+ /**
1137
+ * Tracks the client **SENDS** to the model (client → model).
1138
+ * Each entry produces a `sendonly` transceiver.
1139
+ * Names must be unique across both `receive` and `send`.
1140
+ */
1141
+ send: z2.array(TrackConfigSchema).default([])
991
1142
  });
992
1143
  var Reactor = class {
993
1144
  constructor(options) {
@@ -998,7 +1149,9 @@ var Reactor = class {
998
1149
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
999
1150
  this.model = validatedOptions.modelName;
1000
1151
  this.local = validatedOptions.local;
1001
- if (this.local) {
1152
+ this.receive = validatedOptions.receive;
1153
+ this.send = validatedOptions.send;
1154
+ if (this.local && options.coordinatorUrl === void 0) {
1002
1155
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
1003
1156
  }
1004
1157
  }
@@ -1018,13 +1171,11 @@ var Reactor = class {
1018
1171
  (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
1019
1172
  }
1020
1173
  /**
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.
1174
+ * Sends a command to the model via the data channel.
1175
+ *
1176
+ * @param command The command name.
1024
1177
  * @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
1178
+ * @param scope "application" (default) for model commands, "runtime" for platform messages.
1028
1179
  */
1029
1180
  sendCommand(command, data, scope = "application") {
1030
1181
  return __async(this, null, function* () {
@@ -1048,24 +1199,27 @@ var Reactor = class {
1048
1199
  });
1049
1200
  }
1050
1201
  /**
1051
- * Public method to publish a track to the machine.
1052
- * @param track The track to send to the machine.
1202
+ * Publishes a MediaStreamTrack to a named send track.
1203
+ *
1204
+ * @param name The declared send track name (e.g. "webcam").
1205
+ * @param track The MediaStreamTrack to publish.
1053
1206
  */
1054
- publishTrack(track) {
1207
+ publishTrack(name, track) {
1055
1208
  return __async(this, null, function* () {
1056
1209
  var _a;
1057
1210
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
1058
- const errorMessage = `Cannot publish track, status is ${this.status}`;
1059
- console.warn("[Reactor]", errorMessage);
1211
+ console.warn(
1212
+ `[Reactor] Cannot publish track "${name}", status is ${this.status}`
1213
+ );
1060
1214
  return;
1061
1215
  }
1062
1216
  try {
1063
- yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
1217
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
1064
1218
  } catch (error) {
1065
- console.error("[Reactor] Failed to publish track:", error);
1219
+ console.error(`[Reactor] Failed to publish track "${name}":`, error);
1066
1220
  this.createError(
1067
1221
  "TRACK_PUBLISH_FAILED",
1068
- `Failed to publish track: ${error}`,
1222
+ `Failed to publish track "${name}": ${error}`,
1069
1223
  "gpu",
1070
1224
  true
1071
1225
  );
@@ -1073,18 +1227,20 @@ var Reactor = class {
1073
1227
  });
1074
1228
  }
1075
1229
  /**
1076
- * Public method to unpublish the currently published track.
1230
+ * Unpublishes the track with the given name.
1231
+ *
1232
+ * @param name The declared send track name to unpublish.
1077
1233
  */
1078
- unpublishTrack() {
1234
+ unpublishTrack(name) {
1079
1235
  return __async(this, null, function* () {
1080
1236
  var _a;
1081
1237
  try {
1082
- yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
1238
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
1083
1239
  } catch (error) {
1084
- console.error("[Reactor] Failed to unpublish track:", error);
1240
+ console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
1085
1241
  this.createError(
1086
1242
  "TRACK_UNPUBLISH_FAILED",
1087
- `Failed to unpublish track: ${error}`,
1243
+ `Failed to unpublish track "${name}": ${error}`,
1088
1244
  "gpu",
1089
1245
  true
1090
1246
  );
@@ -1111,7 +1267,10 @@ var Reactor = class {
1111
1267
  this.machineClient = new GPUMachineClient({ iceServers });
1112
1268
  this.setupMachineClientHandlers();
1113
1269
  }
1114
- const sdpOffer = yield this.machineClient.createOffer();
1270
+ const sdpOffer = yield this.machineClient.createOffer({
1271
+ send: this.send,
1272
+ receive: this.receive
1273
+ });
1115
1274
  try {
1116
1275
  const sdpAnswer = yield this.coordinatorClient.connect(
1117
1276
  this.sessionId,
@@ -1166,7 +1325,10 @@ var Reactor = class {
1166
1325
  const iceServers = yield this.coordinatorClient.getIceServers();
1167
1326
  this.machineClient = new GPUMachineClient({ iceServers });
1168
1327
  this.setupMachineClientHandlers();
1169
- const sdpOffer = yield this.machineClient.createOffer();
1328
+ const sdpOffer = yield this.machineClient.createOffer({
1329
+ send: this.send,
1330
+ receive: this.receive
1331
+ });
1170
1332
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1171
1333
  this.setSessionId(sessionId);
1172
1334
  const sdpAnswer = yield this.coordinatorClient.connect(
@@ -1228,8 +1390,8 @@ var Reactor = class {
1228
1390
  });
1229
1391
  this.machineClient.on(
1230
1392
  "trackReceived",
1231
- (track, stream) => {
1232
- this.emit("streamChanged", track, stream);
1393
+ (name, track, stream) => {
1394
+ this.emit("trackReceived", name, track, stream);
1233
1395
  }
1234
1396
  );
1235
1397
  this.machineClient.on("statsUpdate", (stats) => {
@@ -1247,7 +1409,11 @@ var Reactor = class {
1247
1409
  return;
1248
1410
  }
1249
1411
  if (this.coordinatorClient && !recoverable) {
1250
- yield this.coordinatorClient.terminateSession();
1412
+ try {
1413
+ yield this.coordinatorClient.terminateSession();
1414
+ } catch (error) {
1415
+ console.error("[Reactor] Error terminating session:", error);
1416
+ }
1251
1417
  this.coordinatorClient = void 0;
1252
1418
  }
1253
1419
  if (this.machineClient) {
@@ -1352,10 +1518,9 @@ var ReactorContext = createContext(
1352
1518
  );
1353
1519
  var defaultInitState = {
1354
1520
  status: "disconnected",
1355
- videoTrack: null,
1521
+ tracks: {},
1356
1522
  lastError: void 0,
1357
1523
  sessionExpiration: void 0,
1358
- insecureApiKey: void 0,
1359
1524
  jwtToken: void 0,
1360
1525
  sessionId: void 0
1361
1526
  };
@@ -1376,7 +1541,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1376
1541
  oldStatus: get().status,
1377
1542
  newStatus
1378
1543
  });
1379
- set({ status: newStatus });
1544
+ if (newStatus === "disconnected") {
1545
+ set({ status: newStatus, tracks: {} });
1546
+ } else {
1547
+ set({ status: newStatus });
1548
+ }
1380
1549
  });
1381
1550
  reactor.on(
1382
1551
  "sessionExpirationChanged",
@@ -1388,13 +1557,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1388
1557
  set({ sessionExpiration: newSessionExpiration });
1389
1558
  }
1390
1559
  );
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
1560
+ reactor.on("trackReceived", (name, track) => {
1561
+ console.debug("[ReactorStore] Track received", {
1562
+ name,
1563
+ kind: track.kind,
1564
+ id: track.id
1396
1565
  });
1397
- set({ videoTrack });
1566
+ set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
1398
1567
  });
1399
1568
  reactor.on("error", (error) => {
1400
1569
  console.debug("[ReactorStore] Error occurred", error);
@@ -1458,27 +1627,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1458
1627
  throw error;
1459
1628
  }
1460
1629
  }),
1461
- publishVideoStream: (stream) => __async(null, null, function* () {
1462
- console.debug("[ReactorStore] Publishing video stream");
1630
+ publish: (name, track) => __async(null, null, function* () {
1631
+ console.debug(`[ReactorStore] Publishing track "${name}"`);
1463
1632
  try {
1464
- yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
1465
- console.debug("[ReactorStore] Video stream published successfully");
1633
+ yield get().internal.reactor.publishTrack(name, track);
1634
+ console.debug(
1635
+ `[ReactorStore] Track "${name}" published successfully`
1636
+ );
1466
1637
  } catch (error) {
1467
1638
  console.error(
1468
- "[ReactorStore] Failed to publish video stream:",
1639
+ `[ReactorStore] Failed to publish track "${name}":`,
1469
1640
  error
1470
1641
  );
1471
1642
  throw error;
1472
1643
  }
1473
1644
  }),
1474
- unpublishVideoStream: () => __async(null, null, function* () {
1475
- console.debug("[ReactorStore] Unpublishing video stream");
1645
+ unpublish: (name) => __async(null, null, function* () {
1646
+ console.debug(`[ReactorStore] Unpublishing track "${name}"`);
1476
1647
  try {
1477
- yield get().internal.reactor.unpublishTrack();
1478
- console.debug("[ReactorStore] Video stream unpublished successfully");
1648
+ yield get().internal.reactor.unpublishTrack(name);
1649
+ console.debug(
1650
+ `[ReactorStore] Track "${name}" unpublished successfully`
1651
+ );
1479
1652
  } catch (error) {
1480
1653
  console.error(
1481
- "[ReactorStore] Failed to unpublish video stream:",
1654
+ `[ReactorStore] Failed to unpublish track "${name}":`,
1482
1655
  error
1483
1656
  );
1484
1657
  throw error;
@@ -1524,7 +1697,7 @@ function ReactorProvider(_a) {
1524
1697
  console.debug("[ReactorProvider] Reactor store created successfully");
1525
1698
  }
1526
1699
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1527
- const { coordinatorUrl, modelName, local } = props;
1700
+ const { coordinatorUrl, modelName, local, receive, send } = props;
1528
1701
  const maxAttempts = pollingOptions.maxAttempts;
1529
1702
  useEffect(() => {
1530
1703
  const handleBeforeUnload = () => {
@@ -1580,6 +1753,8 @@ function ReactorProvider(_a) {
1580
1753
  coordinatorUrl,
1581
1754
  modelName,
1582
1755
  local,
1756
+ receive,
1757
+ send,
1583
1758
  jwtToken
1584
1759
  })
1585
1760
  );
@@ -1606,7 +1781,16 @@ function ReactorProvider(_a) {
1606
1781
  console.error("[ReactorProvider] Failed to disconnect:", error);
1607
1782
  });
1608
1783
  };
1609
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1784
+ }, [
1785
+ coordinatorUrl,
1786
+ modelName,
1787
+ autoConnect,
1788
+ local,
1789
+ receive,
1790
+ send,
1791
+ jwtToken,
1792
+ maxAttempts
1793
+ ]);
1610
1794
  return /* @__PURE__ */ jsx(ReactorContext.Provider, { value: storeRef.current, children });
1611
1795
  }
1612
1796
  function useReactorStore(selector) {
@@ -1672,50 +1856,63 @@ function useStats() {
1672
1856
  }
1673
1857
 
1674
1858
  // src/react/ReactorView.tsx
1675
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
1859
+ import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
1676
1860
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1677
1861
  function ReactorView({
1862
+ track = "main_video",
1863
+ audioTrack,
1678
1864
  width,
1679
1865
  height,
1680
1866
  className,
1681
1867
  style,
1682
- videoObjectFit = "contain"
1868
+ videoObjectFit = "contain",
1869
+ muted = true
1683
1870
  }) {
1684
- const { videoTrack, status } = useReactor((state) => ({
1685
- videoTrack: state.videoTrack,
1686
- status: state.status
1687
- }));
1871
+ const { videoMediaTrack, audioMediaTrack, status } = useReactor((state) => {
1872
+ var _a, _b;
1873
+ return {
1874
+ videoMediaTrack: (_a = state.tracks[track]) != null ? _a : null,
1875
+ audioMediaTrack: audioTrack ? (_b = state.tracks[audioTrack]) != null ? _b : null : null,
1876
+ status: state.status
1877
+ };
1878
+ });
1688
1879
  const videoRef = useRef3(null);
1880
+ const mediaStream = useMemo(() => {
1881
+ const tracks = [];
1882
+ if (videoMediaTrack) tracks.push(videoMediaTrack);
1883
+ if (audioMediaTrack) tracks.push(audioMediaTrack);
1884
+ if (tracks.length === 0) return null;
1885
+ return new MediaStream(tracks);
1886
+ }, [videoMediaTrack, audioMediaTrack]);
1689
1887
  useEffect3(() => {
1690
- console.debug("[ReactorView] Video track effect triggered", {
1888
+ console.debug("[ReactorView] Media track effect triggered", {
1889
+ track,
1691
1890
  hasVideoElement: !!videoRef.current,
1692
- hasVideoTrack: !!videoTrack,
1693
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind
1891
+ hasVideoTrack: !!videoMediaTrack,
1892
+ hasAudioTrack: !!audioMediaTrack
1694
1893
  });
1695
- if (videoRef.current && videoTrack) {
1696
- console.debug("[ReactorView] Attaching video track to element");
1894
+ if (videoRef.current && mediaStream) {
1895
+ console.debug("[ReactorView] Attaching media stream to element");
1697
1896
  try {
1698
- const stream = new MediaStream([videoTrack]);
1699
- videoRef.current.srcObject = stream;
1897
+ videoRef.current.srcObject = mediaStream;
1700
1898
  videoRef.current.play().catch((e) => {
1701
1899
  console.warn("[ReactorView] Auto-play failed:", e);
1702
1900
  });
1703
- console.debug("[ReactorView] Video track attached successfully");
1901
+ console.debug("[ReactorView] Media stream attached successfully");
1704
1902
  } catch (error) {
1705
- console.error("[ReactorView] Failed to attach video track:", error);
1903
+ console.error("[ReactorView] Failed to attach media stream:", error);
1706
1904
  }
1707
1905
  return () => {
1708
- console.debug("[ReactorView] Detaching video track from element");
1906
+ console.debug("[ReactorView] Detaching media stream from element");
1709
1907
  if (videoRef.current) {
1710
1908
  videoRef.current.srcObject = null;
1711
- console.debug("[ReactorView] Video track detached successfully");
1712
1909
  }
1713
1910
  };
1714
1911
  } else {
1715
- console.debug("[ReactorView] No video track or element to attach");
1912
+ console.debug("[ReactorView] No tracks or element to attach");
1716
1913
  }
1717
- }, [videoTrack]);
1718
- const showPlaceholder = !videoTrack;
1914
+ }, [mediaStream]);
1915
+ const showPlaceholder = !videoMediaTrack;
1719
1916
  return /* @__PURE__ */ jsxs(
1720
1917
  "div",
1721
1918
  {
@@ -1735,7 +1932,7 @@ function ReactorView({
1735
1932
  objectFit: videoObjectFit,
1736
1933
  display: showPlaceholder ? "none" : "block"
1737
1934
  },
1738
- muted: true,
1935
+ muted,
1739
1936
  playsInline: true
1740
1937
  }
1741
1938
  ),
@@ -2223,6 +2420,7 @@ function ReactorController({
2223
2420
  import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
2224
2421
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2225
2422
  function WebcamStream({
2423
+ track,
2226
2424
  className,
2227
2425
  style,
2228
2426
  videoConstraints = {
@@ -2235,10 +2433,10 @@ function WebcamStream({
2235
2433
  const [stream, setStream] = useState4(null);
2236
2434
  const [isPublishing, setIsPublishing] = useState4(false);
2237
2435
  const [permissionDenied, setPermissionDenied] = useState4(false);
2238
- const { status, publishVideoStream, unpublishVideoStream, reactor } = useReactor((state) => ({
2436
+ const { status, publish, unpublish, reactor } = useReactor((state) => ({
2239
2437
  status: state.status,
2240
- publishVideoStream: state.publishVideoStream,
2241
- unpublishVideoStream: state.unpublishVideoStream,
2438
+ publish: state.publish,
2439
+ unpublish: state.unpublish,
2242
2440
  reactor: state.internal.reactor
2243
2441
  }));
2244
2442
  const videoRef = useRef4(null);
@@ -2263,15 +2461,15 @@ function WebcamStream({
2263
2461
  const stopWebcam = () => __async(null, null, function* () {
2264
2462
  console.debug("[WebcamPublisher] Stopping webcam");
2265
2463
  try {
2266
- yield unpublishVideoStream();
2464
+ yield unpublish(track);
2267
2465
  console.debug("[WebcamPublisher] Unpublished before stopping");
2268
2466
  } catch (err) {
2269
2467
  console.error("[WebcamPublisher] Error unpublishing before stop:", err);
2270
2468
  }
2271
2469
  setIsPublishing(false);
2272
- stream == null ? void 0 : stream.getTracks().forEach((track) => {
2273
- track.stop();
2274
- console.debug("[WebcamPublisher] Stopped track:", track.kind);
2470
+ stream == null ? void 0 : stream.getTracks().forEach((t) => {
2471
+ t.stop();
2472
+ console.debug("[WebcamPublisher] Stopped track:", t.kind);
2275
2473
  });
2276
2474
  setStream(null);
2277
2475
  console.debug("[WebcamPublisher] Webcam stopped");
@@ -2301,28 +2499,31 @@ function WebcamStream({
2301
2499
  console.debug(
2302
2500
  "[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
2303
2501
  );
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
- });
2502
+ const videoTrack = stream.getVideoTracks()[0];
2503
+ if (videoTrack) {
2504
+ publish(track, videoTrack).then(() => {
2505
+ console.debug("[WebcamPublisher] Auto-publish successful");
2506
+ setIsPublishing(true);
2507
+ }).catch((err) => {
2508
+ console.error("[WebcamPublisher] Auto-publish failed:", err);
2509
+ });
2510
+ }
2310
2511
  } else if (status !== "ready" && isPublishing) {
2311
2512
  console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
2312
- unpublishVideoStream().then(() => {
2513
+ unpublish(track).then(() => {
2313
2514
  console.debug("[WebcamPublisher] Auto-unpublish successful");
2314
2515
  setIsPublishing(false);
2315
2516
  }).catch((err) => {
2316
2517
  console.error("[WebcamPublisher] Auto-unpublish failed:", err);
2317
2518
  });
2318
2519
  }
2319
- }, [status, stream, isPublishing, publishVideoStream, unpublishVideoStream]);
2520
+ }, [status, stream, isPublishing, publish, unpublish, track]);
2320
2521
  useEffect4(() => {
2321
2522
  const handleError = (error) => {
2322
2523
  console.debug("[WebcamPublisher] Received error event:", error);
2323
- if (error.code === "VIDEO_PUBLISH_FAILED") {
2524
+ if (error.code === "TRACK_PUBLISH_FAILED") {
2324
2525
  console.debug(
2325
- "[WebcamPublisher] Video publish failed, resetting isPublishing state"
2526
+ "[WebcamPublisher] Track publish failed, resetting isPublishing state"
2326
2527
  );
2327
2528
  setIsPublishing(false);
2328
2529
  }
@@ -2437,11 +2638,13 @@ export {
2437
2638
  ReactorProvider,
2438
2639
  ReactorView,
2439
2640
  WebcamStream,
2641
+ audio,
2440
2642
  fetchInsecureJwtToken,
2441
2643
  useReactor,
2442
2644
  useReactorInternalMessage,
2443
2645
  useReactorMessage,
2444
2646
  useReactorStore,
2445
- useStats
2647
+ useStats,
2648
+ video
2446
2649
  };
2447
2650
  //# sourceMappingURL=index.mjs.map