@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.mjs
CHANGED
|
@@ -51,11 +51,25 @@ var __async = (__this, __arguments, generator) => {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
// src/types.ts
|
|
54
|
+
function video(name, _options) {
|
|
55
|
+
return { name, kind: "video" };
|
|
56
|
+
}
|
|
57
|
+
function audio(name, _options) {
|
|
58
|
+
return { name, kind: "audio" };
|
|
59
|
+
}
|
|
54
60
|
var ConflictError = class extends Error {
|
|
55
61
|
constructor(message) {
|
|
56
62
|
super(message);
|
|
57
63
|
}
|
|
58
64
|
};
|
|
65
|
+
var AbortError = class extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(message);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function isAbortError(error) {
|
|
71
|
+
return error instanceof AbortError || error instanceof Error && error.name === "AbortError";
|
|
72
|
+
}
|
|
59
73
|
|
|
60
74
|
// src/core/types.ts
|
|
61
75
|
import { z } from "zod";
|
|
@@ -124,18 +138,105 @@ function createPeerConnection(config) {
|
|
|
124
138
|
function createDataChannel(pc, label) {
|
|
125
139
|
return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
|
|
126
140
|
}
|
|
127
|
-
function
|
|
141
|
+
function rewriteMids(sdp, trackNames) {
|
|
142
|
+
const lines = sdp.split("\r\n");
|
|
143
|
+
let mediaIdx = 0;
|
|
144
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
145
|
+
let inApplication = false;
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
if (lines[i].startsWith("m=")) {
|
|
148
|
+
inApplication = lines[i].startsWith("m=application");
|
|
149
|
+
}
|
|
150
|
+
if (!inApplication && lines[i].startsWith("a=mid:")) {
|
|
151
|
+
const oldMid = lines[i].substring("a=mid:".length);
|
|
152
|
+
if (mediaIdx < trackNames.length) {
|
|
153
|
+
const newMid = trackNames[mediaIdx];
|
|
154
|
+
replacements.set(oldMid, newMid);
|
|
155
|
+
lines[i] = `a=mid:${newMid}`;
|
|
156
|
+
mediaIdx++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (let i = 0; i < lines.length; i++) {
|
|
161
|
+
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
162
|
+
const parts = lines[i].split(" ");
|
|
163
|
+
for (let j = 1; j < parts.length; j++) {
|
|
164
|
+
const replacement = replacements.get(parts[j]);
|
|
165
|
+
if (replacement !== void 0) {
|
|
166
|
+
parts[j] = replacement;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
lines[i] = parts.join(" ");
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\r\n");
|
|
174
|
+
}
|
|
175
|
+
function createOffer(pc, trackNames) {
|
|
128
176
|
return __async(this, null, function* () {
|
|
129
177
|
const offer = yield pc.createOffer();
|
|
130
|
-
|
|
178
|
+
let needsAnswerRestore = false;
|
|
179
|
+
if (trackNames && trackNames.length > 0 && offer.sdp) {
|
|
180
|
+
const munged = rewriteMids(offer.sdp, trackNames);
|
|
181
|
+
try {
|
|
182
|
+
yield pc.setLocalDescription(
|
|
183
|
+
new RTCSessionDescription({ type: "offer", sdp: munged })
|
|
184
|
+
);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
yield pc.setLocalDescription(offer);
|
|
187
|
+
needsAnswerRestore = true;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
yield pc.setLocalDescription(offer);
|
|
191
|
+
}
|
|
131
192
|
yield waitForIceGathering(pc);
|
|
132
193
|
const localDescription = pc.localDescription;
|
|
133
194
|
if (!localDescription) {
|
|
134
195
|
throw new Error("Failed to create local description");
|
|
135
196
|
}
|
|
136
|
-
|
|
197
|
+
let sdp = localDescription.sdp;
|
|
198
|
+
if (needsAnswerRestore && trackNames && trackNames.length > 0) {
|
|
199
|
+
sdp = rewriteMids(sdp, trackNames);
|
|
200
|
+
}
|
|
201
|
+
return { sdp, needsAnswerRestore };
|
|
137
202
|
});
|
|
138
203
|
}
|
|
204
|
+
function buildMidMapping(transceivers) {
|
|
205
|
+
var _a;
|
|
206
|
+
const localToRemote = /* @__PURE__ */ new Map();
|
|
207
|
+
const remoteToLocal = /* @__PURE__ */ new Map();
|
|
208
|
+
for (const entry of transceivers) {
|
|
209
|
+
const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
|
|
210
|
+
if (mid) {
|
|
211
|
+
localToRemote.set(mid, entry.name);
|
|
212
|
+
remoteToLocal.set(entry.name, mid);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { localToRemote, remoteToLocal };
|
|
216
|
+
}
|
|
217
|
+
function restoreAnswerMids(sdp, remoteToLocal) {
|
|
218
|
+
const lines = sdp.split("\r\n");
|
|
219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
220
|
+
if (lines[i].startsWith("a=mid:")) {
|
|
221
|
+
const remoteMid = lines[i].substring("a=mid:".length);
|
|
222
|
+
const localMid = remoteToLocal.get(remoteMid);
|
|
223
|
+
if (localMid !== void 0) {
|
|
224
|
+
lines[i] = `a=mid:${localMid}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
228
|
+
const parts = lines[i].split(" ");
|
|
229
|
+
for (let j = 1; j < parts.length; j++) {
|
|
230
|
+
const localMid = remoteToLocal.get(parts[j]);
|
|
231
|
+
if (localMid !== void 0) {
|
|
232
|
+
parts[j] = localMid;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
lines[i] = parts.join(" ");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return lines.join("\r\n");
|
|
239
|
+
}
|
|
139
240
|
function setRemoteDescription(pc, sdp) {
|
|
140
241
|
return __async(this, null, function* () {
|
|
141
242
|
const sessionDescription = new RTCSessionDescription({
|
|
@@ -263,6 +364,22 @@ var CoordinatorClient = class {
|
|
|
263
364
|
this.baseUrl = options.baseUrl;
|
|
264
365
|
this.jwtToken = options.jwtToken;
|
|
265
366
|
this.model = options.model;
|
|
367
|
+
this.abortController = new AbortController();
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Aborts any in-flight HTTP requests and polling loops.
|
|
371
|
+
* A fresh AbortController is created so the client remains reusable.
|
|
372
|
+
*/
|
|
373
|
+
abort() {
|
|
374
|
+
this.abortController.abort();
|
|
375
|
+
this.abortController = new AbortController();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The current abort signal, passed to every fetch() and sleep() call.
|
|
379
|
+
* Protected so subclasses can forward it to their own fetch calls.
|
|
380
|
+
*/
|
|
381
|
+
get signal() {
|
|
382
|
+
return this.abortController.signal;
|
|
266
383
|
}
|
|
267
384
|
/**
|
|
268
385
|
* Returns the authorization header with JWT Bearer token
|
|
@@ -283,7 +400,8 @@ var CoordinatorClient = class {
|
|
|
283
400
|
`${this.baseUrl}/ice_servers?model=${this.model}`,
|
|
284
401
|
{
|
|
285
402
|
method: "GET",
|
|
286
|
-
headers: this.getAuthHeaders()
|
|
403
|
+
headers: this.getAuthHeaders(),
|
|
404
|
+
signal: this.signal
|
|
287
405
|
}
|
|
288
406
|
);
|
|
289
407
|
if (!response.ok) {
|
|
@@ -317,7 +435,8 @@ var CoordinatorClient = class {
|
|
|
317
435
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
318
436
|
"Content-Type": "application/json"
|
|
319
437
|
}),
|
|
320
|
-
body: JSON.stringify(requestBody)
|
|
438
|
+
body: JSON.stringify(requestBody),
|
|
439
|
+
signal: this.signal
|
|
321
440
|
});
|
|
322
441
|
if (!response.ok) {
|
|
323
442
|
const errorText = yield response.text();
|
|
@@ -351,7 +470,8 @@ var CoordinatorClient = class {
|
|
|
351
470
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
352
471
|
{
|
|
353
472
|
method: "GET",
|
|
354
|
-
headers: this.getAuthHeaders()
|
|
473
|
+
headers: this.getAuthHeaders(),
|
|
474
|
+
signal: this.signal
|
|
355
475
|
}
|
|
356
476
|
);
|
|
357
477
|
if (!response.ok) {
|
|
@@ -364,12 +484,13 @@ var CoordinatorClient = class {
|
|
|
364
484
|
}
|
|
365
485
|
/**
|
|
366
486
|
* Terminates the current session by sending a DELETE request to the coordinator.
|
|
367
|
-
*
|
|
487
|
+
* No-op if no session has been created yet.
|
|
488
|
+
* @throws Error if the request fails (except for 404, which clears local state)
|
|
368
489
|
*/
|
|
369
490
|
terminateSession() {
|
|
370
491
|
return __async(this, null, function* () {
|
|
371
492
|
if (!this.currentSessionId) {
|
|
372
|
-
|
|
493
|
+
return;
|
|
373
494
|
}
|
|
374
495
|
console.debug(
|
|
375
496
|
"[CoordinatorClient] Terminating session:",
|
|
@@ -379,7 +500,8 @@ var CoordinatorClient = class {
|
|
|
379
500
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
380
501
|
{
|
|
381
502
|
method: "DELETE",
|
|
382
|
-
headers: this.getAuthHeaders()
|
|
503
|
+
headers: this.getAuthHeaders(),
|
|
504
|
+
signal: this.signal
|
|
383
505
|
}
|
|
384
506
|
);
|
|
385
507
|
if (response.ok) {
|
|
@@ -429,7 +551,8 @@ var CoordinatorClient = class {
|
|
|
429
551
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
430
552
|
"Content-Type": "application/json"
|
|
431
553
|
}),
|
|
432
|
-
body: JSON.stringify(requestBody)
|
|
554
|
+
body: JSON.stringify(requestBody),
|
|
555
|
+
signal: this.signal
|
|
433
556
|
}
|
|
434
557
|
);
|
|
435
558
|
if (response.status === 200) {
|
|
@@ -465,6 +588,9 @@ var CoordinatorClient = class {
|
|
|
465
588
|
let backoffMs = INITIAL_BACKOFF_MS;
|
|
466
589
|
let attempt = 0;
|
|
467
590
|
while (true) {
|
|
591
|
+
if (this.signal.aborted) {
|
|
592
|
+
throw new AbortError("SDP polling aborted");
|
|
593
|
+
}
|
|
468
594
|
if (attempt >= maxAttempts) {
|
|
469
595
|
throw new Error(
|
|
470
596
|
`SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
|
|
@@ -480,7 +606,8 @@ var CoordinatorClient = class {
|
|
|
480
606
|
method: "GET",
|
|
481
607
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
482
608
|
"Content-Type": "application/json"
|
|
483
|
-
})
|
|
609
|
+
}),
|
|
610
|
+
signal: this.signal
|
|
484
611
|
}
|
|
485
612
|
);
|
|
486
613
|
if (response.status === 200) {
|
|
@@ -525,10 +652,26 @@ var CoordinatorClient = class {
|
|
|
525
652
|
});
|
|
526
653
|
}
|
|
527
654
|
/**
|
|
528
|
-
*
|
|
655
|
+
* Abort-aware sleep. Resolves after `ms` milliseconds unless the
|
|
656
|
+
* abort signal fires first, in which case it rejects with AbortError.
|
|
529
657
|
*/
|
|
530
658
|
sleep(ms) {
|
|
531
|
-
return new Promise((resolve) =>
|
|
659
|
+
return new Promise((resolve, reject) => {
|
|
660
|
+
const { signal } = this;
|
|
661
|
+
if (signal.aborted) {
|
|
662
|
+
reject(new AbortError("Sleep aborted"));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const timer = setTimeout(() => {
|
|
666
|
+
signal.removeEventListener("abort", onAbort);
|
|
667
|
+
resolve();
|
|
668
|
+
}, ms);
|
|
669
|
+
const onAbort = () => {
|
|
670
|
+
clearTimeout(timer);
|
|
671
|
+
reject(new AbortError("Sleep aborted"));
|
|
672
|
+
};
|
|
673
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
674
|
+
});
|
|
532
675
|
}
|
|
533
676
|
};
|
|
534
677
|
|
|
@@ -550,7 +693,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
550
693
|
return __async(this, null, function* () {
|
|
551
694
|
console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
|
|
552
695
|
const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
|
|
553
|
-
method: "GET"
|
|
696
|
+
method: "GET",
|
|
697
|
+
signal: this.signal
|
|
554
698
|
});
|
|
555
699
|
if (!response.ok) {
|
|
556
700
|
throw new Error("Failed to get ICE servers from local coordinator.");
|
|
@@ -574,7 +718,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
574
718
|
console.debug("[LocalCoordinatorClient] Creating local session...");
|
|
575
719
|
this.sdpOffer = sdpOffer;
|
|
576
720
|
const response = yield fetch(`${this.localBaseUrl}/start_session`, {
|
|
577
|
-
method: "POST"
|
|
721
|
+
method: "POST",
|
|
722
|
+
signal: this.signal
|
|
578
723
|
});
|
|
579
724
|
if (!response.ok) {
|
|
580
725
|
throw new Error("Failed to send local start session command.");
|
|
@@ -602,7 +747,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
602
747
|
headers: {
|
|
603
748
|
"Content-Type": "application/json"
|
|
604
749
|
},
|
|
605
|
-
body: JSON.stringify(sdpBody)
|
|
750
|
+
body: JSON.stringify(sdpBody),
|
|
751
|
+
signal: this.signal
|
|
606
752
|
});
|
|
607
753
|
if (!response.ok) {
|
|
608
754
|
if (response.status === 409) {
|
|
@@ -619,7 +765,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
619
765
|
return __async(this, null, function* () {
|
|
620
766
|
console.debug("[LocalCoordinatorClient] Stopping local session...");
|
|
621
767
|
yield fetch(`${this.localBaseUrl}/stop_session`, {
|
|
622
|
-
method: "POST"
|
|
768
|
+
method: "POST",
|
|
769
|
+
signal: this.signal
|
|
623
770
|
});
|
|
624
771
|
});
|
|
625
772
|
}
|
|
@@ -632,6 +779,10 @@ var GPUMachineClient = class {
|
|
|
632
779
|
constructor(config) {
|
|
633
780
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
634
781
|
this.status = "disconnected";
|
|
782
|
+
this.transceiverMap = /* @__PURE__ */ new Map();
|
|
783
|
+
this.publishedTracks = /* @__PURE__ */ new Map();
|
|
784
|
+
this.peerConnected = false;
|
|
785
|
+
this.dataChannelOpen = false;
|
|
635
786
|
this.config = config;
|
|
636
787
|
}
|
|
637
788
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -655,10 +806,18 @@ var GPUMachineClient = class {
|
|
|
655
806
|
// SDP & Connection
|
|
656
807
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
657
808
|
/**
|
|
658
|
-
* Creates an SDP offer
|
|
809
|
+
* Creates an SDP offer based on the declared tracks.
|
|
810
|
+
*
|
|
811
|
+
* **RECEIVE** = client receives from the model (model → client) → `recvonly`
|
|
812
|
+
* **SEND** = client sends to the model (client → model) → `sendonly`
|
|
813
|
+
*
|
|
814
|
+
* Track names must be unique across both arrays. A name that appears in
|
|
815
|
+
* both `receive` and `send` will throw — use distinct names instead.
|
|
816
|
+
*
|
|
817
|
+
* The data channel is always created first (before transceivers).
|
|
659
818
|
* Must be called before connect().
|
|
660
819
|
*/
|
|
661
|
-
createOffer() {
|
|
820
|
+
createOffer(tracks) {
|
|
662
821
|
return __async(this, null, function* () {
|
|
663
822
|
if (!this.peerConnection) {
|
|
664
823
|
this.peerConnection = createPeerConnection(this.config);
|
|
@@ -669,14 +828,63 @@ var GPUMachineClient = class {
|
|
|
669
828
|
this.config.dataChannelLabel
|
|
670
829
|
);
|
|
671
830
|
this.setupDataChannelHandlers();
|
|
672
|
-
this.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
831
|
+
this.transceiverMap.clear();
|
|
832
|
+
const entries = this.buildTransceiverEntries(tracks);
|
|
833
|
+
for (const entry of entries) {
|
|
834
|
+
const transceiver = this.peerConnection.addTransceiver(entry.kind, {
|
|
835
|
+
direction: entry.direction
|
|
836
|
+
});
|
|
837
|
+
entry.transceiver = transceiver;
|
|
838
|
+
this.transceiverMap.set(entry.name, entry);
|
|
839
|
+
console.debug(
|
|
840
|
+
`[GPUMachineClient] Transceiver added: "${entry.name}" (${entry.kind}, ${entry.direction})`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const trackNames = entries.map((e) => e.name);
|
|
844
|
+
const { sdp, needsAnswerRestore } = yield createOffer(
|
|
845
|
+
this.peerConnection,
|
|
846
|
+
trackNames
|
|
847
|
+
);
|
|
848
|
+
if (needsAnswerRestore) {
|
|
849
|
+
this.midMapping = buildMidMapping(entries);
|
|
850
|
+
} else {
|
|
851
|
+
this.midMapping = void 0;
|
|
852
|
+
}
|
|
853
|
+
console.debug(
|
|
854
|
+
"[GPUMachineClient] Created SDP offer with MIDs:",
|
|
855
|
+
trackNames,
|
|
856
|
+
needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
|
|
857
|
+
);
|
|
858
|
+
return sdp;
|
|
678
859
|
});
|
|
679
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* Builds an ordered list of transceiver entries from the receive/send arrays.
|
|
863
|
+
*
|
|
864
|
+
* Each track produces exactly one transceiver — `recvonly` for receive,
|
|
865
|
+
* `sendonly` for send. Bidirectional (`sendrecv`) transceivers are not
|
|
866
|
+
* supported; the same track name in both arrays is an error.
|
|
867
|
+
*/
|
|
868
|
+
buildTransceiverEntries(tracks) {
|
|
869
|
+
const map = /* @__PURE__ */ new Map();
|
|
870
|
+
for (const t of tracks.receive) {
|
|
871
|
+
if (map.has(t.name)) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
`Duplicate receive track name "${t.name}". Track names must be unique.`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
map.set(t.name, { name: t.name, kind: t.kind, direction: "recvonly" });
|
|
877
|
+
}
|
|
878
|
+
for (const t of tracks.send) {
|
|
879
|
+
if (map.has(t.name)) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`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").`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
map.set(t.name, { name: t.name, kind: t.kind, direction: "sendonly" });
|
|
885
|
+
}
|
|
886
|
+
return Array.from(map.values());
|
|
887
|
+
}
|
|
680
888
|
/**
|
|
681
889
|
* Connects to the GPU machine using the provided SDP answer.
|
|
682
890
|
* createOffer() must be called first.
|
|
@@ -696,7 +904,14 @@ var GPUMachineClient = class {
|
|
|
696
904
|
}
|
|
697
905
|
this.setStatus("connecting");
|
|
698
906
|
try {
|
|
699
|
-
|
|
907
|
+
let answer = sdpAnswer;
|
|
908
|
+
if (this.midMapping) {
|
|
909
|
+
answer = restoreAnswerMids(
|
|
910
|
+
answer,
|
|
911
|
+
this.midMapping.remoteToLocal
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
yield setRemoteDescription(this.peerConnection, answer);
|
|
700
915
|
console.debug("[GPUMachineClient] Remote description set");
|
|
701
916
|
} catch (error) {
|
|
702
917
|
console.error("[GPUMachineClient] Failed to connect:", error);
|
|
@@ -712,8 +927,8 @@ var GPUMachineClient = class {
|
|
|
712
927
|
return __async(this, null, function* () {
|
|
713
928
|
this.stopPing();
|
|
714
929
|
this.stopStatsPolling();
|
|
715
|
-
|
|
716
|
-
yield this.unpublishTrack();
|
|
930
|
+
for (const name of Array.from(this.publishedTracks.keys())) {
|
|
931
|
+
yield this.unpublishTrack(name);
|
|
717
932
|
}
|
|
718
933
|
if (this.dataChannel) {
|
|
719
934
|
this.dataChannel.close();
|
|
@@ -723,7 +938,10 @@ var GPUMachineClient = class {
|
|
|
723
938
|
closePeerConnection(this.peerConnection);
|
|
724
939
|
this.peerConnection = void 0;
|
|
725
940
|
}
|
|
726
|
-
this.
|
|
941
|
+
this.transceiverMap.clear();
|
|
942
|
+
this.midMapping = void 0;
|
|
943
|
+
this.peerConnected = false;
|
|
944
|
+
this.dataChannelOpen = false;
|
|
727
945
|
this.setStatus("disconnected");
|
|
728
946
|
console.debug("[GPUMachineClient] Disconnected");
|
|
729
947
|
});
|
|
@@ -751,7 +969,7 @@ var GPUMachineClient = class {
|
|
|
751
969
|
/**
|
|
752
970
|
* Sends a command to the GPU machine via the data channel.
|
|
753
971
|
* @param command The command to send
|
|
754
|
-
* @param data The data to send with the command. These are the parameters for the command, matching the
|
|
972
|
+
* @param data The data to send with the command. These are the parameters for the command, matching the schema in the capabilities dictionary.
|
|
755
973
|
* @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
|
|
756
974
|
*/
|
|
757
975
|
sendCommand(command, data, scope = "application") {
|
|
@@ -768,63 +986,77 @@ var GPUMachineClient = class {
|
|
|
768
986
|
// Track Publishing
|
|
769
987
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
770
988
|
/**
|
|
771
|
-
* Publishes a
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
* @param track The MediaStreamTrack to publish
|
|
989
|
+
* Publishes a MediaStreamTrack to the named send track.
|
|
990
|
+
*
|
|
991
|
+
* @param name The declared track name (must exist in transceiverMap with a sendable direction).
|
|
992
|
+
* @param track The MediaStreamTrack to publish.
|
|
775
993
|
*/
|
|
776
|
-
publishTrack(track) {
|
|
994
|
+
publishTrack(name, track) {
|
|
777
995
|
return __async(this, null, function* () {
|
|
778
996
|
if (!this.peerConnection) {
|
|
779
997
|
throw new Error(
|
|
780
|
-
|
|
998
|
+
`[GPUMachineClient] Cannot publish track "${name}" - not initialized`
|
|
781
999
|
);
|
|
782
1000
|
}
|
|
783
1001
|
if (this.status !== "connected") {
|
|
784
1002
|
throw new Error(
|
|
785
|
-
|
|
1003
|
+
`[GPUMachineClient] Cannot publish track "${name}" - not connected`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
const entry = this.transceiverMap.get(name);
|
|
1007
|
+
if (!entry || !entry.transceiver) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`[GPUMachineClient] Cannot publish track "${name}" - no transceiver (was it declared in tracks.send?)`
|
|
786
1010
|
);
|
|
787
1011
|
}
|
|
788
|
-
if (
|
|
1012
|
+
if (entry.direction === "recvonly") {
|
|
789
1013
|
throw new Error(
|
|
790
|
-
|
|
1014
|
+
`[GPUMachineClient] Cannot publish track "${name}" - transceiver is recvonly`
|
|
791
1015
|
);
|
|
792
1016
|
}
|
|
793
1017
|
try {
|
|
794
|
-
yield
|
|
795
|
-
this.
|
|
1018
|
+
yield entry.transceiver.sender.replaceTrack(track);
|
|
1019
|
+
this.publishedTracks.set(name, track);
|
|
796
1020
|
console.debug(
|
|
797
|
-
|
|
798
|
-
track.kind
|
|
1021
|
+
`[GPUMachineClient] Track "${name}" published successfully`
|
|
799
1022
|
);
|
|
800
1023
|
} catch (error) {
|
|
801
|
-
console.error(
|
|
1024
|
+
console.error(
|
|
1025
|
+
`[GPUMachineClient] Failed to publish track "${name}":`,
|
|
1026
|
+
error
|
|
1027
|
+
);
|
|
802
1028
|
throw error;
|
|
803
1029
|
}
|
|
804
1030
|
});
|
|
805
1031
|
}
|
|
806
1032
|
/**
|
|
807
|
-
* Unpublishes the
|
|
1033
|
+
* Unpublishes the track with the given name.
|
|
808
1034
|
*/
|
|
809
|
-
unpublishTrack() {
|
|
1035
|
+
unpublishTrack(name) {
|
|
810
1036
|
return __async(this, null, function* () {
|
|
811
|
-
|
|
1037
|
+
const entry = this.transceiverMap.get(name);
|
|
1038
|
+
if (!(entry == null ? void 0 : entry.transceiver) || !this.publishedTracks.has(name)) return;
|
|
812
1039
|
try {
|
|
813
|
-
yield
|
|
814
|
-
console.debug(
|
|
1040
|
+
yield entry.transceiver.sender.replaceTrack(null);
|
|
1041
|
+
console.debug(
|
|
1042
|
+
`[GPUMachineClient] Track "${name}" unpublished successfully`
|
|
1043
|
+
);
|
|
815
1044
|
} catch (error) {
|
|
816
|
-
console.error(
|
|
1045
|
+
console.error(
|
|
1046
|
+
`[GPUMachineClient] Failed to unpublish track "${name}":`,
|
|
1047
|
+
error
|
|
1048
|
+
);
|
|
817
1049
|
throw error;
|
|
818
1050
|
} finally {
|
|
819
|
-
this.
|
|
1051
|
+
this.publishedTracks.delete(name);
|
|
820
1052
|
}
|
|
821
1053
|
});
|
|
822
1054
|
}
|
|
823
1055
|
/**
|
|
824
|
-
* Returns the currently published track.
|
|
1056
|
+
* Returns the currently published track for the given name.
|
|
825
1057
|
*/
|
|
826
|
-
getPublishedTrack() {
|
|
827
|
-
return this.
|
|
1058
|
+
getPublishedTrack(name) {
|
|
1059
|
+
return this.publishedTracks.get(name);
|
|
828
1060
|
}
|
|
829
1061
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
830
1062
|
// Getters
|
|
@@ -895,6 +1127,12 @@ var GPUMachineClient = class {
|
|
|
895
1127
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
896
1128
|
// Private Helpers
|
|
897
1129
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1130
|
+
checkFullyConnected() {
|
|
1131
|
+
if (this.peerConnected && this.dataChannelOpen) {
|
|
1132
|
+
this.setStatus("connected");
|
|
1133
|
+
this.startStatsPolling();
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
898
1136
|
setStatus(newStatus) {
|
|
899
1137
|
if (this.status !== newStatus) {
|
|
900
1138
|
this.status = newStatus;
|
|
@@ -910,24 +1148,36 @@ var GPUMachineClient = class {
|
|
|
910
1148
|
if (state) {
|
|
911
1149
|
switch (state) {
|
|
912
1150
|
case "connected":
|
|
913
|
-
this.
|
|
914
|
-
this.
|
|
1151
|
+
this.peerConnected = true;
|
|
1152
|
+
this.checkFullyConnected();
|
|
915
1153
|
break;
|
|
916
1154
|
case "disconnected":
|
|
917
1155
|
case "closed":
|
|
1156
|
+
this.peerConnected = false;
|
|
918
1157
|
this.setStatus("disconnected");
|
|
919
1158
|
break;
|
|
920
1159
|
case "failed":
|
|
1160
|
+
this.peerConnected = false;
|
|
921
1161
|
this.setStatus("error");
|
|
922
1162
|
break;
|
|
923
1163
|
}
|
|
924
1164
|
}
|
|
925
1165
|
};
|
|
926
1166
|
this.peerConnection.ontrack = (event) => {
|
|
927
|
-
var _a;
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1167
|
+
var _a, _b;
|
|
1168
|
+
let trackName;
|
|
1169
|
+
for (const [name, entry] of this.transceiverMap) {
|
|
1170
|
+
if (entry.transceiver === event.transceiver) {
|
|
1171
|
+
trackName = name;
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
|
|
1176
|
+
console.debug(
|
|
1177
|
+
`[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
|
|
1178
|
+
);
|
|
1179
|
+
const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
|
|
1180
|
+
this.emit("trackReceived", trackName, event.track, stream);
|
|
931
1181
|
};
|
|
932
1182
|
this.peerConnection.onicecandidate = (event) => {
|
|
933
1183
|
if (event.candidate) {
|
|
@@ -947,10 +1197,13 @@ var GPUMachineClient = class {
|
|
|
947
1197
|
if (!this.dataChannel) return;
|
|
948
1198
|
this.dataChannel.onopen = () => {
|
|
949
1199
|
console.debug("[GPUMachineClient] Data channel open");
|
|
1200
|
+
this.dataChannelOpen = true;
|
|
950
1201
|
this.startPing();
|
|
1202
|
+
this.checkFullyConnected();
|
|
951
1203
|
};
|
|
952
1204
|
this.dataChannel.onclose = () => {
|
|
953
1205
|
console.debug("[GPUMachineClient] Data channel closed");
|
|
1206
|
+
this.dataChannelOpen = false;
|
|
954
1207
|
this.stopPing();
|
|
955
1208
|
};
|
|
956
1209
|
this.dataChannel.onerror = (error) => {
|
|
@@ -984,10 +1237,29 @@ var GPUMachineClient = class {
|
|
|
984
1237
|
import { z as z2 } from "zod";
|
|
985
1238
|
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
986
1239
|
var PROD_COORDINATOR_URL = "https://api.reactor.inc";
|
|
1240
|
+
var TrackConfigSchema = z2.object({
|
|
1241
|
+
name: z2.string(),
|
|
1242
|
+
kind: z2.enum(["audio", "video"])
|
|
1243
|
+
});
|
|
987
1244
|
var OptionsSchema = z2.object({
|
|
988
1245
|
coordinatorUrl: z2.string().default(PROD_COORDINATOR_URL),
|
|
989
1246
|
modelName: z2.string(),
|
|
990
|
-
local: z2.boolean().default(false)
|
|
1247
|
+
local: z2.boolean().default(false),
|
|
1248
|
+
/**
|
|
1249
|
+
* Tracks the client **RECEIVES** from the model (model → client).
|
|
1250
|
+
* Each entry produces a `recvonly` transceiver.
|
|
1251
|
+
* Names must be unique across both `receive` and `send`.
|
|
1252
|
+
*
|
|
1253
|
+
* When omitted, defaults to a single video track named `"main_video"`.
|
|
1254
|
+
* Pass an explicit empty array to opt out of the default.
|
|
1255
|
+
*/
|
|
1256
|
+
receive: z2.array(TrackConfigSchema).default([{ name: "main_video", kind: "video" }]),
|
|
1257
|
+
/**
|
|
1258
|
+
* Tracks the client **SENDS** to the model (client → model).
|
|
1259
|
+
* Each entry produces a `sendonly` transceiver.
|
|
1260
|
+
* Names must be unique across both `receive` and `send`.
|
|
1261
|
+
*/
|
|
1262
|
+
send: z2.array(TrackConfigSchema).default([])
|
|
991
1263
|
});
|
|
992
1264
|
var Reactor = class {
|
|
993
1265
|
constructor(options) {
|
|
@@ -998,7 +1270,9 @@ var Reactor = class {
|
|
|
998
1270
|
this.coordinatorUrl = validatedOptions.coordinatorUrl;
|
|
999
1271
|
this.model = validatedOptions.modelName;
|
|
1000
1272
|
this.local = validatedOptions.local;
|
|
1001
|
-
|
|
1273
|
+
this.receive = validatedOptions.receive;
|
|
1274
|
+
this.send = validatedOptions.send;
|
|
1275
|
+
if (this.local && options.coordinatorUrl === void 0) {
|
|
1002
1276
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
1003
1277
|
}
|
|
1004
1278
|
}
|
|
@@ -1018,13 +1292,11 @@ var Reactor = class {
|
|
|
1018
1292
|
(_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
|
|
1019
1293
|
}
|
|
1020
1294
|
/**
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
* @param command The command name
|
|
1295
|
+
* Sends a command to the model via the data channel.
|
|
1296
|
+
*
|
|
1297
|
+
* @param command The command name.
|
|
1024
1298
|
* @param data The command payload.
|
|
1025
|
-
* @param scope
|
|
1026
|
-
* "runtime" for platform-level messages (e.g. requestCapabilities).
|
|
1027
|
-
* @throws Error if not in ready state
|
|
1299
|
+
* @param scope "application" (default) for model commands, "runtime" for platform messages.
|
|
1028
1300
|
*/
|
|
1029
1301
|
sendCommand(command, data, scope = "application") {
|
|
1030
1302
|
return __async(this, null, function* () {
|
|
@@ -1048,24 +1320,27 @@ var Reactor = class {
|
|
|
1048
1320
|
});
|
|
1049
1321
|
}
|
|
1050
1322
|
/**
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1323
|
+
* Publishes a MediaStreamTrack to a named send track.
|
|
1324
|
+
*
|
|
1325
|
+
* @param name The declared send track name (e.g. "webcam").
|
|
1326
|
+
* @param track The MediaStreamTrack to publish.
|
|
1053
1327
|
*/
|
|
1054
|
-
publishTrack(track) {
|
|
1328
|
+
publishTrack(name, track) {
|
|
1055
1329
|
return __async(this, null, function* () {
|
|
1056
1330
|
var _a;
|
|
1057
1331
|
if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
|
|
1058
|
-
|
|
1059
|
-
|
|
1332
|
+
console.warn(
|
|
1333
|
+
`[Reactor] Cannot publish track "${name}", status is ${this.status}`
|
|
1334
|
+
);
|
|
1060
1335
|
return;
|
|
1061
1336
|
}
|
|
1062
1337
|
try {
|
|
1063
|
-
yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
|
|
1338
|
+
yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(name, track);
|
|
1064
1339
|
} catch (error) {
|
|
1065
|
-
console.error(
|
|
1340
|
+
console.error(`[Reactor] Failed to publish track "${name}":`, error);
|
|
1066
1341
|
this.createError(
|
|
1067
1342
|
"TRACK_PUBLISH_FAILED",
|
|
1068
|
-
`Failed to publish track: ${error}`,
|
|
1343
|
+
`Failed to publish track "${name}": ${error}`,
|
|
1069
1344
|
"gpu",
|
|
1070
1345
|
true
|
|
1071
1346
|
);
|
|
@@ -1073,18 +1348,20 @@ var Reactor = class {
|
|
|
1073
1348
|
});
|
|
1074
1349
|
}
|
|
1075
1350
|
/**
|
|
1076
|
-
*
|
|
1351
|
+
* Unpublishes the track with the given name.
|
|
1352
|
+
*
|
|
1353
|
+
* @param name The declared send track name to unpublish.
|
|
1077
1354
|
*/
|
|
1078
|
-
unpublishTrack() {
|
|
1355
|
+
unpublishTrack(name) {
|
|
1079
1356
|
return __async(this, null, function* () {
|
|
1080
1357
|
var _a;
|
|
1081
1358
|
try {
|
|
1082
|
-
yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
|
|
1359
|
+
yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack(name);
|
|
1083
1360
|
} catch (error) {
|
|
1084
|
-
console.error(
|
|
1361
|
+
console.error(`[Reactor] Failed to unpublish track "${name}":`, error);
|
|
1085
1362
|
this.createError(
|
|
1086
1363
|
"TRACK_UNPUBLISH_FAILED",
|
|
1087
|
-
`Failed to unpublish track: ${error}`,
|
|
1364
|
+
`Failed to unpublish track "${name}": ${error}`,
|
|
1088
1365
|
"gpu",
|
|
1089
1366
|
true
|
|
1090
1367
|
);
|
|
@@ -1111,7 +1388,10 @@ var Reactor = class {
|
|
|
1111
1388
|
this.machineClient = new GPUMachineClient({ iceServers });
|
|
1112
1389
|
this.setupMachineClientHandlers();
|
|
1113
1390
|
}
|
|
1114
|
-
const sdpOffer = yield this.machineClient.createOffer(
|
|
1391
|
+
const sdpOffer = yield this.machineClient.createOffer({
|
|
1392
|
+
send: this.send,
|
|
1393
|
+
receive: this.receive
|
|
1394
|
+
});
|
|
1115
1395
|
try {
|
|
1116
1396
|
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
1117
1397
|
this.sessionId,
|
|
@@ -1119,8 +1399,8 @@ var Reactor = class {
|
|
|
1119
1399
|
options == null ? void 0 : options.maxAttempts
|
|
1120
1400
|
);
|
|
1121
1401
|
yield this.machineClient.connect(sdpAnswer);
|
|
1122
|
-
this.setStatus("ready");
|
|
1123
1402
|
} catch (error) {
|
|
1403
|
+
if (isAbortError(error)) return;
|
|
1124
1404
|
let recoverable = false;
|
|
1125
1405
|
if (error instanceof ConflictError) {
|
|
1126
1406
|
recoverable = true;
|
|
@@ -1166,7 +1446,10 @@ var Reactor = class {
|
|
|
1166
1446
|
const iceServers = yield this.coordinatorClient.getIceServers();
|
|
1167
1447
|
this.machineClient = new GPUMachineClient({ iceServers });
|
|
1168
1448
|
this.setupMachineClientHandlers();
|
|
1169
|
-
const sdpOffer = yield this.machineClient.createOffer(
|
|
1449
|
+
const sdpOffer = yield this.machineClient.createOffer({
|
|
1450
|
+
send: this.send,
|
|
1451
|
+
receive: this.receive
|
|
1452
|
+
});
|
|
1170
1453
|
const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
|
|
1171
1454
|
this.setSessionId(sessionId);
|
|
1172
1455
|
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
@@ -1176,6 +1459,7 @@ var Reactor = class {
|
|
|
1176
1459
|
);
|
|
1177
1460
|
yield this.machineClient.connect(sdpAnswer);
|
|
1178
1461
|
} catch (error) {
|
|
1462
|
+
if (isAbortError(error)) return;
|
|
1179
1463
|
console.error("[Reactor] Connection failed:", error);
|
|
1180
1464
|
this.createError(
|
|
1181
1465
|
"CONNECTION_FAILED",
|
|
@@ -1197,17 +1481,25 @@ var Reactor = class {
|
|
|
1197
1481
|
}
|
|
1198
1482
|
/**
|
|
1199
1483
|
* Sets up event handlers for the machine client.
|
|
1484
|
+
*
|
|
1485
|
+
* Each handler captures the client reference at registration time and
|
|
1486
|
+
* ignores events if this.machineClient has since changed (e.g. after
|
|
1487
|
+
* disconnect + reconnect), preventing stale WebRTC teardown events from
|
|
1488
|
+
* interfering with a new connection.
|
|
1200
1489
|
*/
|
|
1201
1490
|
setupMachineClientHandlers() {
|
|
1202
1491
|
if (!this.machineClient) return;
|
|
1203
|
-
this.machineClient
|
|
1492
|
+
const client = this.machineClient;
|
|
1493
|
+
client.on("message", (message, scope) => {
|
|
1494
|
+
if (this.machineClient !== client) return;
|
|
1204
1495
|
if (scope === "application") {
|
|
1205
1496
|
this.emit("message", message);
|
|
1206
1497
|
} else if (scope === "runtime") {
|
|
1207
1498
|
this.emit("runtimeMessage", message);
|
|
1208
1499
|
}
|
|
1209
1500
|
});
|
|
1210
|
-
|
|
1501
|
+
client.on("statusChanged", (status) => {
|
|
1502
|
+
if (this.machineClient !== client) return;
|
|
1211
1503
|
switch (status) {
|
|
1212
1504
|
case "connected":
|
|
1213
1505
|
this.setStatus("ready");
|
|
@@ -1226,13 +1518,15 @@ var Reactor = class {
|
|
|
1226
1518
|
break;
|
|
1227
1519
|
}
|
|
1228
1520
|
});
|
|
1229
|
-
|
|
1521
|
+
client.on(
|
|
1230
1522
|
"trackReceived",
|
|
1231
|
-
(track, stream) => {
|
|
1232
|
-
this.
|
|
1523
|
+
(name, track, stream) => {
|
|
1524
|
+
if (this.machineClient !== client) return;
|
|
1525
|
+
this.emit("trackReceived", name, track, stream);
|
|
1233
1526
|
}
|
|
1234
1527
|
);
|
|
1235
|
-
|
|
1528
|
+
client.on("statsUpdate", (stats) => {
|
|
1529
|
+
if (this.machineClient !== client) return;
|
|
1236
1530
|
this.emit("statsUpdate", stats);
|
|
1237
1531
|
});
|
|
1238
1532
|
}
|
|
@@ -1242,12 +1536,18 @@ var Reactor = class {
|
|
|
1242
1536
|
*/
|
|
1243
1537
|
disconnect(recoverable = false) {
|
|
1244
1538
|
return __async(this, null, function* () {
|
|
1539
|
+
var _a;
|
|
1245
1540
|
if (this.status === "disconnected" && !this.sessionId) {
|
|
1246
1541
|
console.warn("[Reactor] Already disconnected");
|
|
1247
1542
|
return;
|
|
1248
1543
|
}
|
|
1544
|
+
(_a = this.coordinatorClient) == null ? void 0 : _a.abort();
|
|
1249
1545
|
if (this.coordinatorClient && !recoverable) {
|
|
1250
|
-
|
|
1546
|
+
try {
|
|
1547
|
+
yield this.coordinatorClient.terminateSession();
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
console.error("[Reactor] Error terminating session:", error);
|
|
1550
|
+
}
|
|
1251
1551
|
this.coordinatorClient = void 0;
|
|
1252
1552
|
}
|
|
1253
1553
|
if (this.machineClient) {
|
|
@@ -1352,10 +1652,9 @@ var ReactorContext = createContext(
|
|
|
1352
1652
|
);
|
|
1353
1653
|
var defaultInitState = {
|
|
1354
1654
|
status: "disconnected",
|
|
1355
|
-
|
|
1655
|
+
tracks: {},
|
|
1356
1656
|
lastError: void 0,
|
|
1357
1657
|
sessionExpiration: void 0,
|
|
1358
|
-
insecureApiKey: void 0,
|
|
1359
1658
|
jwtToken: void 0,
|
|
1360
1659
|
sessionId: void 0
|
|
1361
1660
|
};
|
|
@@ -1376,7 +1675,11 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1376
1675
|
oldStatus: get().status,
|
|
1377
1676
|
newStatus
|
|
1378
1677
|
});
|
|
1379
|
-
|
|
1678
|
+
if (newStatus === "disconnected") {
|
|
1679
|
+
set({ status: newStatus, tracks: {} });
|
|
1680
|
+
} else {
|
|
1681
|
+
set({ status: newStatus });
|
|
1682
|
+
}
|
|
1380
1683
|
});
|
|
1381
1684
|
reactor.on(
|
|
1382
1685
|
"sessionExpirationChanged",
|
|
@@ -1388,13 +1691,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1388
1691
|
set({ sessionExpiration: newSessionExpiration });
|
|
1389
1692
|
}
|
|
1390
1693
|
);
|
|
1391
|
-
reactor.on("
|
|
1392
|
-
console.debug("[ReactorStore]
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1694
|
+
reactor.on("trackReceived", (name, track) => {
|
|
1695
|
+
console.debug("[ReactorStore] Track received", {
|
|
1696
|
+
name,
|
|
1697
|
+
kind: track.kind,
|
|
1698
|
+
id: track.id
|
|
1396
1699
|
});
|
|
1397
|
-
set({
|
|
1700
|
+
set({ tracks: __spreadProps(__spreadValues({}, get().tracks), { [name]: track }) });
|
|
1398
1701
|
});
|
|
1399
1702
|
reactor.on("error", (error) => {
|
|
1400
1703
|
console.debug("[ReactorStore] Error occurred", error);
|
|
@@ -1458,27 +1761,31 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
|
1458
1761
|
throw error;
|
|
1459
1762
|
}
|
|
1460
1763
|
}),
|
|
1461
|
-
|
|
1462
|
-
console.debug(
|
|
1764
|
+
publish: (name, track) => __async(null, null, function* () {
|
|
1765
|
+
console.debug(`[ReactorStore] Publishing track "${name}"`);
|
|
1463
1766
|
try {
|
|
1464
|
-
yield get().internal.reactor.publishTrack(
|
|
1465
|
-
console.debug(
|
|
1767
|
+
yield get().internal.reactor.publishTrack(name, track);
|
|
1768
|
+
console.debug(
|
|
1769
|
+
`[ReactorStore] Track "${name}" published successfully`
|
|
1770
|
+
);
|
|
1466
1771
|
} catch (error) {
|
|
1467
1772
|
console.error(
|
|
1468
|
-
|
|
1773
|
+
`[ReactorStore] Failed to publish track "${name}":`,
|
|
1469
1774
|
error
|
|
1470
1775
|
);
|
|
1471
1776
|
throw error;
|
|
1472
1777
|
}
|
|
1473
1778
|
}),
|
|
1474
|
-
|
|
1475
|
-
console.debug(
|
|
1779
|
+
unpublish: (name) => __async(null, null, function* () {
|
|
1780
|
+
console.debug(`[ReactorStore] Unpublishing track "${name}"`);
|
|
1476
1781
|
try {
|
|
1477
|
-
yield get().internal.reactor.unpublishTrack();
|
|
1478
|
-
console.debug(
|
|
1782
|
+
yield get().internal.reactor.unpublishTrack(name);
|
|
1783
|
+
console.debug(
|
|
1784
|
+
`[ReactorStore] Track "${name}" unpublished successfully`
|
|
1785
|
+
);
|
|
1479
1786
|
} catch (error) {
|
|
1480
1787
|
console.error(
|
|
1481
|
-
|
|
1788
|
+
`[ReactorStore] Failed to unpublish track "${name}":`,
|
|
1482
1789
|
error
|
|
1483
1790
|
);
|
|
1484
1791
|
throw error;
|
|
@@ -1524,7 +1831,7 @@ function ReactorProvider(_a) {
|
|
|
1524
1831
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1525
1832
|
}
|
|
1526
1833
|
const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
|
|
1527
|
-
const { coordinatorUrl, modelName, local } = props;
|
|
1834
|
+
const { coordinatorUrl, modelName, local, receive, send } = props;
|
|
1528
1835
|
const maxAttempts = pollingOptions.maxAttempts;
|
|
1529
1836
|
useEffect(() => {
|
|
1530
1837
|
const handleBeforeUnload = () => {
|
|
@@ -1580,6 +1887,8 @@ function ReactorProvider(_a) {
|
|
|
1580
1887
|
coordinatorUrl,
|
|
1581
1888
|
modelName,
|
|
1582
1889
|
local,
|
|
1890
|
+
receive,
|
|
1891
|
+
send,
|
|
1583
1892
|
jwtToken
|
|
1584
1893
|
})
|
|
1585
1894
|
);
|
|
@@ -1606,7 +1915,16 @@ function ReactorProvider(_a) {
|
|
|
1606
1915
|
console.error("[ReactorProvider] Failed to disconnect:", error);
|
|
1607
1916
|
});
|
|
1608
1917
|
};
|
|
1609
|
-
}, [
|
|
1918
|
+
}, [
|
|
1919
|
+
coordinatorUrl,
|
|
1920
|
+
modelName,
|
|
1921
|
+
autoConnect,
|
|
1922
|
+
local,
|
|
1923
|
+
receive,
|
|
1924
|
+
send,
|
|
1925
|
+
jwtToken,
|
|
1926
|
+
maxAttempts
|
|
1927
|
+
]);
|
|
1610
1928
|
return /* @__PURE__ */ jsx(ReactorContext.Provider, { value: storeRef.current, children });
|
|
1611
1929
|
}
|
|
1612
1930
|
function useReactorStore(selector) {
|
|
@@ -1672,50 +1990,63 @@ function useStats() {
|
|
|
1672
1990
|
}
|
|
1673
1991
|
|
|
1674
1992
|
// src/react/ReactorView.tsx
|
|
1675
|
-
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
1993
|
+
import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "react";
|
|
1676
1994
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1677
1995
|
function ReactorView({
|
|
1996
|
+
track = "main_video",
|
|
1997
|
+
audioTrack,
|
|
1678
1998
|
width,
|
|
1679
1999
|
height,
|
|
1680
2000
|
className,
|
|
1681
2001
|
style,
|
|
1682
|
-
videoObjectFit = "contain"
|
|
2002
|
+
videoObjectFit = "contain",
|
|
2003
|
+
muted = true
|
|
1683
2004
|
}) {
|
|
1684
|
-
const {
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2005
|
+
const { videoMediaTrack, audioMediaTrack, status } = useReactor((state) => {
|
|
2006
|
+
var _a, _b;
|
|
2007
|
+
return {
|
|
2008
|
+
videoMediaTrack: (_a = state.tracks[track]) != null ? _a : null,
|
|
2009
|
+
audioMediaTrack: audioTrack ? (_b = state.tracks[audioTrack]) != null ? _b : null : null,
|
|
2010
|
+
status: state.status
|
|
2011
|
+
};
|
|
2012
|
+
});
|
|
1688
2013
|
const videoRef = useRef3(null);
|
|
2014
|
+
const mediaStream = useMemo(() => {
|
|
2015
|
+
const tracks = [];
|
|
2016
|
+
if (videoMediaTrack) tracks.push(videoMediaTrack);
|
|
2017
|
+
if (audioMediaTrack) tracks.push(audioMediaTrack);
|
|
2018
|
+
if (tracks.length === 0) return null;
|
|
2019
|
+
return new MediaStream(tracks);
|
|
2020
|
+
}, [videoMediaTrack, audioMediaTrack]);
|
|
1689
2021
|
useEffect3(() => {
|
|
1690
|
-
console.debug("[ReactorView]
|
|
2022
|
+
console.debug("[ReactorView] Media track effect triggered", {
|
|
2023
|
+
track,
|
|
1691
2024
|
hasVideoElement: !!videoRef.current,
|
|
1692
|
-
hasVideoTrack: !!
|
|
1693
|
-
|
|
2025
|
+
hasVideoTrack: !!videoMediaTrack,
|
|
2026
|
+
hasAudioTrack: !!audioMediaTrack
|
|
1694
2027
|
});
|
|
1695
|
-
if (videoRef.current &&
|
|
1696
|
-
console.debug("[ReactorView] Attaching
|
|
2028
|
+
if (videoRef.current && mediaStream) {
|
|
2029
|
+
console.debug("[ReactorView] Attaching media stream to element");
|
|
1697
2030
|
try {
|
|
1698
|
-
|
|
1699
|
-
videoRef.current.srcObject = stream;
|
|
2031
|
+
videoRef.current.srcObject = mediaStream;
|
|
1700
2032
|
videoRef.current.play().catch((e) => {
|
|
1701
2033
|
console.warn("[ReactorView] Auto-play failed:", e);
|
|
1702
2034
|
});
|
|
1703
|
-
console.debug("[ReactorView]
|
|
2035
|
+
console.debug("[ReactorView] Media stream attached successfully");
|
|
1704
2036
|
} catch (error) {
|
|
1705
|
-
console.error("[ReactorView] Failed to attach
|
|
2037
|
+
console.error("[ReactorView] Failed to attach media stream:", error);
|
|
1706
2038
|
}
|
|
1707
2039
|
return () => {
|
|
1708
|
-
console.debug("[ReactorView] Detaching
|
|
2040
|
+
console.debug("[ReactorView] Detaching media stream from element");
|
|
1709
2041
|
if (videoRef.current) {
|
|
1710
2042
|
videoRef.current.srcObject = null;
|
|
1711
|
-
console.debug("[ReactorView] Video track detached successfully");
|
|
1712
2043
|
}
|
|
1713
2044
|
};
|
|
1714
2045
|
} else {
|
|
1715
|
-
console.debug("[ReactorView] No
|
|
2046
|
+
console.debug("[ReactorView] No tracks or element to attach");
|
|
1716
2047
|
}
|
|
1717
|
-
}, [
|
|
1718
|
-
const showPlaceholder = !
|
|
2048
|
+
}, [mediaStream]);
|
|
2049
|
+
const showPlaceholder = !videoMediaTrack;
|
|
1719
2050
|
return /* @__PURE__ */ jsxs(
|
|
1720
2051
|
"div",
|
|
1721
2052
|
{
|
|
@@ -1735,7 +2066,7 @@ function ReactorView({
|
|
|
1735
2066
|
objectFit: videoObjectFit,
|
|
1736
2067
|
display: showPlaceholder ? "none" : "block"
|
|
1737
2068
|
},
|
|
1738
|
-
muted
|
|
2069
|
+
muted,
|
|
1739
2070
|
playsInline: true
|
|
1740
2071
|
}
|
|
1741
2072
|
),
|
|
@@ -2223,6 +2554,7 @@ function ReactorController({
|
|
|
2223
2554
|
import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
|
|
2224
2555
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2225
2556
|
function WebcamStream({
|
|
2557
|
+
track,
|
|
2226
2558
|
className,
|
|
2227
2559
|
style,
|
|
2228
2560
|
videoConstraints = {
|
|
@@ -2235,10 +2567,10 @@ function WebcamStream({
|
|
|
2235
2567
|
const [stream, setStream] = useState4(null);
|
|
2236
2568
|
const [isPublishing, setIsPublishing] = useState4(false);
|
|
2237
2569
|
const [permissionDenied, setPermissionDenied] = useState4(false);
|
|
2238
|
-
const { status,
|
|
2570
|
+
const { status, publish, unpublish, reactor } = useReactor((state) => ({
|
|
2239
2571
|
status: state.status,
|
|
2240
|
-
|
|
2241
|
-
|
|
2572
|
+
publish: state.publish,
|
|
2573
|
+
unpublish: state.unpublish,
|
|
2242
2574
|
reactor: state.internal.reactor
|
|
2243
2575
|
}));
|
|
2244
2576
|
const videoRef = useRef4(null);
|
|
@@ -2263,15 +2595,15 @@ function WebcamStream({
|
|
|
2263
2595
|
const stopWebcam = () => __async(null, null, function* () {
|
|
2264
2596
|
console.debug("[WebcamPublisher] Stopping webcam");
|
|
2265
2597
|
try {
|
|
2266
|
-
yield
|
|
2598
|
+
yield unpublish(track);
|
|
2267
2599
|
console.debug("[WebcamPublisher] Unpublished before stopping");
|
|
2268
2600
|
} catch (err) {
|
|
2269
2601
|
console.error("[WebcamPublisher] Error unpublishing before stop:", err);
|
|
2270
2602
|
}
|
|
2271
2603
|
setIsPublishing(false);
|
|
2272
|
-
stream == null ? void 0 : stream.getTracks().forEach((
|
|
2273
|
-
|
|
2274
|
-
console.debug("[WebcamPublisher] Stopped track:",
|
|
2604
|
+
stream == null ? void 0 : stream.getTracks().forEach((t) => {
|
|
2605
|
+
t.stop();
|
|
2606
|
+
console.debug("[WebcamPublisher] Stopped track:", t.kind);
|
|
2275
2607
|
});
|
|
2276
2608
|
setStream(null);
|
|
2277
2609
|
console.debug("[WebcamPublisher] Webcam stopped");
|
|
@@ -2301,28 +2633,31 @@ function WebcamStream({
|
|
|
2301
2633
|
console.debug(
|
|
2302
2634
|
"[WebcamPublisher] Reactor ready, auto-publishing webcam stream"
|
|
2303
2635
|
);
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2636
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
2637
|
+
if (videoTrack) {
|
|
2638
|
+
publish(track, videoTrack).then(() => {
|
|
2639
|
+
console.debug("[WebcamPublisher] Auto-publish successful");
|
|
2640
|
+
setIsPublishing(true);
|
|
2641
|
+
}).catch((err) => {
|
|
2642
|
+
console.error("[WebcamPublisher] Auto-publish failed:", err);
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2310
2645
|
} else if (status !== "ready" && isPublishing) {
|
|
2311
2646
|
console.debug("[WebcamPublisher] Reactor not ready, auto-unpublishing");
|
|
2312
|
-
|
|
2647
|
+
unpublish(track).then(() => {
|
|
2313
2648
|
console.debug("[WebcamPublisher] Auto-unpublish successful");
|
|
2314
2649
|
setIsPublishing(false);
|
|
2315
2650
|
}).catch((err) => {
|
|
2316
2651
|
console.error("[WebcamPublisher] Auto-unpublish failed:", err);
|
|
2317
2652
|
});
|
|
2318
2653
|
}
|
|
2319
|
-
}, [status, stream, isPublishing,
|
|
2654
|
+
}, [status, stream, isPublishing, publish, unpublish, track]);
|
|
2320
2655
|
useEffect4(() => {
|
|
2321
2656
|
const handleError = (error) => {
|
|
2322
2657
|
console.debug("[WebcamPublisher] Received error event:", error);
|
|
2323
|
-
if (error.code === "
|
|
2658
|
+
if (error.code === "TRACK_PUBLISH_FAILED") {
|
|
2324
2659
|
console.debug(
|
|
2325
|
-
"[WebcamPublisher]
|
|
2660
|
+
"[WebcamPublisher] Track publish failed, resetting isPublishing state"
|
|
2326
2661
|
);
|
|
2327
2662
|
setIsPublishing(false);
|
|
2328
2663
|
}
|
|
@@ -2430,6 +2765,7 @@ function fetchInsecureJwtToken(_0) {
|
|
|
2430
2765
|
});
|
|
2431
2766
|
}
|
|
2432
2767
|
export {
|
|
2768
|
+
AbortError,
|
|
2433
2769
|
ConflictError,
|
|
2434
2770
|
PROD_COORDINATOR_URL,
|
|
2435
2771
|
Reactor,
|
|
@@ -2437,11 +2773,14 @@ export {
|
|
|
2437
2773
|
ReactorProvider,
|
|
2438
2774
|
ReactorView,
|
|
2439
2775
|
WebcamStream,
|
|
2776
|
+
audio,
|
|
2440
2777
|
fetchInsecureJwtToken,
|
|
2778
|
+
isAbortError,
|
|
2441
2779
|
useReactor,
|
|
2442
2780
|
useReactorInternalMessage,
|
|
2443
2781
|
useReactorMessage,
|
|
2444
2782
|
useReactorStore,
|
|
2445
|
-
useStats
|
|
2783
|
+
useStats,
|
|
2784
|
+
video
|
|
2446
2785
|
};
|
|
2447
2786
|
//# sourceMappingURL=index.mjs.map
|