@reactor-team/js-sdk 2.4.0 → 2.5.1
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 +148 -20
- package/dist/index.d.ts +148 -20
- package/dist/index.js +494 -151
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +490 -151
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -79,6 +79,7 @@ var __async = (__this, __arguments, generator) => {
|
|
|
79
79
|
// src/index.ts
|
|
80
80
|
var index_exports = {};
|
|
81
81
|
__export(index_exports, {
|
|
82
|
+
AbortError: () => AbortError,
|
|
82
83
|
ConflictError: () => ConflictError,
|
|
83
84
|
PROD_COORDINATOR_URL: () => PROD_COORDINATOR_URL,
|
|
84
85
|
Reactor: () => Reactor,
|
|
@@ -86,21 +87,38 @@ __export(index_exports, {
|
|
|
86
87
|
ReactorProvider: () => ReactorProvider,
|
|
87
88
|
ReactorView: () => ReactorView,
|
|
88
89
|
WebcamStream: () => WebcamStream,
|
|
90
|
+
audio: () => audio,
|
|
89
91
|
fetchInsecureJwtToken: () => fetchInsecureJwtToken,
|
|
92
|
+
isAbortError: () => isAbortError,
|
|
90
93
|
useReactor: () => useReactor,
|
|
91
94
|
useReactorInternalMessage: () => useReactorInternalMessage,
|
|
92
95
|
useReactorMessage: () => useReactorMessage,
|
|
93
96
|
useReactorStore: () => useReactorStore,
|
|
94
|
-
useStats: () => useStats
|
|
97
|
+
useStats: () => useStats,
|
|
98
|
+
video: () => video
|
|
95
99
|
});
|
|
96
100
|
module.exports = __toCommonJS(index_exports);
|
|
97
101
|
|
|
98
102
|
// src/types.ts
|
|
103
|
+
function video(name, _options) {
|
|
104
|
+
return { name, kind: "video" };
|
|
105
|
+
}
|
|
106
|
+
function audio(name, _options) {
|
|
107
|
+
return { name, kind: "audio" };
|
|
108
|
+
}
|
|
99
109
|
var ConflictError = class extends Error {
|
|
100
110
|
constructor(message) {
|
|
101
111
|
super(message);
|
|
102
112
|
}
|
|
103
113
|
};
|
|
114
|
+
var AbortError = class extends Error {
|
|
115
|
+
constructor(message) {
|
|
116
|
+
super(message);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function isAbortError(error) {
|
|
120
|
+
return error instanceof AbortError || error instanceof Error && error.name === "AbortError";
|
|
121
|
+
}
|
|
104
122
|
|
|
105
123
|
// src/core/types.ts
|
|
106
124
|
var import_zod = require("zod");
|
|
@@ -169,18 +187,105 @@ function createPeerConnection(config) {
|
|
|
169
187
|
function createDataChannel(pc, label) {
|
|
170
188
|
return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
|
|
171
189
|
}
|
|
172
|
-
function
|
|
190
|
+
function rewriteMids(sdp, trackNames) {
|
|
191
|
+
const lines = sdp.split("\r\n");
|
|
192
|
+
let mediaIdx = 0;
|
|
193
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
194
|
+
let inApplication = false;
|
|
195
|
+
for (let i = 0; i < lines.length; i++) {
|
|
196
|
+
if (lines[i].startsWith("m=")) {
|
|
197
|
+
inApplication = lines[i].startsWith("m=application");
|
|
198
|
+
}
|
|
199
|
+
if (!inApplication && lines[i].startsWith("a=mid:")) {
|
|
200
|
+
const oldMid = lines[i].substring("a=mid:".length);
|
|
201
|
+
if (mediaIdx < trackNames.length) {
|
|
202
|
+
const newMid = trackNames[mediaIdx];
|
|
203
|
+
replacements.set(oldMid, newMid);
|
|
204
|
+
lines[i] = `a=mid:${newMid}`;
|
|
205
|
+
mediaIdx++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (let i = 0; i < lines.length; i++) {
|
|
210
|
+
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
211
|
+
const parts = lines[i].split(" ");
|
|
212
|
+
for (let j = 1; j < parts.length; j++) {
|
|
213
|
+
const replacement = replacements.get(parts[j]);
|
|
214
|
+
if (replacement !== void 0) {
|
|
215
|
+
parts[j] = replacement;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
lines[i] = parts.join(" ");
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return lines.join("\r\n");
|
|
223
|
+
}
|
|
224
|
+
function createOffer(pc, trackNames) {
|
|
173
225
|
return __async(this, null, function* () {
|
|
174
226
|
const offer = yield pc.createOffer();
|
|
175
|
-
|
|
227
|
+
let needsAnswerRestore = false;
|
|
228
|
+
if (trackNames && trackNames.length > 0 && offer.sdp) {
|
|
229
|
+
const munged = rewriteMids(offer.sdp, trackNames);
|
|
230
|
+
try {
|
|
231
|
+
yield pc.setLocalDescription(
|
|
232
|
+
new RTCSessionDescription({ type: "offer", sdp: munged })
|
|
233
|
+
);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
yield pc.setLocalDescription(offer);
|
|
236
|
+
needsAnswerRestore = true;
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
yield pc.setLocalDescription(offer);
|
|
240
|
+
}
|
|
176
241
|
yield waitForIceGathering(pc);
|
|
177
242
|
const localDescription = pc.localDescription;
|
|
178
243
|
if (!localDescription) {
|
|
179
244
|
throw new Error("Failed to create local description");
|
|
180
245
|
}
|
|
181
|
-
|
|
246
|
+
let sdp = localDescription.sdp;
|
|
247
|
+
if (needsAnswerRestore && trackNames && trackNames.length > 0) {
|
|
248
|
+
sdp = rewriteMids(sdp, trackNames);
|
|
249
|
+
}
|
|
250
|
+
return { sdp, needsAnswerRestore };
|
|
182
251
|
});
|
|
183
252
|
}
|
|
253
|
+
function buildMidMapping(transceivers) {
|
|
254
|
+
var _a;
|
|
255
|
+
const localToRemote = /* @__PURE__ */ new Map();
|
|
256
|
+
const remoteToLocal = /* @__PURE__ */ new Map();
|
|
257
|
+
for (const entry of transceivers) {
|
|
258
|
+
const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
|
|
259
|
+
if (mid) {
|
|
260
|
+
localToRemote.set(mid, entry.name);
|
|
261
|
+
remoteToLocal.set(entry.name, mid);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { localToRemote, remoteToLocal };
|
|
265
|
+
}
|
|
266
|
+
function restoreAnswerMids(sdp, remoteToLocal) {
|
|
267
|
+
const lines = sdp.split("\r\n");
|
|
268
|
+
for (let i = 0; i < lines.length; i++) {
|
|
269
|
+
if (lines[i].startsWith("a=mid:")) {
|
|
270
|
+
const remoteMid = lines[i].substring("a=mid:".length);
|
|
271
|
+
const localMid = remoteToLocal.get(remoteMid);
|
|
272
|
+
if (localMid !== void 0) {
|
|
273
|
+
lines[i] = `a=mid:${localMid}`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
277
|
+
const parts = lines[i].split(" ");
|
|
278
|
+
for (let j = 1; j < parts.length; j++) {
|
|
279
|
+
const localMid = remoteToLocal.get(parts[j]);
|
|
280
|
+
if (localMid !== void 0) {
|
|
281
|
+
parts[j] = localMid;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
lines[i] = parts.join(" ");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return lines.join("\r\n");
|
|
288
|
+
}
|
|
184
289
|
function setRemoteDescription(pc, sdp) {
|
|
185
290
|
return __async(this, null, function* () {
|
|
186
291
|
const sessionDescription = new RTCSessionDescription({
|
|
@@ -308,6 +413,22 @@ var CoordinatorClient = class {
|
|
|
308
413
|
this.baseUrl = options.baseUrl;
|
|
309
414
|
this.jwtToken = options.jwtToken;
|
|
310
415
|
this.model = options.model;
|
|
416
|
+
this.abortController = new AbortController();
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Aborts any in-flight HTTP requests and polling loops.
|
|
420
|
+
* A fresh AbortController is created so the client remains reusable.
|
|
421
|
+
*/
|
|
422
|
+
abort() {
|
|
423
|
+
this.abortController.abort();
|
|
424
|
+
this.abortController = new AbortController();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The current abort signal, passed to every fetch() and sleep() call.
|
|
428
|
+
* Protected so subclasses can forward it to their own fetch calls.
|
|
429
|
+
*/
|
|
430
|
+
get signal() {
|
|
431
|
+
return this.abortController.signal;
|
|
311
432
|
}
|
|
312
433
|
/**
|
|
313
434
|
* Returns the authorization header with JWT Bearer token
|
|
@@ -328,7 +449,8 @@ var CoordinatorClient = class {
|
|
|
328
449
|
`${this.baseUrl}/ice_servers?model=${this.model}`,
|
|
329
450
|
{
|
|
330
451
|
method: "GET",
|
|
331
|
-
headers: this.getAuthHeaders()
|
|
452
|
+
headers: this.getAuthHeaders(),
|
|
453
|
+
signal: this.signal
|
|
332
454
|
}
|
|
333
455
|
);
|
|
334
456
|
if (!response.ok) {
|
|
@@ -362,7 +484,8 @@ var CoordinatorClient = class {
|
|
|
362
484
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
363
485
|
"Content-Type": "application/json"
|
|
364
486
|
}),
|
|
365
|
-
body: JSON.stringify(requestBody)
|
|
487
|
+
body: JSON.stringify(requestBody),
|
|
488
|
+
signal: this.signal
|
|
366
489
|
});
|
|
367
490
|
if (!response.ok) {
|
|
368
491
|
const errorText = yield response.text();
|
|
@@ -396,7 +519,8 @@ var CoordinatorClient = class {
|
|
|
396
519
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
397
520
|
{
|
|
398
521
|
method: "GET",
|
|
399
|
-
headers: this.getAuthHeaders()
|
|
522
|
+
headers: this.getAuthHeaders(),
|
|
523
|
+
signal: this.signal
|
|
400
524
|
}
|
|
401
525
|
);
|
|
402
526
|
if (!response.ok) {
|
|
@@ -409,12 +533,13 @@ var CoordinatorClient = class {
|
|
|
409
533
|
}
|
|
410
534
|
/**
|
|
411
535
|
* Terminates the current session by sending a DELETE request to the coordinator.
|
|
412
|
-
*
|
|
536
|
+
* No-op if no session has been created yet.
|
|
537
|
+
* @throws Error if the request fails (except for 404, which clears local state)
|
|
413
538
|
*/
|
|
414
539
|
terminateSession() {
|
|
415
540
|
return __async(this, null, function* () {
|
|
416
541
|
if (!this.currentSessionId) {
|
|
417
|
-
|
|
542
|
+
return;
|
|
418
543
|
}
|
|
419
544
|
console.debug(
|
|
420
545
|
"[CoordinatorClient] Terminating session:",
|
|
@@ -424,7 +549,8 @@ var CoordinatorClient = class {
|
|
|
424
549
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
425
550
|
{
|
|
426
551
|
method: "DELETE",
|
|
427
|
-
headers: this.getAuthHeaders()
|
|
552
|
+
headers: this.getAuthHeaders(),
|
|
553
|
+
signal: this.signal
|
|
428
554
|
}
|
|
429
555
|
);
|
|
430
556
|
if (response.ok) {
|
|
@@ -474,7 +600,8 @@ var CoordinatorClient = class {
|
|
|
474
600
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
475
601
|
"Content-Type": "application/json"
|
|
476
602
|
}),
|
|
477
|
-
body: JSON.stringify(requestBody)
|
|
603
|
+
body: JSON.stringify(requestBody),
|
|
604
|
+
signal: this.signal
|
|
478
605
|
}
|
|
479
606
|
);
|
|
480
607
|
if (response.status === 200) {
|
|
@@ -510,6 +637,9 @@ var CoordinatorClient = class {
|
|
|
510
637
|
let backoffMs = INITIAL_BACKOFF_MS;
|
|
511
638
|
let attempt = 0;
|
|
512
639
|
while (true) {
|
|
640
|
+
if (this.signal.aborted) {
|
|
641
|
+
throw new AbortError("SDP polling aborted");
|
|
642
|
+
}
|
|
513
643
|
if (attempt >= maxAttempts) {
|
|
514
644
|
throw new Error(
|
|
515
645
|
`SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
|
|
@@ -525,7 +655,8 @@ var CoordinatorClient = class {
|
|
|
525
655
|
method: "GET",
|
|
526
656
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
527
657
|
"Content-Type": "application/json"
|
|
528
|
-
})
|
|
658
|
+
}),
|
|
659
|
+
signal: this.signal
|
|
529
660
|
}
|
|
530
661
|
);
|
|
531
662
|
if (response.status === 200) {
|
|
@@ -570,10 +701,26 @@ var CoordinatorClient = class {
|
|
|
570
701
|
});
|
|
571
702
|
}
|
|
572
703
|
/**
|
|
573
|
-
*
|
|
704
|
+
* Abort-aware sleep. Resolves after `ms` milliseconds unless the
|
|
705
|
+
* abort signal fires first, in which case it rejects with AbortError.
|
|
574
706
|
*/
|
|
575
707
|
sleep(ms) {
|
|
576
|
-
return new Promise((resolve) =>
|
|
708
|
+
return new Promise((resolve, reject) => {
|
|
709
|
+
const { signal } = this;
|
|
710
|
+
if (signal.aborted) {
|
|
711
|
+
reject(new AbortError("Sleep aborted"));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const timer = setTimeout(() => {
|
|
715
|
+
signal.removeEventListener("abort", onAbort);
|
|
716
|
+
resolve();
|
|
717
|
+
}, ms);
|
|
718
|
+
const onAbort = () => {
|
|
719
|
+
clearTimeout(timer);
|
|
720
|
+
reject(new AbortError("Sleep aborted"));
|
|
721
|
+
};
|
|
722
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
723
|
+
});
|
|
577
724
|
}
|
|
578
725
|
};
|
|
579
726
|
|
|
@@ -595,7 +742,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
595
742
|
return __async(this, null, function* () {
|
|
596
743
|
console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
|
|
597
744
|
const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
|
|
598
|
-
method: "GET"
|
|
745
|
+
method: "GET",
|
|
746
|
+
signal: this.signal
|
|
599
747
|
});
|
|
600
748
|
if (!response.ok) {
|
|
601
749
|
throw new Error("Failed to get ICE servers from local coordinator.");
|
|
@@ -619,7 +767,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
619
767
|
console.debug("[LocalCoordinatorClient] Creating local session...");
|
|
620
768
|
this.sdpOffer = sdpOffer;
|
|
621
769
|
const response = yield fetch(`${this.localBaseUrl}/start_session`, {
|
|
622
|
-
method: "POST"
|
|
770
|
+
method: "POST",
|
|
771
|
+
signal: this.signal
|
|
623
772
|
});
|
|
624
773
|
if (!response.ok) {
|
|
625
774
|
throw new Error("Failed to send local start session command.");
|
|
@@ -647,7 +796,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
647
796
|
headers: {
|
|
648
797
|
"Content-Type": "application/json"
|
|
649
798
|
},
|
|
650
|
-
body: JSON.stringify(sdpBody)
|
|
799
|
+
body: JSON.stringify(sdpBody),
|
|
800
|
+
signal: this.signal
|
|
651
801
|
});
|
|
652
802
|
if (!response.ok) {
|
|
653
803
|
if (response.status === 409) {
|
|
@@ -664,7 +814,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
664
814
|
return __async(this, null, function* () {
|
|
665
815
|
console.debug("[LocalCoordinatorClient] Stopping local session...");
|
|
666
816
|
yield fetch(`${this.localBaseUrl}/stop_session`, {
|
|
667
|
-
method: "POST"
|
|
817
|
+
method: "POST",
|
|
818
|
+
signal: this.signal
|
|
668
819
|
});
|
|
669
820
|
});
|
|
670
821
|
}
|
|
@@ -677,6 +828,10 @@ var GPUMachineClient = class {
|
|
|
677
828
|
constructor(config) {
|
|
678
829
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
679
830
|
this.status = "disconnected";
|
|
831
|
+
this.transceiverMap = /* @__PURE__ */ new Map();
|
|
832
|
+
this.publishedTracks = /* @__PURE__ */ new Map();
|
|
833
|
+
this.peerConnected = false;
|
|
834
|
+
this.dataChannelOpen = false;
|
|
680
835
|
this.config = config;
|
|
681
836
|
}
|
|
682
837
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -700,10 +855,18 @@ var GPUMachineClient = class {
|
|
|
700
855
|
// SDP & Connection
|
|
701
856
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
702
857
|
/**
|
|
703
|
-
* Creates an SDP offer
|
|
858
|
+
* Creates an SDP offer based on the declared tracks.
|
|
859
|
+
*
|
|
860
|
+
* **RECEIVE** = client receives from the model (model → client) → `recvonly`
|
|
861
|
+
* **SEND** = client sends to the model (client → model) → `sendonly`
|
|
862
|
+
*
|
|
863
|
+
* Track names must be unique across both arrays. A name that appears in
|
|
864
|
+
* both `receive` and `send` will throw — use distinct names instead.
|
|
865
|
+
*
|
|
866
|
+
* The data channel is always created first (before transceivers).
|
|
704
867
|
* Must be called before connect().
|
|
705
868
|
*/
|
|
706
|
-
createOffer() {
|
|
869
|
+
createOffer(tracks) {
|
|
707
870
|
return __async(this, null, function* () {
|
|
708
871
|
if (!this.peerConnection) {
|
|
709
872
|
this.peerConnection = createPeerConnection(this.config);
|
|
@@ -714,14 +877,63 @@ var GPUMachineClient = class {
|
|
|
714
877
|
this.config.dataChannelLabel
|
|
715
878
|
);
|
|
716
879
|
this.setupDataChannelHandlers();
|
|
717
|
-
this.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
880
|
+
this.transceiverMap.clear();
|
|
881
|
+
const entries = this.buildTransceiverEntries(tracks);
|
|
882
|
+
for (const entry of entries) {
|
|
883
|
+
const transceiver = this.peerConnection.addTransceiver(entry.kind, {
|
|
884
|
+
direction: entry.direction
|
|
885
|
+
});
|
|
886
|
+
entry.transceiver = transceiver;
|
|
887
|
+
this.transceiverMap.set(entry.name, entry);
|
|
888
|
+
console.debug(
|
|
889
|
+
`[GPUMachineClient] Transceiver added: "${entry.name}" (${entry.kind}, ${entry.direction})`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
const trackNames = entries.map((e) => e.name);
|
|
893
|
+
const { sdp, needsAnswerRestore } = yield createOffer(
|
|
894
|
+
this.peerConnection,
|
|
895
|
+
trackNames
|
|
896
|
+
);
|
|
897
|
+
if (needsAnswerRestore) {
|
|
898
|
+
this.midMapping = buildMidMapping(entries);
|
|
899
|
+
} else {
|
|
900
|
+
this.midMapping = void 0;
|
|
901
|
+
}
|
|
902
|
+
console.debug(
|
|
903
|
+
"[GPUMachineClient] Created SDP offer with MIDs:",
|
|
904
|
+
trackNames,
|
|
905
|
+
needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
|
|
906
|
+
);
|
|
907
|
+
return sdp;
|
|
723
908
|
});
|
|
724
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Builds an ordered list of transceiver entries from the receive/send arrays.
|
|
912
|
+
*
|
|
913
|
+
* Each track produces exactly one transceiver — `recvonly` for receive,
|
|
914
|
+
* `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
|
|
915
|
+
* supported; the same track name in both arrays is an error.
|
|
916
|
+
*/
|
|
917
|
+
buildTransceiverEntries(tracks) {
|
|
918
|
+
const map = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const t of tracks.receive) {
|
|
920
|
+
if (map.has(t.name)) {
|
|
921
|
+
throw new Error(
|
|
922
|
+
`Duplicate receive track name "${t.name}". Track names must be unique.`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
|
|
926
|
+
}
|
|
927
|
+
for (const t of tracks.send) {
|
|
928
|
+
if (map.has(t.name)) {
|
|
929
|
+
throw new Error(
|
|
930
|
+
`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").`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
|
|
934
|
+
}
|
|
935
|
+
return Array.from(map.values());
|
|
936
|
+
}
|
|
725
937
|
/**
|
|
726
938
|
* Connects to the GPU machine using the provided SDP answer.
|
|
727
939
|
* createOffer() must be called first.
|
|
@@ -741,7 +953,14 @@ var GPUMachineClient = class {
|
|
|
741
953
|
}
|
|
742
954
|
this.setStatus("connecting");
|
|
743
955
|
try {
|
|
744
|
-
|
|
956
|
+
let answer = sdpAnswer;
|
|
957
|
+
if (this.midMapping) {
|
|
958
|
+
answer = restoreAnswerMids(
|
|
959
|
+
answer,
|
|
960
|
+
this.midMapping.remoteToLocal
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
yield setRemoteDescription(this.peerConnection, answer);
|
|
745
964
|
console.debug("[GPUMachineClient] Remote description set");
|
|
746
965
|
} catch (error) {
|
|
747
966
|
console.error("[GPUMachineClient] Failed to connect:", error);
|
|
@@ -757,8 +976,8 @@ var GPUMachineClient = class {
|
|
|
757
976
|
return __async(this, null, function* () {
|
|
758
977
|
this.stopPing();
|
|
759
978
|
this.stopStatsPolling();
|
|
760
|
-
|
|
761
|
-
yield this.unpublishTrack();
|
|
979
|
+
for (const name of Array.from(this.publishedTracks.keys())) {
|
|
980
|
+
yield this.unpublishTrack(name);
|
|
762
981
|
}
|
|
763
982
|
if (this.dataChannel) {
|
|
764
983
|
this.dataChannel.close();
|
|
@@ -768,7 +987,10 @@ var GPUMachineClient = class {
|
|
|
768
987
|
closePeerConnection(this.peerConnection);
|
|
769
988
|
this.peerConnection = void 0;
|
|
770
989
|
}
|
|
771
|
-
this.
|
|
990
|
+
this.transceiverMap.clear();
|
|
991
|
+
this.midMapping = void 0;
|
|
992
|
+
this.peerConnected = false;
|
|
993
|
+
this.dataChannelOpen = false;
|
|
772
994
|
this.setStatus("disconnected");
|
|
773
995
|
console.debug("[GPUMachineClient] Disconnected");
|
|
774
996
|
});
|
|
@@ -796,7 +1018,7 @@ var GPUMachineClient = class {
|
|
|
796
1018
|
/**
|
|
797
1019
|
* Sends a command to the GPU machine via the data channel.
|
|
798
1020
|
* @param command The command to send
|
|
799
|
-
* @param data The data to send with the command. These are the parameters for the command, matching the
|
|
1021
|
+
* @param data The data to send with the command. These are the parameters for the command, matching the schema in the capabilities dictionary.
|
|
800
1022
|
* @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
|
|
801
1023
|
*/
|
|
802
1024
|
sendCommand(command, data, scope = "application") {
|
|
@@ -813,63 +1035,77 @@ var GPUMachineClient = class {
|
|
|
813
1035
|
// Track Publishing
|
|
814
1036
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
815
1037
|
/**
|
|
816
|
-
* Publishes a
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
* @param track The MediaStreamTrack to publish
|
|
1038
|
+
* Publishes a MediaStreamTrack to the named send track.
|
|
1039
|
+
*
|
|
1040
|
+
* @param name The declared track name (must exist in transceiverMap with a sendable direction).
|
|
1041
|
+
* @param track The MediaStreamTrack to publish.
|
|
820
1042
|
*/
|
|
821
|
-
publishTrack(track) {
|
|
1043
|
+
publishTrack(name, track) {
|
|
822
1044
|
return __async(this, null, function* () {
|
|
823
1045
|
if (!this.peerConnection) {
|
|
824
1046
|
throw new Error(
|
|
825
|
-
|
|
1047
|
+
`[GPUMachineClient] Cannot publish track "${name}" - not initialized`
|
|
826
1048
|
);
|
|
827
1049
|
}
|
|
828
1050
|
if (this.status !== "connected") {
|
|
829
1051
|
throw new Error(
|
|
830
|
-
|
|
1052
|
+
`[GPUMachineClient] Cannot publish track "${name}" - not connected`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
const entry = this.transceiverMap.get(name);
|
|
1056
|
+
if (!entry || !entry.transceiver) {
|
|
1057
|
+
throw new Error(
|
|
1058
|
+
`[GPUMachineClient] Cannot publish track "${name}" - no transceiver (was it declared in tracks.send?)`
|
|
831
1059
|
);
|
|
832
1060
|
}
|
|
833
|
-
if (
|
|
1061
|
+
if (entry.direction === "recvonly") {
|
|
834
1062
|
throw new Error(
|
|
835
|
-
|
|
1063
|
+
`[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
|
|
836
1064
|
);
|
|
837
1065
|
}
|
|
838
1066
|
try {
|
|
839
|
-
yield
|
|
840
|
-
this.
|
|
1067
|
+
yield entry.transceiver.sender.replaceTrack(track);
|
|
1068
|
+
this.publishedTracks.set(name, track);
|
|
841
1069
|
console.debug(
|
|
842
|
-
|
|
843
|
-
track.kind
|
|
1070
|
+
`[GPUMachineClient] Track "${name}" published successfully`
|
|
844
1071
|
);
|
|
845
1072
|
} catch (error) {
|
|
846
|
-
console.error(
|
|
1073
|
+
console.error(
|
|
1074
|
+
`[GPUMachineClient] Failed to publish track "${name}":`,
|
|
1075
|
+
error
|
|
1076
|
+
);
|
|
847
1077
|
throw error;
|
|
848
1078
|
}
|
|
849
1079
|
});
|
|
850
1080
|
}
|
|
851
1081
|
/**
|
|
852
|
-
* Unpublishes the
|
|
1082
|
+
* Unpublishes the track with the given name.
|
|
853
1083
|
*/
|
|
854
|
-
unpublishTrack() {
|
|
1084
|
+
unpublishTrack(name) {
|
|
855
1085
|
return __async(this, null, function* () {
|
|
856
|
-
|
|
1086
|
+
const entry = this.transceiverMap.get(name);
|
|
1087
|
+
if (!(entry == null ? void 0 : entry.transceiver) || !this.publishedTracks.has(name)) return;
|
|
857
1088
|
try {
|
|
858
|
-
yield
|
|
859
|
-
console.debug(
|
|
1089
|
+
yield entry.transceiver.sender.replaceTrack(null);
|
|
1090
|
+
console.debug(
|
|
1091
|
+
`[GPUMachineClient] Track "${name}" unpublished successfully`
|
|
1092
|
+
);
|
|
860
1093
|
} catch (error) {
|
|
861
|
-
console.error(
|
|
1094
|
+
console.error(
|
|
1095
|
+
`[GPUMachineClient] Failed to unpublish track "${name}":`,
|
|
1096
|
+
error
|
|
1097
|
+
);
|
|
862
1098
|
throw error;
|
|
863
1099
|
} finally {
|
|
864
|
-
this.
|
|
1100
|
+
this.publishedTracks.delete(name);
|
|
865
1101
|
}
|
|
866
1102
|
});
|
|
867
1103
|
}
|
|
868
1104
|
/**
|
|
869
|
-
* Returns the currently published track.
|
|
1105
|
+
* Returns the currently published track for the given name.
|
|
870
1106
|
*/
|
|
871
|
-
getPublishedTrack() {
|
|
872
|
-
return this.
|
|
1107
|
+
getPublishedTrack(name) {
|
|
1108
|
+
return this.publishedTracks.get(name);
|
|
873
1109
|
}
|
|
874
1110
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
875
1111
|
// Getters
|
|
@@ -940,6 +1176,12 @@ var GPUMachineClient = class {
|
|
|
940
1176
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
941
1177
|
// Private Helpers
|
|
942
1178
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1179
|
+
checkFullyConnected() {
|
|
1180
|
+
if (this.peerConnected && this.dataChannelOpen) {
|
|
1181
|
+
this.setStatus("connected");
|
|
1182
|
+
this.startStatsPolling();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
943
1185
|
setStatus(newStatus) {
|
|
944
1186
|
if (this.status !== newStatus) {
|
|
945
1187
|
this.status = newStatus;
|
|
@@ -955,24 +1197,36 @@ var GPUMachineClient = class {
|
|
|
955
1197
|
if (state) {
|
|
956
1198
|
switch (state) {
|
|
957
1199
|
case "connected":
|
|
958
|
-
this.
|
|
959
|
-
this.
|
|
1200
|
+
this.peerConnected = true;
|
|
1201
|
+
this.checkFullyConnected();
|
|
960
1202
|
break;
|
|
961
1203
|
case "disconnected":
|
|
962
1204
|
case "closed":
|
|
1205
|
+
this.peerConnected = false;
|
|
963
1206
|
this.setStatus("disconnected");
|
|
964
1207
|
break;
|
|
965
1208
|
case "failed":
|
|
1209
|
+
this.peerConnected = false;
|
|
966
1210
|
this.setStatus("error");
|
|
967
1211
|
break;
|
|
968
1212
|
}
|
|
969
1213
|
}
|
|
970
1214
|
};
|
|
971
1215
|
this.peerConnection.ontrack = (event) => {
|
|
972
|
-
var _a;
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1216
|
+
var _a, _b;
|
|
1217
|
+
let trackName;
|
|
1218
|
+
for (const [name, entry] of this.transceiverMap) {
|
|
1219
|
+
if (entry.transceiver === event.transceiver) {
|
|
1220
|
+
trackName = name;
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
|
|
1225
|
+
console.debug(
|
|
1226
|
+
`[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
|
|
1227
|
+
);
|
|
1228
|
+
const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
|
|
1229
|
+
this.emit("trackReceived", trackName, event.track, stream);
|
|
976
1230
|
};
|
|
977
1231
|
this.peerConnection.onicecandidate = (event) => {
|
|
978
1232
|
if (event.candidate) {
|
|
@@ -992,10 +1246,13 @@ var GPUMachineClient = class {
|
|
|
992
1246
|
if (!this.dataChannel) return;
|
|
993
1247
|
this.dataChannel.onopen = () => {
|
|
994
1248
|
console.debug("[GPUMachineClient] Data channel open");
|
|
1249
|
+
this.dataChannelOpen = true;
|
|
995
1250
|
this.startPing();
|
|
1251
|
+
this.checkFullyConnected();
|
|
996
1252
|
};
|
|
997
1253
|
this.dataChannel.onclose = () => {
|
|
998
1254
|
console.debug("[GPUMachineClient] Data channel closed");
|
|
1255
|
+
this.dataChannelOpen = false;
|
|
999
1256
|
this.stopPing();
|
|
1000
1257
|
};
|
|
1001
1258
|
this.dataChannel.onerror = (error) => {
|
|
@@ -1029,10 +1286,29 @@ var GPUMachineClient = class {
|
|
|
1029
1286
|
var import_zod2 = require("zod");
|
|
1030
1287
|
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
1031
1288
|
var PROD_COORDINATOR_URL = "https://api.reactor.inc";
|
|
1289
|
+
var TrackConfigSchema = import_zod2.z.object({
|
|
1290
|
+
name: import_zod2.z.string(),
|
|
1291
|
+
kind: import_zod2.z.enum(["audio", "video"])
|
|
1292
|
+
});
|
|
1032
1293
|
var OptionsSchema = import_zod2.z.object({
|
|
1033
1294
|
coordinatorUrl: import_zod2.z.string().default(PROD_COORDINATOR_URL),
|
|
1034
1295
|
modelName: import_zod2.z.string(),
|
|
1035
|
-
local: import_zod2.z.boolean().default(false)
|
|
1296
|
+
local: import_zod2.z.boolean().default(false),
|
|
1297
|
+
/**
|
|
1298
|
+
* Tracks the client **RECEIVES** from the model (model → client).
|
|
1299
|
+
* Each entry produces a `recvonly` transceiver.
|
|
1300
|
+
* Names must be unique across both `receive` and `send`.
|
|
1301
|
+
*
|
|
1302
|
+
* When omitted, defaults to a single video track named `"main_video"`.
|
|
1303
|
+
* Pass an explicit empty array to opt out of the default.
|
|
1304
|
+
*/
|
|
1305
|
+
receive: import_zod2.z.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
|
|
1306
|
+
/**
|
|
1307
|
+
* Tracks the client **SENDS** to the model (client → model).
|
|
1308
|
+
* Each entry produces a `sendonly` transceiver.
|
|
1309
|
+
* Names must be unique across both `receive` and `send`.
|
|
1310
|
+
*/
|
|
1311
|
+
send: import_zod2.z.array(TrackConfigSchema).default([])
|
|
1036
1312
|
});
|
|
1037
1313
|
var Reactor = class {
|
|
1038
1314
|
constructor(options) {
|
|
@@ -1043,7 +1319,9 @@ var Reactor = class {
|
|
|
1043
1319
|
this.coordinatorUrl = validatedOptions.coordinatorUrl;
|
|
1044
1320
|
this.model = validatedOptions.modelName;
|
|
1045
1321
|
this.local = validatedOptions.local;
|
|
1046
|
-
|
|
1322
|
+
this.receive = validatedOptions.receive;
|
|
1323
|
+
this.send = validatedOptions.send;
|
|
1324
|
+
if (this.local && options.coordinatorUrl === void 0) {
|
|
1047
1325
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
1048
1326
|
}
|
|
1049
1327
|
}
|
|
@@ -1063,13 +1341,11 @@ var Reactor = class {
|
|
|
1063
1341
|
(_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
|
|
1064
1342
|
}
|
|
1065
1343
|
/**
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* @param command The command name
|
|
1344
|
+
* Sends a command to the model via the data channel.
|
|
1345
|
+
*
|
|
1346
|
+
* @param command The command name.
|
|
1069
1347
|
* @param data The command payload.
|
|
1070
|
-
* @param scope
|
|
1071
|
-
* "runtime" for platform-level messages (e.g. requestCapabilities).
|
|
1072
|
-
* @throws Error if not in ready state
|
|
1348
|
+
* @param scope "application" (default) for model commands, "runtime" for platform messages.
|
|
1073
1349
|
*/
|
|
1074
1350
|
sendCommand(command, data, scope = "application") {
|
|
1075
1351
|
return __async(this, null, function* () {
|
|
@@ -1093,24 +1369,27 @@ var Reactor = class {
|
|
|
1093
1369
|
});
|
|
1094
1370
|
}
|
|
1095
1371
|
/**
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1372
|
+
* Publishes a MediaStreamTrack to a named send track.
|
|
1373
|
+
*
|
|
1374
|
+
* @param name The declared send track name (e.g. "webcam").
|
|
1375
|
+
* @param track The MediaStreamTrack to publish.
|
|
1098
1376
|
*/
|
|
1099
|
-
publishTrack(track) {
|
|
1377
|
+
publishTrack(name, track) {
|
|
1100
1378
|
return __async(this, null, function* () {
|
|
1101
1379
|
var _a;
|
|
1102
1380
|
if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
|
|
1103
|
-
|
|
1104
|
-
|
|
1381
|
+
console.warn(
|
|
1382
|
+
`[Reactor] Cannot publish track "${name}", status is ${this.status}`
|
|
1383
|
+
);
|
|
1105
1384
|
return;
|
|
1106
1385
|
}
|
|
1107
1386
|
try {
|
|
1108
|
-
yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
|
|
1387
|
+
yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
|
|
1109
1388
|
} catch (error) {
|
|
1110
|
-
console.error(
|
|
1389
|
+
console.error(`[Reactor] Failed to publish track "${name}":`, error);
|
|
1111
1390
|
this.createError(
|
|
1112
1391
|
"TRACK_PUBLISH_FAILED",
|
|
1113
|
-
`Failed to publish track: ${error}`,
|
|
1392
|
+
`Failed to publish track "${name}": ${error}`,
|
|
1114
1393
|
"gpu",
|
|
1115
1394
|
true
|
|
1116
1395
|
);
|
|
@@ -1118,18 +1397,20 @@ var Reactor = class {
|
|
|
1118
1397
|
});
|
|
1119
1398
|
}
|
|
1120
1399
|
/**
|
|
1121
|
-
*
|
|
1400
|
+
* Unpublishes the track with the given name.
|
|
1401
|
+
*
|
|
1402
|
+
* @param name The declared send track name to unpublish.
|
|
1122
1403
|
*/
|
|
1123
|
-
unpublishTrack() {
|
|
1404
|
+
unpublishTrack(name) {
|
|
1124
1405
|
return __async(this, null, function* () {
|
|
1125
1406
|
var _a;
|
|
1126
1407
|
try {
|
|
1127
|
-
yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
|
|
1408
|
+
yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
|
|
1128
1409
|
} catch (error) {
|
|
1129
|
-
console.error(
|
|
1410
|
+
console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
|
|
1130
1411
|
this.createError(
|
|
1131
1412
|
"TRACK_UNPUBLISH_FAILED",
|
|
1132
|
-
`Failed to unpublish track: ${error}`,
|
|
1413
|
+
`Failed to unpublish track "${name}": ${error}`,
|
|
1133
1414
|
"gpu",
|
|
1134
1415
|
true
|
|
1135
1416
|
);
|
|
@@ -1156,7 +1437,10 @@ var Reactor = class {
|
|
|
1156
1437
|
this.machineClient = new GPUMachineClient({ iceServers });
|
|
1157
1438
|
this.setupMachineClientHandlers();
|
|
1158
1439
|
}
|
|
1159
|
-
const sdpOffer = yield this.machineClient.createOffer(
|
|
1440
|
+
const sdpOffer = yield this.machineClient.createOffer({
|
|
1441
|
+
send: this.send,
|
|
1442
|
+
receive: this.receive
|
|
1443
|
+
});
|
|
1160
1444
|
try {
|
|
1161
1445
|
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
1162
1446
|
this.sessionId,
|
|
@@ -1164,8 +1448,8 @@ var Reactor = class {
|
|
|
1164
1448
|
options == null ? void 0 : options.maxAttempts
|
|
1165
1449
|
);
|
|
1166
1450
|
yield this.machineClient.connect(sdpAnswer);
|
|
1167
|
-
this.setStatus("ready");
|
|
1168
1451
|
} catch (error) {
|
|
1452
|
+
if (isAbortError(error)) return;
|
|
1169
1453
|
let recoverable = false;
|
|
1170
1454
|
if (error instanceof ConflictError) {
|
|
1171
1455
|
recoverable = true;
|
|
@@ -1211,7 +1495,10 @@ var Reactor = class {
|
|
|
1211
1495
|
const iceServers = yield this.coordinatorClient.getIceServers();
|
|
1212
1496
|
this.machineClient = new GPUMachineClient({ iceServers });
|
|
1213
1497
|
this.setupMachineClientHandlers();
|
|
1214
|
-
const sdpOffer = yield this.machineClient.createOffer(
|
|
1498
|
+
const sdpOffer = yield this.machineClient.createOffer({
|
|
1499
|
+
send: this.send,
|
|
1500
|
+
receive: this.receive
|
|
1501
|
+
});
|
|
1215
1502
|
const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
|
|
1216
1503
|
this.setSessionId(sessionId);
|
|
1217
1504
|
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
@@ -1221,6 +1508,7 @@ var Reactor = class {
|
|
|
1221
1508
|
);
|
|
1222
1509
|
yield this.machineClient.connect(sdpAnswer);
|
|
1223
1510
|
} catch (error) {
|
|
1511
|
+
if (isAbortError(error)) return;
|
|
1224
1512
|
console.error("[Reactor] Connection failed:", error);
|
|
1225
1513
|
this.createError(
|
|
1226
1514
|
"CONNECTION_FAILED",
|
|
@@ -1242,17 +1530,25 @@ var Reactor = class {
|
|
|
1242
1530
|
}
|
|
1243
1531
|
/**
|
|
1244
1532
|
* Sets up event handlers for the machine client.
|
|
1533
|
+
*
|
|
1534
|
+
* Each handler captures the client reference at registration time and
|
|
1535
|
+
* ignores events if this.machineClient has since changed (e.g. after
|
|
1536
|
+
* disconnect + reconnect), preventing stale WebRTC teardown events from
|
|
1537
|
+
* interfering with a new connection.
|
|
1245
1538
|
*/
|
|
1246
1539
|
setupMachineClientHandlers() {
|
|
1247
1540
|
if (!this.machineClient) return;
|
|
1248
|
-
this.machineClient
|
|
1541
|
+
const client = this.machineClient;
|
|
1542
|
+
client.on("message", (message, scope) => {
|
|
1543
|
+
if (this.machineClient !== client) return;
|
|
1249
1544
|
if (scope === "application") {
|
|
1250
1545
|
this.emit("message", message);
|
|
1251
1546
|
} else if (scope === "runtime") {
|
|
1252
1547
|
this.emit("runtimeMessage", message);
|
|
1253
1548
|
}
|
|
1254
1549
|
});
|
|
1255
|
-
|
|
1550
|
+
client.on("statusChanged", (status) => {
|
|
1551
|
+
if (this.machineClient !== client) return;
|
|
1256
1552
|
switch (status) {
|
|
1257
1553
|
case "connected":
|
|
1258
1554
|
this.setStatus("ready");
|
|
@@ -1271,13 +1567,15 @@ var Reactor = class {
|
|
|
1271
1567
|
break;
|
|
1272
1568
|
}
|
|
1273
1569
|
});
|
|
1274
|
-
|
|
1570
|
+
client.on(
|
|
1275
1571
|
"trackReceived",
|
|
1276
|
-
(track, stream) => {
|
|
1277
|
-
this.
|
|
1572
|
+
(name, track, stream) => {
|
|
1573
|
+
if (this.machineClient !== client) return;
|
|
1574
|
+
this.emit("trackReceived", name, track, stream);
|
|
1278
1575
|
}
|
|
1279
1576
|
);
|
|
1280
|
-
|
|
1577
|
+
client.on("statsUpdate", (stats) => {
|
|
1578
|
+
if (this.machineClient !== client) return;
|
|
1281
1579
|
this.emit("statsUpdate", stats);
|
|
1282
1580
|
});
|
|
1283
1581
|
}
|
|
@@ -1287,12 +1585,18 @@ var Reactor = class {
|
|
|
1287
1585
|
*/
|
|
1288
1586
|
disconnect(recoverable = false) {
|
|
1289
1587
|
return __async(this, null, function* () {
|
|
1588
|
+
var _a;
|
|
1290
1589
|
if (this.status === "disconnected" && !this.sessionId) {
|
|
1291
1590
|
console.warn("[Reactor] Already disconnected");
|
|
1292
1591
|
return;
|
|
1293
1592
|
}
|
|
1593
|
+
(_a = this.coordinatorClient) == null ? void 0 : _a.abort();
|
|
1294
1594
|
if (this.coordinatorClient && !recoverable) {
|
|
1295
|
-
|
|
1595
|
+
try {
|
|
1596
|
+
yield this.coordinatorClient.terminateSession();
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
console.error("[Reactor] Error terminating session:", error);
|
|
1599
|
+
}
|
|
1296
1600
|
this.coordinatorClient = void 0;
|
|
1297
1601
|
}
|
|
1298
1602
|
if (this.machineClient) {
|
|
@@ -1397,10 +1701,9 @@ var ReactorContext = (0, import_react2.createContext)(
|
|
|
1397
1701
|
);
|
|
1398
1702
|
var defaultInitState = {
|
|
1399
1703
|
status: "disconnected",
|
|
1400
|
-
|
|
1704
|
+
tracks: {},
|
|
1401
1705
|
lastError: void 0,
|
|
1402
1706
|
sessionExpiration: void 0,
|
|
1403
|
-
insecureApiKey: void 0,
|
|
1404
1707
|
jwtToken: void 0,
|
|
1405
1708
|
sessionId: void 0
|
|
1406
1709
|
};
|
|
@@ -1421,7 +1724,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1421
1724
|
oldStatus: get().status,
|
|
1422
1725
|
newStatus
|
|
1423
1726
|
});
|
|
1424
|
-
|
|
1727
|
+
if (newStatus === "disconnected") {
|
|
1728
|
+
set({ status: newStatus, tracks: {} });
|
|
1729
|
+
} else {
|
|
1730
|
+
set({ status: newStatus });
|
|
1731
|
+
}
|
|
1425
1732
|
});
|
|
1426
1733
|
reactor.on(
|
|
1427
1734
|
"sessionExpirationChanged",
|
|
@@ -1433,13 +1740,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1433
1740
|
set({ sessionExpiration: newSessionExpiration });
|
|
1434
1741
|
}
|
|
1435
1742
|
);
|
|
1436
|
-
reactor.on("
|
|
1437
|
-
console.debug("[ReactorStore]
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1743
|
+
reactor.on("trackReceived", (name, track) => {
|
|
1744
|
+
console.debug("[ReactorStore] Track received", {
|
|
1745
|
+
name,
|
|
1746
|
+
kind: track.kind,
|
|
1747
|
+
id: track.id
|
|
1441
1748
|
});
|
|
1442
|
-
set({
|
|
1749
|
+
set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
|
|
1443
1750
|
});
|
|
1444
1751
|
reactor.on("error", (error) => {
|
|
1445
1752
|
console.debug("[ReactorStore] Error occurred", error);
|
|
@@ -1503,27 +1810,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1503
1810
|
throw error;
|
|
1504
1811
|
}
|
|
1505
1812
|
}),
|
|
1506
|
-
|
|
1507
|
-
console.debug(
|
|
1813
|
+
publish: (name, track) => __async(null, null, function* () {
|
|
1814
|
+
console.debug(`[ReactorStore] Publishing track "${name}"`);
|
|
1508
1815
|
try {
|
|
1509
|
-
yield get().internal.reactor.publishTrack(
|
|
1510
|
-
console.debug(
|
|
1816
|
+
yield get().internal.reactor.publishTrack(name, track);
|
|
1817
|
+
console.debug(
|
|
1818
|
+
`[ReactorStore] Track "${name}" published successfully`
|
|
1819
|
+
);
|
|
1511
1820
|
} catch (error) {
|
|
1512
1821
|
console.error(
|
|
1513
|
-
|
|
1822
|
+
`[ReactorStore] Failed to publish track "${name}":`,
|
|
1514
1823
|
error
|
|
1515
1824
|
);
|
|
1516
1825
|
throw error;
|
|
1517
1826
|
}
|
|
1518
1827
|
}),
|
|
1519
|
-
|
|
1520
|
-
console.debug(
|
|
1828
|
+
unpublish: (name) => __async(null, null, function* () {
|
|
1829
|
+
console.debug(`[ReactorStore] Unpublishing track "${name}"`);
|
|
1521
1830
|
try {
|
|
1522
|
-
yield get().internal.reactor.unpublishTrack();
|
|
1523
|
-
console.debug(
|
|
1831
|
+
yield get().internal.reactor.unpublishTrack(name);
|
|
1832
|
+
console.debug(
|
|
1833
|
+
`[ReactorStore] Track "${name}" unpublished successfully`
|
|
1834
|
+
);
|
|
1524
1835
|
} catch (error) {
|
|
1525
1836
|
console.error(
|
|
1526
|
-
|
|
1837
|
+
`[ReactorStore] Failed to unpublish track "${name}":`,
|
|
1527
1838
|
error
|
|
1528
1839
|
);
|
|
1529
1840
|
throw error;
|
|
@@ -1569,7 +1880,7 @@ function ReactorProvider(_a) {
|
|
|
1569
1880
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1570
1881
|
}
|
|
1571
1882
|
const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
|
|
1572
|
-
const { coordinatorUrl, modelName, local } = props;
|
|
1883
|
+
const { coordinatorUrl, modelName, local, receive, send } = props;
|
|
1573
1884
|
const maxAttempts = pollingOptions.maxAttempts;
|
|
1574
1885
|
(0, import_react3.useEffect)(() => {
|
|
1575
1886
|
const handleBeforeUnload = () => {
|
|
@@ -1625,6 +1936,8 @@ function ReactorProvider(_a) {
|
|
|
1625
1936
|
coordinatorUrl,
|
|
1626
1937
|
modelName,
|
|
1627
1938
|
local,
|
|
1939
|
+
receive,
|
|
1940
|
+
send,
|
|
1628
1941
|
jwtToken
|
|
1629
1942
|
})
|
|
1630
1943
|
);
|
|
@@ -1651,7 +1964,16 @@ function ReactorProvider(_a) {
|
|
|
1651
1964
|
console.error("[ReactorProvider] Failed to disconnect:", error);
|
|
1652
1965
|
});
|
|
1653
1966
|
};
|
|
1654
|
-
}, [
|
|
1967
|
+
}, [
|
|
1968
|
+
coordinatorUrl,
|
|
1969
|
+
modelName,
|
|
1970
|
+
autoConnect,
|
|
1971
|
+
local,
|
|
1972
|
+
receive,
|
|
1973
|
+
send,
|
|
1974
|
+
jwtToken,
|
|
1975
|
+
maxAttempts
|
|
1976
|
+
]);
|
|
1655
1977
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
|
|
1656
1978
|
}
|
|
1657
1979
|
function useReactorStore(selector) {
|
|
@@ -1720,47 +2042,60 @@ function useStats() {
|
|
|
1720
2042
|
var import_react5 = require("react");
|
|
1721
2043
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1722
2044
|
function ReactorView({
|
|
2045
|
+
track = "main_video",
|
|
2046
|
+
audioTrack,
|
|
1723
2047
|
width,
|
|
1724
2048
|
height,
|
|
1725
2049
|
className,
|
|
1726
2050
|
style,
|
|
1727
|
-
videoObjectFit = "contain"
|
|
2051
|
+
videoObjectFit = "contain",
|
|
2052
|
+
muted = true
|
|
1728
2053
|
}) {
|
|
1729
|
-
const {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2054
|
+
const { videoMediaTrack, audioMediaTrack, status } = useReactor((state) => {
|
|
2055
|
+
var _a, _b;
|
|
2056
|
+
return {
|
|
2057
|
+
videoMediaTrack: (_a = state.tracks[track]) != null ? _a : null,
|
|
2058
|
+
audioMediaTrack: audioTrack ? (_b = state.tracks[audioTrack]) != null ? _b : null : null,
|
|
2059
|
+
status: state.status
|
|
2060
|
+
};
|
|
2061
|
+
});
|
|
1733
2062
|
const videoRef = (0, import_react5.useRef)(null);
|
|
2063
|
+
const mediaStream = (0, import_react5.useMemo)(() => {
|
|
2064
|
+
const tracks = [];
|
|
2065
|
+
if (videoMediaTrack) tracks.push(videoMediaTrack);
|
|
2066
|
+
if (audioMediaTrack) tracks.push(audioMediaTrack);
|
|
2067
|
+
if (tracks.length === 0) return null;
|
|
2068
|
+
return new MediaStream(tracks);
|
|
2069
|
+
}, [videoMediaTrack, audioMediaTrack]);
|
|
1734
2070
|
(0, import_react5.useEffect)(() => {
|
|
1735
|
-
console.debug("[ReactorView]
|
|
2071
|
+
console.debug("[ReactorView] Media track effect triggered", {
|
|
2072
|
+
track,
|
|
1736
2073
|
hasVideoElement: !!videoRef.current,
|
|
1737
|
-
hasVideoTrack: !!
|
|
1738
|
-
|
|
2074
|
+
hasVideoTrack: !!videoMediaTrack,
|
|
2075
|
+
hasAudioTrack: !!audioMediaTrack
|
|
1739
2076
|
});
|
|
1740
|
-
if (videoRef.current &&
|
|
1741
|
-
console.debug("[ReactorView] Attaching
|
|
2077
|
+
if (videoRef.current && mediaStream) {
|
|
2078
|
+
console.debug("[ReactorView] Attaching media stream to element");
|
|
1742
2079
|
try {
|
|
1743
|
-
|
|
1744
|
-
videoRef.current.srcObject = stream;
|
|
2080
|
+
videoRef.current.srcObject = mediaStream;
|
|
1745
2081
|
videoRef.current.play().catch((e) => {
|
|
1746
2082
|
console.warn("[ReactorView] Auto-play failed:", e);
|
|
1747
2083
|
});
|
|
1748
|
-
console.debug("[ReactorView]
|
|
2084
|
+
console.debug("[ReactorView] Media stream attached successfully");
|
|
1749
2085
|
} catch (error) {
|
|
1750
|
-
console.error("[ReactorView] Failed to attach
|
|
2086
|
+
console.error("[ReactorView] Failed to attach media stream:", error);
|
|
1751
2087
|
}
|
|
1752
2088
|
return () => {
|
|
1753
|
-
console.debug("[ReactorView] Detaching
|
|
2089
|
+
console.debug("[ReactorView] Detaching media stream from element");
|
|
1754
2090
|
if (videoRef.current) {
|
|
1755
2091
|
videoRef.current.srcObject = null;
|
|
1756
|
-
console.debug("[ReactorView] Video track detached successfully");
|
|
1757
2092
|
}
|
|
1758
2093
|
};
|
|
1759
2094
|
} else {
|
|
1760
|
-
console.debug("[ReactorView] No
|
|
2095
|
+
console.debug("[ReactorView] No tracks or element to attach");
|
|
1761
2096
|
}
|
|
1762
|
-
}, [
|
|
1763
|
-
const showPlaceholder = !
|
|
2097
|
+
}, [mediaStream]);
|
|
2098
|
+
const showPlaceholder = !videoMediaTrack;
|
|
1764
2099
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1765
2100
|
"div",
|
|
1766
2101
|
{
|
|
@@ -1780,7 +2115,7 @@ function ReactorView({
|
|
|
1780
2115
|
objectFit: videoObjectFit,
|
|
1781
2116
|
display: showPlaceholder ? "none" : "block"
|
|
1782
2117
|
},
|
|
1783
|
-
muted
|
|
2118
|
+
muted,
|
|
1784
2119
|
playsInline: true
|
|
1785
2120
|
}
|
|
1786
2121
|
),
|
|
@@ -2268,6 +2603,7 @@ function ReactorController({
|
|
|
2268
2603
|
var import_react7 = require("react");
|
|
2269
2604
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
2270
2605
|
function WebcamStream({
|
|
2606
|
+
track,
|
|
2271
2607
|
className,
|
|
2272
2608
|
style,
|
|
2273
2609
|
videoConstraints = {
|
|
@@ -2280,10 +2616,10 @@ function WebcamStream({
|
|
|
2280
2616
|
const [stream, setStream] = (0, import_react7.useState)(null);
|
|
2281
2617
|
const [isPublishing, setIsPublishing] = (0, import_react7.useState)(false);
|
|
2282
2618
|
const [permissionDenied, setPermissionDenied] = (0, import_react7.useState)(false);
|
|
2283
|
-
const { status,
|
|
2619
|
+
const { status, publish, unpublish, reactor } = useReactor((state) => ({
|
|
2284
2620
|
status: state.status,
|
|
2285
|
-
|
|
2286
|
-
|
|
2621
|
+
publish: state.publish,
|
|
2622
|
+
unpublish: state.unpublish,
|
|
2287
2623
|
reactor: state.internal.reactor
|
|
2288
2624
|
}));
|
|
2289
2625
|
const videoRef = (0, import_react7.useRef)(null);
|
|
@@ -2308,15 +2644,15 @@ function WebcamStream({
|
|
|
2308
2644
|
const stopWebcam = () => __async(null, null, function* () {
|
|
2309
2645
|
console.debug("[WebcamPublisher] Stopping webcam");
|
|
2310
2646
|
try {
|
|
2311
|
-
yield
|
|
2647
|
+
yield unpublish(track);
|
|
2312
2648
|
console.debug("[WebcamPublisher] Unpublished before stopping");
|
|
2313
2649
|
} catch (err) {
|
|
2314
2650
|
console.error("[WebcamPublisher] Error unpublishing before stop:", err);
|
|
2315
2651
|
}
|
|
2316
2652
|
setIsPublishing(false);
|
|
2317
|
-
stream == null ? void 0 : stream.getTracks().forEach((
|
|
2318
|
-
|
|
2319
|
-
console.debug("[WebcamPublisher] Stopped track:",
|
|
2653
|
+
stream == null ? void 0 : stream.getTracks().forEach((t) => {
|
|
2654
|
+
t.stop();
|
|
2655
|
+
console.debug("[WebcamPublisher] Stopped track:", t.kind);
|
|
2320
2656
|
});
|
|
2321
2657
|
setStream(null);
|
|
2322
2658
|
console.debug("[WebcamPublisher] Webcam stopped");
|
|
@@ -2346,28 +2682,31 @@ function WebcamStream({
|
|
|
2346
2682
|
console.debug(
|
|
2347
2683
|
"[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
|
|
2348
2684
|
);
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2685
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
2686
|
+
if (videoTrack) {
|
|
2687
|
+
publish(track, videoTrack).then(() => {
|
|
2688
|
+
console.debug("[WebcamPublisher] Auto-publish successful");
|
|
2689
|
+
setIsPublishing(true);
|
|
2690
|
+
}).catch((err) => {
|
|
2691
|
+
console.error("[WebcamPublisher] Auto-publish failed:", err);
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2355
2694
|
} else if (status !== "ready" && isPublishing) {
|
|
2356
2695
|
console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
|
|
2357
|
-
|
|
2696
|
+
unpublish(track).then(() => {
|
|
2358
2697
|
console.debug("[WebcamPublisher] Auto-unpublish successful");
|
|
2359
2698
|
setIsPublishing(false);
|
|
2360
2699
|
}).catch((err) => {
|
|
2361
2700
|
console.error("[WebcamPublisher] Auto-unpublish failed:", err);
|
|
2362
2701
|
});
|
|
2363
2702
|
}
|
|
2364
|
-
}, [status, stream, isPublishing,
|
|
2703
|
+
}, [status, stream, isPublishing, publish, unpublish, track]);
|
|
2365
2704
|
(0, import_react7.useEffect)(() => {
|
|
2366
2705
|
const handleError = (error) => {
|
|
2367
2706
|
console.debug("[WebcamPublisher] Received error event:", error);
|
|
2368
|
-
if (error.code === "
|
|
2707
|
+
if (error.code === "TRACK_PUBLISH_FAILED") {
|
|
2369
2708
|
console.debug(
|
|
2370
|
-
"[WebcamPublisher]
|
|
2709
|
+
"[WebcamPublisher] Track publish failed, resetting isPublishing state"
|
|
2371
2710
|
);
|
|
2372
2711
|
setIsPublishing(false);
|
|
2373
2712
|
}
|
|
@@ -2476,6 +2815,7 @@ function fetchInsecureJwtToken(_0) {
|
|
|
2476
2815
|
}
|
|
2477
2816
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2478
2817
|
0 && (module.exports = {
|
|
2818
|
+
AbortError,
|
|
2479
2819
|
ConflictError,
|
|
2480
2820
|
PROD_COORDINATOR_URL,
|
|
2481
2821
|
Reactor,
|
|
@@ -2483,11 +2823,14 @@ function fetchInsecureJwtToken(_0) {
|
|
|
2483
2823
|
ReactorProvider,
|
|
2484
2824
|
ReactorView,
|
|
2485
2825
|
WebcamStream,
|
|
2826
|
+
audio,
|
|
2486
2827
|
fetchInsecureJwtToken,
|
|
2828
|
+
isAbortError,
|
|
2487
2829
|
useReactor,
|
|
2488
2830
|
useReactorInternalMessage,
|
|
2489
2831
|
useReactorMessage,
|
|
2490
2832
|
useReactorStore,
|
|
2491
|
-
useStats
|
|
2833
|
+
useStats,
|
|
2834
|
+
video
|
|
2492
2835
|
});
|
|
2493
2836
|
//# sourceMappingURL=index.js.map
|