@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.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) {
@@ -206,6 +255,52 @@ function parseMessage(data) {
206
255
  function closePeerConnection(pc) {
207
256
  pc.close();
208
257
  }
258
+ function extractConnectionStats(report) {
259
+ let rtt;
260
+ let availableOutgoingBitrate;
261
+ let localCandidateId;
262
+ let framesPerSecond;
263
+ let jitter;
264
+ let packetLossRatio;
265
+ report.forEach((stat) => {
266
+ if (stat.type === "candidate-pair" && stat.state === "succeeded") {
267
+ if (stat.currentRoundTripTime !== void 0) {
268
+ rtt = stat.currentRoundTripTime * 1e3;
269
+ }
270
+ if (stat.availableOutgoingBitrate !== void 0) {
271
+ availableOutgoingBitrate = stat.availableOutgoingBitrate;
272
+ }
273
+ localCandidateId = stat.localCandidateId;
274
+ }
275
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
276
+ if (stat.framesPerSecond !== void 0) {
277
+ framesPerSecond = stat.framesPerSecond;
278
+ }
279
+ if (stat.jitter !== void 0) {
280
+ jitter = stat.jitter;
281
+ }
282
+ if (stat.packetsReceived !== void 0 && stat.packetsLost !== void 0 && stat.packetsReceived + stat.packetsLost > 0) {
283
+ packetLossRatio = stat.packetsLost / (stat.packetsReceived + stat.packetsLost);
284
+ }
285
+ }
286
+ });
287
+ let candidateType;
288
+ if (localCandidateId) {
289
+ const localCandidate = report.get(localCandidateId);
290
+ if (localCandidate == null ? void 0 : localCandidate.candidateType) {
291
+ candidateType = localCandidate.candidateType;
292
+ }
293
+ }
294
+ return {
295
+ rtt,
296
+ candidateType,
297
+ availableOutgoingBitrate,
298
+ framesPerSecond,
299
+ packetLossRatio,
300
+ jitter,
301
+ timestamp: Date.now()
302
+ };
303
+ }
209
304
 
210
305
  // src/core/CoordinatorClient.ts
211
306
  var INITIAL_BACKOFF_MS = 500;
@@ -581,10 +676,15 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
581
676
 
582
677
  // src/core/GPUMachineClient.ts
583
678
  var PING_INTERVAL_MS = 5e3;
679
+ var STATS_INTERVAL_MS = 2e3;
584
680
  var GPUMachineClient = class {
585
681
  constructor(config) {
586
682
  this.eventListeners = /* @__PURE__ */ new Map();
587
683
  this.status = "disconnected";
684
+ this.transceiverMap = /* @__PURE__ */ new Map();
685
+ this.publishedTracks = /* @__PURE__ */ new Map();
686
+ this.peerConnected = false;
687
+ this.dataChannelOpen = false;
588
688
  this.config = config;
589
689
  }
590
690
  // ─────────────────────────────────────────────────────────────────────────────
@@ -608,10 +708,18 @@ var GPUMachineClient = class {
608
708
  // SDP & Connection
609
709
  // ─────────────────────────────────────────────────────────────────────────────
610
710
  /**
611
- * 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).
612
720
  * Must be called before connect().
613
721
  */
614
- createOffer() {
722
+ createOffer(tracks) {
615
723
  return __async(this, null, function* () {
616
724
  if (!this.peerConnection) {
617
725
  this.peerConnection = createPeerConnection(this.config);
@@ -622,14 +730,54 @@ var GPUMachineClient = class {
622
730
  this.config.dataChannelLabel
623
731
  );
624
732
  this.setupDataChannelHandlers();
625
- this.videoTransceiver = this.peerConnection.addTransceiver("video", {
626
- direction: "sendrecv"
627
- });
628
- const offer = yield createOffer(this.peerConnection);
629
- 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
+ );
630
751
  return offer;
631
752
  });
632
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
+ }
633
781
  /**
634
782
  * Connects to the GPU machine using the provided SDP answer.
635
783
  * createOffer() must be called first.
@@ -664,8 +812,9 @@ var GPUMachineClient = class {
664
812
  disconnect() {
665
813
  return __async(this, null, function* () {
666
814
  this.stopPing();
667
- if (this.publishedTrack) {
668
- yield this.unpublishTrack();
815
+ this.stopStatsPolling();
816
+ for (const name of Array.from(this.publishedTracks.keys())) {
817
+ yield this.unpublishTrack(name);
669
818
  }
670
819
  if (this.dataChannel) {
671
820
  this.dataChannel.close();
@@ -675,7 +824,9 @@ var GPUMachineClient = class {
675
824
  closePeerConnection(this.peerConnection);
676
825
  this.peerConnection = void 0;
677
826
  }
678
- this.videoTransceiver = void 0;
827
+ this.transceiverMap.clear();
828
+ this.peerConnected = false;
829
+ this.dataChannelOpen = false;
679
830
  this.setStatus("disconnected");
680
831
  console.debug("[GPUMachineClient] Disconnected");
681
832
  });
@@ -703,7 +854,7 @@ var GPUMachineClient = class {
703
854
  /**
704
855
  * Sends a command to the GPU machine via the data channel.
705
856
  * @param command The command to send
706
- * @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.
707
858
  * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
708
859
  */
709
860
  sendCommand(command, data, scope = "application") {
@@ -720,63 +871,77 @@ var GPUMachineClient = class {
720
871
  // Track Publishing
721
872
  // ─────────────────────────────────────────────────────────────────────────────
722
873
  /**
723
- * Publishes a track to the GPU machine.
724
- * Only one track can be published at a time.
725
- * Uses the existing transceiver's sender to replace the track.
726
- * @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.
727
878
  */
728
- publishTrack(track) {
879
+ publishTrack(name, track) {
729
880
  return __async(this, null, function* () {
730
881
  if (!this.peerConnection) {
731
882
  throw new Error(
732
- "[GPUMachineClient] Cannot publish track - not initialized"
883
+ `[GPUMachineClient] Cannot publish track "${name}" - not initialized`
733
884
  );
734
885
  }
735
886
  if (this.status !== "connected") {
736
887
  throw new Error(
737
- "[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?)`
738
895
  );
739
896
  }
740
- if (!this.videoTransceiver) {
897
+ if (entry.direction === "recvonly") {
741
898
  throw new Error(
742
- "[GPUMachineClient] Cannot publish track - no video transceiver"
899
+ `[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
743
900
  );
744
901
  }
745
902
  try {
746
- yield this.videoTransceiver.sender.replaceTrack(track);
747
- this.publishedTrack = track;
903
+ yield entry.transceiver.sender.replaceTrack(track);
904
+ this.publishedTracks.set(name, track);
748
905
  console.debug(
749
- "[GPUMachineClient] Track published successfully:",
750
- track.kind
906
+ `[GPUMachineClient] Track "${name}" published successfully`
751
907
  );
752
908
  } catch (error) {
753
- console.error("[GPUMachineClient] Failed to publish track:", error);
909
+ console.error(
910
+ `[GPUMachineClient] Failed to publish track "${name}":`,
911
+ error
912
+ );
754
913
  throw error;
755
914
  }
756
915
  });
757
916
  }
758
917
  /**
759
- * Unpublishes the currently published track.
918
+ * Unpublishes the track with the given name.
760
919
  */
761
- unpublishTrack() {
920
+ unpublishTrack(name) {
762
921
  return __async(this, null, function* () {
763
- 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;
764
924
  try {
765
- yield this.videoTransceiver.sender.replaceTrack(null);
766
- console.debug("[GPUMachineClient] Track unpublished successfully");
925
+ yield entry.transceiver.sender.replaceTrack(null);
926
+ console.debug(
927
+ `[GPUMachineClient] Track "${name}" unpublished successfully`
928
+ );
767
929
  } catch (error) {
768
- console.error("[GPUMachineClient] Failed to unpublish track:", error);
930
+ console.error(
931
+ `[GPUMachineClient] Failed to unpublish track "${name}":`,
932
+ error
933
+ );
769
934
  throw error;
770
935
  } finally {
771
- this.publishedTrack = void 0;
936
+ this.publishedTracks.delete(name);
772
937
  }
773
938
  });
774
939
  }
775
940
  /**
776
- * Returns the currently published track.
941
+ * Returns the currently published track for the given name.
777
942
  */
778
- getPublishedTrack() {
779
- return this.publishedTrack;
943
+ getPublishedTrack(name) {
944
+ return this.publishedTracks.get(name);
780
945
  }
781
946
  // ─────────────────────────────────────────────────────────────────────────────
782
947
  // Getters
@@ -820,8 +985,39 @@ var GPUMachineClient = class {
820
985
  }
821
986
  }
822
987
  // ─────────────────────────────────────────────────────────────────────────────
988
+ // Stats Polling (RTT)
989
+ // ─────────────────────────────────────────────────────────────────────────────
990
+ getStats() {
991
+ return this.stats;
992
+ }
993
+ startStatsPolling() {
994
+ this.stopStatsPolling();
995
+ this.statsInterval = setInterval(() => __async(this, null, function* () {
996
+ if (!this.peerConnection) return;
997
+ try {
998
+ const report = yield this.peerConnection.getStats();
999
+ this.stats = extractConnectionStats(report);
1000
+ this.emit("statsUpdate", this.stats);
1001
+ } catch (e) {
1002
+ }
1003
+ }), STATS_INTERVAL_MS);
1004
+ }
1005
+ stopStatsPolling() {
1006
+ if (this.statsInterval !== void 0) {
1007
+ clearInterval(this.statsInterval);
1008
+ this.statsInterval = void 0;
1009
+ }
1010
+ this.stats = void 0;
1011
+ }
1012
+ // ─────────────────────────────────────────────────────────────────────────────
823
1013
  // Private Helpers
824
1014
  // ─────────────────────────────────────────────────────────────────────────────
1015
+ checkFullyConnected() {
1016
+ if (this.peerConnected && this.dataChannelOpen) {
1017
+ this.setStatus("connected");
1018
+ this.startStatsPolling();
1019
+ }
1020
+ }
825
1021
  setStatus(newStatus) {
826
1022
  if (this.status !== newStatus) {
827
1023
  this.status = newStatus;
@@ -837,13 +1033,16 @@ var GPUMachineClient = class {
837
1033
  if (state) {
838
1034
  switch (state) {
839
1035
  case "connected":
840
- this.setStatus("connected");
1036
+ this.peerConnected = true;
1037
+ this.checkFullyConnected();
841
1038
  break;
842
1039
  case "disconnected":
843
1040
  case "closed":
1041
+ this.peerConnected = false;
844
1042
  this.setStatus("disconnected");
845
1043
  break;
846
1044
  case "failed":
1045
+ this.peerConnected = false;
847
1046
  this.setStatus("error");
848
1047
  break;
849
1048
  }
@@ -851,9 +1050,13 @@ var GPUMachineClient = class {
851
1050
  };
852
1051
  this.peerConnection.ontrack = (event) => {
853
1052
  var _a;
854
- 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
+ );
855
1058
  const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
856
- this.emit("trackReceived", event.track, stream);
1059
+ this.emit("trackReceived", trackName, event.track, stream);
857
1060
  };
858
1061
  this.peerConnection.onicecandidate = (event) => {
859
1062
  if (event.candidate) {
@@ -873,10 +1076,13 @@ var GPUMachineClient = class {
873
1076
  if (!this.dataChannel) return;
874
1077
  this.dataChannel.onopen = () => {
875
1078
  console.debug("[GPUMachineClient] Data channel open");
1079
+ this.dataChannelOpen = true;
876
1080
  this.startPing();
1081
+ this.checkFullyConnected();
877
1082
  };
878
1083
  this.dataChannel.onclose = () => {
879
1084
  console.debug("[GPUMachineClient] Data channel closed");
1085
+ this.dataChannelOpen = false;
880
1086
  this.stopPing();
881
1087
  };
882
1088
  this.dataChannel.onerror = (error) => {
@@ -910,10 +1116,29 @@ var GPUMachineClient = class {
910
1116
  import { z as z2 } from "zod";
911
1117
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
912
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
+ });
913
1123
  var OptionsSchema = z2.object({
914
1124
  coordinatorUrl: z2.string().default(PROD_COORDINATOR_URL),
915
1125
  modelName: z2.string(),
916
- 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([])
917
1142
  });
918
1143
  var Reactor = class {
919
1144
  constructor(options) {
@@ -924,7 +1149,9 @@ var Reactor = class {
924
1149
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
925
1150
  this.model = validatedOptions.modelName;
926
1151
  this.local = validatedOptions.local;
927
- if (this.local) {
1152
+ this.receive = validatedOptions.receive;
1153
+ this.send = validatedOptions.send;
1154
+ if (this.local && options.coordinatorUrl === void 0) {
928
1155
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
929
1156
  }
930
1157
  }
@@ -944,13 +1171,11 @@ var Reactor = class {
944
1171
  (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
945
1172
  }
946
1173
  /**
947
- * Public method to send a message to the machine.
948
- * Wraps the message in the specified channel envelope (defaults to "application").
949
- * @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.
950
1177
  * @param data The command payload.
951
- * @param scope The envelope scope – "application" (default) for model commands,
952
- * "runtime" for platform-level messages (e.g. requestCapabilities).
953
- * @throws Error if not in ready state
1178
+ * @param scope "application" (default) for model commands, "runtime" for platform messages.
954
1179
  */
955
1180
  sendCommand(command, data, scope = "application") {
956
1181
  return __async(this, null, function* () {
@@ -974,24 +1199,27 @@ var Reactor = class {
974
1199
  });
975
1200
  }
976
1201
  /**
977
- * Public method to publish a track to the machine.
978
- * @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.
979
1206
  */
980
- publishTrack(track) {
1207
+ publishTrack(name, track) {
981
1208
  return __async(this, null, function* () {
982
1209
  var _a;
983
1210
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
984
- const errorMessage = `Cannot publish track, status is ${this.status}`;
985
- console.warn("[Reactor]", errorMessage);
1211
+ console.warn(
1212
+ `[Reactor] Cannot publish track "${name}", status is ${this.status}`
1213
+ );
986
1214
  return;
987
1215
  }
988
1216
  try {
989
- yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
1217
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
990
1218
  } catch (error) {
991
- console.error("[Reactor] Failed to publish track:", error);
1219
+ console.error(`[Reactor] Failed to publish track "${name}":`, error);
992
1220
  this.createError(
993
1221
  "TRACK_PUBLISH_FAILED",
994
- `Failed to publish track: ${error}`,
1222
+ `Failed to publish track "${name}": ${error}`,
995
1223
  "gpu",
996
1224
  true
997
1225
  );
@@ -999,18 +1227,20 @@ var Reactor = class {
999
1227
  });
1000
1228
  }
1001
1229
  /**
1002
- * 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.
1003
1233
  */
1004
- unpublishTrack() {
1234
+ unpublishTrack(name) {
1005
1235
  return __async(this, null, function* () {
1006
1236
  var _a;
1007
1237
  try {
1008
- yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
1238
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
1009
1239
  } catch (error) {
1010
- console.error("[Reactor] Failed to unpublish track:", error);
1240
+ console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
1011
1241
  this.createError(
1012
1242
  "TRACK_UNPUBLISH_FAILED",
1013
- `Failed to unpublish track: ${error}`,
1243
+ `Failed to unpublish track "${name}": ${error}`,
1014
1244
  "gpu",
1015
1245
  true
1016
1246
  );
@@ -1037,7 +1267,10 @@ var Reactor = class {
1037
1267
  this.machineClient = new GPUMachineClient({ iceServers });
1038
1268
  this.setupMachineClientHandlers();
1039
1269
  }
1040
- const sdpOffer = yield this.machineClient.createOffer();
1270
+ const sdpOffer = yield this.machineClient.createOffer({
1271
+ send: this.send,
1272
+ receive: this.receive
1273
+ });
1041
1274
  try {
1042
1275
  const sdpAnswer = yield this.coordinatorClient.connect(
1043
1276
  this.sessionId,
@@ -1092,7 +1325,10 @@ var Reactor = class {
1092
1325
  const iceServers = yield this.coordinatorClient.getIceServers();
1093
1326
  this.machineClient = new GPUMachineClient({ iceServers });
1094
1327
  this.setupMachineClientHandlers();
1095
- const sdpOffer = yield this.machineClient.createOffer();
1328
+ const sdpOffer = yield this.machineClient.createOffer({
1329
+ send: this.send,
1330
+ receive: this.receive
1331
+ });
1096
1332
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1097
1333
  this.setSessionId(sessionId);
1098
1334
  const sdpAnswer = yield this.coordinatorClient.connect(
@@ -1127,7 +1363,11 @@ var Reactor = class {
1127
1363
  setupMachineClientHandlers() {
1128
1364
  if (!this.machineClient) return;
1129
1365
  this.machineClient.on("message", (message, scope) => {
1130
- this.emit("newMessage", message, scope);
1366
+ if (scope === "application") {
1367
+ this.emit("message", message);
1368
+ } else if (scope === "runtime") {
1369
+ this.emit("runtimeMessage", message);
1370
+ }
1131
1371
  });
1132
1372
  this.machineClient.on("statusChanged", (status) => {
1133
1373
  switch (status) {
@@ -1150,10 +1390,13 @@ var Reactor = class {
1150
1390
  });
1151
1391
  this.machineClient.on(
1152
1392
  "trackReceived",
1153
- (track, stream) => {
1154
- this.emit("streamChanged", track, stream);
1393
+ (name, track, stream) => {
1394
+ this.emit("trackReceived", name, track, stream);
1155
1395
  }
1156
1396
  );
1397
+ this.machineClient.on("statsUpdate", (stats) => {
1398
+ this.emit("statsUpdate", stats);
1399
+ });
1157
1400
  }
1158
1401
  /**
1159
1402
  * Disconnects from the coordinator and the gpu machine.
@@ -1166,7 +1409,11 @@ var Reactor = class {
1166
1409
  return;
1167
1410
  }
1168
1411
  if (this.coordinatorClient && !recoverable) {
1169
- yield this.coordinatorClient.terminateSession();
1412
+ try {
1413
+ yield this.coordinatorClient.terminateSession();
1414
+ } catch (error) {
1415
+ console.error("[Reactor] Error terminating session:", error);
1416
+ }
1170
1417
  this.coordinatorClient = void 0;
1171
1418
  }
1172
1419
  if (this.machineClient) {
@@ -1240,6 +1487,10 @@ var Reactor = class {
1240
1487
  getLastError() {
1241
1488
  return this.lastError;
1242
1489
  }
1490
+ getStats() {
1491
+ var _a;
1492
+ return (_a = this.machineClient) == null ? void 0 : _a.getStats();
1493
+ }
1243
1494
  /**
1244
1495
  * Create and store an error
1245
1496
  */
@@ -1267,10 +1518,9 @@ var ReactorContext = createContext(
1267
1518
  );
1268
1519
  var defaultInitState = {
1269
1520
  status: "disconnected",
1270
- videoTrack: null,
1521
+ tracks: {},
1271
1522
  lastError: void 0,
1272
1523
  sessionExpiration: void 0,
1273
- insecureApiKey: void 0,
1274
1524
  jwtToken: void 0,
1275
1525
  sessionId: void 0
1276
1526
  };
@@ -1291,7 +1541,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1291
1541
  oldStatus: get().status,
1292
1542
  newStatus
1293
1543
  });
1294
- set({ status: newStatus });
1544
+ if (newStatus === "disconnected") {
1545
+ set({ status: newStatus, tracks: {} });
1546
+ } else {
1547
+ set({ status: newStatus });
1548
+ }
1295
1549
  });
1296
1550
  reactor.on(
1297
1551
  "sessionExpirationChanged",
@@ -1303,13 +1557,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1303
1557
  set({ sessionExpiration: newSessionExpiration });
1304
1558
  }
1305
1559
  );
1306
- reactor.on("streamChanged", (videoTrack) => {
1307
- console.debug("[ReactorStore] Stream changed", {
1308
- hasVideoTrack: !!videoTrack,
1309
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind,
1310
- 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
1311
1565
  });
1312
- set({ videoTrack });
1566
+ set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
1313
1567
  });
1314
1568
  reactor.on("error", (error) => {
1315
1569
  console.debug("[ReactorStore] Error occurred", error);
@@ -1328,10 +1582,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1328
1582
  // actions
1329
1583
  onMessage: (handler) => {
1330
1584
  console.debug("[ReactorStore] Registering message handler");
1331
- get().internal.reactor.on("newMessage", handler);
1585
+ get().internal.reactor.on("message", handler);
1332
1586
  return () => {
1333
1587
  console.debug("[ReactorStore] Cleaning up message handler");
1334
- get().internal.reactor.off("newMessage", handler);
1588
+ get().internal.reactor.off("message", handler);
1335
1589
  };
1336
1590
  },
1337
1591
  sendCommand: (command, data, scope) => __async(null, null, function* () {
@@ -1373,27 +1627,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1373
1627
  throw error;
1374
1628
  }
1375
1629
  }),
1376
- publishVideoStream: (stream) => __async(null, null, function* () {
1377
- console.debug("[ReactorStore] Publishing video stream");
1630
+ publish: (name, track) => __async(null, null, function* () {
1631
+ console.debug(`[ReactorStore] Publishing track "${name}"`);
1378
1632
  try {
1379
- yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
1380
- 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
+ );
1381
1637
  } catch (error) {
1382
1638
  console.error(
1383
- "[ReactorStore] Failed to publish video stream:",
1639
+ `[ReactorStore] Failed to publish track "${name}":`,
1384
1640
  error
1385
1641
  );
1386
1642
  throw error;
1387
1643
  }
1388
1644
  }),
1389
- unpublishVideoStream: () => __async(null, null, function* () {
1390
- console.debug("[ReactorStore] Unpublishing video stream");
1645
+ unpublish: (name) => __async(null, null, function* () {
1646
+ console.debug(`[ReactorStore] Unpublishing track "${name}"`);
1391
1647
  try {
1392
- yield get().internal.reactor.unpublishTrack();
1393
- console.debug("[ReactorStore] Video stream unpublished successfully");
1648
+ yield get().internal.reactor.unpublishTrack(name);
1649
+ console.debug(
1650
+ `[ReactorStore] Track "${name}" unpublished successfully`
1651
+ );
1394
1652
  } catch (error) {
1395
1653
  console.error(
1396
- "[ReactorStore] Failed to unpublish video stream:",
1654
+ `[ReactorStore] Failed to unpublish track "${name}":`,
1397
1655
  error
1398
1656
  );
1399
1657
  throw error;
@@ -1439,7 +1697,7 @@ function ReactorProvider(_a) {
1439
1697
  console.debug("[ReactorProvider] Reactor store created successfully");
1440
1698
  }
1441
1699
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1442
- const { coordinatorUrl, modelName, local } = props;
1700
+ const { coordinatorUrl, modelName, local, receive, send } = props;
1443
1701
  const maxAttempts = pollingOptions.maxAttempts;
1444
1702
  useEffect(() => {
1445
1703
  const handleBeforeUnload = () => {
@@ -1495,6 +1753,8 @@ function ReactorProvider(_a) {
1495
1753
  coordinatorUrl,
1496
1754
  modelName,
1497
1755
  local,
1756
+ receive,
1757
+ send,
1498
1758
  jwtToken
1499
1759
  })
1500
1760
  );
@@ -1521,7 +1781,16 @@ function ReactorProvider(_a) {
1521
1781
  console.error("[ReactorProvider] Failed to disconnect:", error);
1522
1782
  });
1523
1783
  };
1524
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1784
+ }, [
1785
+ coordinatorUrl,
1786
+ modelName,
1787
+ autoConnect,
1788
+ local,
1789
+ receive,
1790
+ send,
1791
+ jwtToken,
1792
+ maxAttempts
1793
+ ]);
1525
1794
  return /* @__PURE__ */ jsx(ReactorContext.Provider, { value: storeRef.current, children });
1526
1795
  }
1527
1796
  function useReactorStore(selector) {
@@ -1534,7 +1803,7 @@ function useReactorStore(selector) {
1534
1803
 
1535
1804
  // src/react/hooks.ts
1536
1805
  import { useShallow } from "zustand/shallow";
1537
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
1806
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1538
1807
  function useReactor(selector) {
1539
1808
  return useReactorStore(useShallow(selector));
1540
1809
  }
@@ -1545,68 +1814,105 @@ function useReactorMessage(handler) {
1545
1814
  handlerRef.current = handler;
1546
1815
  }, [handler]);
1547
1816
  useEffect2(() => {
1548
- console.debug("[useReactorMessage] Setting up message subscription");
1549
- const stableHandler = (message, scope) => {
1550
- console.debug("[useReactorMessage] Message received", {
1551
- message,
1552
- scope
1553
- });
1554
- handlerRef.current(message, scope);
1817
+ const stableHandler = (message) => {
1818
+ handlerRef.current(message);
1555
1819
  };
1556
- reactor.on("newMessage", stableHandler);
1557
- console.debug("[useReactorMessage] Message handler registered");
1820
+ reactor.on("message", stableHandler);
1558
1821
  return () => {
1559
- console.debug("[useReactorMessage] Cleaning up message subscription");
1560
- reactor.off("newMessage", stableHandler);
1822
+ reactor.off("message", stableHandler);
1561
1823
  };
1562
1824
  }, [reactor]);
1563
1825
  }
1826
+ function useReactorInternalMessage(handler) {
1827
+ const reactor = useReactor((state) => state.internal.reactor);
1828
+ const handlerRef = useRef2(handler);
1829
+ useEffect2(() => {
1830
+ handlerRef.current = handler;
1831
+ }, [handler]);
1832
+ useEffect2(() => {
1833
+ const stableHandler = (message) => {
1834
+ handlerRef.current(message);
1835
+ };
1836
+ reactor.on("runtimeMessage", stableHandler);
1837
+ return () => {
1838
+ reactor.off("runtimeMessage", stableHandler);
1839
+ };
1840
+ }, [reactor]);
1841
+ }
1842
+ function useStats() {
1843
+ const reactor = useReactor((state) => state.internal.reactor);
1844
+ const [stats, setStats] = useState2(void 0);
1845
+ useEffect2(() => {
1846
+ const handler = (newStats) => {
1847
+ setStats(newStats);
1848
+ };
1849
+ reactor.on("statsUpdate", handler);
1850
+ return () => {
1851
+ reactor.off("statsUpdate", handler);
1852
+ setStats(void 0);
1853
+ };
1854
+ }, [reactor]);
1855
+ return stats;
1856
+ }
1564
1857
 
1565
1858
  // src/react/ReactorView.tsx
1566
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
1859
+ import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
1567
1860
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1568
1861
  function ReactorView({
1862
+ track = "main_video",
1863
+ audioTrack,
1569
1864
  width,
1570
1865
  height,
1571
1866
  className,
1572
1867
  style,
1573
- videoObjectFit = "contain"
1868
+ videoObjectFit = "contain",
1869
+ muted = true
1574
1870
  }) {
1575
- const { videoTrack, status } = useReactor((state) => ({
1576
- videoTrack: state.videoTrack,
1577
- status: state.status
1578
- }));
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
+ });
1579
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]);
1580
1887
  useEffect3(() => {
1581
- console.debug("[ReactorView] Video track effect triggered", {
1888
+ console.debug("[ReactorView] Media track effect triggered", {
1889
+ track,
1582
1890
  hasVideoElement: !!videoRef.current,
1583
- hasVideoTrack: !!videoTrack,
1584
- videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind
1891
+ hasVideoTrack: !!videoMediaTrack,
1892
+ hasAudioTrack: !!audioMediaTrack
1585
1893
  });
1586
- if (videoRef.current && videoTrack) {
1587
- console.debug("[ReactorView] Attaching video track to element");
1894
+ if (videoRef.current && mediaStream) {
1895
+ console.debug("[ReactorView] Attaching media stream to element");
1588
1896
  try {
1589
- const stream = new MediaStream([videoTrack]);
1590
- videoRef.current.srcObject = stream;
1897
+ videoRef.current.srcObject = mediaStream;
1591
1898
  videoRef.current.play().catch((e) => {
1592
1899
  console.warn("[ReactorView] Auto-play failed:", e);
1593
1900
  });
1594
- console.debug("[ReactorView] Video track attached successfully");
1901
+ console.debug("[ReactorView] Media stream attached successfully");
1595
1902
  } catch (error) {
1596
- console.error("[ReactorView] Failed to attach video track:", error);
1903
+ console.error("[ReactorView] Failed to attach media stream:", error);
1597
1904
  }
1598
1905
  return () => {
1599
- console.debug("[ReactorView] Detaching video track from element");
1906
+ console.debug("[ReactorView] Detaching media stream from element");
1600
1907
  if (videoRef.current) {
1601
1908
  videoRef.current.srcObject = null;
1602
- console.debug("[ReactorView] Video track detached successfully");
1603
1909
  }
1604
1910
  };
1605
1911
  } else {
1606
- console.debug("[ReactorView] No video track or element to attach");
1912
+ console.debug("[ReactorView] No tracks or element to attach");
1607
1913
  }
1608
- }, [videoTrack]);
1609
- const showPlaceholder = !videoTrack;
1914
+ }, [mediaStream]);
1915
+ const showPlaceholder = !videoMediaTrack;
1610
1916
  return /* @__PURE__ */ jsxs(
1611
1917
  "div",
1612
1918
  {
@@ -1626,7 +1932,7 @@ function ReactorView({
1626
1932
  objectFit: videoObjectFit,
1627
1933
  display: showPlaceholder ? "none" : "block"
1628
1934
  },
1629
- muted: true,
1935
+ muted,
1630
1936
  playsInline: true
1631
1937
  }
1632
1938
  ),
@@ -1658,7 +1964,7 @@ function ReactorView({
1658
1964
  }
1659
1965
 
1660
1966
  // src/react/ReactorController.tsx
1661
- import React, { useState as useState2, useCallback } from "react";
1967
+ import React, { useState as useState3, useCallback } from "react";
1662
1968
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1663
1969
  function ReactorController({
1664
1970
  className,
@@ -1668,9 +1974,9 @@ function ReactorController({
1668
1974
  sendCommand: state.sendCommand,
1669
1975
  status: state.status
1670
1976
  }));
1671
- const [commands, setCommands] = useState2({});
1672
- const [formValues, setFormValues] = useState2({});
1673
- const [expandedCommands, setExpandedCommands] = useState2({});
1977
+ const [commands, setCommands] = useState3({});
1978
+ const [formValues, setFormValues] = useState3({});
1979
+ const [expandedCommands, setExpandedCommands] = useState3({});
1674
1980
  React.useEffect(() => {
1675
1981
  if (status === "disconnected") {
1676
1982
  setCommands({});
@@ -1697,8 +2003,8 @@ function ReactorController({
1697
2003
  }, 5e3);
1698
2004
  return () => clearInterval(interval);
1699
2005
  }, [status, commands, requestCapabilities]);
1700
- useReactorMessage((message, scope) => {
1701
- if (scope === "runtime" && message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
2006
+ useReactorInternalMessage((message) => {
2007
+ if (message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1702
2008
  const commandsMessage = message.data;
1703
2009
  setCommands(commandsMessage.commands);
1704
2010
  const initialValues = {};
@@ -2111,9 +2417,10 @@ function ReactorController({
2111
2417
  }
2112
2418
 
2113
2419
  // src/react/WebcamStream.tsx
2114
- import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
2420
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
2115
2421
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2116
2422
  function WebcamStream({
2423
+ track,
2117
2424
  className,
2118
2425
  style,
2119
2426
  videoConstraints = {
@@ -2123,13 +2430,13 @@ function WebcamStream({
2123
2430
  showWebcam = true,
2124
2431
  videoObjectFit = "contain"
2125
2432
  }) {
2126
- const [stream, setStream] = useState3(null);
2127
- const [isPublishing, setIsPublishing] = useState3(false);
2128
- const [permissionDenied, setPermissionDenied] = useState3(false);
2129
- const { status, publishVideoStream, unpublishVideoStream, reactor } = useReactor((state) => ({
2433
+ const [stream, setStream] = useState4(null);
2434
+ const [isPublishing, setIsPublishing] = useState4(false);
2435
+ const [permissionDenied, setPermissionDenied] = useState4(false);
2436
+ const { status, publish, unpublish, reactor } = useReactor((state) => ({
2130
2437
  status: state.status,
2131
- publishVideoStream: state.publishVideoStream,
2132
- unpublishVideoStream: state.unpublishVideoStream,
2438
+ publish: state.publish,
2439
+ unpublish: state.unpublish,
2133
2440
  reactor: state.internal.reactor
2134
2441
  }));
2135
2442
  const videoRef = useRef4(null);
@@ -2154,15 +2461,15 @@ function WebcamStream({
2154
2461
  const stopWebcam = () => __async(null, null, function* () {
2155
2462
  console.debug("[WebcamPublisher] Stopping webcam");
2156
2463
  try {
2157
- yield unpublishVideoStream();
2464
+ yield unpublish(track);
2158
2465
  console.debug("[WebcamPublisher] Unpublished before stopping");
2159
2466
  } catch (err) {
2160
2467
  console.error("[WebcamPublisher] Error unpublishing before stop:", err);
2161
2468
  }
2162
2469
  setIsPublishing(false);
2163
- stream == null ? void 0 : stream.getTracks().forEach((track) => {
2164
- track.stop();
2165
- 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);
2166
2473
  });
2167
2474
  setStream(null);
2168
2475
  console.debug("[WebcamPublisher] Webcam stopped");
@@ -2192,28 +2499,31 @@ function WebcamStream({
2192
2499
  console.debug(
2193
2500
  "[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
2194
2501
  );
2195
- publishVideoStream(stream).then(() => {
2196
- console.debug("[WebcamPublisher] Auto-publish successful");
2197
- setIsPublishing(true);
2198
- }).catch((err) => {
2199
- console.error("[WebcamPublisher] Auto-publish failed:", err);
2200
- });
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
+ }
2201
2511
  } else if (status !== "ready" && isPublishing) {
2202
2512
  console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
2203
- unpublishVideoStream().then(() => {
2513
+ unpublish(track).then(() => {
2204
2514
  console.debug("[WebcamPublisher] Auto-unpublish successful");
2205
2515
  setIsPublishing(false);
2206
2516
  }).catch((err) => {
2207
2517
  console.error("[WebcamPublisher] Auto-unpublish failed:", err);
2208
2518
  });
2209
2519
  }
2210
- }, [status, stream, isPublishing, publishVideoStream, unpublishVideoStream]);
2520
+ }, [status, stream, isPublishing, publish, unpublish, track]);
2211
2521
  useEffect4(() => {
2212
2522
  const handleError = (error) => {
2213
2523
  console.debug("[WebcamPublisher] Received error event:", error);
2214
- if (error.code === "VIDEO_PUBLISH_FAILED") {
2524
+ if (error.code === "TRACK_PUBLISH_FAILED") {
2215
2525
  console.debug(
2216
- "[WebcamPublisher] Video publish failed, resetting isPublishing state"
2526
+ "[WebcamPublisher] Track publish failed, resetting isPublishing state"
2217
2527
  );
2218
2528
  setIsPublishing(false);
2219
2529
  }
@@ -2328,9 +2638,13 @@ export {
2328
2638
  ReactorProvider,
2329
2639
  ReactorView,
2330
2640
  WebcamStream,
2641
+ audio,
2331
2642
  fetchInsecureJwtToken,
2332
2643
  useReactor,
2644
+ useReactorInternalMessage,
2333
2645
  useReactorMessage,
2334
- useReactorStore
2646
+ useReactorStore,
2647
+ useStats,
2648
+ video
2335
2649
  };
2336
2650
  //# sourceMappingURL=index.mjs.map