@pollen-robotics/reachy-mini-sdk 1.8.0 → 1.8.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/APP_CREATION_GUIDE.md +1795 -0
- package/README.md +2 -5
- package/dist/animation/distance.d.ts +87 -0
- package/dist/animation/distance.d.ts.map +1 -0
- package/dist/animation/distance.js +140 -0
- package/dist/animation/distance.js.map +1 -0
- package/dist/animation/index.d.ts +34 -0
- package/dist/animation/index.d.ts.map +1 -0
- package/dist/animation/index.js +33 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/pose.d.ts +40 -0
- package/dist/animation/pose.d.ts.map +1 -0
- package/dist/animation/pose.js +15 -0
- package/dist/animation/pose.js.map +1 -0
- package/dist/animation/presets.d.ts +85 -0
- package/dist/animation/presets.d.ts.map +1 -0
- package/dist/animation/presets.js +82 -0
- package/dist/animation/presets.js.map +1 -0
- package/dist/animation/safe-return.d.ts +90 -0
- package/dist/animation/safe-return.d.ts.map +1 -0
- package/dist/animation/safe-return.js +123 -0
- package/dist/animation/safe-return.js.map +1 -0
- package/dist/lib/reachy-mini.d.ts +99 -0
- package/dist/lib/reachy-mini.d.ts.map +1 -1
- package/dist/lib/reachy-mini.js +314 -9
- package/dist/lib/reachy-mini.js.map +1 -1
- package/dist/lib/types.d.ts +27 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/host/README.md +14 -12
- package/host/dist/ReachyHost.d.ts +2 -1
- package/host/dist/ReachyHost.d.ts.map +1 -1
- package/host/dist/chunks/index-lMs2sCXG.js +439 -0
- package/host/dist/chunks/{mountHost-D7cgkU9Q.js → mountHost-DjE5zE9R.js} +11142 -11014
- package/host/dist/chunks/{reachy-mini-B1hlBmDQ.js → reachy-mini-GHNS4GSp.js} +494 -307
- package/host/dist/components/PickerView.d.ts.map +1 -1
- package/host/dist/components/PostOAuthSplash.d.ts +25 -0
- package/host/dist/components/PostOAuthSplash.d.ts.map +1 -0
- package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
- package/host/dist/components/TopBar.d.ts +8 -1
- package/host/dist/components/TopBar.d.ts.map +1 -1
- package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
- package/host/dist/embed/index.d.ts +3 -2
- package/host/dist/embed/index.d.ts.map +1 -1
- package/host/dist/entry/auto.js +2 -2
- package/host/dist/entry/embed.js +2 -2
- package/host/dist/hooks/useHostBridge.d.ts +6 -0
- package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
- package/host/dist/hooks/useSdk.d.ts.map +1 -1
- package/host/dist/index.js +3 -3
- package/host/dist/lib/protocol.d.ts +17 -3
- package/host/dist/lib/protocol.d.ts.map +1 -1
- package/host/dist/lib/settings.d.ts +3 -2
- package/host/dist/lib/settings.d.ts.map +1 -1
- package/host/dist/mountHost.d.ts.map +1 -1
- package/host/dist/ui/design/IdentityChipBar.d.ts +46 -0
- package/host/dist/ui/design/IdentityChipBar.d.ts.map +1 -0
- package/host/dist/ui/design/LinkQualityBars.d.ts +32 -0
- package/host/dist/ui/design/LinkQualityBars.d.ts.map +1 -0
- package/host/dist/ui/design/MetaPill.d.ts +41 -0
- package/host/dist/ui/design/MetaPill.d.ts.map +1 -0
- package/host/dist/ui/design/TransportChip.d.ts +39 -0
- package/host/dist/ui/design/TransportChip.d.ts.map +1 -0
- package/package.json +7 -4
- package/CHANGELOG.md +0 -188
- package/host/APP_AUTHOR_GUIDE.md +0 -646
- package/host/SPEC.md +0 -618
- package/host/dist/chunks/index-CyLPysJS.js +0 -400
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
var
|
|
2
|
-
async function
|
|
3
|
-
var t,
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
if ((
|
|
8
|
-
const
|
|
9
|
-
|
|
1
|
+
var $ = Object.defineProperty, O = (r, e, t) => e in r ? $(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t, C = (r, e, t) => (O(r, typeof e != "symbol" ? e + "" : e, t), t), D = "https://huggingface.co";
|
|
2
|
+
async function A(r, e) {
|
|
3
|
+
var t, i;
|
|
4
|
+
const s = new x(r.url, r.status, (t = r.headers.get("X-Request-Id")) != null ? t : void 0);
|
|
5
|
+
s.message = `Api error with status ${s.statusCode}`;
|
|
6
|
+
const o = [`URL: ${s.url}`, s.requestId ? `Request ID: ${s.requestId}` : void 0].filter(Boolean).join(". ");
|
|
7
|
+
if ((i = r.headers.get("Content-Type")) != null && i.startsWith("application/json")) {
|
|
8
|
+
const n = await r.json();
|
|
9
|
+
s.message = n.error || n.message || s.message, s.data = n;
|
|
10
10
|
} else
|
|
11
|
-
|
|
12
|
-
throw
|
|
11
|
+
s.data = { message: await r.text() };
|
|
12
|
+
throw s.message += `. ${o}`, s;
|
|
13
13
|
}
|
|
14
|
-
var
|
|
15
|
-
constructor(
|
|
16
|
-
super(
|
|
14
|
+
var x = class extends Error {
|
|
15
|
+
constructor(r, e, t, i) {
|
|
16
|
+
super(i), C(this, "statusCode"), C(this, "url"), C(this, "requestId"), C(this, "data"), this.statusCode = e, this.requestId = t, this.url = r;
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
-
new Promise((
|
|
19
|
+
new Promise((r) => {
|
|
20
20
|
});
|
|
21
|
-
function
|
|
21
|
+
function N(r) {
|
|
22
22
|
if (globalThis.Buffer)
|
|
23
|
-
return globalThis.Buffer.from(
|
|
23
|
+
return globalThis.Buffer.from(r).toString("base64");
|
|
24
24
|
{
|
|
25
25
|
const e = [];
|
|
26
|
-
return
|
|
26
|
+
return r.forEach((t) => {
|
|
27
27
|
e.push(String.fromCharCode(t));
|
|
28
28
|
}), globalThis.btoa(e.join(""));
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
async function M(
|
|
31
|
+
async function M(r) {
|
|
32
32
|
var e, t;
|
|
33
33
|
if (typeof window > "u")
|
|
34
34
|
throw new Error("oauthHandleRedirect is only available in the browser");
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
throw new Error(`${
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
35
|
+
const i = new URLSearchParams(window.location.search), [s, o] = [i.get("error"), i.get("error_description")];
|
|
36
|
+
if (s)
|
|
37
|
+
throw new Error(`${s}: ${o}`);
|
|
38
|
+
const n = i.get("code"), a = localStorage.getItem("huggingface.co:oauth:nonce");
|
|
39
|
+
if (!n)
|
|
40
40
|
throw new Error("Missing oauth code from query parameters in redirected URL");
|
|
41
41
|
if (!a)
|
|
42
42
|
throw new Error("Missing oauth nonce from localStorage");
|
|
43
43
|
const l = localStorage.getItem("huggingface.co:oauth:code_verifier");
|
|
44
44
|
if (!l)
|
|
45
45
|
throw new Error("Missing oauth code_verifier from localStorage");
|
|
46
|
-
const c =
|
|
46
|
+
const c = i.get("state");
|
|
47
47
|
if (!c)
|
|
48
48
|
throw new Error("Missing oauth state from query parameters in redirected URL");
|
|
49
49
|
let h;
|
|
@@ -54,34 +54,34 @@ async function M(n) {
|
|
|
54
54
|
}
|
|
55
55
|
if (h.nonce !== a)
|
|
56
56
|
throw new Error("Invalid oauth state in redirected URL");
|
|
57
|
-
const d =
|
|
57
|
+
const d = D, p = `${new URL(d).origin}/.well-known/openid-configuration`, f = await fetch(p, {
|
|
58
58
|
headers: {
|
|
59
59
|
Accept: "application/json"
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
if (!f.ok)
|
|
63
|
-
throw await
|
|
64
|
-
const
|
|
63
|
+
throw await A(f);
|
|
64
|
+
const m = await f.json(), v = await fetch(m.token_endpoint, {
|
|
65
65
|
method: "POST",
|
|
66
66
|
headers: {
|
|
67
67
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
68
68
|
},
|
|
69
69
|
body: new URLSearchParams({
|
|
70
70
|
grant_type: "authorization_code",
|
|
71
|
-
code:
|
|
71
|
+
code: n,
|
|
72
72
|
redirect_uri: h.redirectUri,
|
|
73
73
|
code_verifier: l
|
|
74
74
|
}).toString()
|
|
75
75
|
});
|
|
76
76
|
if (localStorage.removeItem("huggingface.co:oauth:code_verifier"), localStorage.removeItem("huggingface.co:oauth:nonce"), !v.ok)
|
|
77
|
-
throw await
|
|
78
|
-
const w = await v.json(), g = new Date(Date.now() + w.expires_in * 1e3), S = await fetch(
|
|
77
|
+
throw await A(v);
|
|
78
|
+
const w = await v.json(), g = new Date(Date.now() + w.expires_in * 1e3), S = await fetch(m.userinfo_endpoint, {
|
|
79
79
|
headers: {
|
|
80
80
|
Authorization: `Bearer ${w.access_token}`
|
|
81
81
|
}
|
|
82
82
|
});
|
|
83
83
|
if (!S.ok)
|
|
84
|
-
throw await
|
|
84
|
+
throw await A(S);
|
|
85
85
|
const u = await S.json();
|
|
86
86
|
return {
|
|
87
87
|
accessToken: w.access_token,
|
|
@@ -109,7 +109,7 @@ async function M(n) {
|
|
|
109
109
|
scope: w.scope
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
-
async function
|
|
112
|
+
async function H(r) {
|
|
113
113
|
if (typeof window > "u")
|
|
114
114
|
throw new Error("oauthHandleRedirect is only available in the browser");
|
|
115
115
|
const e = new URLSearchParams(window.location.search);
|
|
@@ -117,32 +117,32 @@ async function O(n) {
|
|
|
117
117
|
"Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."
|
|
118
118
|
), !1) : !1;
|
|
119
119
|
}
|
|
120
|
-
async function
|
|
120
|
+
async function V(r) {
|
|
121
121
|
var e, t;
|
|
122
122
|
if (typeof window > "u")
|
|
123
123
|
throw new Error("oauthLogin is only available in the browser");
|
|
124
|
-
const
|
|
124
|
+
const i = r?.hubUrl || D, s = `${new URL(i).origin}/.well-known/openid-configuration`, o = await fetch(s, {
|
|
125
125
|
headers: {
|
|
126
126
|
Accept: "application/json"
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
|
-
if (!
|
|
130
|
-
throw await
|
|
131
|
-
const
|
|
129
|
+
if (!o.ok)
|
|
130
|
+
throw await A(o);
|
|
131
|
+
const n = await o.json(), a = globalThis.crypto.randomUUID(), l = globalThis.crypto.randomUUID() + globalThis.crypto.randomUUID();
|
|
132
132
|
localStorage.setItem("huggingface.co:oauth:nonce", a), localStorage.setItem("huggingface.co:oauth:code_verifier", l);
|
|
133
|
-
const c =
|
|
133
|
+
const c = r?.redirectUrl || window.location.href, h = JSON.stringify({
|
|
134
134
|
nonce: a,
|
|
135
135
|
redirectUri: c,
|
|
136
|
-
state:
|
|
137
|
-
}), d = (t = (e = window?.huggingface) == null ? void 0 : e.variables) != null ? t : null,
|
|
138
|
-
if (!
|
|
136
|
+
state: r?.state
|
|
137
|
+
}), d = (t = (e = window?.huggingface) == null ? void 0 : e.variables) != null ? t : null, p = r?.clientId || d?.OAUTH_CLIENT_ID;
|
|
138
|
+
if (!p)
|
|
139
139
|
throw d ? new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space") : new Error("Missing clientId");
|
|
140
|
-
const f =
|
|
140
|
+
const f = N(
|
|
141
141
|
new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(l)))
|
|
142
142
|
).replace(/[+]/g, "-").replace(/[/]/g, "_").replace(/=/g, "");
|
|
143
|
-
return `${
|
|
144
|
-
client_id:
|
|
145
|
-
scope:
|
|
143
|
+
return `${n.authorization_endpoint}?${new URLSearchParams({
|
|
144
|
+
client_id: p,
|
|
145
|
+
scope: r?.scopes || d?.OAUTH_SCOPES || "openid profile",
|
|
146
146
|
response_type: "code",
|
|
147
147
|
redirect_uri: c,
|
|
148
148
|
state: h,
|
|
@@ -150,48 +150,48 @@ async function N(n) {
|
|
|
150
150
|
code_challenge_method: "S256"
|
|
151
151
|
}).toString()}`;
|
|
152
152
|
}
|
|
153
|
-
function R(
|
|
154
|
-
return
|
|
153
|
+
function R(r) {
|
|
154
|
+
return r * Math.PI / 180;
|
|
155
155
|
}
|
|
156
|
-
function F(
|
|
157
|
-
const
|
|
156
|
+
function F(r, e, t) {
|
|
157
|
+
const i = R(r), s = R(e), o = R(t), n = Math.cos(o), a = Math.sin(o), l = Math.cos(s), c = Math.sin(s), h = Math.cos(i), d = Math.sin(i);
|
|
158
158
|
return [
|
|
159
|
-
[
|
|
160
|
-
[a * l, a * c * d +
|
|
159
|
+
[n * l, n * c * d - a * h, n * c * h + a * d, 0],
|
|
160
|
+
[a * l, a * c * d + n * h, a * c * h - n * d, 0],
|
|
161
161
|
[-c, l * d, l * h, 0],
|
|
162
162
|
[0, 0, 0, 1]
|
|
163
163
|
];
|
|
164
164
|
}
|
|
165
165
|
function B() {
|
|
166
166
|
if (typeof window > "u" || !window.location.hash) return;
|
|
167
|
-
const
|
|
167
|
+
const r = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
168
168
|
let e;
|
|
169
169
|
try {
|
|
170
|
-
e = new URLSearchParams(
|
|
170
|
+
e = new URLSearchParams(r);
|
|
171
171
|
} catch {
|
|
172
172
|
return;
|
|
173
173
|
}
|
|
174
174
|
const t = e.get("hf_token");
|
|
175
175
|
if (!t) return;
|
|
176
|
-
const
|
|
176
|
+
const i = e.get("hf_username") || "user", s = e.get("hf_token_expires"), o = s && !Number.isNaN(new Date(s).getTime()) ? s : new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toISOString();
|
|
177
177
|
try {
|
|
178
|
-
sessionStorage.setItem("hf_token", t), sessionStorage.setItem("hf_username",
|
|
178
|
+
sessionStorage.setItem("hf_token", t), sessionStorage.setItem("hf_username", i), sessionStorage.setItem("hf_token_expires", o);
|
|
179
179
|
} catch (l) {
|
|
180
180
|
console.warn("[reachy-mini] could not persist pre-seeded HF credentials:", l);
|
|
181
181
|
}
|
|
182
182
|
e.delete("hf_token"), e.delete("hf_username"), e.delete("hf_token_expires");
|
|
183
|
-
const
|
|
183
|
+
const n = e.toString(), a = window.location.pathname + window.location.search + (n ? "#" + n : "");
|
|
184
184
|
try {
|
|
185
185
|
window.history.replaceState(null, "", a);
|
|
186
186
|
} catch {
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
function
|
|
189
|
+
function G() {
|
|
190
190
|
if (typeof window > "u") return null;
|
|
191
191
|
if (window.location.hash) {
|
|
192
|
-
const
|
|
192
|
+
const r = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
193
193
|
try {
|
|
194
|
-
const t = new URLSearchParams(
|
|
194
|
+
const t = new URLSearchParams(r).get("robot_peer_id");
|
|
195
195
|
if (t) return t;
|
|
196
196
|
} catch {
|
|
197
197
|
}
|
|
@@ -204,41 +204,42 @@ function V() {
|
|
|
204
204
|
}
|
|
205
205
|
return null;
|
|
206
206
|
}
|
|
207
|
-
function q(
|
|
208
|
-
const e =
|
|
207
|
+
function q(r) {
|
|
208
|
+
const e = r.split(`\r
|
|
209
209
|
`);
|
|
210
210
|
let t = !1;
|
|
211
|
-
for (const
|
|
212
|
-
if (
|
|
211
|
+
for (const i of e)
|
|
212
|
+
if (i.startsWith("m=audio") ? t = !0 : i.startsWith("m=") && (t = !1), t && i === "a=sendrecv") return !0;
|
|
213
213
|
return !1;
|
|
214
214
|
}
|
|
215
|
-
const y = 12 * 1024,
|
|
216
|
-
function
|
|
215
|
+
const y = 12 * 1024, E = 1 * 1024 * 1024, W = 512 * 1024;
|
|
216
|
+
function z() {
|
|
217
217
|
return typeof CompressionStream < "u";
|
|
218
218
|
}
|
|
219
219
|
function U() {
|
|
220
220
|
return "u" + Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
|
|
221
221
|
}
|
|
222
|
-
function
|
|
222
|
+
function k(r) {
|
|
223
223
|
let e = "";
|
|
224
|
-
for (let
|
|
224
|
+
for (let i = 0; i < r.length; i += 32768)
|
|
225
225
|
e += String.fromCharCode.apply(
|
|
226
226
|
null,
|
|
227
|
-
Array.from(
|
|
227
|
+
Array.from(r.subarray(i, i + 32768))
|
|
228
228
|
);
|
|
229
229
|
return btoa(e);
|
|
230
230
|
}
|
|
231
|
-
async function
|
|
232
|
-
const e = new TextEncoder().encode(
|
|
231
|
+
async function J(r) {
|
|
232
|
+
const e = new TextEncoder().encode(r), t = await new Response(
|
|
233
233
|
new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))
|
|
234
234
|
).arrayBuffer();
|
|
235
|
-
return
|
|
235
|
+
return k(new Uint8Array(t));
|
|
236
236
|
}
|
|
237
|
-
function
|
|
238
|
-
const e = Math.round(Number(
|
|
237
|
+
function L(r) {
|
|
238
|
+
const e = Math.round(Number(r) || 0);
|
|
239
239
|
return Math.max(0, Math.min(100, e));
|
|
240
240
|
}
|
|
241
|
-
|
|
241
|
+
const j = 3e3, P = 1e3, X = 6e4;
|
|
242
|
+
class Y extends EventTarget {
|
|
242
243
|
// ─── Config ──────────────────────────────────────────────────────────
|
|
243
244
|
_signalingUrl;
|
|
244
245
|
_clientId;
|
|
@@ -293,6 +294,19 @@ class J extends EventTarget {
|
|
|
293
294
|
_sessionReject = null;
|
|
294
295
|
_iceConnected = !1;
|
|
295
296
|
_dcOpen = !1;
|
|
297
|
+
// ─── Resilience: ICE-blip debounce + network awareness ──────────────
|
|
298
|
+
// Backs `_scheduleIceGrace` / `_armIceGraceOnVisibility` and the
|
|
299
|
+
// `networkOnline` / `networkOffline` / `networkChange` forwarders.
|
|
300
|
+
// All three handler slots are scoped to the lifetime of a live
|
|
301
|
+
// session (installed in `startSession`, cleared in
|
|
302
|
+
// `stopSession` / `disconnect` / `_handleEndSession` /
|
|
303
|
+
// `_failSessionRejected`).
|
|
304
|
+
_iceGraceTimer = null;
|
|
305
|
+
_iceGraceReason = null;
|
|
306
|
+
_pendingVisibilityHandler = null;
|
|
307
|
+
_onlineHandler = null;
|
|
308
|
+
_offlineHandler = null;
|
|
309
|
+
_connectionChangeHandler = null;
|
|
296
310
|
// ─── Motion completion plumbing (wake_up / goto_sleep) ───────────────
|
|
297
311
|
_pendingMotionCompletions = {
|
|
298
312
|
wake_up: [],
|
|
@@ -301,7 +315,7 @@ class J extends EventTarget {
|
|
|
301
315
|
// ─── Video element ───────────────────────────────────────────────────
|
|
302
316
|
_videoElement = null;
|
|
303
317
|
constructor(e = {}) {
|
|
304
|
-
super(), this._signalingUrl = e.signalingUrl || "https://pollen-robotics-reachy-mini-central.hf.space", this._clientId = e.clientId || null, this._appName = e.appName || "unknown", this._videoJitterBufferTargetMs = e.videoJitterBufferTargetMs ?? 0, this._autoStartFromUrl = e.autoStartFromUrl === !0, this._autoStartAttempted = !1, this._preselectedRobotId =
|
|
318
|
+
super(), this._signalingUrl = e.signalingUrl || "https://pollen-robotics-reachy-mini-central.hf.space", this._clientId = e.clientId || null, this._appName = e.appName || "unknown", this._videoJitterBufferTargetMs = e.videoJitterBufferTargetMs ?? 0, this._autoStartFromUrl = e.autoStartFromUrl === !0, this._autoStartAttempted = !1, this._preselectedRobotId = G();
|
|
305
319
|
}
|
|
306
320
|
// ─── Read-only properties ────────────────────────────────────────────
|
|
307
321
|
get state() {
|
|
@@ -341,12 +355,12 @@ class J extends EventTarget {
|
|
|
341
355
|
* SDK is already `connected` still triggers the auto-start.
|
|
342
356
|
*/
|
|
343
357
|
_maybeAutoStart() {
|
|
344
|
-
if (!this._autoStartFromUrl || this._autoStartAttempted || !this._preselectedRobotId || this._state !== "connected" || !this._robots.find((
|
|
358
|
+
if (!this._autoStartFromUrl || this._autoStartAttempted || !this._preselectedRobotId || this._state !== "connected" || !this._robots.find((i) => i.id === this._preselectedRobotId)) return;
|
|
345
359
|
this._autoStartAttempted = !0;
|
|
346
360
|
const t = this._preselectedRobotId;
|
|
347
361
|
setTimeout(() => {
|
|
348
|
-
this._state === "connected" && this.startSession(t).catch((
|
|
349
|
-
console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:",
|
|
362
|
+
this._state === "connected" && this.startSession(t).catch((i) => {
|
|
363
|
+
console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:", i);
|
|
350
364
|
});
|
|
351
365
|
}, 0);
|
|
352
366
|
}
|
|
@@ -354,21 +368,21 @@ class J extends EventTarget {
|
|
|
354
368
|
async authenticate() {
|
|
355
369
|
try {
|
|
356
370
|
B();
|
|
357
|
-
const e = await
|
|
371
|
+
const e = await H();
|
|
358
372
|
if (e)
|
|
359
373
|
return this._username = e.userInfo.preferred_username || e.userInfo.name || null, this._token = e.accessToken, this._tokenExpires = e.accessTokenExpiresAt, sessionStorage.setItem("hf_token", this._token), sessionStorage.setItem("hf_username", this._username ?? ""), sessionStorage.setItem(
|
|
360
374
|
"hf_token_expires",
|
|
361
375
|
typeof this._tokenExpires == "string" ? this._tokenExpires : this._tokenExpires.toISOString()
|
|
362
376
|
), !0;
|
|
363
|
-
const t = sessionStorage.getItem("hf_token"),
|
|
364
|
-
return t &&
|
|
377
|
+
const t = sessionStorage.getItem("hf_token"), i = sessionStorage.getItem("hf_username"), s = sessionStorage.getItem("hf_token_expires");
|
|
378
|
+
return t && i && s && new Date(s) > /* @__PURE__ */ new Date() ? (this._token = t, this._username = i, this._tokenExpires = s, !0) : !1;
|
|
365
379
|
} catch (e) {
|
|
366
380
|
return console.error("Auth error:", e), !1;
|
|
367
381
|
}
|
|
368
382
|
}
|
|
369
383
|
async login() {
|
|
370
384
|
const e = {};
|
|
371
|
-
this._clientId && (e.clientId = this._clientId), window.location.href = await
|
|
385
|
+
this._clientId && (e.clientId = this._clientId), window.location.href = await V(e);
|
|
372
386
|
}
|
|
373
387
|
logout() {
|
|
374
388
|
sessionStorage.removeItem("hf_token"), sessionStorage.removeItem("hf_username"), sessionStorage.removeItem("hf_token_expires"), this._username = null, this._tokenExpires = null, this.disconnect();
|
|
@@ -387,53 +401,53 @@ class J extends EventTarget {
|
|
|
387
401
|
headers: { Authorization: `Bearer ${this._token}` }
|
|
388
402
|
}
|
|
389
403
|
);
|
|
390
|
-
} catch (
|
|
391
|
-
throw this._sseAbortController = null,
|
|
404
|
+
} catch (i) {
|
|
405
|
+
throw this._sseAbortController = null, i;
|
|
392
406
|
}
|
|
393
407
|
if (!t.ok)
|
|
394
408
|
throw this._sseAbortController = null, new Error(`HTTP ${t.status}`);
|
|
395
|
-
return new Promise((
|
|
396
|
-
let
|
|
397
|
-
const
|
|
409
|
+
return new Promise((i, s) => {
|
|
410
|
+
let o = !1;
|
|
411
|
+
const n = t.body.getReader(), a = new TextDecoder();
|
|
398
412
|
let l = "";
|
|
399
413
|
(async () => {
|
|
400
414
|
try {
|
|
401
415
|
for (; ; ) {
|
|
402
|
-
const { done: h, value: d } = await
|
|
416
|
+
const { done: h, value: d } = await n.read();
|
|
403
417
|
if (h) break;
|
|
404
418
|
l += a.decode(d, { stream: !0 });
|
|
405
|
-
const
|
|
419
|
+
const p = l.split(`
|
|
406
420
|
`);
|
|
407
|
-
l =
|
|
408
|
-
for (const f of
|
|
421
|
+
l = p.pop() ?? "";
|
|
422
|
+
for (const f of p)
|
|
409
423
|
if (f.startsWith("data:"))
|
|
410
424
|
try {
|
|
411
|
-
const
|
|
412
|
-
!
|
|
425
|
+
const m = JSON.parse(f.slice(5).trim());
|
|
426
|
+
!o && m.type === "welcome" && m.peerId && (o = !0, this._state = "connected", await this._sendToServer({
|
|
413
427
|
type: "setPeerStatus",
|
|
414
428
|
roles: ["listener"],
|
|
415
429
|
meta: { name: this._appName }
|
|
416
|
-
}), this._emit("connected", { peerId:
|
|
430
|
+
}), this._emit("connected", { peerId: m.peerId }), i()), this._handleSignalingMessage(m);
|
|
417
431
|
} catch {
|
|
418
432
|
}
|
|
419
433
|
}
|
|
420
434
|
} catch (h) {
|
|
421
|
-
if (h.name !== "AbortError" && this._emit("error", { source: "signaling", error: h }), !
|
|
422
|
-
|
|
435
|
+
if (h.name !== "AbortError" && this._emit("error", { source: "signaling", error: h }), !o) {
|
|
436
|
+
s(h);
|
|
423
437
|
return;
|
|
424
438
|
}
|
|
425
439
|
}
|
|
426
|
-
this._state !== "disconnected" && (this._state = "disconnected", this._emit("disconnected", { reason: "SSE closed" })),
|
|
440
|
+
this._state !== "disconnected" && (this._state = "disconnected", this._emit("disconnected", { reason: "SSE closed" })), o || s(new Error("Connection closed before welcome"));
|
|
427
441
|
})();
|
|
428
442
|
});
|
|
429
443
|
}
|
|
430
444
|
async autoConnect(e = {}) {
|
|
431
445
|
const {
|
|
432
446
|
token: t,
|
|
433
|
-
pickRobot:
|
|
434
|
-
autoPickIfSingle:
|
|
435
|
-
filterBusy:
|
|
436
|
-
wakeOnConnect:
|
|
447
|
+
pickRobot: i,
|
|
448
|
+
autoPickIfSingle: s = !0,
|
|
449
|
+
filterBusy: o = !0,
|
|
450
|
+
wakeOnConnect: n = !0
|
|
437
451
|
} = e;
|
|
438
452
|
if (this._state === "streaming") {
|
|
439
453
|
const l = this._robots?.find((c) => c.id === this._selectedRobotId);
|
|
@@ -461,21 +475,21 @@ class J extends EventTarget {
|
|
|
461
475
|
}
|
|
462
476
|
c = this._robots?.find((d) => d.id === l)?.meta?.name ?? null;
|
|
463
477
|
} else {
|
|
464
|
-
const h = await this._fetchOwnedRobots({ filterBusy:
|
|
478
|
+
const h = await this._fetchOwnedRobots({ filterBusy: o });
|
|
465
479
|
if (h.length === 0)
|
|
466
480
|
throw new Error("No reachable robots");
|
|
467
|
-
if (
|
|
481
|
+
if (s && h.length === 1 && !h[0].busy)
|
|
468
482
|
l = h[0].id, c = h[0].name;
|
|
469
|
-
else if (
|
|
470
|
-
const d = await
|
|
483
|
+
else if (i) {
|
|
484
|
+
const d = await i(h);
|
|
471
485
|
if (!d) throw new Error("Robot selection cancelled");
|
|
472
|
-
l = d, c = h.find((
|
|
486
|
+
l = d, c = h.find((p) => p.id === d)?.name ?? null;
|
|
473
487
|
} else
|
|
474
488
|
throw new Error(
|
|
475
489
|
"Multiple robots available — pass a pickRobot callback to autoConnect()"
|
|
476
490
|
);
|
|
477
491
|
}
|
|
478
|
-
if (await this.startSession(l),
|
|
492
|
+
if (await this.startSession(l), n && typeof this.ensureAwake == "function")
|
|
479
493
|
try {
|
|
480
494
|
await this.ensureAwake();
|
|
481
495
|
} catch (h) {
|
|
@@ -492,52 +506,52 @@ class J extends EventTarget {
|
|
|
492
506
|
headers: { Authorization: `Bearer ${this._token}` }
|
|
493
507
|
});
|
|
494
508
|
if (!t.ok) throw new Error(`HTTP ${t.status}`);
|
|
495
|
-
const
|
|
496
|
-
for (const
|
|
497
|
-
if (e &&
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
id:
|
|
501
|
-
name:
|
|
502
|
-
busy: !!
|
|
503
|
-
activeApp:
|
|
504
|
-
meta:
|
|
505
|
-
lastSeenAgeSeconds:
|
|
509
|
+
const i = await t.json(), s = /* @__PURE__ */ new Map();
|
|
510
|
+
for (const o of i.robots || []) {
|
|
511
|
+
if (e && o.busy) continue;
|
|
512
|
+
const n = o.meta?.install_id ?? o.meta?.hardware_id ?? o.peerId;
|
|
513
|
+
s.set(n, {
|
|
514
|
+
id: o.peerId,
|
|
515
|
+
name: o.robotName ?? o.meta?.name ?? null,
|
|
516
|
+
busy: !!o.busy,
|
|
517
|
+
activeApp: o.activeApp ?? null,
|
|
518
|
+
meta: o.meta ?? {},
|
|
519
|
+
lastSeenAgeSeconds: o.last_seen_age_seconds ?? null
|
|
506
520
|
});
|
|
507
521
|
}
|
|
508
|
-
return Array.from(
|
|
509
|
-
(
|
|
522
|
+
return Array.from(s.values()).sort(
|
|
523
|
+
(o, n) => (o.lastSeenAgeSeconds ?? 1 / 0) - (n.lastSeenAgeSeconds ?? 1 / 0)
|
|
510
524
|
);
|
|
511
525
|
} catch (t) {
|
|
512
|
-
return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:", t), (this._robots || []).map((
|
|
513
|
-
id:
|
|
514
|
-
name:
|
|
526
|
+
return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:", t), (this._robots || []).map((i) => ({
|
|
527
|
+
id: i.id,
|
|
528
|
+
name: i.meta?.name ?? null,
|
|
515
529
|
busy: !1,
|
|
516
530
|
activeApp: null,
|
|
517
|
-
meta:
|
|
531
|
+
meta: i.meta ?? {},
|
|
518
532
|
lastSeenAgeSeconds: null
|
|
519
533
|
}));
|
|
520
534
|
}
|
|
521
535
|
}
|
|
522
536
|
_waitForRobotInList(e, t) {
|
|
523
|
-
return this._robots?.find((
|
|
524
|
-
const
|
|
525
|
-
this._robots?.find((a) => a.id === e) && (this.removeEventListener("robotsChanged",
|
|
526
|
-
},
|
|
527
|
-
this.removeEventListener("robotsChanged",
|
|
537
|
+
return this._robots?.find((i) => i.id === e) ? Promise.resolve() : new Promise((i, s) => {
|
|
538
|
+
const o = () => {
|
|
539
|
+
this._robots?.find((a) => a.id === e) && (this.removeEventListener("robotsChanged", o), clearTimeout(n), i());
|
|
540
|
+
}, n = setTimeout(() => {
|
|
541
|
+
this.removeEventListener("robotsChanged", o), s(new Error(`Timeout waiting for robot ${e} in list`));
|
|
528
542
|
}, t);
|
|
529
|
-
this.addEventListener("robotsChanged",
|
|
543
|
+
this.addEventListener("robotsChanged", o);
|
|
530
544
|
});
|
|
531
545
|
}
|
|
532
546
|
async startSession(e) {
|
|
533
547
|
if (this._state !== "connected") throw new Error("Not connected");
|
|
534
548
|
this._selectedRobotId = e, this._iceConnected = !1, this._dcOpen = !1, this._micSupported = !1, this._pendingRemoteIce = [];
|
|
535
549
|
try {
|
|
536
|
-
const t = window,
|
|
537
|
-
if (!
|
|
538
|
-
const
|
|
539
|
-
a.gain.value = 0,
|
|
540
|
-
const l =
|
|
550
|
+
const t = window, i = t.AudioContext ?? t.webkitAudioContext;
|
|
551
|
+
if (!i) throw new Error("AudioContext not supported");
|
|
552
|
+
const s = new i(), o = s.createMediaStreamDestination(), n = s.createOscillator(), a = s.createGain();
|
|
553
|
+
a.gain.value = 0, n.connect(a).connect(o), n.start();
|
|
554
|
+
const l = o.stream;
|
|
541
555
|
l.getAudioTracks().forEach((c) => {
|
|
542
556
|
c.enabled = !1;
|
|
543
557
|
}), this._micStream = l, this._micMuted = !0;
|
|
@@ -546,49 +560,56 @@ class J extends EventTarget {
|
|
|
546
560
|
}
|
|
547
561
|
return this._pc = new RTCPeerConnection({
|
|
548
562
|
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
|
549
|
-
}), new Promise((t,
|
|
550
|
-
this._sessionResolve = t, this._sessionReject =
|
|
551
|
-
if (
|
|
552
|
-
const
|
|
563
|
+
}), this._installNetworkListeners(), new Promise((t, i) => {
|
|
564
|
+
this._sessionResolve = t, this._sessionReject = i, this._pc.ontrack = (s) => {
|
|
565
|
+
if (s.track.kind === "video") {
|
|
566
|
+
const o = this._videoJitterBufferTargetMs;
|
|
553
567
|
try {
|
|
554
|
-
|
|
568
|
+
s.receiver.jitterBufferTarget = o;
|
|
555
569
|
} catch {
|
|
556
570
|
}
|
|
557
571
|
try {
|
|
558
|
-
|
|
572
|
+
s.receiver.playoutDelayHint = o / 1e3;
|
|
559
573
|
} catch {
|
|
560
574
|
}
|
|
561
|
-
this._emit("videoTrack", { track:
|
|
575
|
+
this._emit("videoTrack", { track: s.track, stream: s.streams[0] });
|
|
562
576
|
}
|
|
563
|
-
}, this._pc.onicecandidate = async (
|
|
564
|
-
|
|
577
|
+
}, this._pc.onicecandidate = async (s) => {
|
|
578
|
+
s.candidate && this._sessionId && await this._sendToServer({
|
|
565
579
|
type: "peer",
|
|
566
580
|
sessionId: this._sessionId,
|
|
567
581
|
ice: {
|
|
568
|
-
candidate:
|
|
569
|
-
sdpMLineIndex:
|
|
570
|
-
sdpMid:
|
|
582
|
+
candidate: s.candidate.candidate,
|
|
583
|
+
sdpMLineIndex: s.candidate.sdpMLineIndex,
|
|
584
|
+
sdpMid: s.candidate.sdpMid
|
|
571
585
|
}
|
|
572
586
|
});
|
|
573
587
|
}, this._pc.oniceconnectionstatechange = () => {
|
|
574
|
-
const
|
|
575
|
-
if (
|
|
576
|
-
if (
|
|
577
|
-
this._iceConnected = !0, this._checkSessionReady();
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
588
|
+
const s = this._pc?.iceConnectionState;
|
|
589
|
+
if (s) {
|
|
590
|
+
if (this._emit("iceStateChange", { state: s }), s === "connected" || s === "completed") {
|
|
591
|
+
this._clearIceGrace(), this._iceConnected = !0, this._checkSessionReady();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (s === "disconnected") {
|
|
595
|
+
typeof document < "u" && document.hidden ? this._armIceGraceOnVisibility() : this._scheduleIceGrace(j, "disconnected");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (s === "failed") {
|
|
599
|
+
this._scheduleIceGrace(P, "failed");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}, this._pc.ondatachannel = (s) => {
|
|
604
|
+
this._dc = s.channel, this._dc.onopen = () => {
|
|
584
605
|
this._dcOpen = !0, this._checkSessionReady();
|
|
585
|
-
}, this._dc.onmessage = (
|
|
586
|
-
}, this._sendToServer({ type: "startSession", peerId: e }).then((
|
|
587
|
-
if (
|
|
588
|
-
this._failSessionRejected(
|
|
606
|
+
}, this._dc.onmessage = (o) => this._handleRobotMessage(JSON.parse(o.data));
|
|
607
|
+
}, this._sendToServer({ type: "startSession", peerId: e }).then((s) => {
|
|
608
|
+
if (s?.type === "sessionRejected") {
|
|
609
|
+
this._failSessionRejected(s);
|
|
589
610
|
return;
|
|
590
611
|
}
|
|
591
|
-
|
|
612
|
+
s?.sessionId && (this._sessionId = s.sessionId);
|
|
592
613
|
});
|
|
593
614
|
});
|
|
594
615
|
}
|
|
@@ -596,74 +617,237 @@ class J extends EventTarget {
|
|
|
596
617
|
const t = new Error(
|
|
597
618
|
e.reason === "robot_busy" ? `Robot is busy: "${e.activeApp || "another app"}" is already connected` : `Session rejected: ${e.reason || "unknown reason"}`
|
|
598
619
|
);
|
|
599
|
-
if (t.reason = e.reason ?? null, t.activeApp = e.activeApp ?? null, this._pc && (this._pc.close(), this._pc = null), this._micStream && (this._micStream.getTracks().forEach((
|
|
600
|
-
const
|
|
601
|
-
this._sessionResolve = null, this._sessionReject = null,
|
|
620
|
+
if (t.reason = e.reason ?? null, t.activeApp = e.activeApp ?? null, this._clearIceGrace(), this._uninstallNetworkListeners(), this._pc && (this._pc.close(), this._pc = null), this._micStream && (this._micStream.getTracks().forEach((i) => i.stop()), this._micStream = null), this._iceConnected = !1, this._dcOpen = !1, this._micMuted = !0, this._micSupported = !1, this._emit("sessionRejected", { reason: e.reason, activeApp: e.activeApp }), this._sessionReject) {
|
|
621
|
+
const i = this._sessionReject;
|
|
622
|
+
this._sessionResolve = null, this._sessionReject = null, i(t);
|
|
602
623
|
}
|
|
603
624
|
}
|
|
604
625
|
async stopSession() {
|
|
605
|
-
this._versionResolve && (this._versionResolve(null), this._versionResolve = null), this._hardwareIdResolve && (this._hardwareIdResolve(null), this._hardwareIdResolve = null), this._volumeResolve && (this._volumeResolve(null), this._volumeResolve = null), this._micVolumeResolve && (this._micVolumeResolve(null), this._micVolumeResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._rejectPendingMotionCompletions(new Error("Session stopped")), this._sessionReject && (this._sessionReject(new Error("Session stopped")), this._sessionResolve = null, this._sessionReject = null), this._stateRefreshInterval && (clearInterval(this._stateRefreshInterval), this._stateRefreshInterval = null), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), this._sessionId && await this._sendToServer({ type: "endSession", sessionId: this._sessionId }), this._micStream && (this._micStream.getTracks().forEach((t) => t.stop()), this._micStream = null), this._micMuted = !0, this._micSupported = !1, this._pc && (this._pc.close(), this._pc = null), this._dc && (this._dc.close(), this._dc = null), this._sessionId = null, this._iceConnected = !1, this._dcOpen = !1, this._state === "streaming" && (this._state = "connected", this._emit("sessionStopped", { reason: "user" }));
|
|
626
|
+
this._versionResolve && (this._versionResolve(null), this._versionResolve = null), this._hardwareIdResolve && (this._hardwareIdResolve(null), this._hardwareIdResolve = null), this._volumeResolve && (this._volumeResolve(null), this._volumeResolve = null), this._micVolumeResolve && (this._micVolumeResolve(null), this._micVolumeResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._rejectPendingMotionCompletions(new Error("Session stopped")), this._clearIceGrace(), this._uninstallNetworkListeners(), this._sessionReject && (this._sessionReject(new Error("Session stopped")), this._sessionResolve = null, this._sessionReject = null), this._stateRefreshInterval && (clearInterval(this._stateRefreshInterval), this._stateRefreshInterval = null), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), this._sessionId && await this._sendToServer({ type: "endSession", sessionId: this._sessionId }), this._micStream && (this._micStream.getTracks().forEach((t) => t.stop()), this._micStream = null), this._micMuted = !0, this._micSupported = !1, this._pc && (this._pc.close(), this._pc = null), this._dc && (this._dc.close(), this._dc = null), this._sessionId = null, this._iceConnected = !1, this._dcOpen = !1, this._state === "streaming" && (this._state = "connected", this._emit("sessionStopped", { reason: "user" }));
|
|
606
627
|
}
|
|
607
628
|
disconnect() {
|
|
608
|
-
this._sseAbortController && (this._sseAbortController.abort(), this._sseAbortController = null), this._versionResolve && (this._versionResolve(null), this._versionResolve = null), this._hardwareIdResolve && (this._hardwareIdResolve(null), this._hardwareIdResolve = null), this._volumeResolve && (this._volumeResolve(null), this._volumeResolve = null), this._micVolumeResolve && (this._micVolumeResolve(null), this._micVolumeResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._rejectPendingMotionCompletions(new Error("Disconnected")), this._sessionReject && (this._sessionReject(new Error("Disconnected")), this._sessionResolve = null, this._sessionReject = null), this._stateRefreshInterval && (clearInterval(this._stateRefreshInterval), this._stateRefreshInterval = null), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), this._sessionId && this._token && this._sendToServer({ type: "endSession", sessionId: this._sessionId }), this._micStream && (this._micStream.getTracks().forEach((e) => e.stop()), this._micStream = null), this._pc && (this._pc.close(), this._pc = null), this._dc && (this._dc.close(), this._dc = null), this._sessionId = null, this._micMuted = !0, this._micSupported = !1, this._iceConnected = !1, this._dcOpen = !1, this._robots = [], this._state = "disconnected", this._emit("disconnected", { reason: "user" });
|
|
629
|
+
this._sseAbortController && (this._sseAbortController.abort(), this._sseAbortController = null), this._versionResolve && (this._versionResolve(null), this._versionResolve = null), this._hardwareIdResolve && (this._hardwareIdResolve(null), this._hardwareIdResolve = null), this._volumeResolve && (this._volumeResolve(null), this._volumeResolve = null), this._micVolumeResolve && (this._micVolumeResolve(null), this._micVolumeResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._rejectPendingMotionCompletions(new Error("Disconnected")), this._clearIceGrace(), this._uninstallNetworkListeners(), this._sessionReject && (this._sessionReject(new Error("Disconnected")), this._sessionResolve = null, this._sessionReject = null), this._stateRefreshInterval && (clearInterval(this._stateRefreshInterval), this._stateRefreshInterval = null), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), this._sessionId && this._token && this._sendToServer({ type: "endSession", sessionId: this._sessionId }), this._micStream && (this._micStream.getTracks().forEach((e) => e.stop()), this._micStream = null), this._pc && (this._pc.close(), this._pc = null), this._dc && (this._dc.close(), this._dc = null), this._sessionId = null, this._micMuted = !0, this._micSupported = !1, this._iceConnected = !1, this._dcOpen = !1, this._robots = [], this._state = "disconnected", this._emit("disconnected", { reason: "user" });
|
|
630
|
+
}
|
|
631
|
+
// ─── Resilience: ICE-blip debounce + network awareness ───────────────
|
|
632
|
+
//
|
|
633
|
+
// Both halves below are intentionally generic (they don't know about
|
|
634
|
+
// motion, audio, or the FSM): they just smooth out browser-level
|
|
635
|
+
// events so the consumer's own state machine doesn't get torn down
|
|
636
|
+
// by routine WiFi/4G/screen-off noise.
|
|
637
|
+
/**
|
|
638
|
+
* Cancel any pending ICE grace timer and visibility handler. Called
|
|
639
|
+
* on a healed `connected`/`completed` transition AND from the
|
|
640
|
+
* lifecycle teardown paths so a callback can't fire after `_pc`
|
|
641
|
+
* is closed.
|
|
642
|
+
*/
|
|
643
|
+
_clearIceGrace() {
|
|
644
|
+
this._iceGraceTimer !== null && (clearTimeout(this._iceGraceTimer), this._iceGraceTimer = null), this._iceGraceReason = null, this._pendingVisibilityHandler && typeof document < "u" && document.removeEventListener("visibilitychange", this._pendingVisibilityHandler), this._pendingVisibilityHandler = null;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Start a grace window. After `ms`, re-check the live ICE state:
|
|
648
|
+
* - If we healed back to `connected`/`completed`, the timer was
|
|
649
|
+
* already cancelled in `oniceconnectionstatechange`, so we
|
|
650
|
+
* never get here.
|
|
651
|
+
* - If we're still in the originally-observed bad state (or
|
|
652
|
+
* worse), surface the error and reject any pending session
|
|
653
|
+
* promise. The original code path is preserved verbatim so
|
|
654
|
+
* downstream consumers see the same `error` payload shape.
|
|
655
|
+
*/
|
|
656
|
+
_scheduleIceGrace(e, t) {
|
|
657
|
+
if (this._iceGraceTimer !== null) {
|
|
658
|
+
if (this._iceGraceReason === t) return;
|
|
659
|
+
clearTimeout(this._iceGraceTimer);
|
|
660
|
+
}
|
|
661
|
+
this._iceGraceReason = t, this._iceGraceTimer = setTimeout(() => {
|
|
662
|
+
this._iceGraceTimer = null;
|
|
663
|
+
const i = this._iceGraceReason;
|
|
664
|
+
this._iceGraceReason = null;
|
|
665
|
+
const s = this._pc?.iceConnectionState;
|
|
666
|
+
if (!(s === "connected" || s === "completed")) {
|
|
667
|
+
if (i === "disconnected" && s === "disconnected") {
|
|
668
|
+
this._emit("error", {
|
|
669
|
+
source: "webrtc",
|
|
670
|
+
error: new Error(`ICE stuck in 'disconnected' for > ${e}ms`)
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (i === "failed" || s === "failed") {
|
|
675
|
+
const o = new Error("ICE connection failed");
|
|
676
|
+
this._sessionReject && (this._sessionReject(o), this._sessionResolve = null, this._sessionReject = null), this._emit("error", { source: "webrtc", error: o });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}, e);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* `disconnected` while the tab is hidden. JS timers are throttled
|
|
683
|
+
* in background tabs (Chrome clamps to ~1 Hz, Safari can pause
|
|
684
|
+
* altogether), so a foreground grace timer would either miss the
|
|
685
|
+
* window or fire long after the connection healed. Wait for the
|
|
686
|
+
* tab to come back, then re-evaluate.
|
|
687
|
+
*/
|
|
688
|
+
_armIceGraceOnVisibility() {
|
|
689
|
+
if (this._pendingVisibilityHandler) return;
|
|
690
|
+
const e = Date.now(), t = () => {
|
|
691
|
+
if (typeof document < "u" && document.hidden || (document.removeEventListener("visibilitychange", t), this._pendingVisibilityHandler = null, !this._pc)) return;
|
|
692
|
+
const i = this._pc.iceConnectionState;
|
|
693
|
+
if (!(i === "connected" || i === "completed")) {
|
|
694
|
+
if (Date.now() - e > X) {
|
|
695
|
+
const s = new Error(
|
|
696
|
+
"Session expired while tab was backgrounded"
|
|
697
|
+
);
|
|
698
|
+
this._sessionReject && (this._sessionReject(s), this._sessionResolve = null, this._sessionReject = null), this._emit("error", { source: "webrtc", error: s });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (i === "failed") {
|
|
702
|
+
this._scheduleIceGrace(P, "failed");
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
this._scheduleIceGrace(j, "disconnected");
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
document.addEventListener("visibilitychange", t), this._pendingVisibilityHandler = t;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Install browser-level network listeners and forward them as
|
|
712
|
+
* public `networkOnline` / `networkOffline` / `networkChange`
|
|
713
|
+
* events on this instance. Idempotent: called from
|
|
714
|
+
* `startSession()`, removed by `_uninstallNetworkListeners` on
|
|
715
|
+
* teardown. Reachable only when there's a live `window`
|
|
716
|
+
* (defensive guard for SSR / test environments).
|
|
717
|
+
*
|
|
718
|
+
* `online` / `offline` are semantically about CONNECTIVITY:
|
|
719
|
+
* "does the OS think we can reach the internet". They flip
|
|
720
|
+
* symmetrically.
|
|
721
|
+
*
|
|
722
|
+
* `connection.change` (NetworkInformation API, Chrome / Android
|
|
723
|
+
* WebView only) is semantically about the TRANSPORT: it fires
|
|
724
|
+
* on Wi-Fi → 4G swaps, AP roams, etc. without necessarily going
|
|
725
|
+
* through `offline`. We forward it as its own `networkChange`
|
|
726
|
+
* event rather than aliasing it onto `networkOnline`, so
|
|
727
|
+
* consumers don't have to guess whether they're seeing a real
|
|
728
|
+
* connectivity recovery or a silent transport swap.
|
|
729
|
+
*/
|
|
730
|
+
_installNetworkListeners() {
|
|
731
|
+
if (this._onlineHandler || typeof window > "u") return;
|
|
732
|
+
const e = () => this._emit("networkOnline", {}), t = () => this._emit("networkOffline", {});
|
|
733
|
+
window.addEventListener("online", e), window.addEventListener("offline", t), this._onlineHandler = e, this._offlineHandler = t;
|
|
734
|
+
const i = navigator.connection;
|
|
735
|
+
if (i && typeof i.addEventListener == "function") {
|
|
736
|
+
const s = () => this._emit("networkChange", {
|
|
737
|
+
effectiveType: i.effectiveType,
|
|
738
|
+
downlink: i.downlink,
|
|
739
|
+
rtt: i.rtt,
|
|
740
|
+
saveData: i.saveData
|
|
741
|
+
});
|
|
742
|
+
i.addEventListener("change", s), this._connectionChangeHandler = s;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/** Counterpart to `_installNetworkListeners`. */
|
|
746
|
+
_uninstallNetworkListeners() {
|
|
747
|
+
typeof window < "u" && (this._onlineHandler && window.removeEventListener("online", this._onlineHandler), this._offlineHandler && window.removeEventListener("offline", this._offlineHandler));
|
|
748
|
+
const e = navigator.connection;
|
|
749
|
+
e && this._connectionChangeHandler && typeof e.removeEventListener == "function" && e.removeEventListener("change", this._connectionChangeHandler), this._onlineHandler = null, this._offlineHandler = null, this._connectionChangeHandler = null;
|
|
609
750
|
}
|
|
610
751
|
// ─── Commands ────────────────────────────────────────────────────────
|
|
611
|
-
|
|
612
|
-
|
|
752
|
+
/**
|
|
753
|
+
* Atomic raw-units pose update over the data channel. Channels you
|
|
754
|
+
* omit are held at their last commanded value (per-axis, independent).
|
|
755
|
+
*
|
|
756
|
+
* **Head pose is in the WORLD frame.** The daemon's IK splits the
|
|
757
|
+
* requested head world-yaw between body rotation and the stewart
|
|
758
|
+
* platform, subject to the mechanical limit
|
|
759
|
+
* `|head_yaw_world − body_yaw| ≤ 65°`.
|
|
760
|
+
*
|
|
761
|
+
* **If you want the head to FOLLOW the body** (tank-style rotation):
|
|
762
|
+
* a `setTarget({ body_yaw })` on its own does NOT rotate the head —
|
|
763
|
+
* the head's commanded world yaw is unchanged, so its gaze stays
|
|
764
|
+
* pinned in world frame while the body turns under it. To make the
|
|
765
|
+
* head turn with the body, include a `head` matrix in the SAME call
|
|
766
|
+
* with the body-yaw delta added to the head RPY's yaw:
|
|
767
|
+
*
|
|
768
|
+
* ```ts
|
|
769
|
+
* // Body-yaw drag handler: tank-couple the head so it follows.
|
|
770
|
+
* const delta = newBodyDeg - lastCommandedBodyDeg;
|
|
771
|
+
* const nextHeadYaw = lastCommandedHeadYawDeg + delta;
|
|
772
|
+
* robot.setTarget({
|
|
773
|
+
* head: rpyToMatrix(headRoll, headPitch, nextHeadYaw).flat(),
|
|
774
|
+
* body_yaw: degToRad(newBodyDeg),
|
|
775
|
+
* });
|
|
776
|
+
* lastCommandedHeadYawDeg = nextHeadYaw;
|
|
777
|
+
* lastCommandedBodyDeg = newBodyDeg;
|
|
778
|
+
* ```
|
|
779
|
+
*
|
|
780
|
+
* **Baseline must be the last COMMANDED value, not telemetry.** For
|
|
781
|
+
* continuous-input controllers (slider drag, joystick), do not use
|
|
782
|
+
* `state.head` from the `state` event as the baseline for incremental
|
|
783
|
+
* commands — telemetry lags one WebRTC round-trip, so cumulative
|
|
784
|
+
* deltas computed against it stall (every iteration in a rapid drag
|
|
785
|
+
* adds the same `delta` to the same stale baseline → the head fails
|
|
786
|
+
* to keep up). Track the last-commanded RPY in your own buffer.
|
|
787
|
+
*
|
|
788
|
+
* @param head Flat row-major 4×4 matrix (16 finite numbers) in
|
|
789
|
+
* the world frame. Omit to hold the previous head target.
|
|
790
|
+
* @param antennas `[rightRad, leftRad]` (radians). Omit to hold.
|
|
791
|
+
* @param body_yaw Signed radians. Omit to hold.
|
|
792
|
+
* @returns `true` if the command was queued on the data channel,
|
|
793
|
+
* `false` if the channel is not open.
|
|
794
|
+
*/
|
|
795
|
+
setTarget({ head: e, antennas: t, body_yaw: i } = {}) {
|
|
796
|
+
const s = { type: "set_full_target" };
|
|
613
797
|
if (e !== void 0) {
|
|
614
|
-
if (!Array.isArray(e) || e.length !== 16 || !e.every((
|
|
798
|
+
if (!Array.isArray(e) || e.length !== 16 || !e.every((o) => Number.isFinite(o)))
|
|
615
799
|
throw new TypeError(
|
|
616
800
|
`setTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e) ? `Array(${e.length})` : typeof e}`
|
|
617
801
|
);
|
|
618
|
-
|
|
802
|
+
s.head = e;
|
|
619
803
|
}
|
|
620
804
|
if (t !== void 0) {
|
|
621
|
-
if (!Array.isArray(t) || t.length !== 2 || !t.every((
|
|
805
|
+
if (!Array.isArray(t) || t.length !== 2 || !t.every((o) => Number.isFinite(o)))
|
|
622
806
|
throw new TypeError(
|
|
623
807
|
`setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
|
|
624
808
|
);
|
|
625
|
-
|
|
809
|
+
s.antennas = t;
|
|
626
810
|
}
|
|
627
|
-
if (
|
|
628
|
-
if (!Number.isFinite(
|
|
811
|
+
if (i !== void 0) {
|
|
812
|
+
if (!Number.isFinite(i))
|
|
629
813
|
throw new TypeError(
|
|
630
|
-
`setTarget: body_yaw must be a finite number (radians); got ${
|
|
814
|
+
`setTarget: body_yaw must be a finite number (radians); got ${i}`
|
|
631
815
|
);
|
|
632
|
-
|
|
816
|
+
s.body_yaw = i;
|
|
633
817
|
}
|
|
634
|
-
return this._sendCommand(
|
|
818
|
+
return this._sendCommand(s);
|
|
635
819
|
}
|
|
636
|
-
gotoTarget({ head: e, antennas: t, body_yaw:
|
|
637
|
-
const
|
|
820
|
+
gotoTarget({ head: e, antennas: t, body_yaw: i, duration: s }) {
|
|
821
|
+
const o = { type: "goto_target" };
|
|
638
822
|
if (e !== void 0) {
|
|
639
|
-
if (!Array.isArray(e) || e.length !== 16 || !e.every((
|
|
823
|
+
if (!Array.isArray(e) || e.length !== 16 || !e.every((n) => Number.isFinite(n)))
|
|
640
824
|
throw new TypeError(
|
|
641
825
|
`gotoTarget: head must be a 16-element flat row-major 4×4 matrix of finite numbers; got ${Array.isArray(e) ? `Array(${e.length})` : typeof e}`
|
|
642
826
|
);
|
|
643
|
-
|
|
827
|
+
o.head = e;
|
|
644
828
|
}
|
|
645
829
|
if (t !== void 0) {
|
|
646
|
-
if (!Array.isArray(t) || t.length !== 2 || !t.every((
|
|
830
|
+
if (!Array.isArray(t) || t.length !== 2 || !t.every((n) => Number.isFinite(n)))
|
|
647
831
|
throw new TypeError(
|
|
648
832
|
`gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
|
|
649
833
|
);
|
|
650
|
-
|
|
834
|
+
o.antennas = t;
|
|
651
835
|
}
|
|
652
|
-
if (
|
|
653
|
-
if (!Number.isFinite(
|
|
836
|
+
if (i !== void 0) {
|
|
837
|
+
if (!Number.isFinite(i))
|
|
654
838
|
throw new TypeError(
|
|
655
|
-
`gotoTarget: body_yaw must be a finite number (radians); got ${
|
|
839
|
+
`gotoTarget: body_yaw must be a finite number (radians); got ${i}`
|
|
656
840
|
);
|
|
657
|
-
|
|
841
|
+
o.body_yaw = i;
|
|
658
842
|
}
|
|
659
|
-
if (!Number.isFinite(
|
|
843
|
+
if (!Number.isFinite(s) || s <= 0)
|
|
660
844
|
throw new TypeError(
|
|
661
|
-
`gotoTarget: duration must be a positive finite number (seconds); got ${
|
|
845
|
+
`gotoTarget: duration must be a positive finite number (seconds); got ${s}`
|
|
662
846
|
);
|
|
663
|
-
return
|
|
847
|
+
return o.duration = s, this._sendCommand(o);
|
|
664
848
|
}
|
|
665
|
-
setHeadRpyDeg(e, t,
|
|
666
|
-
return this.setTarget({ head: F(e, t,
|
|
849
|
+
setHeadRpyDeg(e, t, i) {
|
|
850
|
+
return this.setTarget({ head: F(e, t, i).flat() });
|
|
667
851
|
}
|
|
668
852
|
setAntennasDeg(e, t) {
|
|
669
853
|
return this.setTarget({ antennas: [R(e), R(t)] });
|
|
@@ -674,6 +858,9 @@ class J extends EventTarget {
|
|
|
674
858
|
playSound(e) {
|
|
675
859
|
return this._sendCommand({ type: "play_sound", file: e });
|
|
676
860
|
}
|
|
861
|
+
clearIncomingAudio() {
|
|
862
|
+
return this._sendCommand({ type: "clear_incoming_audio" });
|
|
863
|
+
}
|
|
677
864
|
setMotorMode(e) {
|
|
678
865
|
return this._sendCommand({ type: "set_motor_mode", mode: e });
|
|
679
866
|
}
|
|
@@ -687,24 +874,24 @@ class J extends EventTarget {
|
|
|
687
874
|
return this._sendCommandAwaitCompletion("goto_sleep", e);
|
|
688
875
|
}
|
|
689
876
|
_sendCommandAwaitCompletion(e, t) {
|
|
690
|
-
return this._sendCommand({ type: e }) ? new Promise((
|
|
691
|
-
const
|
|
692
|
-
resolve:
|
|
693
|
-
reject:
|
|
877
|
+
return this._sendCommand({ type: e }) ? new Promise((i, s) => {
|
|
878
|
+
const o = {
|
|
879
|
+
resolve: i,
|
|
880
|
+
reject: s,
|
|
694
881
|
timer: setTimeout(() => {
|
|
695
|
-
const
|
|
696
|
-
a !== -1 &&
|
|
882
|
+
const n = this._pendingMotionCompletions[e], a = n.indexOf(o);
|
|
883
|
+
a !== -1 && n.splice(a, 1), s(new Error(`${e} timed out after ${t}ms`));
|
|
697
884
|
}, t)
|
|
698
885
|
};
|
|
699
|
-
this._pendingMotionCompletions[e].push(
|
|
886
|
+
this._pendingMotionCompletions[e].push(o);
|
|
700
887
|
}) : Promise.reject(new Error(`${e}: data channel not open`));
|
|
701
888
|
}
|
|
702
889
|
_rejectPendingMotionCompletions(e) {
|
|
703
890
|
for (const t of Object.keys(this._pendingMotionCompletions)) {
|
|
704
|
-
const
|
|
705
|
-
for (;
|
|
706
|
-
const
|
|
707
|
-
clearTimeout(
|
|
891
|
+
const i = this._pendingMotionCompletions[t];
|
|
892
|
+
for (; i.length; ) {
|
|
893
|
+
const s = i.shift();
|
|
894
|
+
clearTimeout(s.timer), s.reject(e);
|
|
708
895
|
}
|
|
709
896
|
}
|
|
710
897
|
}
|
|
@@ -714,10 +901,10 @@ class J extends EventTarget {
|
|
|
714
901
|
}
|
|
715
902
|
async ensureAwake(e = 1e3) {
|
|
716
903
|
return this._robotState?.motor_mode === void 0 && await new Promise((t) => {
|
|
717
|
-
const
|
|
718
|
-
this.removeEventListener("state",
|
|
719
|
-
},
|
|
720
|
-
this.addEventListener("state",
|
|
904
|
+
const i = () => {
|
|
905
|
+
this.removeEventListener("state", i), clearTimeout(s), t();
|
|
906
|
+
}, s = setTimeout(i, e);
|
|
907
|
+
this.addEventListener("state", i), this.requestState();
|
|
721
908
|
}), this.isAwake() || this.wakeUp().catch(() => {
|
|
722
909
|
}), !0;
|
|
723
910
|
}
|
|
@@ -754,7 +941,7 @@ class J extends EventTarget {
|
|
|
754
941
|
(t) => {
|
|
755
942
|
this._volumeResolve = t;
|
|
756
943
|
},
|
|
757
|
-
{ type: "set_volume", volume:
|
|
944
|
+
{ type: "set_volume", volume: L(e) }
|
|
758
945
|
);
|
|
759
946
|
}
|
|
760
947
|
getMicrophoneVolume() {
|
|
@@ -772,17 +959,17 @@ class J extends EventTarget {
|
|
|
772
959
|
(t) => {
|
|
773
960
|
this._micVolumeResolve = t;
|
|
774
961
|
},
|
|
775
|
-
{ type: "set_microphone_volume", volume:
|
|
962
|
+
{ type: "set_microphone_volume", volume: L(e) }
|
|
776
963
|
);
|
|
777
964
|
}
|
|
778
965
|
applyAudioConfig(e, { verify: t = !0 } = {}) {
|
|
779
966
|
return this._slotRoundtrip(
|
|
780
967
|
() => this._applyAudioConfigResolve,
|
|
781
|
-
(
|
|
782
|
-
this._applyAudioConfigResolve =
|
|
968
|
+
(i) => {
|
|
969
|
+
this._applyAudioConfigResolve = i;
|
|
783
970
|
},
|
|
784
971
|
{ type: "apply_audio_config", config: e, verify: t }
|
|
785
|
-
).then((
|
|
972
|
+
).then((i) => i === !0);
|
|
786
973
|
}
|
|
787
974
|
readAudioParameter(e) {
|
|
788
975
|
return this._slotRoundtrip(
|
|
@@ -807,14 +994,14 @@ class J extends EventTarget {
|
|
|
807
994
|
* inferred from the slot's resolver type at each call site) with no
|
|
808
995
|
* indexed-property casts.
|
|
809
996
|
*/
|
|
810
|
-
_slotRoundtrip(e, t,
|
|
811
|
-
return new Promise((
|
|
997
|
+
_slotRoundtrip(e, t, i) {
|
|
998
|
+
return new Promise((s, o) => {
|
|
812
999
|
if (!this._dc || this._dc.readyState !== "open") {
|
|
813
|
-
|
|
1000
|
+
o(new Error("Data channel not open"));
|
|
814
1001
|
return;
|
|
815
1002
|
}
|
|
816
|
-
const
|
|
817
|
-
|
|
1003
|
+
const n = e();
|
|
1004
|
+
n && n(null), t(s), this._sendCommand(i);
|
|
818
1005
|
});
|
|
819
1006
|
}
|
|
820
1007
|
sendRaw(e) {
|
|
@@ -823,11 +1010,11 @@ class J extends EventTarget {
|
|
|
823
1010
|
subscribeLogs({ onLine: e, onError: t }) {
|
|
824
1011
|
if (typeof e != "function")
|
|
825
1012
|
throw new TypeError("subscribeLogs: onLine callback is required");
|
|
826
|
-
const
|
|
827
|
-
this._logSubscribers.add(
|
|
828
|
-
let
|
|
1013
|
+
const i = { onLine: e, onError: t }, s = this._logSubscribers.size === 0;
|
|
1014
|
+
this._logSubscribers.add(i), s && this._sendCommand({ type: "subscribe_logs" });
|
|
1015
|
+
let o = !1;
|
|
829
1016
|
return () => {
|
|
830
|
-
|
|
1017
|
+
o || (o = !0, this._logSubscribers.delete(i), this._logSubscribers.size === 0 && this._sendCommand({ type: "unsubscribe_logs" }));
|
|
831
1018
|
};
|
|
832
1019
|
}
|
|
833
1020
|
requestState() {
|
|
@@ -845,23 +1032,23 @@ class J extends EventTarget {
|
|
|
845
1032
|
// ─── Video helper ────────────────────────────────────────────────────
|
|
846
1033
|
attachVideo(e) {
|
|
847
1034
|
this._videoElement = e, e.muted = this._audioMuted;
|
|
848
|
-
const t = (
|
|
849
|
-
const
|
|
850
|
-
e.srcObject =
|
|
851
|
-
},
|
|
1035
|
+
const t = (s) => {
|
|
1036
|
+
const o = s;
|
|
1037
|
+
e.srcObject = o.detail.stream, e.playsInline = !0, "requestVideoFrameCallback" in e && this._startLatencyMonitor(e);
|
|
1038
|
+
}, i = () => {
|
|
852
1039
|
e.srcObject = null;
|
|
853
1040
|
};
|
|
854
|
-
return this.addEventListener("videoTrack", t), this.addEventListener("sessionStopped",
|
|
855
|
-
this.removeEventListener("videoTrack", t), this.removeEventListener("sessionStopped",
|
|
1041
|
+
return this.addEventListener("videoTrack", t), this.addEventListener("sessionStopped", i), () => {
|
|
1042
|
+
this.removeEventListener("videoTrack", t), this.removeEventListener("sessionStopped", i), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), e.srcObject = null, this._videoElement = null;
|
|
856
1043
|
};
|
|
857
1044
|
}
|
|
858
1045
|
// ─── Daemon-side recorded-move playback ──────────────────────────────
|
|
859
1046
|
async playMove(e, {
|
|
860
1047
|
audioBlob: t = null,
|
|
861
|
-
audioLeadMs:
|
|
862
|
-
description:
|
|
863
|
-
encoding:
|
|
864
|
-
playFrequency:
|
|
1048
|
+
audioLeadMs: i = -100,
|
|
1049
|
+
description: s = "move",
|
|
1050
|
+
encoding: o = "gzip+base64",
|
|
1051
|
+
playFrequency: n = 100,
|
|
865
1052
|
initialGotoDuration: a = 0,
|
|
866
1053
|
startTimeoutMs: l = 8e3,
|
|
867
1054
|
onProgress: c = () => {
|
|
@@ -875,39 +1062,39 @@ class J extends EventTarget {
|
|
|
875
1062
|
throw new Error("playMove: motion must have time + set_target_data");
|
|
876
1063
|
const d = U();
|
|
877
1064
|
this._activeMoveUploadId = d;
|
|
878
|
-
const
|
|
879
|
-
description:
|
|
1065
|
+
const p = {
|
|
1066
|
+
description: s,
|
|
880
1067
|
time: e.time,
|
|
881
1068
|
set_target_data: e.set_target_data
|
|
882
|
-
}, f = JSON.stringify(
|
|
883
|
-
let
|
|
884
|
-
|
|
885
|
-
const w = Math.ceil(
|
|
1069
|
+
}, f = JSON.stringify(p);
|
|
1070
|
+
let m, v;
|
|
1071
|
+
o === "gzip+base64" && z() ? (m = await J(f), v = "gzip+base64") : (m = f, v = "json");
|
|
1072
|
+
const w = Math.ceil(m.length / y) || 1;
|
|
886
1073
|
c({
|
|
887
1074
|
phase: "starting",
|
|
888
1075
|
sent: 0,
|
|
889
1076
|
total: w,
|
|
890
|
-
bytes:
|
|
1077
|
+
bytes: m.length,
|
|
891
1078
|
encoding: v
|
|
892
1079
|
}), this._sendCommand({
|
|
893
1080
|
type: "upload_move_start",
|
|
894
1081
|
upload_id: d,
|
|
895
1082
|
total_chunks: w,
|
|
896
|
-
description:
|
|
1083
|
+
description: s,
|
|
897
1084
|
encoding: v
|
|
898
1085
|
});
|
|
899
1086
|
for (let u = 0; u < w; u++) {
|
|
900
|
-
this._dc.bufferedAmount >
|
|
1087
|
+
this._dc.bufferedAmount > E && await this._awaitDataChannelDrain();
|
|
901
1088
|
const _ = u * y;
|
|
902
1089
|
this._sendCommand({
|
|
903
1090
|
type: "upload_move_chunk",
|
|
904
1091
|
upload_id: d,
|
|
905
1092
|
chunk_index: u,
|
|
906
|
-
chunk:
|
|
1093
|
+
chunk: m.slice(_, _ + y)
|
|
907
1094
|
}), c({ phase: "upload", sent: u + 1, total: w });
|
|
908
1095
|
}
|
|
909
1096
|
if (this._sendCommand({ type: "upload_move_finish", upload_id: d }), c({ phase: "uploaded", sent: w, total: w }), t) {
|
|
910
|
-
const u = new Uint8Array(await t.arrayBuffer()), _ =
|
|
1097
|
+
const u = new Uint8Array(await t.arrayBuffer()), _ = k(u), b = Math.ceil(_.length / y) || 1;
|
|
911
1098
|
c({
|
|
912
1099
|
phase: "audio-starting",
|
|
913
1100
|
sent: 0,
|
|
@@ -918,10 +1105,10 @@ class J extends EventTarget {
|
|
|
918
1105
|
upload_id: d,
|
|
919
1106
|
total_chunks: b,
|
|
920
1107
|
encoding: "wav-base64",
|
|
921
|
-
description:
|
|
1108
|
+
description: s
|
|
922
1109
|
});
|
|
923
1110
|
for (let I = 0; I < b; I++) {
|
|
924
|
-
this._dc.bufferedAmount >
|
|
1111
|
+
this._dc.bufferedAmount > E && await this._awaitDataChannelDrain();
|
|
925
1112
|
const T = I * y;
|
|
926
1113
|
this._sendCommand({
|
|
927
1114
|
type: "upload_audio_chunk",
|
|
@@ -935,9 +1122,9 @@ class J extends EventTarget {
|
|
|
935
1122
|
this._sendCommand({
|
|
936
1123
|
type: "play_uploaded_move",
|
|
937
1124
|
upload_id: d,
|
|
938
|
-
play_frequency:
|
|
1125
|
+
play_frequency: n,
|
|
939
1126
|
initial_goto_duration: a,
|
|
940
|
-
audio_lead_ms:
|
|
1127
|
+
audio_lead_ms: i
|
|
941
1128
|
});
|
|
942
1129
|
let g;
|
|
943
1130
|
try {
|
|
@@ -974,43 +1161,43 @@ class J extends EventTarget {
|
|
|
974
1161
|
const t = e ?? this._activeMoveUploadId;
|
|
975
1162
|
return t ? this._sendCommand({ type: "cancel_move", upload_id: t }) : !1;
|
|
976
1163
|
}
|
|
977
|
-
async uploadAudio(e, { description: t = "audio", onProgress:
|
|
1164
|
+
async uploadAudio(e, { description: t = "audio", onProgress: i = () => {
|
|
978
1165
|
} } = {}) {
|
|
979
1166
|
if (!this._dc || this._dc.readyState !== "open")
|
|
980
1167
|
throw new Error("data channel not open");
|
|
981
1168
|
if (!(e instanceof Blob))
|
|
982
1169
|
throw new TypeError("uploadAudio: expected a Blob");
|
|
983
|
-
const
|
|
984
|
-
|
|
1170
|
+
const s = U(), o = new Uint8Array(await e.arrayBuffer()), n = k(o), a = Math.ceil(n.length / y) || 1;
|
|
1171
|
+
i({ phase: "audio-starting", sent: 0, total: a, bytes: n.length }), this._sendCommand({
|
|
985
1172
|
type: "upload_audio_start",
|
|
986
|
-
upload_id:
|
|
1173
|
+
upload_id: s,
|
|
987
1174
|
total_chunks: a,
|
|
988
1175
|
encoding: "wav-base64",
|
|
989
1176
|
description: t
|
|
990
1177
|
});
|
|
991
1178
|
for (let l = 0; l < a; l++) {
|
|
992
|
-
this._dc.bufferedAmount >
|
|
1179
|
+
this._dc.bufferedAmount > E && await this._awaitDataChannelDrain();
|
|
993
1180
|
const c = l * y;
|
|
994
1181
|
this._sendCommand({
|
|
995
1182
|
type: "upload_audio_chunk",
|
|
996
|
-
upload_id:
|
|
1183
|
+
upload_id: s,
|
|
997
1184
|
chunk_index: l,
|
|
998
|
-
chunk:
|
|
999
|
-
}),
|
|
1185
|
+
chunk: n.slice(c, c + y)
|
|
1186
|
+
}), i({ phase: "audio-upload", sent: l + 1, total: a });
|
|
1000
1187
|
}
|
|
1001
|
-
return this._sendCommand({ type: "upload_audio_finish", upload_id:
|
|
1188
|
+
return this._sendCommand({ type: "upload_audio_finish", upload_id: s }), i({ phase: "audio-uploaded", sent: a, total: a }), s;
|
|
1002
1189
|
}
|
|
1003
1190
|
async playUploadedAudio(e, { timeoutMs: t = 8e3 } = {}) {
|
|
1004
1191
|
if (!this._dc || this._dc.readyState !== "open")
|
|
1005
1192
|
throw new Error("data channel not open");
|
|
1006
|
-
const
|
|
1007
|
-
(
|
|
1193
|
+
const i = this._waitForBroadcast(
|
|
1194
|
+
(o) => o?.type === "play_uploaded_audio" && o?.upload_id === e && (o.started === !0 || typeof o.error == "string"),
|
|
1008
1195
|
{ timeoutMs: t, debugLabel: "play_uploaded_audio started" }
|
|
1009
1196
|
);
|
|
1010
1197
|
this._sendCommand({ type: "play_uploaded_audio", upload_id: e });
|
|
1011
|
-
const
|
|
1012
|
-
if (typeof
|
|
1013
|
-
return this._activeAudioUploadId = e,
|
|
1198
|
+
const s = await i;
|
|
1199
|
+
if (typeof s.error == "string") throw new Error(s.error);
|
|
1200
|
+
return this._activeAudioUploadId = e, s;
|
|
1014
1201
|
}
|
|
1015
1202
|
cancelAudio(e = null) {
|
|
1016
1203
|
const t = e ?? this._activeAudioUploadId;
|
|
@@ -1020,21 +1207,21 @@ class J extends EventTarget {
|
|
|
1020
1207
|
_emit(e, t) {
|
|
1021
1208
|
this.dispatchEvent(new CustomEvent(e, { detail: t }));
|
|
1022
1209
|
}
|
|
1023
|
-
_waitForBroadcast(e, { timeoutMs: t = 5e3, debugLabel:
|
|
1024
|
-
return new Promise((
|
|
1025
|
-
const
|
|
1210
|
+
_waitForBroadcast(e, { timeoutMs: t = 5e3, debugLabel: i = "" } = {}) {
|
|
1211
|
+
return new Promise((s, o) => {
|
|
1212
|
+
const n = {
|
|
1026
1213
|
predicate: e,
|
|
1027
|
-
resolve:
|
|
1214
|
+
resolve: s,
|
|
1028
1215
|
timer: setTimeout(() => {
|
|
1029
|
-
const a = this._broadcastWaiters.indexOf(
|
|
1030
|
-
a !== -1 && this._broadcastWaiters.splice(a, 1),
|
|
1216
|
+
const a = this._broadcastWaiters.indexOf(n);
|
|
1217
|
+
a !== -1 && this._broadcastWaiters.splice(a, 1), o(new Error(`broadcast timeout (${t} ms): ${i}`));
|
|
1031
1218
|
}, t)
|
|
1032
1219
|
};
|
|
1033
|
-
this._broadcastWaiters.push(
|
|
1220
|
+
this._broadcastWaiters.push(n);
|
|
1034
1221
|
});
|
|
1035
1222
|
}
|
|
1036
1223
|
async _awaitDataChannelDrain() {
|
|
1037
|
-
for (; this._dc && this._dc.bufferedAmount >
|
|
1224
|
+
for (; this._dc && this._dc.bufferedAmount > W; )
|
|
1038
1225
|
if (await new Promise((e) => setTimeout(e, 30)), !this._dc || this._dc.readyState !== "open")
|
|
1039
1226
|
throw new Error("data channel closed mid-upload");
|
|
1040
1227
|
}
|
|
@@ -1050,13 +1237,13 @@ class J extends EventTarget {
|
|
|
1050
1237
|
body: JSON.stringify(e)
|
|
1051
1238
|
});
|
|
1052
1239
|
if (!t.ok) {
|
|
1053
|
-
let
|
|
1240
|
+
let i = "";
|
|
1054
1241
|
try {
|
|
1055
|
-
|
|
1242
|
+
i = await t.text();
|
|
1056
1243
|
} catch {
|
|
1057
1244
|
}
|
|
1058
1245
|
return console.warn(
|
|
1059
|
-
`[reachy-mini] /send rejected (${t.status}) for type=${e?.type}; body=${
|
|
1246
|
+
`[reachy-mini] /send rejected (${t.status}) for type=${e?.type}; body=${i || "<empty>"}`
|
|
1060
1247
|
), null;
|
|
1061
1248
|
}
|
|
1062
1249
|
return await t.json();
|
|
@@ -1097,19 +1284,19 @@ class J extends EventTarget {
|
|
|
1097
1284
|
}
|
|
1098
1285
|
}
|
|
1099
1286
|
_handleEndSession(e) {
|
|
1100
|
-
const t = e.reason,
|
|
1287
|
+
const t = e.reason, i = t === "robot_busy_local_app" ? "Robot is busy: a local Python app is running" : t === "local_app_started" ? "Disconnected: a local Python app started on the robot" : t === "robot_busy_local" ? "Robot is busy: another session is already active" : null;
|
|
1101
1288
|
if (this._sessionReject) {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1289
|
+
const s = new Error(
|
|
1290
|
+
i || `Session ended before it could start: ${t || "unknown reason"}`
|
|
1104
1291
|
);
|
|
1105
|
-
|
|
1106
|
-
const
|
|
1107
|
-
this._sessionResolve = null, this._sessionReject = null,
|
|
1292
|
+
s.reason = t ?? null, this._emit("sessionRejected", { reason: t, activeApp: null }), this._clearIceGrace(), this._uninstallNetworkListeners(), this._pc && (this._pc.close(), this._pc = null), this._micStream && (this._micStream.getTracks().forEach((n) => n.stop()), this._micStream = null), this._iceConnected = !1, this._dcOpen = !1, this._micMuted = !0, this._micSupported = !1;
|
|
1293
|
+
const o = this._sessionReject;
|
|
1294
|
+
this._sessionResolve = null, this._sessionReject = null, o(s);
|
|
1108
1295
|
return;
|
|
1109
1296
|
}
|
|
1110
1297
|
this._state === "streaming" && (this._emit("sessionStopped", {
|
|
1111
1298
|
reason: t || "remote_end",
|
|
1112
|
-
message:
|
|
1299
|
+
message: i
|
|
1113
1300
|
}), this.stopSession().catch(() => {
|
|
1114
1301
|
}));
|
|
1115
1302
|
}
|
|
@@ -1119,27 +1306,27 @@ class J extends EventTarget {
|
|
|
1119
1306
|
if (e.sdp) {
|
|
1120
1307
|
const t = e.sdp;
|
|
1121
1308
|
if (t.type === "offer") {
|
|
1122
|
-
const
|
|
1123
|
-
if (this._micSupported =
|
|
1124
|
-
for (const
|
|
1125
|
-
this._pc.addTrack(
|
|
1309
|
+
const s = q(t.sdp);
|
|
1310
|
+
if (this._micSupported = s, this._emit("micSupported", { supported: s }), s && this._micStream)
|
|
1311
|
+
for (const n of this._micStream.getAudioTracks())
|
|
1312
|
+
this._pc.addTrack(n, this._micStream);
|
|
1126
1313
|
await this._pc.setRemoteDescription(new RTCSessionDescription(t));
|
|
1127
|
-
const
|
|
1128
|
-
await this._pc.setLocalDescription(
|
|
1314
|
+
const o = await this._pc.createAnswer();
|
|
1315
|
+
await this._pc.setLocalDescription(o), await this._sendToServer({
|
|
1129
1316
|
type: "peer",
|
|
1130
1317
|
sessionId: this._sessionId,
|
|
1131
|
-
sdp: { type: "answer", sdp:
|
|
1318
|
+
sdp: { type: "answer", sdp: o.sdp }
|
|
1132
1319
|
});
|
|
1133
1320
|
} else
|
|
1134
1321
|
await this._pc.setRemoteDescription(new RTCSessionDescription(t));
|
|
1135
|
-
const
|
|
1136
|
-
if (
|
|
1322
|
+
const i = this._pendingRemoteIce;
|
|
1323
|
+
if (i && i.length) {
|
|
1137
1324
|
this._pendingRemoteIce = [];
|
|
1138
|
-
for (const
|
|
1325
|
+
for (const s of i)
|
|
1139
1326
|
try {
|
|
1140
|
-
await this._pc.addIceCandidate(new RTCIceCandidate(
|
|
1141
|
-
} catch (
|
|
1142
|
-
console.warn("[reachy-mini] buffered ICE candidate rejected:",
|
|
1327
|
+
await this._pc.addIceCandidate(new RTCIceCandidate(s));
|
|
1328
|
+
} catch (o) {
|
|
1329
|
+
console.warn("[reachy-mini] buffered ICE candidate rejected:", o);
|
|
1143
1330
|
}
|
|
1144
1331
|
}
|
|
1145
1332
|
}
|
|
@@ -1181,13 +1368,13 @@ class J extends EventTarget {
|
|
|
1181
1368
|
if ((e.command === "wake_up" || e.command === "goto_sleep") && this._pendingMotionCompletions && this._pendingMotionCompletions[e.command]) {
|
|
1182
1369
|
const t = this._pendingMotionCompletions[e.command];
|
|
1183
1370
|
if (e.completed === !0 && t.length > 0) {
|
|
1184
|
-
const
|
|
1185
|
-
clearTimeout(
|
|
1371
|
+
const i = t.shift();
|
|
1372
|
+
clearTimeout(i.timer), i.resolve();
|
|
1186
1373
|
return;
|
|
1187
1374
|
}
|
|
1188
1375
|
if (e.error && t.length > 0) {
|
|
1189
|
-
const
|
|
1190
|
-
clearTimeout(
|
|
1376
|
+
const i = t.shift();
|
|
1377
|
+
clearTimeout(i.timer), i.reject(new Error(`${e.command}: ${e.error}`));
|
|
1191
1378
|
return;
|
|
1192
1379
|
}
|
|
1193
1380
|
}
|
|
@@ -1195,8 +1382,8 @@ class J extends EventTarget {
|
|
|
1195
1382
|
for (const t of this._logSubscribers)
|
|
1196
1383
|
try {
|
|
1197
1384
|
t.onLine({ timestamp: e.timestamp, line: e.line });
|
|
1198
|
-
} catch (
|
|
1199
|
-
console.error("subscribeLogs onLine threw:",
|
|
1385
|
+
} catch (i) {
|
|
1386
|
+
console.error("subscribeLogs onLine threw:", i);
|
|
1200
1387
|
}
|
|
1201
1388
|
return;
|
|
1202
1389
|
}
|
|
@@ -1205,8 +1392,8 @@ class J extends EventTarget {
|
|
|
1205
1392
|
if (typeof t.onError == "function")
|
|
1206
1393
|
try {
|
|
1207
1394
|
t.onError(e.error);
|
|
1208
|
-
} catch (
|
|
1209
|
-
console.error("subscribeLogs onError threw:",
|
|
1395
|
+
} catch (i) {
|
|
1396
|
+
console.error("subscribeLogs onError threw:", i);
|
|
1210
1397
|
}
|
|
1211
1398
|
return;
|
|
1212
1399
|
}
|
|
@@ -1216,9 +1403,9 @@ class J extends EventTarget {
|
|
|
1216
1403
|
}
|
|
1217
1404
|
if (e.error && this._emit("error", { source: "robot", error: e.error }), this._broadcastWaiters.length > 0)
|
|
1218
1405
|
for (let t = this._broadcastWaiters.length - 1; t >= 0; t--) {
|
|
1219
|
-
const
|
|
1220
|
-
if (
|
|
1221
|
-
this._broadcastWaiters.splice(t, 1), clearTimeout(
|
|
1406
|
+
const i = this._broadcastWaiters[t];
|
|
1407
|
+
if (i.predicate(e)) {
|
|
1408
|
+
this._broadcastWaiters.splice(t, 1), clearTimeout(i.timer), i.resolve(e);
|
|
1222
1409
|
return;
|
|
1223
1410
|
}
|
|
1224
1411
|
}
|
|
@@ -1229,12 +1416,12 @@ class J extends EventTarget {
|
|
|
1229
1416
|
if (!e.srcObject || e.paused) return;
|
|
1230
1417
|
const t = e.buffered;
|
|
1231
1418
|
if (t.length > 0) {
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1419
|
+
const i = t.end(t.length - 1), s = i - e.currentTime;
|
|
1420
|
+
s > 0.5 && (console.log(`Latency correction: was ${s.toFixed(2)}s behind`), e.currentTime = i - 0.1);
|
|
1234
1421
|
}
|
|
1235
1422
|
}, 2e3);
|
|
1236
1423
|
}
|
|
1237
1424
|
}
|
|
1238
1425
|
export {
|
|
1239
|
-
|
|
1426
|
+
Y as R
|
|
1240
1427
|
};
|