@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.js CHANGED
@@ -86,16 +86,24 @@ __export(index_exports, {
86
86
  ReactorProvider: () => ReactorProvider,
87
87
  ReactorView: () => ReactorView,
88
88
  WebcamStream: () => WebcamStream,
89
+ audio: () => audio,
89
90
  fetchInsecureJwtToken: () => fetchInsecureJwtToken,
90
91
  useReactor: () => useReactor,
91
92
  useReactorInternalMessage: () => useReactorInternalMessage,
92
93
  useReactorMessage: () => useReactorMessage,
93
94
  useReactorStore: () => useReactorStore,
94
- useStats: () => useStats
95
+ useStats: () => useStats,
96
+ video: () => video
95
97
  });
96
98
  module.exports = __toCommonJS(index_exports);
97
99
 
98
100
  // src/types.ts
101
+ function video(name, _options) {
102
+ return { name, kind: "video" };
103
+ }
104
+ function audio(name, _options) {
105
+ return { name, kind: "audio" };
106
+ }
99
107
  var ConflictError = class extends Error {
100
108
  constructor(message) {
101
109
  super(message);
@@ -169,10 +177,53 @@ function createPeerConnection(config) {
169
177
  function createDataChannel(pc, label) {
170
178
  return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
171
179
  }
172
- function createOffer(pc) {
180
+ function rewriteMids(sdp, trackNames) {
181
+ const lines = sdp.split("\r\n");
182
+ let mediaIdx = 0;
183
+ const replacements = /* @__PURE__ */ new Map();
184
+ let inApplication = false;
185
+ for (let i = 0; i < lines.length; i++) {
186
+ if (lines[i].startsWith("m=")) {
187
+ inApplication = lines[i].startsWith("m=application");
188
+ }
189
+ if (!inApplication && lines[i].startsWith("a=mid:")) {
190
+ const oldMid = lines[i].substring("a=mid:".length);
191
+ if (mediaIdx < trackNames.length) {
192
+ const newMid = trackNames[mediaIdx];
193
+ replacements.set(oldMid, newMid);
194
+ lines[i] = `a=mid:${newMid}`;
195
+ mediaIdx++;
196
+ }
197
+ }
198
+ }
199
+ for (let i = 0; i < lines.length; i++) {
200
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
201
+ const parts = lines[i].split(" ");
202
+ for (let j = 1; j < parts.length; j++) {
203
+ const replacement = replacements.get(parts[j]);
204
+ if (replacement !== void 0) {
205
+ parts[j] = replacement;
206
+ }
207
+ }
208
+ lines[i] = parts.join(" ");
209
+ break;
210
+ }
211
+ }
212
+ return lines.join("\r\n");
213
+ }
214
+ function createOffer(pc, trackNames) {
173
215
  return __async(this, null, function* () {
174
216
  const offer = yield pc.createOffer();
175
- yield pc.setLocalDescription(offer);
217
+ if (trackNames && trackNames.length > 0 && offer.sdp) {
218
+ const munged = rewriteMids(offer.sdp, trackNames);
219
+ const mungedOffer = new RTCSessionDescription({
220
+ type: "offer",
221
+ sdp: munged
222
+ });
223
+ yield pc.setLocalDescription(mungedOffer);
224
+ } else {
225
+ yield pc.setLocalDescription(offer);
226
+ }
176
227
  yield waitForIceGathering(pc);
177
228
  const localDescription = pc.localDescription;
178
229
  if (!localDescription) {
@@ -677,6 +728,10 @@ var GPUMachineClient = class {
677
728
  constructor(config) {
678
729
  this.eventListeners = /* @__PURE__ */ new Map();
679
730
  this.status = "disconnected";
731
+ this.transceiverMap = /* @__PURE__ */ new Map();
732
+ this.publishedTracks = /* @__PURE__ */ new Map();
733
+ this.peerConnected = false;
734
+ this.dataChannelOpen = false;
680
735
  this.config = config;
681
736
  }
682
737
  // ─────────────────────────────────────────────────────────────────────────────
@@ -700,10 +755,18 @@ var GPUMachineClient = class {
700
755
  // SDP & Connection
701
756
  // ─────────────────────────────────────────────────────────────────────────────
702
757
  /**
703
- * Creates an SDP offer for initiating a connection.
758
+ * Creates an SDP offer based on the declared tracks.
759
+ *
760
+ * **RECEIVE** = client receives from the model (model → client) → `recvonly`
761
+ * **SEND** = client sends to the model (client → model) → `sendonly`
762
+ *
763
+ * Track names must be unique across both arrays. A name that appears in
764
+ * both `receive` and `send` will throw — use distinct names instead.
765
+ *
766
+ * The data channel is always created first (before transceivers).
704
767
  * Must be called before connect().
705
768
  */
706
- createOffer() {
769
+ createOffer(tracks) {
707
770
  return __async(this, null, function* () {
708
771
  if (!this.peerConnection) {
709
772
  this.peerConnection = createPeerConnection(this.config);
@@ -714,14 +777,54 @@ var GPUMachineClient = class {
714
777
  this.config.dataChannelLabel
715
778
  );
716
779
  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");
780
+ this.transceiverMap.clear();
781
+ const entries = this.buildTransceiverEntries(tracks);
782
+ for (const entry of entries) {
783
+ const transceiver = this.peerConnection.addTransceiver(entry.kind, {
784
+ direction: entry.direction
785
+ });
786
+ entry.transceiver = transceiver;
787
+ this.transceiverMap.set(entry.name, entry);
788
+ console.debug(
789
+ `[GPUMachineClient] Transceiver added: "${entry.name}" (${entry.kind}, ${entry.direction})`
790
+ );
791
+ }
792
+ const trackNames = entries.map((e) => e.name);
793
+ const offer = yield createOffer(this.peerConnection, trackNames);
794
+ console.debug(
795
+ "[GPUMachineClient] Created SDP offer with MIDs:",
796
+ trackNames
797
+ );
722
798
  return offer;
723
799
  });
724
800
  }
801
+ /**
802
+ * Builds an ordered list of transceiver entries from the receive/send arrays.
803
+ *
804
+ * Each track produces exactly one transceiver — `recvonly` for receive,
805
+ * `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
806
+ * supported; the same track name in both arrays is an error.
807
+ */
808
+ buildTransceiverEntries(tracks) {
809
+ const map = /* @__PURE__ */ new Map();
810
+ for (const t of tracks.receive) {
811
+ if (map.has(t.name)) {
812
+ throw new Error(
813
+ `Duplicate receive track name "${t.name}". Track names must be unique.`
814
+ );
815
+ }
816
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
817
+ }
818
+ for (const t of tracks.send) {
819
+ if (map.has(t.name)) {
820
+ throw new Error(
821
+ `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").`
822
+ );
823
+ }
824
+ map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
825
+ }
826
+ return Array.from(map.values());
827
+ }
725
828
  /**
726
829
  * Connects to the GPU machine using the provided SDP answer.
727
830
  * createOffer() must be called first.
@@ -757,8 +860,8 @@ var GPUMachineClient = class {
757
860
  return __async(this, null, function* () {
758
861
  this.stopPing();
759
862
  this.stopStatsPolling();
760
- if (this.publishedTrack) {
761
- yield this.unpublishTrack();
863
+ for (const name of Array.from(this.publishedTracks.keys())) {
864
+ yield this.unpublishTrack(name);
762
865
  }
763
866
  if (this.dataChannel) {
764
867
  this.dataChannel.close();
@@ -768,7 +871,9 @@ var GPUMachineClient = class {
768
871
  closePeerConnection(this.peerConnection);
769
872
  this.peerConnection = void 0;
770
873
  }
771
- this.videoTransceiver = void 0;
874
+ this.transceiverMap.clear();
875
+ this.peerConnected = false;
876
+ this.dataChannelOpen = false;
772
877
  this.setStatus("disconnected");
773
878
  console.debug("[GPUMachineClient] Disconnected");
774
879
  });
@@ -796,7 +901,7 @@ var GPUMachineClient = class {
796
901
  /**
797
902
  * Sends a command to the GPU machine via the data channel.
798
903
  * @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.
904
+ * @param data The data to send with the command. These are the parameters for the command, matching the schema in the capabilities dictionary.
800
905
  * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
801
906
  */
802
907
  sendCommand(command, data, scope = "application") {
@@ -813,63 +918,77 @@ var GPUMachineClient = class {
813
918
  // Track Publishing
814
919
  // ─────────────────────────────────────────────────────────────────────────────
815
920
  /**
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
921
+ * Publishes a MediaStreamTrack to the named send track.
922
+ *
923
+ * @param name The declared track name (must exist in transceiverMap with a sendable direction).
924
+ * @param track The MediaStreamTrack to publish.
820
925
  */
821
- publishTrack(track) {
926
+ publishTrack(name, track) {
822
927
  return __async(this, null, function* () {
823
928
  if (!this.peerConnection) {
824
929
  throw new Error(
825
- "[GPUMachineClient] Cannot publish track - not initialized"
930
+ `[GPUMachineClient] Cannot publish track "${name}" - not initialized`
826
931
  );
827
932
  }
828
933
  if (this.status !== "connected") {
829
934
  throw new Error(
830
- "[GPUMachineClient] Cannot publish track - not connected"
935
+ `[GPUMachineClient] Cannot publish track "${name}" - not connected`
936
+ );
937
+ }
938
+ const entry = this.transceiverMap.get(name);
939
+ if (!entry || !entry.transceiver) {
940
+ throw new Error(
941
+ `[GPUMachineClient] Cannot publish track "${name}" - no transceiver (was it declared in tracks.send?)`
831
942
  );
832
943
  }
833
- if (!this.videoTransceiver) {
944
+ if (entry.direction === "recvonly") {
834
945
  throw new Error(
835
- "[GPUMachineClient] Cannot publish track - no video transceiver"
946
+ `[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
836
947
  );
837
948
  }
838
949
  try {
839
- yield this.videoTransceiver.sender.replaceTrack(track);
840
- this.publishedTrack = track;
950
+ yield entry.transceiver.sender.replaceTrack(track);
951
+ this.publishedTracks.set(name, track);
841
952
  console.debug(
842
- "[GPUMachineClient] Track published successfully:",
843
- track.kind
953
+ `[GPUMachineClient] Track "${name}" published successfully`
844
954
  );
845
955
  } catch (error) {
846
- console.error("[GPUMachineClient] Failed to publish track:", error);
956
+ console.error(
957
+ `[GPUMachineClient] Failed to publish track "${name}":`,
958
+ error
959
+ );
847
960
  throw error;
848
961
  }
849
962
  });
850
963
  }
851
964
  /**
852
- * Unpublishes the currently published track.
965
+ * Unpublishes the track with the given name.
853
966
  */
854
- unpublishTrack() {
967
+ unpublishTrack(name) {
855
968
  return __async(this, null, function* () {
856
- if (!this.videoTransceiver || !this.publishedTrack) return;
969
+ const entry = this.transceiverMap.get(name);
970
+ if (!(entry == null ? void 0 : entry.transceiver) || !this.publishedTracks.has(name)) return;
857
971
  try {
858
- yield this.videoTransceiver.sender.replaceTrack(null);
859
- console.debug("[GPUMachineClient] Track unpublished successfully");
972
+ yield entry.transceiver.sender.replaceTrack(null);
973
+ console.debug(
974
+ `[GPUMachineClient] Track "${name}" unpublished successfully`
975
+ );
860
976
  } catch (error) {
861
- console.error("[GPUMachineClient] Failed to unpublish track:", error);
977
+ console.error(
978
+ `[GPUMachineClient] Failed to unpublish track "${name}":`,
979
+ error
980
+ );
862
981
  throw error;
863
982
  } finally {
864
- this.publishedTrack = void 0;
983
+ this.publishedTracks.delete(name);
865
984
  }
866
985
  });
867
986
  }
868
987
  /**
869
- * Returns the currently published track.
988
+ * Returns the currently published track for the given name.
870
989
  */
871
- getPublishedTrack() {
872
- return this.publishedTrack;
990
+ getPublishedTrack(name) {
991
+ return this.publishedTracks.get(name);
873
992
  }
874
993
  // ─────────────────────────────────────────────────────────────────────────────
875
994
  // Getters
@@ -940,6 +1059,12 @@ var GPUMachineClient = class {
940
1059
  // ─────────────────────────────────────────────────────────────────────────────
941
1060
  // Private Helpers
942
1061
  // ─────────────────────────────────────────────────────────────────────────────
1062
+ checkFullyConnected() {
1063
+ if (this.peerConnected && this.dataChannelOpen) {
1064
+ this.setStatus("connected");
1065
+ this.startStatsPolling();
1066
+ }
1067
+ }
943
1068
  setStatus(newStatus) {
944
1069
  if (this.status !== newStatus) {
945
1070
  this.status = newStatus;
@@ -955,14 +1080,16 @@ var GPUMachineClient = class {
955
1080
  if (state) {
956
1081
  switch (state) {
957
1082
  case "connected":
958
- this.setStatus("connected");
959
- this.startStatsPolling();
1083
+ this.peerConnected = true;
1084
+ this.checkFullyConnected();
960
1085
  break;
961
1086
  case "disconnected":
962
1087
  case "closed":
1088
+ this.peerConnected = false;
963
1089
  this.setStatus("disconnected");
964
1090
  break;
965
1091
  case "failed":
1092
+ this.peerConnected = false;
966
1093
  this.setStatus("error");
967
1094
  break;
968
1095
  }
@@ -970,9 +1097,13 @@ var GPUMachineClient = class {
970
1097
  };
971
1098
  this.peerConnection.ontrack = (event) => {
972
1099
  var _a;
973
- console.debug("[GPUMachineClient] Track received:", event.track.kind);
1100
+ const mid = event.transceiver.mid;
1101
+ const trackName = mid != null ? mid : `unknown-${event.track.id}`;
1102
+ console.debug(
1103
+ `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${mid})`
1104
+ );
974
1105
  const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
975
- this.emit("trackReceived", event.track, stream);
1106
+ this.emit("trackReceived", trackName, event.track, stream);
976
1107
  };
977
1108
  this.peerConnection.onicecandidate = (event) => {
978
1109
  if (event.candidate) {
@@ -992,10 +1123,13 @@ var GPUMachineClient = class {
992
1123
  if (!this.dataChannel) return;
993
1124
  this.dataChannel.onopen = () => {
994
1125
  console.debug("[GPUMachineClient] Data channel open");
1126
+ this.dataChannelOpen = true;
995
1127
  this.startPing();
1128
+ this.checkFullyConnected();
996
1129
  };
997
1130
  this.dataChannel.onclose = () => {
998
1131
  console.debug("[GPUMachineClient] Data channel closed");
1132
+ this.dataChannelOpen = false;
999
1133
  this.stopPing();
1000
1134
  };
1001
1135
  this.dataChannel.onerror = (error) => {
@@ -1029,10 +1163,29 @@ var GPUMachineClient = class {
1029
1163
  var import_zod2 = require("zod");
1030
1164
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
1031
1165
  var PROD_COORDINATOR_URL = "https://api.reactor.inc";
1166
+ var TrackConfigSchema = import_zod2.z.object({
1167
+ name: import_zod2.z.string(),
1168
+ kind: import_zod2.z.enum(["audio", "video"])
1169
+ });
1032
1170
  var OptionsSchema = import_zod2.z.object({
1033
1171
  coordinatorUrl: import_zod2.z.string().default(PROD_COORDINATOR_URL),
1034
1172
  modelName: import_zod2.z.string(),
1035
- local: import_zod2.z.boolean().default(false)
1173
+ local: import_zod2.z.boolean().default(false),
1174
+ /**
1175
+ * Tracks the client **RECEIVES** from the model (model → client).
1176
+ * Each entry produces a `recvonly` transceiver.
1177
+ * Names must be unique across both `receive` and `send`.
1178
+ *
1179
+ * When omitted, defaults to a single video track named `"main_video"`.
1180
+ * Pass an explicit empty array to opt out of the default.
1181
+ */
1182
+ receive: import_zod2.z.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
1183
+ /**
1184
+ * Tracks the client **SENDS** to the model (client → model).
1185
+ * Each entry produces a `sendonly` transceiver.
1186
+ * Names must be unique across both `receive` and `send`.
1187
+ */
1188
+ send: import_zod2.z.array(TrackConfigSchema).default([])
1036
1189
  });
1037
1190
  var Reactor = class {
1038
1191
  constructor(options) {
@@ -1043,7 +1196,9 @@ var Reactor = class {
1043
1196
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
1044
1197
  this.model = validatedOptions.modelName;
1045
1198
  this.local = validatedOptions.local;
1046
- if (this.local) {
1199
+ this.receive = validatedOptions.receive;
1200
+ this.send = validatedOptions.send;
1201
+ if (this.local && options.coordinatorUrl === void 0) {
1047
1202
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
1048
1203
  }
1049
1204
  }
@@ -1063,13 +1218,11 @@ var Reactor = class {
1063
1218
  (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
1064
1219
  }
1065
1220
  /**
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.
1221
+ * Sends a command to the model via the data channel.
1222
+ *
1223
+ * @param command The command name.
1069
1224
  * @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
1225
+ * @param scope "application" (default) for model commands, "runtime" for platform messages.
1073
1226
  */
1074
1227
  sendCommand(command, data, scope = "application") {
1075
1228
  return __async(this, null, function* () {
@@ -1093,24 +1246,27 @@ var Reactor = class {
1093
1246
  });
1094
1247
  }
1095
1248
  /**
1096
- * Public method to publish a track to the machine.
1097
- * @param track The track to send to the machine.
1249
+ * Publishes a MediaStreamTrack to a named send track.
1250
+ *
1251
+ * @param name The declared send track name (e.g. "webcam").
1252
+ * @param track The MediaStreamTrack to publish.
1098
1253
  */
1099
- publishTrack(track) {
1254
+ publishTrack(name, track) {
1100
1255
  return __async(this, null, function* () {
1101
1256
  var _a;
1102
1257
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
1103
- const errorMessage = `Cannot publish track, status is ${this.status}`;
1104
- console.warn("[Reactor]", errorMessage);
1258
+ console.warn(
1259
+ `[Reactor] Cannot publish track "${name}", status is ${this.status}`
1260
+ );
1105
1261
  return;
1106
1262
  }
1107
1263
  try {
1108
- yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
1264
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
1109
1265
  } catch (error) {
1110
- console.error("[Reactor] Failed to publish track:", error);
1266
+ console.error(`[Reactor] Failed to publish track "${name}":`, error);
1111
1267
  this.createError(
1112
1268
  "TRACK_PUBLISH_FAILED",
1113
- `Failed to publish track: ${error}`,
1269
+ `Failed to publish track "${name}": ${error}`,
1114
1270
  "gpu",
1115
1271
  true
1116
1272
  );
@@ -1118,18 +1274,20 @@ var Reactor = class {
1118
1274
  });
1119
1275
  }
1120
1276
  /**
1121
- * Public method to unpublish the currently published track.
1277
+ * Unpublishes the track with the given name.
1278
+ *
1279
+ * @param name The declared send track name to unpublish.
1122
1280
  */
1123
- unpublishTrack() {
1281
+ unpublishTrack(name) {
1124
1282
  return __async(this, null, function* () {
1125
1283
  var _a;
1126
1284
  try {
1127
- yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
1285
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
1128
1286
  } catch (error) {
1129
- console.error("[Reactor] Failed to unpublish track:", error);
1287
+ console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
1130
1288
  this.createError(
1131
1289
  "TRACK_UNPUBLISH_FAILED",
1132
- `Failed to unpublish track: ${error}`,
1290
+ `Failed to unpublish track "${name}": ${error}`,
1133
1291
  "gpu",
1134
1292
  true
1135
1293
  );
@@ -1156,7 +1314,10 @@ var Reactor = class {
1156
1314
  this.machineClient = new GPUMachineClient({ iceServers });
1157
1315
  this.setupMachineClientHandlers();
1158
1316
  }
1159
- const sdpOffer = yield this.machineClient.createOffer();
1317
+ const sdpOffer = yield this.machineClient.createOffer({
1318
+ send: this.send,
1319
+ receive: this.receive
1320
+ });
1160
1321
  try {
1161
1322
  const sdpAnswer = yield this.coordinatorClient.connect(
1162
1323
  this.sessionId,
@@ -1211,7 +1372,10 @@ var Reactor = class {
1211
1372
  const iceServers = yield this.coordinatorClient.getIceServers();
1212
1373
  this.machineClient = new GPUMachineClient({ iceServers });
1213
1374
  this.setupMachineClientHandlers();
1214
- const sdpOffer = yield this.machineClient.createOffer();
1375
+ const sdpOffer = yield this.machineClient.createOffer({
1376
+ send: this.send,
1377
+ receive: this.receive
1378
+ });
1215
1379
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1216
1380
  this.setSessionId(sessionId);
1217
1381
  const sdpAnswer = yield this.coordinatorClient.connect(
@@ -1273,8 +1437,8 @@ var Reactor = class {
1273
1437
  });
1274
1438
  this.machineClient.on(
1275
1439
  "trackReceived",
1276
- (track, stream) => {
1277
- this.emit("streamChanged", track, stream);
1440
+ (name, track, stream) => {
1441
+ this.emit("trackReceived", name, track, stream);
1278
1442
  }
1279
1443
  );
1280
1444
  this.machineClient.on("statsUpdate", (stats) => {
@@ -1292,7 +1456,11 @@ var Reactor = class {
1292
1456
  return;
1293
1457
  }
1294
1458
  if (this.coordinatorClient && !recoverable) {
1295
- yield this.coordinatorClient.terminateSession();
1459
+ try {
1460
+ yield this.coordinatorClient.terminateSession();
1461
+ } catch (error) {
1462
+ console.error("[Reactor] Error terminating session:", error);
1463
+ }
1296
1464
  this.coordinatorClient = void 0;
1297
1465
  }
1298
1466
  if (this.machineClient) {
@@ -1397,10 +1565,9 @@ var ReactorContext = (0, import_react2.createContext)(
1397
1565
  );
1398
1566
  var defaultInitState = {
1399
1567
  status: "disconnected",
1400
- videoTrack: null,
1568
+ tracks: {},
1401
1569
  lastError: void 0,
1402
1570
  sessionExpiration: void 0,
1403
- insecureApiKey: void 0,
1404
1571
  jwtToken: void 0,
1405
1572
  sessionId: void 0
1406
1573
  };
@@ -1421,7 +1588,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1421
1588
  oldStatus: get().status,
1422
1589
  newStatus
1423
1590
  });
1424
- set({ status: newStatus });
1591
+ if (newStatus === "disconnected") {
1592
+ set({ status: newStatus, tracks: {} });
1593
+ } else {
1594
+ set({ status: newStatus });
1595
+ }
1425
1596
  });
1426
1597
  reactor.on(
1427
1598
  "sessionExpirationChanged",
@@ -1433,13 +1604,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1433
1604
  set({ sessionExpiration: newSessionExpiration });
1434
1605
  }
1435
1606
  );
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
1607
+ reactor.on("trackReceived", (name, track) => {
1608
+ console.debug("[ReactorStore] Track received", {
1609
+ name,
1610
+ kind: track.kind,
1611
+ id: track.id
1441
1612
  });
1442
- set({ videoTrack });
1613
+ set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
1443
1614
  });
1444
1615
  reactor.on("error", (error) => {
1445
1616
  console.debug("[ReactorStore] Error occurred", error);
@@ -1503,27 +1674,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1503
1674
  throw error;
1504
1675
  }
1505
1676
  }),
1506
- publishVideoStream: (stream) => __async(null, null, function* () {
1507
- console.debug("[ReactorStore] Publishing video stream");
1677
+ publish: (name, track) => __async(null, null, function* () {
1678
+ console.debug(`[ReactorStore] Publishing track "${name}"`);
1508
1679
  try {
1509
- yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
1510
- console.debug("[ReactorStore] Video stream published successfully");
1680
+ yield get().internal.reactor.publishTrack(name, track);
1681
+ console.debug(
1682
+ `[ReactorStore] Track "${name}" published successfully`
1683
+ );
1511
1684
  } catch (error) {
1512
1685
  console.error(
1513
- "[ReactorStore] Failed to publish video stream:",
1686
+ `[ReactorStore] Failed to publish track "${name}":`,
1514
1687
  error
1515
1688
  );
1516
1689
  throw error;
1517
1690
  }
1518
1691
  }),
1519
- unpublishVideoStream: () => __async(null, null, function* () {
1520
- console.debug("[ReactorStore] Unpublishing video stream");
1692
+ unpublish: (name) => __async(null, null, function* () {
1693
+ console.debug(`[ReactorStore] Unpublishing track "${name}"`);
1521
1694
  try {
1522
- yield get().internal.reactor.unpublishTrack();
1523
- console.debug("[ReactorStore] Video stream unpublished successfully");
1695
+ yield get().internal.reactor.unpublishTrack(name);
1696
+ console.debug(
1697
+ `[ReactorStore] Track "${name}" unpublished successfully`
1698
+ );
1524
1699
  } catch (error) {
1525
1700
  console.error(
1526
- "[ReactorStore] Failed to unpublish video stream:",
1701
+ `[ReactorStore] Failed to unpublish track "${name}":`,
1527
1702
  error
1528
1703
  );
1529
1704
  throw error;
@@ -1569,7 +1744,7 @@ function ReactorProvider(_a) {
1569
1744
  console.debug("[ReactorProvider] Reactor store created successfully");
1570
1745
  }
1571
1746
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1572
- const { coordinatorUrl, modelName, local } = props;
1747
+ const { coordinatorUrl, modelName, local, receive, send } = props;
1573
1748
  const maxAttempts = pollingOptions.maxAttempts;
1574
1749
  (0, import_react3.useEffect)(() => {
1575
1750
  const handleBeforeUnload = () => {
@@ -1625,6 +1800,8 @@ function ReactorProvider(_a) {
1625
1800
  coordinatorUrl,
1626
1801
  modelName,
1627
1802
  local,
1803
+ receive,
1804
+ send,
1628
1805
  jwtToken
1629
1806
  })
1630
1807
  );
@@ -1651,7 +1828,16 @@ function ReactorProvider(_a) {
1651
1828
  console.error("[ReactorProvider] Failed to disconnect:", error);
1652
1829
  });
1653
1830
  };
1654
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1831
+ }, [
1832
+ coordinatorUrl,
1833
+ modelName,
1834
+ autoConnect,
1835
+ local,
1836
+ receive,
1837
+ send,
1838
+ jwtToken,
1839
+ maxAttempts
1840
+ ]);
1655
1841
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
1656
1842
  }
1657
1843
  function useReactorStore(selector) {
@@ -1720,47 +1906,60 @@ function useStats() {
1720
1906
  var import_react5 = require("react");
1721
1907
  var import_jsx_runtime2 = require("react/jsx-runtime");
1722
1908
  function ReactorView({
1909
+ track = "main_video",
1910
+ audioTrack,
1723
1911
  width,
1724
1912
  height,
1725
1913
  className,
1726
1914
  style,
1727
- videoObjectFit = "contain"
1915
+ videoObjectFit = "contain",
1916
+ muted = true
1728
1917
  }) {
1729
- const { videoTrack, status } = useReactor((state) => ({
1730
- videoTrack: state.videoTrack,
1731
- status: state.status
1732
- }));
1918
+ const { videoMediaTrack, audioMediaTrack, status } = useReactor((state) => {
1919
+ var _a, _b;
1920
+ return {
1921
+ videoMediaTrack: (_a = state.tracks[track]) != null ? _a : null,
1922
+ audioMediaTrack: audioTrack ? (_b = state.tracks[audioTrack]) != null ? _b : null : null,
1923
+ status: state.status
1924
+ };
1925
+ });
1733
1926
  const videoRef = (0, import_react5.useRef)(null);
1927
+ const mediaStream = (0, import_react5.useMemo)(() => {
1928
+ const tracks = [];
1929
+ if (videoMediaTrack) tracks.push(videoMediaTrack);
1930
+ if (audioMediaTrack) tracks.push(audioMediaTrack);
1931
+ if (tracks.length === 0) return null;
1932
+ return new MediaStream(tracks);
1933
+ }, [videoMediaTrack, audioMediaTrack]);
1734
1934
  (0, import_react5.useEffect)(() => {
1735
- console.debug("[ReactorView] Video track effect triggered", {
1935
+ console.debug("[ReactorView] Media track effect triggered", {
1936
+ track,
1736
1937
  hasVideoElement: !!videoRef.current,
1737
- hasVideoTrack: !!videoTrack,
1738
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind
1938
+ hasVideoTrack: !!videoMediaTrack,
1939
+ hasAudioTrack: !!audioMediaTrack
1739
1940
  });
1740
- if (videoRef.current && videoTrack) {
1741
- console.debug("[ReactorView] Attaching video track to element");
1941
+ if (videoRef.current && mediaStream) {
1942
+ console.debug("[ReactorView] Attaching media stream to element");
1742
1943
  try {
1743
- const stream = new MediaStream([videoTrack]);
1744
- videoRef.current.srcObject = stream;
1944
+ videoRef.current.srcObject = mediaStream;
1745
1945
  videoRef.current.play().catch((e) => {
1746
1946
  console.warn("[ReactorView] Auto-play failed:", e);
1747
1947
  });
1748
- console.debug("[ReactorView] Video track attached successfully");
1948
+ console.debug("[ReactorView] Media stream attached successfully");
1749
1949
  } catch (error) {
1750
- console.error("[ReactorView] Failed to attach video track:", error);
1950
+ console.error("[ReactorView] Failed to attach media stream:", error);
1751
1951
  }
1752
1952
  return () => {
1753
- console.debug("[ReactorView] Detaching video track from element");
1953
+ console.debug("[ReactorView] Detaching media stream from element");
1754
1954
  if (videoRef.current) {
1755
1955
  videoRef.current.srcObject = null;
1756
- console.debug("[ReactorView] Video track detached successfully");
1757
1956
  }
1758
1957
  };
1759
1958
  } else {
1760
- console.debug("[ReactorView] No video track or element to attach");
1959
+ console.debug("[ReactorView] No tracks or element to attach");
1761
1960
  }
1762
- }, [videoTrack]);
1763
- const showPlaceholder = !videoTrack;
1961
+ }, [mediaStream]);
1962
+ const showPlaceholder = !videoMediaTrack;
1764
1963
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1765
1964
  "div",
1766
1965
  {
@@ -1780,7 +1979,7 @@ function ReactorView({
1780
1979
  objectFit: videoObjectFit,
1781
1980
  display: showPlaceholder ? "none" : "block"
1782
1981
  },
1783
- muted: true,
1982
+ muted,
1784
1983
  playsInline: true
1785
1984
  }
1786
1985
  ),
@@ -2268,6 +2467,7 @@ function ReactorController({
2268
2467
  var import_react7 = require("react");
2269
2468
  var import_jsx_runtime4 = require("react/jsx-runtime");
2270
2469
  function WebcamStream({
2470
+ track,
2271
2471
  className,
2272
2472
  style,
2273
2473
  videoConstraints = {
@@ -2280,10 +2480,10 @@ function WebcamStream({
2280
2480
  const [stream, setStream] = (0, import_react7.useState)(null);
2281
2481
  const [isPublishing, setIsPublishing] = (0, import_react7.useState)(false);
2282
2482
  const [permissionDenied, setPermissionDenied] = (0, import_react7.useState)(false);
2283
- const { status, publishVideoStream, unpublishVideoStream, reactor } = useReactor((state) => ({
2483
+ const { status, publish, unpublish, reactor } = useReactor((state) => ({
2284
2484
  status: state.status,
2285
- publishVideoStream: state.publishVideoStream,
2286
- unpublishVideoStream: state.unpublishVideoStream,
2485
+ publish: state.publish,
2486
+ unpublish: state.unpublish,
2287
2487
  reactor: state.internal.reactor
2288
2488
  }));
2289
2489
  const videoRef = (0, import_react7.useRef)(null);
@@ -2308,15 +2508,15 @@ function WebcamStream({
2308
2508
  const stopWebcam = () => __async(null, null, function* () {
2309
2509
  console.debug("[WebcamPublisher] Stopping webcam");
2310
2510
  try {
2311
- yield unpublishVideoStream();
2511
+ yield unpublish(track);
2312
2512
  console.debug("[WebcamPublisher] Unpublished before stopping");
2313
2513
  } catch (err) {
2314
2514
  console.error("[WebcamPublisher] Error unpublishing before stop:", err);
2315
2515
  }
2316
2516
  setIsPublishing(false);
2317
- stream == null ? void 0 : stream.getTracks().forEach((track) => {
2318
- track.stop();
2319
- console.debug("[WebcamPublisher] Stopped track:", track.kind);
2517
+ stream == null ? void 0 : stream.getTracks().forEach((t) => {
2518
+ t.stop();
2519
+ console.debug("[WebcamPublisher] Stopped track:", t.kind);
2320
2520
  });
2321
2521
  setStream(null);
2322
2522
  console.debug("[WebcamPublisher] Webcam stopped");
@@ -2346,28 +2546,31 @@ function WebcamStream({
2346
2546
  console.debug(
2347
2547
  "[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
2348
2548
  );
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
- });
2549
+ const videoTrack = stream.getVideoTracks()[0];
2550
+ if (videoTrack) {
2551
+ publish(track, videoTrack).then(() => {
2552
+ console.debug("[WebcamPublisher] Auto-publish successful");
2553
+ setIsPublishing(true);
2554
+ }).catch((err) => {
2555
+ console.error("[WebcamPublisher] Auto-publish failed:", err);
2556
+ });
2557
+ }
2355
2558
  } else if (status !== "ready" && isPublishing) {
2356
2559
  console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
2357
- unpublishVideoStream().then(() => {
2560
+ unpublish(track).then(() => {
2358
2561
  console.debug("[WebcamPublisher] Auto-unpublish successful");
2359
2562
  setIsPublishing(false);
2360
2563
  }).catch((err) => {
2361
2564
  console.error("[WebcamPublisher] Auto-unpublish failed:", err);
2362
2565
  });
2363
2566
  }
2364
- }, [status, stream, isPublishing, publishVideoStream, unpublishVideoStream]);
2567
+ }, [status, stream, isPublishing, publish, unpublish, track]);
2365
2568
  (0, import_react7.useEffect)(() => {
2366
2569
  const handleError = (error) => {
2367
2570
  console.debug("[WebcamPublisher] Received error event:", error);
2368
- if (error.code === "VIDEO_PUBLISH_FAILED") {
2571
+ if (error.code === "TRACK_PUBLISH_FAILED") {
2369
2572
  console.debug(
2370
- "[WebcamPublisher] Video publish failed, resetting isPublishing state"
2573
+ "[WebcamPublisher] Track publish failed, resetting isPublishing state"
2371
2574
  );
2372
2575
  setIsPublishing(false);
2373
2576
  }
@@ -2483,11 +2686,13 @@ function fetchInsecureJwtToken(_0) {
2483
2686
  ReactorProvider,
2484
2687
  ReactorView,
2485
2688
  WebcamStream,
2689
+ audio,
2486
2690
  fetchInsecureJwtToken,
2487
2691
  useReactor,
2488
2692
  useReactorInternalMessage,
2489
2693
  useReactorMessage,
2490
2694
  useReactorStore,
2491
- useStats
2695
+ useStats,
2696
+ video
2492
2697
  });
2493
2698
  //# sourceMappingURL=index.js.map