@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.d.mts +175 -26
- package/dist/index.d.ts +175 -26
- package/dist/index.js +460 -142
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +465 -151
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
711
|
-
|
|
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.
|
|
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
|
|
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
|
|
767
|
-
*
|
|
768
|
-
*
|
|
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
|
-
|
|
930
|
+
`[GPUMachineClient] Cannot publish track "${name}" - not initialized`
|
|
776
931
|
);
|
|
777
932
|
}
|
|
778
933
|
if (this.status !== "connected") {
|
|
779
934
|
throw new Error(
|
|
780
|
-
|
|
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 (
|
|
944
|
+
if (entry.direction === "recvonly") {
|
|
784
945
|
throw new Error(
|
|
785
|
-
|
|
946
|
+
`[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
|
|
786
947
|
);
|
|
787
948
|
}
|
|
788
949
|
try {
|
|
789
|
-
yield
|
|
790
|
-
this.
|
|
950
|
+
yield entry.transceiver.sender.replaceTrack(track);
|
|
951
|
+
this.publishedTracks.set(name, track);
|
|
791
952
|
console.debug(
|
|
792
|
-
|
|
793
|
-
track.kind
|
|
953
|
+
`[GPUMachineClient] Track "${name}" published successfully`
|
|
794
954
|
);
|
|
795
955
|
} catch (error) {
|
|
796
|
-
console.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
|
|
965
|
+
* Unpublishes the track with the given name.
|
|
803
966
|
*/
|
|
804
|
-
unpublishTrack() {
|
|
967
|
+
unpublishTrack(name) {
|
|
805
968
|
return __async(this, null, function* () {
|
|
806
|
-
|
|
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
|
|
809
|
-
console.debug(
|
|
972
|
+
yield entry.transceiver.sender.replaceTrack(null);
|
|
973
|
+
console.debug(
|
|
974
|
+
`[GPUMachineClient] Track "${name}" unpublished successfully`
|
|
975
|
+
);
|
|
810
976
|
} catch (error) {
|
|
811
|
-
console.error(
|
|
977
|
+
console.error(
|
|
978
|
+
`[GPUMachineClient] Failed to unpublish track "${name}":`,
|
|
979
|
+
error
|
|
980
|
+
);
|
|
812
981
|
throw error;
|
|
813
982
|
} finally {
|
|
814
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
991
|
-
*
|
|
992
|
-
* @param command The command name
|
|
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
|
|
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
|
-
*
|
|
1021
|
-
*
|
|
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
|
-
|
|
1028
|
-
|
|
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(
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
1350
|
-
console.debug("[ReactorStore]
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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({
|
|
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("
|
|
1632
|
+
get().internal.reactor.on("message", handler);
|
|
1375
1633
|
return () => {
|
|
1376
1634
|
console.debug("[ReactorStore] Cleaning up message handler");
|
|
1377
|
-
get().internal.reactor.off("
|
|
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
|
-
|
|
1420
|
-
console.debug(
|
|
1677
|
+
publish: (name, track) => __async(null, null, function* () {
|
|
1678
|
+
console.debug(`[ReactorStore] Publishing track "${name}"`);
|
|
1421
1679
|
try {
|
|
1422
|
-
yield get().internal.reactor.publishTrack(
|
|
1423
|
-
console.debug(
|
|
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
|
-
|
|
1686
|
+
`[ReactorStore] Failed to publish track "${name}":`,
|
|
1427
1687
|
error
|
|
1428
1688
|
);
|
|
1429
1689
|
throw error;
|
|
1430
1690
|
}
|
|
1431
1691
|
}),
|
|
1432
|
-
|
|
1433
|
-
console.debug(
|
|
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(
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
1592
|
-
|
|
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("
|
|
1600
|
-
console.debug("[useReactorMessage] Message handler registered");
|
|
1867
|
+
reactor.on("message", stableHandler);
|
|
1601
1868
|
return () => {
|
|
1602
|
-
|
|
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 {
|
|
1619
|
-
|
|
1620
|
-
|
|
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]
|
|
1935
|
+
console.debug("[ReactorView] Media track effect triggered", {
|
|
1936
|
+
track,
|
|
1625
1937
|
hasVideoElement: !!videoRef.current,
|
|
1626
|
-
hasVideoTrack: !!
|
|
1627
|
-
|
|
1938
|
+
hasVideoTrack: !!videoMediaTrack,
|
|
1939
|
+
hasAudioTrack: !!audioMediaTrack
|
|
1628
1940
|
});
|
|
1629
|
-
if (videoRef.current &&
|
|
1630
|
-
console.debug("[ReactorView] Attaching
|
|
1941
|
+
if (videoRef.current && mediaStream) {
|
|
1942
|
+
console.debug("[ReactorView] Attaching media stream to element");
|
|
1631
1943
|
try {
|
|
1632
|
-
|
|
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]
|
|
1948
|
+
console.debug("[ReactorView] Media stream attached successfully");
|
|
1638
1949
|
} catch (error) {
|
|
1639
|
-
console.error("[ReactorView] Failed to attach
|
|
1950
|
+
console.error("[ReactorView] Failed to attach media stream:", error);
|
|
1640
1951
|
}
|
|
1641
1952
|
return () => {
|
|
1642
|
-
console.debug("[ReactorView] Detaching
|
|
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
|
|
1959
|
+
console.debug("[ReactorView] No tracks or element to attach");
|
|
1650
1960
|
}
|
|
1651
|
-
}, [
|
|
1652
|
-
const showPlaceholder = !
|
|
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
|
|
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
|
-
|
|
1744
|
-
if (
|
|
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,
|
|
2483
|
+
const { status, publish, unpublish, reactor } = useReactor((state) => ({
|
|
2173
2484
|
status: state.status,
|
|
2174
|
-
|
|
2175
|
-
|
|
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
|
|
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((
|
|
2207
|
-
|
|
2208
|
-
console.debug("[WebcamPublisher] Stopped track:",
|
|
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
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
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
|
-
|
|
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,
|
|
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 === "
|
|
2571
|
+
if (error.code === "TRACK_PUBLISH_FAILED") {
|
|
2258
2572
|
console.debug(
|
|
2259
|
-
"[WebcamPublisher]
|
|
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
|