@reactor-team/js-sdk 2.3.2 → 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,14 +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,
92
+ useReactorInternalMessage: () => useReactorInternalMessage,
91
93
  useReactorMessage: () => useReactorMessage,
92
- useReactorStore: () => useReactorStore
94
+ useReactorStore: () => useReactorStore,
95
+ useStats: () => useStats,
96
+ video: () => video
93
97
  });
94
98
  module.exports = __toCommonJS(index_exports);
95
99
 
96
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
+ }
97
107
  var ConflictError = class extends Error {
98
108
  constructor(message) {
99
109
  super(message);
@@ -167,10 +177,53 @@ function createPeerConnection(config) {
167
177
  function createDataChannel(pc, label) {
168
178
  return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
169
179
  }
170
- 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) {
171
215
  return __async(this, null, function* () {
172
216
  const offer = yield pc.createOffer();
173
- 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
+ }
174
227
  yield waitForIceGathering(pc);
175
228
  const localDescription = pc.localDescription;
176
229
  if (!localDescription) {
@@ -249,6 +302,52 @@ function parseMessage(data) {
249
302
  function closePeerConnection(pc) {
250
303
  pc.close();
251
304
  }
305
+ function extractConnectionStats(report) {
306
+ let rtt;
307
+ let availableOutgoingBitrate;
308
+ let localCandidateId;
309
+ let framesPerSecond;
310
+ let jitter;
311
+ let packetLossRatio;
312
+ report.forEach((stat) => {
313
+ if (stat.type === "candidate-pair" && stat.state === "succeeded") {
314
+ if (stat.currentRoundTripTime !== void 0) {
315
+ rtt = stat.currentRoundTripTime * 1e3;
316
+ }
317
+ if (stat.availableOutgoingBitrate !== void 0) {
318
+ availableOutgoingBitrate = stat.availableOutgoingBitrate;
319
+ }
320
+ localCandidateId = stat.localCandidateId;
321
+ }
322
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
323
+ if (stat.framesPerSecond !== void 0) {
324
+ framesPerSecond = stat.framesPerSecond;
325
+ }
326
+ if (stat.jitter !== void 0) {
327
+ jitter = stat.jitter;
328
+ }
329
+ if (stat.packetsReceived !== void 0 && stat.packetsLost !== void 0 && stat.packetsReceived + stat.packetsLost > 0) {
330
+ packetLossRatio = stat.packetsLost / (stat.packetsReceived + stat.packetsLost);
331
+ }
332
+ }
333
+ });
334
+ let candidateType;
335
+ if (localCandidateId) {
336
+ const localCandidate = report.get(localCandidateId);
337
+ if (localCandidate == null ? void 0 : localCandidate.candidateType) {
338
+ candidateType = localCandidate.candidateType;
339
+ }
340
+ }
341
+ return {
342
+ rtt,
343
+ candidateType,
344
+ availableOutgoingBitrate,
345
+ framesPerSecond,
346
+ packetLossRatio,
347
+ jitter,
348
+ timestamp: Date.now()
349
+ };
350
+ }
252
351
 
253
352
  // src/core/CoordinatorClient.ts
254
353
  var INITIAL_BACKOFF_MS = 500;
@@ -624,10 +723,15 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
624
723
 
625
724
  // src/core/GPUMachineClient.ts
626
725
  var PING_INTERVAL_MS = 5e3;
726
+ var STATS_INTERVAL_MS = 2e3;
627
727
  var GPUMachineClient = class {
628
728
  constructor(config) {
629
729
  this.eventListeners = /* @__PURE__ */ new Map();
630
730
  this.status = "disconnected";
731
+ this.transceiverMap = /* @__PURE__ */ new Map();
732
+ this.publishedTracks = /* @__PURE__ */ new Map();
733
+ this.peerConnected = false;
734
+ this.dataChannelOpen = false;
631
735
  this.config = config;
632
736
  }
633
737
  // ─────────────────────────────────────────────────────────────────────────────
@@ -651,10 +755,18 @@ var GPUMachineClient = class {
651
755
  // SDP & Connection
652
756
  // ─────────────────────────────────────────────────────────────────────────────
653
757
  /**
654
- * 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).
655
767
  * Must be called before connect().
656
768
  */
657
- createOffer() {
769
+ createOffer(tracks) {
658
770
  return __async(this, null, function* () {
659
771
  if (!this.peerConnection) {
660
772
  this.peerConnection = createPeerConnection(this.config);
@@ -665,14 +777,54 @@ var GPUMachineClient = class {
665
777
  this.config.dataChannelLabel
666
778
  );
667
779
  this.setupDataChannelHandlers();
668
- this.videoTransceiver = this.peerConnection.addTransceiver("video", {
669
- direction: "sendrecv"
670
- });
671
- const offer = yield createOffer(this.peerConnection);
672
- 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
+ );
673
798
  return offer;
674
799
  });
675
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
+ }
676
828
  /**
677
829
  * Connects to the GPU machine using the provided SDP answer.
678
830
  * createOffer() must be called first.
@@ -707,8 +859,9 @@ var GPUMachineClient = class {
707
859
  disconnect() {
708
860
  return __async(this, null, function* () {
709
861
  this.stopPing();
710
- if (this.publishedTrack) {
711
- yield this.unpublishTrack();
862
+ this.stopStatsPolling();
863
+ for (const name of Array.from(this.publishedTracks.keys())) {
864
+ yield this.unpublishTrack(name);
712
865
  }
713
866
  if (this.dataChannel) {
714
867
  this.dataChannel.close();
@@ -718,7 +871,9 @@ var GPUMachineClient = class {
718
871
  closePeerConnection(this.peerConnection);
719
872
  this.peerConnection = void 0;
720
873
  }
721
- this.videoTransceiver = void 0;
874
+ this.transceiverMap.clear();
875
+ this.peerConnected = false;
876
+ this.dataChannelOpen = false;
722
877
  this.setStatus("disconnected");
723
878
  console.debug("[GPUMachineClient] Disconnected");
724
879
  });
@@ -746,7 +901,7 @@ var GPUMachineClient = class {
746
901
  /**
747
902
  * Sends a command to the GPU machine via the data channel.
748
903
  * @param command The command to send
749
- * @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.
750
905
  * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
751
906
  */
752
907
  sendCommand(command, data, scope = "application") {
@@ -763,63 +918,77 @@ var GPUMachineClient = class {
763
918
  // Track Publishing
764
919
  // ─────────────────────────────────────────────────────────────────────────────
765
920
  /**
766
- * Publishes a track to the GPU machine.
767
- * Only one track can be published at a time.
768
- * Uses the existing transceiver's sender to replace the track.
769
- * @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.
770
925
  */
771
- publishTrack(track) {
926
+ publishTrack(name, track) {
772
927
  return __async(this, null, function* () {
773
928
  if (!this.peerConnection) {
774
929
  throw new Error(
775
- "[GPUMachineClient] Cannot publish track - not initialized"
930
+ `[GPUMachineClient] Cannot publish track "${name}" - not initialized`
776
931
  );
777
932
  }
778
933
  if (this.status !== "connected") {
779
934
  throw new Error(
780
- "[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?)`
781
942
  );
782
943
  }
783
- if (!this.videoTransceiver) {
944
+ if (entry.direction === "recvonly") {
784
945
  throw new Error(
785
- "[GPUMachineClient] Cannot publish track - no video transceiver"
946
+ `[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
786
947
  );
787
948
  }
788
949
  try {
789
- yield this.videoTransceiver.sender.replaceTrack(track);
790
- this.publishedTrack = track;
950
+ yield entry.transceiver.sender.replaceTrack(track);
951
+ this.publishedTracks.set(name, track);
791
952
  console.debug(
792
- "[GPUMachineClient] Track published successfully:",
793
- track.kind
953
+ `[GPUMachineClient] Track "${name}" published successfully`
794
954
  );
795
955
  } catch (error) {
796
- console.error("[GPUMachineClient] Failed to publish track:", error);
956
+ console.error(
957
+ `[GPUMachineClient] Failed to publish track "${name}":`,
958
+ error
959
+ );
797
960
  throw error;
798
961
  }
799
962
  });
800
963
  }
801
964
  /**
802
- * Unpublishes the currently published track.
965
+ * Unpublishes the track with the given name.
803
966
  */
804
- unpublishTrack() {
967
+ unpublishTrack(name) {
805
968
  return __async(this, null, function* () {
806
- 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;
807
971
  try {
808
- yield this.videoTransceiver.sender.replaceTrack(null);
809
- console.debug("[GPUMachineClient] Track unpublished successfully");
972
+ yield entry.transceiver.sender.replaceTrack(null);
973
+ console.debug(
974
+ `[GPUMachineClient] Track "${name}" unpublished successfully`
975
+ );
810
976
  } catch (error) {
811
- console.error("[GPUMachineClient] Failed to unpublish track:", error);
977
+ console.error(
978
+ `[GPUMachineClient] Failed to unpublish track "${name}":`,
979
+ error
980
+ );
812
981
  throw error;
813
982
  } finally {
814
- this.publishedTrack = void 0;
983
+ this.publishedTracks.delete(name);
815
984
  }
816
985
  });
817
986
  }
818
987
  /**
819
- * Returns the currently published track.
988
+ * Returns the currently published track for the given name.
820
989
  */
821
- getPublishedTrack() {
822
- return this.publishedTrack;
990
+ getPublishedTrack(name) {
991
+ return this.publishedTracks.get(name);
823
992
  }
824
993
  // ─────────────────────────────────────────────────────────────────────────────
825
994
  // Getters
@@ -863,8 +1032,39 @@ var GPUMachineClient = class {
863
1032
  }
864
1033
  }
865
1034
  // ─────────────────────────────────────────────────────────────────────────────
1035
+ // Stats Polling (RTT)
1036
+ // ─────────────────────────────────────────────────────────────────────────────
1037
+ getStats() {
1038
+ return this.stats;
1039
+ }
1040
+ startStatsPolling() {
1041
+ this.stopStatsPolling();
1042
+ this.statsInterval = setInterval(() => __async(this, null, function* () {
1043
+ if (!this.peerConnection) return;
1044
+ try {
1045
+ const report = yield this.peerConnection.getStats();
1046
+ this.stats = extractConnectionStats(report);
1047
+ this.emit("statsUpdate", this.stats);
1048
+ } catch (e) {
1049
+ }
1050
+ }), STATS_INTERVAL_MS);
1051
+ }
1052
+ stopStatsPolling() {
1053
+ if (this.statsInterval !== void 0) {
1054
+ clearInterval(this.statsInterval);
1055
+ this.statsInterval = void 0;
1056
+ }
1057
+ this.stats = void 0;
1058
+ }
1059
+ // ─────────────────────────────────────────────────────────────────────────────
866
1060
  // Private Helpers
867
1061
  // ─────────────────────────────────────────────────────────────────────────────
1062
+ checkFullyConnected() {
1063
+ if (this.peerConnected && this.dataChannelOpen) {
1064
+ this.setStatus("connected");
1065
+ this.startStatsPolling();
1066
+ }
1067
+ }
868
1068
  setStatus(newStatus) {
869
1069
  if (this.status !== newStatus) {
870
1070
  this.status = newStatus;
@@ -880,13 +1080,16 @@ var GPUMachineClient = class {
880
1080
  if (state) {
881
1081
  switch (state) {
882
1082
  case "connected":
883
- this.setStatus("connected");
1083
+ this.peerConnected = true;
1084
+ this.checkFullyConnected();
884
1085
  break;
885
1086
  case "disconnected":
886
1087
  case "closed":
1088
+ this.peerConnected = false;
887
1089
  this.setStatus("disconnected");
888
1090
  break;
889
1091
  case "failed":
1092
+ this.peerConnected = false;
890
1093
  this.setStatus("error");
891
1094
  break;
892
1095
  }
@@ -894,9 +1097,13 @@ var GPUMachineClient = class {
894
1097
  };
895
1098
  this.peerConnection.ontrack = (event) => {
896
1099
  var _a;
897
- 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
+ );
898
1105
  const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
899
- this.emit("trackReceived", event.track, stream);
1106
+ this.emit("trackReceived", trackName, event.track, stream);
900
1107
  };
901
1108
  this.peerConnection.onicecandidate = (event) => {
902
1109
  if (event.candidate) {
@@ -916,10 +1123,13 @@ var GPUMachineClient = class {
916
1123
  if (!this.dataChannel) return;
917
1124
  this.dataChannel.onopen = () => {
918
1125
  console.debug("[GPUMachineClient] Data channel open");
1126
+ this.dataChannelOpen = true;
919
1127
  this.startPing();
1128
+ this.checkFullyConnected();
920
1129
  };
921
1130
  this.dataChannel.onclose = () => {
922
1131
  console.debug("[GPUMachineClient] Data channel closed");
1132
+ this.dataChannelOpen = false;
923
1133
  this.stopPing();
924
1134
  };
925
1135
  this.dataChannel.onerror = (error) => {
@@ -953,10 +1163,29 @@ var GPUMachineClient = class {
953
1163
  var import_zod2 = require("zod");
954
1164
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
955
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
+ });
956
1170
  var OptionsSchema = import_zod2.z.object({
957
1171
  coordinatorUrl: import_zod2.z.string().default(PROD_COORDINATOR_URL),
958
1172
  modelName: import_zod2.z.string(),
959
- 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([])
960
1189
  });
961
1190
  var Reactor = class {
962
1191
  constructor(options) {
@@ -967,7 +1196,9 @@ var Reactor = class {
967
1196
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
968
1197
  this.model = validatedOptions.modelName;
969
1198
  this.local = validatedOptions.local;
970
- if (this.local) {
1199
+ this.receive = validatedOptions.receive;
1200
+ this.send = validatedOptions.send;
1201
+ if (this.local && options.coordinatorUrl === void 0) {
971
1202
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
972
1203
  }
973
1204
  }
@@ -987,13 +1218,11 @@ var Reactor = class {
987
1218
  (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
988
1219
  }
989
1220
  /**
990
- * Public method to send a message to the machine.
991
- * Wraps the message in the specified channel envelope (defaults to "application").
992
- * @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.
993
1224
  * @param data The command payload.
994
- * @param scope The envelope scope – "application" (default) for model commands,
995
- * "runtime" for platform-level messages (e.g. requestCapabilities).
996
- * @throws Error if not in ready state
1225
+ * @param scope "application" (default) for model commands, "runtime" for platform messages.
997
1226
  */
998
1227
  sendCommand(command, data, scope = "application") {
999
1228
  return __async(this, null, function* () {
@@ -1017,24 +1246,27 @@ var Reactor = class {
1017
1246
  });
1018
1247
  }
1019
1248
  /**
1020
- * Public method to publish a track to the machine.
1021
- * @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.
1022
1253
  */
1023
- publishTrack(track) {
1254
+ publishTrack(name, track) {
1024
1255
  return __async(this, null, function* () {
1025
1256
  var _a;
1026
1257
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
1027
- const errorMessage = `Cannot publish track, status is ${this.status}`;
1028
- console.warn("[Reactor]", errorMessage);
1258
+ console.warn(
1259
+ `[Reactor] Cannot publish track "${name}", status is ${this.status}`
1260
+ );
1029
1261
  return;
1030
1262
  }
1031
1263
  try {
1032
- yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
1264
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
1033
1265
  } catch (error) {
1034
- console.error("[Reactor] Failed to publish track:", error);
1266
+ console.error(`[Reactor] Failed to publish track "${name}":`, error);
1035
1267
  this.createError(
1036
1268
  "TRACK_PUBLISH_FAILED",
1037
- `Failed to publish track: ${error}`,
1269
+ `Failed to publish track "${name}": ${error}`,
1038
1270
  "gpu",
1039
1271
  true
1040
1272
  );
@@ -1042,18 +1274,20 @@ var Reactor = class {
1042
1274
  });
1043
1275
  }
1044
1276
  /**
1045
- * 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.
1046
1280
  */
1047
- unpublishTrack() {
1281
+ unpublishTrack(name) {
1048
1282
  return __async(this, null, function* () {
1049
1283
  var _a;
1050
1284
  try {
1051
- yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
1285
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
1052
1286
  } catch (error) {
1053
- console.error("[Reactor] Failed to unpublish track:", error);
1287
+ console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
1054
1288
  this.createError(
1055
1289
  "TRACK_UNPUBLISH_FAILED",
1056
- `Failed to unpublish track: ${error}`,
1290
+ `Failed to unpublish track "${name}": ${error}`,
1057
1291
  "gpu",
1058
1292
  true
1059
1293
  );
@@ -1080,7 +1314,10 @@ var Reactor = class {
1080
1314
  this.machineClient = new GPUMachineClient({ iceServers });
1081
1315
  this.setupMachineClientHandlers();
1082
1316
  }
1083
- const sdpOffer = yield this.machineClient.createOffer();
1317
+ const sdpOffer = yield this.machineClient.createOffer({
1318
+ send: this.send,
1319
+ receive: this.receive
1320
+ });
1084
1321
  try {
1085
1322
  const sdpAnswer = yield this.coordinatorClient.connect(
1086
1323
  this.sessionId,
@@ -1135,7 +1372,10 @@ var Reactor = class {
1135
1372
  const iceServers = yield this.coordinatorClient.getIceServers();
1136
1373
  this.machineClient = new GPUMachineClient({ iceServers });
1137
1374
  this.setupMachineClientHandlers();
1138
- const sdpOffer = yield this.machineClient.createOffer();
1375
+ const sdpOffer = yield this.machineClient.createOffer({
1376
+ send: this.send,
1377
+ receive: this.receive
1378
+ });
1139
1379
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1140
1380
  this.setSessionId(sessionId);
1141
1381
  const sdpAnswer = yield this.coordinatorClient.connect(
@@ -1170,7 +1410,11 @@ var Reactor = class {
1170
1410
  setupMachineClientHandlers() {
1171
1411
  if (!this.machineClient) return;
1172
1412
  this.machineClient.on("message", (message, scope) => {
1173
- this.emit("newMessage", message, scope);
1413
+ if (scope === "application") {
1414
+ this.emit("message", message);
1415
+ } else if (scope === "runtime") {
1416
+ this.emit("runtimeMessage", message);
1417
+ }
1174
1418
  });
1175
1419
  this.machineClient.on("statusChanged", (status) => {
1176
1420
  switch (status) {
@@ -1193,10 +1437,13 @@ var Reactor = class {
1193
1437
  });
1194
1438
  this.machineClient.on(
1195
1439
  "trackReceived",
1196
- (track, stream) => {
1197
- this.emit("streamChanged", track, stream);
1440
+ (name, track, stream) => {
1441
+ this.emit("trackReceived", name, track, stream);
1198
1442
  }
1199
1443
  );
1444
+ this.machineClient.on("statsUpdate", (stats) => {
1445
+ this.emit("statsUpdate", stats);
1446
+ });
1200
1447
  }
1201
1448
  /**
1202
1449
  * Disconnects from the coordinator and the gpu machine.
@@ -1209,7 +1456,11 @@ var Reactor = class {
1209
1456
  return;
1210
1457
  }
1211
1458
  if (this.coordinatorClient && !recoverable) {
1212
- yield this.coordinatorClient.terminateSession();
1459
+ try {
1460
+ yield this.coordinatorClient.terminateSession();
1461
+ } catch (error) {
1462
+ console.error("[Reactor] Error terminating session:", error);
1463
+ }
1213
1464
  this.coordinatorClient = void 0;
1214
1465
  }
1215
1466
  if (this.machineClient) {
@@ -1283,6 +1534,10 @@ var Reactor = class {
1283
1534
  getLastError() {
1284
1535
  return this.lastError;
1285
1536
  }
1537
+ getStats() {
1538
+ var _a;
1539
+ return (_a = this.machineClient) == null ? void 0 : _a.getStats();
1540
+ }
1286
1541
  /**
1287
1542
  * Create and store an error
1288
1543
  */
@@ -1310,10 +1565,9 @@ var ReactorContext = (0, import_react2.createContext)(
1310
1565
  );
1311
1566
  var defaultInitState = {
1312
1567
  status: "disconnected",
1313
- videoTrack: null,
1568
+ tracks: {},
1314
1569
  lastError: void 0,
1315
1570
  sessionExpiration: void 0,
1316
- insecureApiKey: void 0,
1317
1571
  jwtToken: void 0,
1318
1572
  sessionId: void 0
1319
1573
  };
@@ -1334,7 +1588,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1334
1588
  oldStatus: get().status,
1335
1589
  newStatus
1336
1590
  });
1337
- set({ status: newStatus });
1591
+ if (newStatus === "disconnected") {
1592
+ set({ status: newStatus, tracks: {} });
1593
+ } else {
1594
+ set({ status: newStatus });
1595
+ }
1338
1596
  });
1339
1597
  reactor.on(
1340
1598
  "sessionExpirationChanged",
@@ -1346,13 +1604,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1346
1604
  set({ sessionExpiration: newSessionExpiration });
1347
1605
  }
1348
1606
  );
1349
- reactor.on("streamChanged", (videoTrack) => {
1350
- console.debug("[ReactorStore] Stream changed", {
1351
- hasVideoTrack: !!videoTrack,
1352
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind,
1353
- 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
1354
1612
  });
1355
- set({ videoTrack });
1613
+ set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
1356
1614
  });
1357
1615
  reactor.on("error", (error) => {
1358
1616
  console.debug("[ReactorStore] Error occurred", error);
@@ -1371,10 +1629,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1371
1629
  // actions
1372
1630
  onMessage: (handler) => {
1373
1631
  console.debug("[ReactorStore] Registering message handler");
1374
- get().internal.reactor.on("newMessage", handler);
1632
+ get().internal.reactor.on("message", handler);
1375
1633
  return () => {
1376
1634
  console.debug("[ReactorStore] Cleaning up message handler");
1377
- get().internal.reactor.off("newMessage", handler);
1635
+ get().internal.reactor.off("message", handler);
1378
1636
  };
1379
1637
  },
1380
1638
  sendCommand: (command, data, scope) => __async(null, null, function* () {
@@ -1416,27 +1674,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1416
1674
  throw error;
1417
1675
  }
1418
1676
  }),
1419
- publishVideoStream: (stream) => __async(null, null, function* () {
1420
- console.debug("[ReactorStore] Publishing video stream");
1677
+ publish: (name, track) => __async(null, null, function* () {
1678
+ console.debug(`[ReactorStore] Publishing track "${name}"`);
1421
1679
  try {
1422
- yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
1423
- 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
+ );
1424
1684
  } catch (error) {
1425
1685
  console.error(
1426
- "[ReactorStore] Failed to publish video stream:",
1686
+ `[ReactorStore] Failed to publish track "${name}":`,
1427
1687
  error
1428
1688
  );
1429
1689
  throw error;
1430
1690
  }
1431
1691
  }),
1432
- unpublishVideoStream: () => __async(null, null, function* () {
1433
- console.debug("[ReactorStore] Unpublishing video stream");
1692
+ unpublish: (name) => __async(null, null, function* () {
1693
+ console.debug(`[ReactorStore] Unpublishing track "${name}"`);
1434
1694
  try {
1435
- yield get().internal.reactor.unpublishTrack();
1436
- console.debug("[ReactorStore] Video stream unpublished successfully");
1695
+ yield get().internal.reactor.unpublishTrack(name);
1696
+ console.debug(
1697
+ `[ReactorStore] Track "${name}" unpublished successfully`
1698
+ );
1437
1699
  } catch (error) {
1438
1700
  console.error(
1439
- "[ReactorStore] Failed to unpublish video stream:",
1701
+ `[ReactorStore] Failed to unpublish track "${name}":`,
1440
1702
  error
1441
1703
  );
1442
1704
  throw error;
@@ -1482,7 +1744,7 @@ function ReactorProvider(_a) {
1482
1744
  console.debug("[ReactorProvider] Reactor store created successfully");
1483
1745
  }
1484
1746
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1485
- const { coordinatorUrl, modelName, local } = props;
1747
+ const { coordinatorUrl, modelName, local, receive, send } = props;
1486
1748
  const maxAttempts = pollingOptions.maxAttempts;
1487
1749
  (0, import_react3.useEffect)(() => {
1488
1750
  const handleBeforeUnload = () => {
@@ -1538,6 +1800,8 @@ function ReactorProvider(_a) {
1538
1800
  coordinatorUrl,
1539
1801
  modelName,
1540
1802
  local,
1803
+ receive,
1804
+ send,
1541
1805
  jwtToken
1542
1806
  })
1543
1807
  );
@@ -1564,7 +1828,16 @@ function ReactorProvider(_a) {
1564
1828
  console.error("[ReactorProvider] Failed to disconnect:", error);
1565
1829
  });
1566
1830
  };
1567
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1831
+ }, [
1832
+ coordinatorUrl,
1833
+ modelName,
1834
+ autoConnect,
1835
+ local,
1836
+ receive,
1837
+ send,
1838
+ jwtToken,
1839
+ maxAttempts
1840
+ ]);
1568
1841
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
1569
1842
  }
1570
1843
  function useReactorStore(selector) {
@@ -1588,68 +1861,105 @@ function useReactorMessage(handler) {
1588
1861
  handlerRef.current = handler;
1589
1862
  }, [handler]);
1590
1863
  (0, import_react4.useEffect)(() => {
1591
- console.debug("[useReactorMessage] Setting up message subscription");
1592
- const stableHandler = (message, scope) => {
1593
- console.debug("[useReactorMessage] Message received", {
1594
- message,
1595
- scope
1596
- });
1597
- handlerRef.current(message, scope);
1864
+ const stableHandler = (message) => {
1865
+ handlerRef.current(message);
1598
1866
  };
1599
- reactor.on("newMessage", stableHandler);
1600
- console.debug("[useReactorMessage] Message handler registered");
1867
+ reactor.on("message", stableHandler);
1601
1868
  return () => {
1602
- console.debug("[useReactorMessage] Cleaning up message subscription");
1603
- reactor.off("newMessage", stableHandler);
1869
+ reactor.off("message", stableHandler);
1604
1870
  };
1605
1871
  }, [reactor]);
1606
1872
  }
1873
+ function useReactorInternalMessage(handler) {
1874
+ const reactor = useReactor((state) => state.internal.reactor);
1875
+ const handlerRef = (0, import_react4.useRef)(handler);
1876
+ (0, import_react4.useEffect)(() => {
1877
+ handlerRef.current = handler;
1878
+ }, [handler]);
1879
+ (0, import_react4.useEffect)(() => {
1880
+ const stableHandler = (message) => {
1881
+ handlerRef.current(message);
1882
+ };
1883
+ reactor.on("runtimeMessage", stableHandler);
1884
+ return () => {
1885
+ reactor.off("runtimeMessage", stableHandler);
1886
+ };
1887
+ }, [reactor]);
1888
+ }
1889
+ function useStats() {
1890
+ const reactor = useReactor((state) => state.internal.reactor);
1891
+ const [stats, setStats] = (0, import_react4.useState)(void 0);
1892
+ (0, import_react4.useEffect)(() => {
1893
+ const handler = (newStats) => {
1894
+ setStats(newStats);
1895
+ };
1896
+ reactor.on("statsUpdate", handler);
1897
+ return () => {
1898
+ reactor.off("statsUpdate", handler);
1899
+ setStats(void 0);
1900
+ };
1901
+ }, [reactor]);
1902
+ return stats;
1903
+ }
1607
1904
 
1608
1905
  // src/react/ReactorView.tsx
1609
1906
  var import_react5 = require("react");
1610
1907
  var import_jsx_runtime2 = require("react/jsx-runtime");
1611
1908
  function ReactorView({
1909
+ track = "main_video",
1910
+ audioTrack,
1612
1911
  width,
1613
1912
  height,
1614
1913
  className,
1615
1914
  style,
1616
- videoObjectFit = "contain"
1915
+ videoObjectFit = "contain",
1916
+ muted = true
1617
1917
  }) {
1618
- const { videoTrack, status } = useReactor((state) => ({
1619
- videoTrack: state.videoTrack,
1620
- status: state.status
1621
- }));
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
+ });
1622
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]);
1623
1934
  (0, import_react5.useEffect)(() => {
1624
- console.debug("[ReactorView] Video track effect triggered", {
1935
+ console.debug("[ReactorView] Media track effect triggered", {
1936
+ track,
1625
1937
  hasVideoElement: !!videoRef.current,
1626
- hasVideoTrack: !!videoTrack,
1627
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind
1938
+ hasVideoTrack: !!videoMediaTrack,
1939
+ hasAudioTrack: !!audioMediaTrack
1628
1940
  });
1629
- if (videoRef.current && videoTrack) {
1630
- console.debug("[ReactorView] Attaching video track to element");
1941
+ if (videoRef.current && mediaStream) {
1942
+ console.debug("[ReactorView] Attaching media stream to element");
1631
1943
  try {
1632
- const stream = new MediaStream([videoTrack]);
1633
- videoRef.current.srcObject = stream;
1944
+ videoRef.current.srcObject = mediaStream;
1634
1945
  videoRef.current.play().catch((e) => {
1635
1946
  console.warn("[ReactorView] Auto-play failed:", e);
1636
1947
  });
1637
- console.debug("[ReactorView] Video track attached successfully");
1948
+ console.debug("[ReactorView] Media stream attached successfully");
1638
1949
  } catch (error) {
1639
- console.error("[ReactorView] Failed to attach video track:", error);
1950
+ console.error("[ReactorView] Failed to attach media stream:", error);
1640
1951
  }
1641
1952
  return () => {
1642
- console.debug("[ReactorView] Detaching video track from element");
1953
+ console.debug("[ReactorView] Detaching media stream from element");
1643
1954
  if (videoRef.current) {
1644
1955
  videoRef.current.srcObject = null;
1645
- console.debug("[ReactorView] Video track detached successfully");
1646
1956
  }
1647
1957
  };
1648
1958
  } else {
1649
- console.debug("[ReactorView] No video track or element to attach");
1959
+ console.debug("[ReactorView] No tracks or element to attach");
1650
1960
  }
1651
- }, [videoTrack]);
1652
- const showPlaceholder = !videoTrack;
1961
+ }, [mediaStream]);
1962
+ const showPlaceholder = !videoMediaTrack;
1653
1963
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1654
1964
  "div",
1655
1965
  {
@@ -1669,7 +1979,7 @@ function ReactorView({
1669
1979
  objectFit: videoObjectFit,
1670
1980
  display: showPlaceholder ? "none" : "block"
1671
1981
  },
1672
- muted: true,
1982
+ muted,
1673
1983
  playsInline: true
1674
1984
  }
1675
1985
  ),
@@ -1740,8 +2050,8 @@ function ReactorController({
1740
2050
  }, 5e3);
1741
2051
  return () => clearInterval(interval);
1742
2052
  }, [status, commands, requestCapabilities]);
1743
- useReactorMessage((message, scope) => {
1744
- if (scope === "runtime" && message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
2053
+ useReactorInternalMessage((message) => {
2054
+ if (message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1745
2055
  const commandsMessage = message.data;
1746
2056
  setCommands(commandsMessage.commands);
1747
2057
  const initialValues = {};
@@ -2157,6 +2467,7 @@ function ReactorController({
2157
2467
  var import_react7 = require("react");
2158
2468
  var import_jsx_runtime4 = require("react/jsx-runtime");
2159
2469
  function WebcamStream({
2470
+ track,
2160
2471
  className,
2161
2472
  style,
2162
2473
  videoConstraints = {
@@ -2169,10 +2480,10 @@ function WebcamStream({
2169
2480
  const [stream, setStream] = (0, import_react7.useState)(null);
2170
2481
  const [isPublishing, setIsPublishing] = (0, import_react7.useState)(false);
2171
2482
  const [permissionDenied, setPermissionDenied] = (0, import_react7.useState)(false);
2172
- const { status, publishVideoStream, unpublishVideoStream, reactor } = useReactor((state) => ({
2483
+ const { status, publish, unpublish, reactor } = useReactor((state) => ({
2173
2484
  status: state.status,
2174
- publishVideoStream: state.publishVideoStream,
2175
- unpublishVideoStream: state.unpublishVideoStream,
2485
+ publish: state.publish,
2486
+ unpublish: state.unpublish,
2176
2487
  reactor: state.internal.reactor
2177
2488
  }));
2178
2489
  const videoRef = (0, import_react7.useRef)(null);
@@ -2197,15 +2508,15 @@ function WebcamStream({
2197
2508
  const stopWebcam = () => __async(null, null, function* () {
2198
2509
  console.debug("[WebcamPublisher] Stopping webcam");
2199
2510
  try {
2200
- yield unpublishVideoStream();
2511
+ yield unpublish(track);
2201
2512
  console.debug("[WebcamPublisher] Unpublished before stopping");
2202
2513
  } catch (err) {
2203
2514
  console.error("[WebcamPublisher] Error unpublishing before stop:", err);
2204
2515
  }
2205
2516
  setIsPublishing(false);
2206
- stream == null ? void 0 : stream.getTracks().forEach((track) => {
2207
- track.stop();
2208
- 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);
2209
2520
  });
2210
2521
  setStream(null);
2211
2522
  console.debug("[WebcamPublisher] Webcam stopped");
@@ -2235,28 +2546,31 @@ function WebcamStream({
2235
2546
  console.debug(
2236
2547
  "[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
2237
2548
  );
2238
- publishVideoStream(stream).then(() => {
2239
- console.debug("[WebcamPublisher] Auto-publish successful");
2240
- setIsPublishing(true);
2241
- }).catch((err) => {
2242
- console.error("[WebcamPublisher] Auto-publish failed:", err);
2243
- });
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
+ }
2244
2558
  } else if (status !== "ready" && isPublishing) {
2245
2559
  console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
2246
- unpublishVideoStream().then(() => {
2560
+ unpublish(track).then(() => {
2247
2561
  console.debug("[WebcamPublisher] Auto-unpublish successful");
2248
2562
  setIsPublishing(false);
2249
2563
  }).catch((err) => {
2250
2564
  console.error("[WebcamPublisher] Auto-unpublish failed:", err);
2251
2565
  });
2252
2566
  }
2253
- }, [status, stream, isPublishing, publishVideoStream, unpublishVideoStream]);
2567
+ }, [status, stream, isPublishing, publish, unpublish, track]);
2254
2568
  (0, import_react7.useEffect)(() => {
2255
2569
  const handleError = (error) => {
2256
2570
  console.debug("[WebcamPublisher] Received error event:", error);
2257
- if (error.code === "VIDEO_PUBLISH_FAILED") {
2571
+ if (error.code === "TRACK_PUBLISH_FAILED") {
2258
2572
  console.debug(
2259
- "[WebcamPublisher] Video publish failed, resetting isPublishing state"
2573
+ "[WebcamPublisher] Track publish failed, resetting isPublishing state"
2260
2574
  );
2261
2575
  setIsPublishing(false);
2262
2576
  }
@@ -2372,9 +2686,13 @@ function fetchInsecureJwtToken(_0) {
2372
2686
  ReactorProvider,
2373
2687
  ReactorView,
2374
2688
  WebcamStream,
2689
+ audio,
2375
2690
  fetchInsecureJwtToken,
2376
2691
  useReactor,
2692
+ useReactorInternalMessage,
2377
2693
  useReactorMessage,
2378
- useReactorStore
2694
+ useReactorStore,
2695
+ useStats,
2696
+ video
2379
2697
  });
2380
2698
  //# sourceMappingURL=index.js.map