@pollen-robotics/reachy-mini-sdk 1.10.0-dev.0.main.b50d97e → 1.10.0-dev.0.main.c8367cd
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 +98 -0
- package/dist/lib/central-listener.d.ts +71 -0
- package/dist/lib/central-listener.d.ts.map +1 -0
- package/dist/lib/central-listener.js +277 -0
- package/dist/lib/central-listener.js.map +1 -0
- package/dist/lib/central-listener.test.d.ts +2 -0
- package/dist/lib/central-listener.test.d.ts.map +1 -0
- package/dist/lib/central-listener.test.js +378 -0
- package/dist/lib/central-listener.test.js.map +1 -0
- package/dist/lib/dc-watchdog.test.d.ts +2 -0
- package/dist/lib/dc-watchdog.test.d.ts.map +1 -0
- package/dist/lib/dc-watchdog.test.js +154 -0
- package/dist/lib/dc-watchdog.test.js.map +1 -0
- package/dist/lib/ensure-awake.test.d.ts +2 -0
- package/dist/lib/ensure-awake.test.d.ts.map +1 -0
- package/dist/lib/ensure-awake.test.js +117 -0
- package/dist/lib/ensure-awake.test.js.map +1 -0
- package/dist/lib/ice-grace.test.d.ts +2 -0
- package/dist/lib/ice-grace.test.d.ts.map +1 -0
- package/dist/lib/ice-grace.test.js +211 -0
- package/dist/lib/ice-grace.test.js.map +1 -0
- package/dist/lib/imu.test.d.ts +2 -0
- package/dist/lib/imu.test.d.ts.map +1 -0
- package/dist/lib/imu.test.js +78 -0
- package/dist/lib/imu.test.js.map +1 -0
- package/dist/lib/logger.d.ts +38 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +81 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/pending-replies.d.ts +127 -0
- package/dist/lib/pending-replies.d.ts.map +1 -0
- package/dist/lib/pending-replies.js +255 -0
- package/dist/lib/pending-replies.js.map +1 -0
- package/dist/lib/pending-replies.test.d.ts +2 -0
- package/dist/lib/pending-replies.test.d.ts.map +1 -0
- package/dist/lib/pending-replies.test.js +217 -0
- package/dist/lib/pending-replies.test.js.map +1 -0
- package/dist/lib/reachy-mini.d.ts +202 -88
- package/dist/lib/reachy-mini.d.ts.map +1 -1
- package/dist/lib/reachy-mini.js +651 -566
- package/dist/lib/reachy-mini.js.map +1 -1
- package/dist/lib/redial.test.d.ts +2 -0
- package/dist/lib/redial.test.d.ts.map +1 -0
- package/dist/lib/redial.test.js +206 -0
- package/dist/lib/redial.test.js.map +1 -0
- package/dist/lib/request.test.d.ts +2 -0
- package/dist/lib/request.test.d.ts.map +1 -0
- package/dist/lib/request.test.js +90 -0
- package/dist/lib/request.test.js.map +1 -0
- package/dist/lib/session-supervisor.d.ts +201 -0
- package/dist/lib/session-supervisor.d.ts.map +1 -0
- package/dist/lib/session-supervisor.js +520 -0
- package/dist/lib/session-supervisor.js.map +1 -0
- package/dist/lib/token-store.d.ts +50 -0
- package/dist/lib/token-store.d.ts.map +1 -0
- package/dist/lib/token-store.js +120 -0
- package/dist/lib/token-store.js.map +1 -0
- package/dist/lib/token-store.test.d.ts +2 -0
- package/dist/lib/token-store.test.d.ts.map +1 -0
- package/dist/lib/token-store.test.js +82 -0
- package/dist/lib/token-store.test.js.map +1 -0
- package/dist/lib/types.d.ts +280 -6
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/url-helpers.d.ts.map +1 -1
- package/dist/lib/url-helpers.js +3 -1
- package/dist/lib/url-helpers.js.map +1 -1
- package/dist/lib/version.d.ts +2 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +5 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/reachy-mini-sdk.d.ts +15 -1
- package/dist/reachy-mini-sdk.d.ts.map +1 -1
- package/dist/reachy-mini-sdk.js +12 -0
- package/dist/reachy-mini-sdk.js.map +1 -1
- package/host/dist/ReachyHost.d.ts.map +1 -1
- package/host/dist/chunks/centralRest-uXNMidlL.js +2315 -0
- package/host/dist/chunks/index-Beu4GNzi.js +821 -0
- package/host/dist/chunks/{mountHost-DjE5zE9R.js → mountHost-BTTF4Uu9.js} +24933 -27488
- package/host/dist/components/ConnectingView.d.ts.map +1 -1
- package/host/dist/components/DaemonUpdateGate.d.ts +65 -0
- package/host/dist/components/DaemonUpdateGate.d.ts.map +1 -0
- package/host/dist/components/ErrorView.d.ts.map +1 -1
- package/host/dist/components/HostErrorBoundary.d.ts +30 -0
- package/host/dist/components/HostErrorBoundary.d.ts.map +1 -0
- package/host/dist/components/LeavingView.d.ts.map +1 -1
- package/host/dist/components/PickerView.d.ts +16 -14
- package/host/dist/components/PickerView.d.ts.map +1 -1
- package/host/dist/components/PostOAuthSplash.d.ts +14 -9
- package/host/dist/components/PostOAuthSplash.d.ts.map +1 -1
- package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
- package/host/dist/components/SignInView.d.ts.map +1 -1
- package/host/dist/components/TopBar.d.ts +5 -6
- 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 +68 -5
- package/host/dist/embed/index.d.ts.map +1 -1
- package/host/dist/entry/auto.js +2 -2
- package/host/dist/entry/embed.d.ts +5 -4
- package/host/dist/entry/embed.d.ts.map +1 -1
- package/host/dist/entry/embed.js +5 -4
- package/host/dist/hooks/useHostBridge.d.ts +35 -1
- package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
- package/host/dist/hooks/useLatestDaemonVersion.d.ts +7 -0
- package/host/dist/hooks/useLatestDaemonVersion.d.ts.map +1 -0
- package/host/dist/hooks/useOAuth.d.ts +18 -0
- package/host/dist/hooks/useOAuth.d.ts.map +1 -1
- package/host/dist/hooks/useRobots.d.ts +0 -2
- package/host/dist/hooks/useRobots.d.ts.map +1 -1
- package/host/dist/index.d.ts +2 -2
- package/host/dist/index.d.ts.map +1 -1
- package/host/dist/index.js +11 -10
- package/host/dist/lib/daemonRelease.d.ts +84 -0
- package/host/dist/lib/daemonRelease.d.ts.map +1 -0
- package/host/dist/lib/protocol.d.ts +141 -7
- package/host/dist/lib/protocol.d.ts.map +1 -1
- package/host/dist/lib/rebootWatch.d.ts +51 -0
- package/host/dist/lib/rebootWatch.d.ts.map +1 -0
- package/host/dist/lib/sdk-types.d.ts +1 -1
- package/host/dist/lib/sdk-types.d.ts.map +1 -1
- package/host/dist/lib/sdkStaleness.d.ts +78 -0
- package/host/dist/lib/sdkStaleness.d.ts.map +1 -0
- package/host/dist/lib/settings.d.ts +16 -5
- package/host/dist/lib/settings.d.ts.map +1 -1
- package/package.json +11 -4
- package/host/dist/chunks/index-lMs2sCXG.js +0 -439
- package/host/dist/chunks/reachy-mini-CdsdQLIU.js +0 -1523
- package/host/dist/lib/centralListener.d.ts +0 -73
- package/host/dist/lib/centralListener.d.ts.map +0 -1
|
@@ -1,1523 +0,0 @@
|
|
|
1
|
-
var $ = Object.defineProperty, F = (r, e, t) => e in r ? $(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t, k = (r, e, t) => (F(r, typeof e != "symbol" ? e + "" : e, t), t), x = "https://huggingface.co";
|
|
2
|
-
async function C(r, e) {
|
|
3
|
-
var t, s;
|
|
4
|
-
const i = new O(r.url, r.status, (t = r.headers.get("X-Request-Id")) != null ? t : void 0);
|
|
5
|
-
i.message = `Api error with status ${i.statusCode}`;
|
|
6
|
-
const o = [`URL: ${i.url}`, i.requestId ? `Request ID: ${i.requestId}` : void 0].filter(Boolean).join(". ");
|
|
7
|
-
if ((s = r.headers.get("Content-Type")) != null && s.startsWith("application/json")) {
|
|
8
|
-
const n = await r.json();
|
|
9
|
-
i.message = n.error || n.message || i.message, i.data = n;
|
|
10
|
-
} else
|
|
11
|
-
i.data = { message: await r.text() };
|
|
12
|
-
throw i.message += `. ${o}`, i;
|
|
13
|
-
}
|
|
14
|
-
var O = class extends Error {
|
|
15
|
-
constructor(r, e, t, s) {
|
|
16
|
-
super(s), k(this, "statusCode"), k(this, "url"), k(this, "requestId"), k(this, "data"), this.statusCode = e, this.requestId = t, this.url = r;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
new Promise((r) => {
|
|
20
|
-
});
|
|
21
|
-
function H(r) {
|
|
22
|
-
if (globalThis.Buffer)
|
|
23
|
-
return globalThis.Buffer.from(r).toString("base64");
|
|
24
|
-
{
|
|
25
|
-
const e = [];
|
|
26
|
-
return r.forEach((t) => {
|
|
27
|
-
e.push(String.fromCharCode(t));
|
|
28
|
-
}), globalThis.btoa(e.join(""));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
async function M(r) {
|
|
32
|
-
var e, t;
|
|
33
|
-
if (typeof window > "u")
|
|
34
|
-
throw new Error("oauthHandleRedirect is only available in the browser");
|
|
35
|
-
const s = new URLSearchParams(window.location.search), [i, o] = [s.get("error"), s.get("error_description")];
|
|
36
|
-
if (i)
|
|
37
|
-
throw new Error(`${i}: ${o}`);
|
|
38
|
-
const n = s.get("code"), a = localStorage.getItem("huggingface.co:oauth:nonce");
|
|
39
|
-
if (!n)
|
|
40
|
-
throw new Error("Missing oauth code from query parameters in redirected URL");
|
|
41
|
-
if (!a)
|
|
42
|
-
throw new Error("Missing oauth nonce from localStorage");
|
|
43
|
-
const c = localStorage.getItem("huggingface.co:oauth:code_verifier");
|
|
44
|
-
if (!c)
|
|
45
|
-
throw new Error("Missing oauth code_verifier from localStorage");
|
|
46
|
-
const l = s.get("state");
|
|
47
|
-
if (!l)
|
|
48
|
-
throw new Error("Missing oauth state from query parameters in redirected URL");
|
|
49
|
-
let h;
|
|
50
|
-
try {
|
|
51
|
-
h = JSON.parse(l);
|
|
52
|
-
} catch {
|
|
53
|
-
throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: " + l);
|
|
54
|
-
}
|
|
55
|
-
if (h.nonce !== a)
|
|
56
|
-
throw new Error("Invalid oauth state in redirected URL");
|
|
57
|
-
const d = x, m = `${new URL(d).origin}/.well-known/openid-configuration`, f = await fetch(m, {
|
|
58
|
-
headers: {
|
|
59
|
-
Accept: "application/json"
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
if (!f.ok)
|
|
63
|
-
throw await C(f);
|
|
64
|
-
const p = await f.json(), v = await fetch(p.token_endpoint, {
|
|
65
|
-
method: "POST",
|
|
66
|
-
headers: {
|
|
67
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
68
|
-
},
|
|
69
|
-
body: new URLSearchParams({
|
|
70
|
-
grant_type: "authorization_code",
|
|
71
|
-
code: n,
|
|
72
|
-
redirect_uri: h.redirectUri,
|
|
73
|
-
code_verifier: c
|
|
74
|
-
}).toString()
|
|
75
|
-
});
|
|
76
|
-
if (localStorage.removeItem("huggingface.co:oauth:code_verifier"), localStorage.removeItem("huggingface.co:oauth:nonce"), !v.ok)
|
|
77
|
-
throw await C(v);
|
|
78
|
-
const g = await v.json(), w = new Date(Date.now() + g.expires_in * 1e3), S = await fetch(p.userinfo_endpoint, {
|
|
79
|
-
headers: {
|
|
80
|
-
Authorization: `Bearer ${g.access_token}`
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
if (!S.ok)
|
|
84
|
-
throw await C(S);
|
|
85
|
-
const u = await S.json();
|
|
86
|
-
return {
|
|
87
|
-
accessToken: g.access_token,
|
|
88
|
-
accessTokenExpiresAt: w,
|
|
89
|
-
userInfo: {
|
|
90
|
-
id: u.sub,
|
|
91
|
-
name: u.name,
|
|
92
|
-
fullname: u.preferred_username,
|
|
93
|
-
email: u.email,
|
|
94
|
-
emailVerified: u.email_verified,
|
|
95
|
-
avatarUrl: u.picture,
|
|
96
|
-
websiteUrl: u.website,
|
|
97
|
-
isPro: u.isPro,
|
|
98
|
-
orgs: (t = (e = u.orgs) == null ? void 0 : e.map((_) => ({
|
|
99
|
-
id: _.sub,
|
|
100
|
-
name: _.name,
|
|
101
|
-
fullname: _.name,
|
|
102
|
-
isEnterprise: _.isEnterprise,
|
|
103
|
-
canPay: _.canPay,
|
|
104
|
-
avatarUrl: _.picture,
|
|
105
|
-
roleInOrg: _.roleInOrg
|
|
106
|
-
}))) != null ? t : []
|
|
107
|
-
},
|
|
108
|
-
state: h.state,
|
|
109
|
-
scope: g.scope
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
async function N(r) {
|
|
113
|
-
if (typeof window > "u")
|
|
114
|
-
throw new Error("oauthHandleRedirect is only available in the browser");
|
|
115
|
-
const e = new URLSearchParams(window.location.search);
|
|
116
|
-
return e.has("error") ? M() : e.has("code") ? localStorage.getItem("huggingface.co:oauth:nonce") ? M() : (console.warn(
|
|
117
|
-
"Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."
|
|
118
|
-
), !1) : !1;
|
|
119
|
-
}
|
|
120
|
-
async function V(r) {
|
|
121
|
-
var e, t;
|
|
122
|
-
if (typeof window > "u")
|
|
123
|
-
throw new Error("oauthLogin is only available in the browser");
|
|
124
|
-
const s = r?.hubUrl || x, i = `${new URL(s).origin}/.well-known/openid-configuration`, o = await fetch(i, {
|
|
125
|
-
headers: {
|
|
126
|
-
Accept: "application/json"
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
if (!o.ok)
|
|
130
|
-
throw await C(o);
|
|
131
|
-
const n = await o.json(), a = globalThis.crypto.randomUUID(), c = globalThis.crypto.randomUUID() + globalThis.crypto.randomUUID();
|
|
132
|
-
localStorage.setItem("huggingface.co:oauth:nonce", a), localStorage.setItem("huggingface.co:oauth:code_verifier", c);
|
|
133
|
-
const l = r?.redirectUrl || window.location.href, h = JSON.stringify({
|
|
134
|
-
nonce: a,
|
|
135
|
-
redirectUri: l,
|
|
136
|
-
state: r?.state
|
|
137
|
-
}), d = (t = (e = window?.huggingface) == null ? void 0 : e.variables) != null ? t : null, m = r?.clientId || d?.OAUTH_CLIENT_ID;
|
|
138
|
-
if (!m)
|
|
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 = H(
|
|
141
|
-
new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(c)))
|
|
142
|
-
).replace(/[+]/g, "-").replace(/[/]/g, "_").replace(/=/g, "");
|
|
143
|
-
return `${n.authorization_endpoint}?${new URLSearchParams({
|
|
144
|
-
client_id: m,
|
|
145
|
-
scope: r?.scopes || d?.OAUTH_SCOPES || "openid profile",
|
|
146
|
-
response_type: "code",
|
|
147
|
-
redirect_uri: l,
|
|
148
|
-
state: h,
|
|
149
|
-
code_challenge: f,
|
|
150
|
-
code_challenge_method: "S256"
|
|
151
|
-
}).toString()}`;
|
|
152
|
-
}
|
|
153
|
-
function R(r) {
|
|
154
|
-
return r * Math.PI / 180;
|
|
155
|
-
}
|
|
156
|
-
function B(r, e, t) {
|
|
157
|
-
const s = R(r), i = R(e), o = R(t), n = Math.cos(o), a = Math.sin(o), c = Math.cos(i), l = Math.sin(i), h = Math.cos(s), d = Math.sin(s);
|
|
158
|
-
return [
|
|
159
|
-
[n * c, n * l * d - a * h, n * l * h + a * d, 0],
|
|
160
|
-
[a * c, a * l * d + n * h, a * l * h - n * d, 0],
|
|
161
|
-
[-l, c * d, c * h, 0],
|
|
162
|
-
[0, 0, 0, 1]
|
|
163
|
-
];
|
|
164
|
-
}
|
|
165
|
-
function G() {
|
|
166
|
-
if (typeof window > "u" || !window.location.hash) return;
|
|
167
|
-
const r = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
168
|
-
let e;
|
|
169
|
-
try {
|
|
170
|
-
e = new URLSearchParams(r);
|
|
171
|
-
} catch {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const t = e.get("hf_token");
|
|
175
|
-
if (!t) return;
|
|
176
|
-
const s = e.get("hf_username") || "user", i = e.get("hf_token_expires"), o = i && !Number.isNaN(new Date(i).getTime()) ? i : new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toISOString();
|
|
177
|
-
try {
|
|
178
|
-
sessionStorage.setItem("hf_token", t), sessionStorage.setItem("hf_username", s), sessionStorage.setItem("hf_token_expires", o);
|
|
179
|
-
} catch (c) {
|
|
180
|
-
console.warn("[reachy-mini] could not persist pre-seeded HF credentials:", c);
|
|
181
|
-
}
|
|
182
|
-
e.delete("hf_token"), e.delete("hf_username"), e.delete("hf_token_expires");
|
|
183
|
-
const n = e.toString(), a = window.location.pathname + window.location.search + (n ? "#" + n : "");
|
|
184
|
-
try {
|
|
185
|
-
window.history.replaceState(null, "", a);
|
|
186
|
-
} catch {
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function q() {
|
|
190
|
-
if (typeof window > "u") return null;
|
|
191
|
-
if (window.location.hash) {
|
|
192
|
-
const r = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
193
|
-
try {
|
|
194
|
-
const t = new URLSearchParams(r).get("robot_peer_id");
|
|
195
|
-
if (t) return t;
|
|
196
|
-
} catch {
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (window.location.search)
|
|
200
|
-
try {
|
|
201
|
-
const e = new URLSearchParams(window.location.search).get("robot_peer_id");
|
|
202
|
-
if (e) return e;
|
|
203
|
-
} catch {
|
|
204
|
-
}
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
function W(r) {
|
|
208
|
-
const e = r.split(`\r
|
|
209
|
-
`);
|
|
210
|
-
let t = !1;
|
|
211
|
-
for (const s of e)
|
|
212
|
-
if (s.startsWith("m=audio") ? t = !0 : s.startsWith("m=") && (t = !1), t && s === "a=sendrecv") return !0;
|
|
213
|
-
return !1;
|
|
214
|
-
}
|
|
215
|
-
const y = 12 * 1024, A = 1 * 1024 * 1024, z = 512 * 1024;
|
|
216
|
-
function J() {
|
|
217
|
-
return typeof CompressionStream < "u";
|
|
218
|
-
}
|
|
219
|
-
function U() {
|
|
220
|
-
return "u" + Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
|
|
221
|
-
}
|
|
222
|
-
function E(r) {
|
|
223
|
-
let e = "";
|
|
224
|
-
for (let s = 0; s < r.length; s += 32768)
|
|
225
|
-
e += String.fromCharCode.apply(
|
|
226
|
-
null,
|
|
227
|
-
Array.from(r.subarray(s, s + 32768))
|
|
228
|
-
);
|
|
229
|
-
return btoa(e);
|
|
230
|
-
}
|
|
231
|
-
async function X(r) {
|
|
232
|
-
const e = new TextEncoder().encode(r), t = await new Response(
|
|
233
|
-
new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))
|
|
234
|
-
).arrayBuffer();
|
|
235
|
-
return E(new Uint8Array(t));
|
|
236
|
-
}
|
|
237
|
-
function L(r) {
|
|
238
|
-
const e = Math.round(Number(r) || 0);
|
|
239
|
-
return Math.max(0, Math.min(100, e));
|
|
240
|
-
}
|
|
241
|
-
function P(r) {
|
|
242
|
-
switch ((r.type || "").toLowerCase().split(";")[0]?.trim() ?? "") {
|
|
243
|
-
case "audio/ogg":
|
|
244
|
-
case "application/ogg":
|
|
245
|
-
return "ogg-base64";
|
|
246
|
-
case "audio/opus":
|
|
247
|
-
return "opus-base64";
|
|
248
|
-
case "audio/mpeg":
|
|
249
|
-
case "audio/mp3":
|
|
250
|
-
return "mp3-base64";
|
|
251
|
-
case "audio/flac":
|
|
252
|
-
case "audio/x-flac":
|
|
253
|
-
return "flac-base64";
|
|
254
|
-
case "audio/mp4":
|
|
255
|
-
case "audio/m4a":
|
|
256
|
-
case "audio/x-m4a":
|
|
257
|
-
return "m4a-base64";
|
|
258
|
-
case "audio/aac":
|
|
259
|
-
return "aac-base64";
|
|
260
|
-
default:
|
|
261
|
-
return "wav-base64";
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const j = 3e3, D = 1e3, Y = 6e4;
|
|
265
|
-
class K extends EventTarget {
|
|
266
|
-
// ─── Config ──────────────────────────────────────────────────────────
|
|
267
|
-
_signalingUrl;
|
|
268
|
-
_clientId;
|
|
269
|
-
_appName;
|
|
270
|
-
_videoJitterBufferTargetMs;
|
|
271
|
-
_autoStartFromUrl;
|
|
272
|
-
_autoStartAttempted;
|
|
273
|
-
// ─── Public-ish state mirrors ────────────────────────────────────────
|
|
274
|
-
_state = "disconnected";
|
|
275
|
-
_robots = [];
|
|
276
|
-
_robotState = {};
|
|
277
|
-
_preselectedRobotId;
|
|
278
|
-
// ─── Auth ────────────────────────────────────────────────────────────
|
|
279
|
-
_token = null;
|
|
280
|
-
_username = null;
|
|
281
|
-
_tokenExpires = null;
|
|
282
|
-
// ─── Signaling ───────────────────────────────────────────────────────
|
|
283
|
-
_sseAbortController = null;
|
|
284
|
-
// ─── WebRTC ──────────────────────────────────────────────────────────
|
|
285
|
-
_pc = null;
|
|
286
|
-
_dc = null;
|
|
287
|
-
_sessionId = null;
|
|
288
|
-
_selectedRobotId = null;
|
|
289
|
-
_pendingRemoteIce = [];
|
|
290
|
-
// ─── Audio ───────────────────────────────────────────────────────────
|
|
291
|
-
_micStream = null;
|
|
292
|
-
_micMuted = !0;
|
|
293
|
-
_audioMuted = !0;
|
|
294
|
-
_micSupported = !1;
|
|
295
|
-
// ─── Timers ──────────────────────────────────────────────────────────
|
|
296
|
-
_latencyMonitorId = null;
|
|
297
|
-
_stateRefreshInterval = null;
|
|
298
|
-
// ─── Single-slot promise resolvers ───────────────────────────────────
|
|
299
|
-
_versionResolve = null;
|
|
300
|
-
_hardwareIdResolve = null;
|
|
301
|
-
_volumeResolve = null;
|
|
302
|
-
_micVolumeResolve = null;
|
|
303
|
-
_trackedFaceResolve = null;
|
|
304
|
-
// applyAudioConfig() / readAudioParameter() share the same single-slot
|
|
305
|
-
// pattern as the volume helpers. Separate slots so the two can be
|
|
306
|
-
// in-flight concurrently without collision.
|
|
307
|
-
_applyAudioConfigResolve = null;
|
|
308
|
-
_readAudioParameterResolve = null;
|
|
309
|
-
// ─── Log subscribers ─────────────────────────────────────────────────
|
|
310
|
-
_logSubscribers = /* @__PURE__ */ new Set();
|
|
311
|
-
_updateProgressSubscribers = /* @__PURE__ */ new Set();
|
|
312
|
-
// ─── Broadcast waiters (playMove / playUploadedAudio) ────────────────
|
|
313
|
-
_broadcastWaiters = [];
|
|
314
|
-
// ─── Active upload ids for no-arg cancels ────────────────────────────
|
|
315
|
-
_activeMoveUploadId = null;
|
|
316
|
-
_activeAudioUploadId = null;
|
|
317
|
-
// ─── Session promise plumbing ────────────────────────────────────────
|
|
318
|
-
_sessionResolve = null;
|
|
319
|
-
_sessionReject = null;
|
|
320
|
-
_iceConnected = !1;
|
|
321
|
-
_dcOpen = !1;
|
|
322
|
-
// ─── Resilience: ICE-blip debounce + network awareness ──────────────
|
|
323
|
-
// Backs `_scheduleIceGrace` / `_armIceGraceOnVisibility` and the
|
|
324
|
-
// `networkOnline` / `networkOffline` / `networkChange` forwarders.
|
|
325
|
-
// All three handler slots are scoped to the lifetime of a live
|
|
326
|
-
// session (installed in `startSession`, cleared in
|
|
327
|
-
// `stopSession` / `disconnect` / `_handleEndSession` /
|
|
328
|
-
// `_failSessionRejected`).
|
|
329
|
-
_iceGraceTimer = null;
|
|
330
|
-
_iceGraceReason = null;
|
|
331
|
-
_pendingVisibilityHandler = null;
|
|
332
|
-
_onlineHandler = null;
|
|
333
|
-
_offlineHandler = null;
|
|
334
|
-
_connectionChangeHandler = null;
|
|
335
|
-
// ─── Motion completion plumbing (wake_up / goto_sleep) ───────────────
|
|
336
|
-
_pendingMotionCompletions = {
|
|
337
|
-
wake_up: [],
|
|
338
|
-
goto_sleep: []
|
|
339
|
-
};
|
|
340
|
-
// ─── Video element ───────────────────────────────────────────────────
|
|
341
|
-
_videoElement = null;
|
|
342
|
-
constructor(e = {}) {
|
|
343
|
-
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 = q();
|
|
344
|
-
}
|
|
345
|
-
// ─── Read-only properties ────────────────────────────────────────────
|
|
346
|
-
get state() {
|
|
347
|
-
return this._state;
|
|
348
|
-
}
|
|
349
|
-
get robots() {
|
|
350
|
-
return this._robots;
|
|
351
|
-
}
|
|
352
|
-
get robotState() {
|
|
353
|
-
return this._robotState;
|
|
354
|
-
}
|
|
355
|
-
get username() {
|
|
356
|
-
return this._username;
|
|
357
|
-
}
|
|
358
|
-
get isAuthenticated() {
|
|
359
|
-
return !!this._token;
|
|
360
|
-
}
|
|
361
|
-
get micSupported() {
|
|
362
|
-
return this._micSupported;
|
|
363
|
-
}
|
|
364
|
-
get micMuted() {
|
|
365
|
-
return this._micMuted;
|
|
366
|
-
}
|
|
367
|
-
get audioMuted() {
|
|
368
|
-
return this._audioMuted;
|
|
369
|
-
}
|
|
370
|
-
get preselectedRobotId() {
|
|
371
|
-
return this._preselectedRobotId;
|
|
372
|
-
}
|
|
373
|
-
get isEmbedded() {
|
|
374
|
-
return this._preselectedRobotId !== null;
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Internal: try to honour the `autoStartFromUrl` constructor
|
|
378
|
-
* option. Called from the signaling-message handler after every
|
|
379
|
-
* `robotsChanged` emit, so a robot that comes online after the
|
|
380
|
-
* SDK is already `connected` still triggers the auto-start.
|
|
381
|
-
*/
|
|
382
|
-
_maybeAutoStart() {
|
|
383
|
-
if (!this._autoStartFromUrl || this._autoStartAttempted || !this._preselectedRobotId || this._state !== "connected" || !this._robots.find((s) => s.id === this._preselectedRobotId)) return;
|
|
384
|
-
this._autoStartAttempted = !0;
|
|
385
|
-
const t = this._preselectedRobotId;
|
|
386
|
-
setTimeout(() => {
|
|
387
|
-
this._state === "connected" && this.startSession(t).catch((s) => {
|
|
388
|
-
console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:", s);
|
|
389
|
-
});
|
|
390
|
-
}, 0);
|
|
391
|
-
}
|
|
392
|
-
// ─── Auth ────────────────────────────────────────────────────────────
|
|
393
|
-
async authenticate() {
|
|
394
|
-
try {
|
|
395
|
-
G();
|
|
396
|
-
const e = await N();
|
|
397
|
-
if (e)
|
|
398
|
-
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(
|
|
399
|
-
"hf_token_expires",
|
|
400
|
-
typeof this._tokenExpires == "string" ? this._tokenExpires : this._tokenExpires.toISOString()
|
|
401
|
-
), !0;
|
|
402
|
-
const t = sessionStorage.getItem("hf_token"), s = sessionStorage.getItem("hf_username"), i = sessionStorage.getItem("hf_token_expires");
|
|
403
|
-
return t && s && i && new Date(i) > /* @__PURE__ */ new Date() ? (this._token = t, this._username = s, this._tokenExpires = i, !0) : !1;
|
|
404
|
-
} catch (e) {
|
|
405
|
-
return console.error("Auth error:", e), !1;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
async login() {
|
|
409
|
-
const e = {};
|
|
410
|
-
this._clientId && (e.clientId = this._clientId), window.location.href = await V(e);
|
|
411
|
-
}
|
|
412
|
-
logout() {
|
|
413
|
-
sessionStorage.removeItem("hf_token"), sessionStorage.removeItem("hf_username"), sessionStorage.removeItem("hf_token_expires"), this._username = null, this._tokenExpires = null, this.disconnect();
|
|
414
|
-
}
|
|
415
|
-
// ─── Lifecycle ───────────────────────────────────────────────────────
|
|
416
|
-
async connect(e) {
|
|
417
|
-
if (this._state !== "disconnected") throw new Error("Already connected");
|
|
418
|
-
if (e && (this._token = e), !this._token) throw new Error("No token — call authenticate() first or pass a token");
|
|
419
|
-
this._sseAbortController = new AbortController();
|
|
420
|
-
let t;
|
|
421
|
-
try {
|
|
422
|
-
t = await fetch(
|
|
423
|
-
`${this._signalingUrl}/events`,
|
|
424
|
-
{
|
|
425
|
-
signal: this._sseAbortController.signal,
|
|
426
|
-
headers: { Authorization: `Bearer ${this._token}` }
|
|
427
|
-
}
|
|
428
|
-
);
|
|
429
|
-
} catch (s) {
|
|
430
|
-
throw this._sseAbortController = null, s;
|
|
431
|
-
}
|
|
432
|
-
if (!t.ok)
|
|
433
|
-
throw this._sseAbortController = null, new Error(`HTTP ${t.status}`);
|
|
434
|
-
return new Promise((s, i) => {
|
|
435
|
-
let o = !1;
|
|
436
|
-
const n = t.body.getReader(), a = new TextDecoder();
|
|
437
|
-
let c = "";
|
|
438
|
-
(async () => {
|
|
439
|
-
try {
|
|
440
|
-
for (; ; ) {
|
|
441
|
-
const { done: h, value: d } = await n.read();
|
|
442
|
-
if (h) break;
|
|
443
|
-
c += a.decode(d, { stream: !0 });
|
|
444
|
-
const m = c.split(`
|
|
445
|
-
`);
|
|
446
|
-
c = m.pop() ?? "";
|
|
447
|
-
for (const f of m)
|
|
448
|
-
if (f.startsWith("data:"))
|
|
449
|
-
try {
|
|
450
|
-
const p = JSON.parse(f.slice(5).trim());
|
|
451
|
-
!o && p.type === "welcome" && p.peerId && (o = !0, this._state = "connected", await this._sendToServer({
|
|
452
|
-
type: "setPeerStatus",
|
|
453
|
-
roles: ["listener"],
|
|
454
|
-
meta: { name: this._appName }
|
|
455
|
-
}), this._emit("connected", { peerId: p.peerId }), s()), this._handleSignalingMessage(p);
|
|
456
|
-
} catch {
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
} catch (h) {
|
|
460
|
-
if (h.name !== "AbortError" && this._emit("error", { source: "signaling", error: h }), !o) {
|
|
461
|
-
i(h);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
this._state !== "disconnected" && (this._state = "disconnected", this._emit("disconnected", { reason: "SSE closed" })), o || i(new Error("Connection closed before welcome"));
|
|
466
|
-
})();
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
async autoConnect(e = {}) {
|
|
470
|
-
const {
|
|
471
|
-
token: t,
|
|
472
|
-
pickRobot: s,
|
|
473
|
-
autoPickIfSingle: i = !0,
|
|
474
|
-
filterBusy: o = !0,
|
|
475
|
-
wakeOnConnect: n = !0
|
|
476
|
-
} = e;
|
|
477
|
-
if (this._state === "streaming") {
|
|
478
|
-
const c = this._robots?.find((l) => l.id === this._selectedRobotId);
|
|
479
|
-
return {
|
|
480
|
-
robotId: this._selectedRobotId,
|
|
481
|
-
robotName: c?.meta?.name ?? null,
|
|
482
|
-
isEmbedded: this.isEmbedded,
|
|
483
|
-
alreadyStreaming: !0
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
const a = this._autoStartFromUrl;
|
|
487
|
-
this._autoStartFromUrl = !1;
|
|
488
|
-
try {
|
|
489
|
-
if (t)
|
|
490
|
-
this._token = t;
|
|
491
|
-
else if (!this._token && !await this.authenticate())
|
|
492
|
-
throw new Error("Not authenticated — call login() or pass a token");
|
|
493
|
-
this._state === "disconnected" && await this.connect();
|
|
494
|
-
let c, l = null;
|
|
495
|
-
if (this.isEmbedded) {
|
|
496
|
-
c = this._preselectedRobotId;
|
|
497
|
-
try {
|
|
498
|
-
await this._waitForRobotInList(c, 5e3);
|
|
499
|
-
} catch {
|
|
500
|
-
}
|
|
501
|
-
l = this._robots?.find((d) => d.id === c)?.meta?.name ?? null;
|
|
502
|
-
} else {
|
|
503
|
-
const h = await this._fetchOwnedRobots({ filterBusy: o });
|
|
504
|
-
if (h.length === 0)
|
|
505
|
-
throw new Error("No reachable robots");
|
|
506
|
-
if (i && h.length === 1 && !h[0].busy)
|
|
507
|
-
c = h[0].id, l = h[0].name;
|
|
508
|
-
else if (s) {
|
|
509
|
-
const d = await s(h);
|
|
510
|
-
if (!d) throw new Error("Robot selection cancelled");
|
|
511
|
-
c = d, l = h.find((m) => m.id === d)?.name ?? null;
|
|
512
|
-
} else
|
|
513
|
-
throw new Error(
|
|
514
|
-
"Multiple robots available — pass a pickRobot callback to autoConnect()"
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
if (await this.startSession(c), n && typeof this.ensureAwake == "function")
|
|
518
|
-
try {
|
|
519
|
-
await this.ensureAwake();
|
|
520
|
-
} catch (h) {
|
|
521
|
-
console.warn("[reachy-mini] autoConnect: ensureAwake failed:", h);
|
|
522
|
-
}
|
|
523
|
-
return { robotId: c, robotName: l, isEmbedded: this.isEmbedded };
|
|
524
|
-
} finally {
|
|
525
|
-
this._autoStartFromUrl = a;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
async _fetchOwnedRobots({ filterBusy: e = !0 } = {}) {
|
|
529
|
-
try {
|
|
530
|
-
const t = await fetch(`${this._signalingUrl}/api/robot-status`, {
|
|
531
|
-
headers: { Authorization: `Bearer ${this._token}` }
|
|
532
|
-
});
|
|
533
|
-
if (!t.ok) throw new Error(`HTTP ${t.status}`);
|
|
534
|
-
const s = await t.json(), i = /* @__PURE__ */ new Map();
|
|
535
|
-
for (const o of s.robots || []) {
|
|
536
|
-
if (e && o.busy) continue;
|
|
537
|
-
const n = o.meta?.install_id ?? o.meta?.hardware_id ?? o.peerId;
|
|
538
|
-
i.set(n, {
|
|
539
|
-
id: o.peerId,
|
|
540
|
-
name: o.robotName ?? o.meta?.name ?? null,
|
|
541
|
-
busy: !!o.busy,
|
|
542
|
-
activeApp: o.activeApp ?? null,
|
|
543
|
-
meta: o.meta ?? {},
|
|
544
|
-
lastSeenAgeSeconds: o.last_seen_age_seconds ?? null
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
return Array.from(i.values()).sort(
|
|
548
|
-
(o, n) => (o.lastSeenAgeSeconds ?? 1 / 0) - (n.lastSeenAgeSeconds ?? 1 / 0)
|
|
549
|
-
);
|
|
550
|
-
} catch (t) {
|
|
551
|
-
return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:", t), (this._robots || []).map((s) => ({
|
|
552
|
-
id: s.id,
|
|
553
|
-
name: s.meta?.name ?? null,
|
|
554
|
-
busy: !1,
|
|
555
|
-
activeApp: null,
|
|
556
|
-
meta: s.meta ?? {},
|
|
557
|
-
lastSeenAgeSeconds: null
|
|
558
|
-
}));
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
_waitForRobotInList(e, t) {
|
|
562
|
-
return this._robots?.find((s) => s.id === e) ? Promise.resolve() : new Promise((s, i) => {
|
|
563
|
-
const o = () => {
|
|
564
|
-
this._robots?.find((a) => a.id === e) && (this.removeEventListener("robotsChanged", o), clearTimeout(n), s());
|
|
565
|
-
}, n = setTimeout(() => {
|
|
566
|
-
this.removeEventListener("robotsChanged", o), i(new Error(`Timeout waiting for robot ${e} in list`));
|
|
567
|
-
}, t);
|
|
568
|
-
this.addEventListener("robotsChanged", o);
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
async startSession(e) {
|
|
572
|
-
if (this._state !== "connected") throw new Error("Not connected");
|
|
573
|
-
this._selectedRobotId = e, this._iceConnected = !1, this._dcOpen = !1, this._micSupported = !1, this._pendingRemoteIce = [];
|
|
574
|
-
try {
|
|
575
|
-
const t = window, s = t.AudioContext ?? t.webkitAudioContext;
|
|
576
|
-
if (!s) throw new Error("AudioContext not supported");
|
|
577
|
-
const i = new s(), o = i.createMediaStreamDestination(), n = i.createOscillator(), a = i.createGain();
|
|
578
|
-
a.gain.value = 0, n.connect(a).connect(o), n.start();
|
|
579
|
-
const c = o.stream;
|
|
580
|
-
c.getAudioTracks().forEach((l) => {
|
|
581
|
-
l.enabled = !1;
|
|
582
|
-
}), this._micStream = c, this._micMuted = !0;
|
|
583
|
-
} catch (t) {
|
|
584
|
-
console.warn("Audio sender placeholder setup failed:", t), this._micStream = null;
|
|
585
|
-
}
|
|
586
|
-
return this._pc = new RTCPeerConnection({
|
|
587
|
-
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
|
588
|
-
}), this._installNetworkListeners(), new Promise((t, s) => {
|
|
589
|
-
this._sessionResolve = t, this._sessionReject = s, this._pc.ontrack = (i) => {
|
|
590
|
-
if (i.track.kind === "video") {
|
|
591
|
-
const o = this._videoJitterBufferTargetMs;
|
|
592
|
-
try {
|
|
593
|
-
i.receiver.jitterBufferTarget = o;
|
|
594
|
-
} catch {
|
|
595
|
-
}
|
|
596
|
-
try {
|
|
597
|
-
i.receiver.playoutDelayHint = o / 1e3;
|
|
598
|
-
} catch {
|
|
599
|
-
}
|
|
600
|
-
this._emit("videoTrack", { track: i.track, stream: i.streams[0] });
|
|
601
|
-
}
|
|
602
|
-
}, this._pc.onicecandidate = async (i) => {
|
|
603
|
-
i.candidate && this._sessionId && await this._sendToServer({
|
|
604
|
-
type: "peer",
|
|
605
|
-
sessionId: this._sessionId,
|
|
606
|
-
ice: {
|
|
607
|
-
candidate: i.candidate.candidate,
|
|
608
|
-
sdpMLineIndex: i.candidate.sdpMLineIndex,
|
|
609
|
-
sdpMid: i.candidate.sdpMid
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
}, this._pc.oniceconnectionstatechange = () => {
|
|
613
|
-
const i = this._pc?.iceConnectionState;
|
|
614
|
-
if (i) {
|
|
615
|
-
if (this._emit("iceStateChange", { state: i }), i === "connected" || i === "completed") {
|
|
616
|
-
this._clearIceGrace(), this._iceConnected = !0, this._checkSessionReady();
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
if (i === "disconnected") {
|
|
620
|
-
typeof document < "u" && document.hidden ? this._armIceGraceOnVisibility() : this._scheduleIceGrace(j, "disconnected");
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
if (i === "failed") {
|
|
624
|
-
this._scheduleIceGrace(D, "failed");
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}, this._pc.ondatachannel = (i) => {
|
|
629
|
-
this._dc = i.channel, this._dc.onopen = () => {
|
|
630
|
-
this._dcOpen = !0, this._checkSessionReady();
|
|
631
|
-
}, this._dc.onmessage = (o) => this._handleRobotMessage(JSON.parse(o.data));
|
|
632
|
-
}, this._sendToServer({ type: "startSession", peerId: e }).then((i) => {
|
|
633
|
-
if (i?.type === "sessionRejected") {
|
|
634
|
-
this._failSessionRejected(i);
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
i?.sessionId && (this._sessionId = i.sessionId);
|
|
638
|
-
});
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
_failSessionRejected(e) {
|
|
642
|
-
const t = new Error(
|
|
643
|
-
e.reason === "robot_busy" ? `Robot is busy: "${e.activeApp || "another app"}" is already connected` : `Session rejected: ${e.reason || "unknown reason"}`
|
|
644
|
-
);
|
|
645
|
-
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((s) => s.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) {
|
|
646
|
-
const s = this._sessionReject;
|
|
647
|
-
this._sessionResolve = null, this._sessionReject = null, s(t);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
async stopSession() {
|
|
651
|
-
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._trackedFaceResolve && (this._trackedFaceResolve(null), this._trackedFaceResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._updateProgressSubscribers.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" }));
|
|
652
|
-
}
|
|
653
|
-
disconnect() {
|
|
654
|
-
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._trackedFaceResolve && (this._trackedFaceResolve(null), this._trackedFaceResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._updateProgressSubscribers.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" });
|
|
655
|
-
}
|
|
656
|
-
// ─── Resilience: ICE-blip debounce + network awareness ───────────────
|
|
657
|
-
//
|
|
658
|
-
// Both halves below are intentionally generic (they don't know about
|
|
659
|
-
// motion, audio, or the FSM): they just smooth out browser-level
|
|
660
|
-
// events so the consumer's own state machine doesn't get torn down
|
|
661
|
-
// by routine WiFi/4G/screen-off noise.
|
|
662
|
-
/**
|
|
663
|
-
* Cancel any pending ICE grace timer and visibility handler. Called
|
|
664
|
-
* on a healed `connected`/`completed` transition AND from the
|
|
665
|
-
* lifecycle teardown paths so a callback can't fire after `_pc`
|
|
666
|
-
* is closed.
|
|
667
|
-
*/
|
|
668
|
-
_clearIceGrace() {
|
|
669
|
-
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;
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Start a grace window. After `ms`, re-check the live ICE state:
|
|
673
|
-
* - If we healed back to `connected`/`completed`, the timer was
|
|
674
|
-
* already cancelled in `oniceconnectionstatechange`, so we
|
|
675
|
-
* never get here.
|
|
676
|
-
* - If we're still in the originally-observed bad state (or
|
|
677
|
-
* worse), surface the error and reject any pending session
|
|
678
|
-
* promise. The original code path is preserved verbatim so
|
|
679
|
-
* downstream consumers see the same `error` payload shape.
|
|
680
|
-
*/
|
|
681
|
-
_scheduleIceGrace(e, t) {
|
|
682
|
-
if (this._iceGraceTimer !== null) {
|
|
683
|
-
if (this._iceGraceReason === t) return;
|
|
684
|
-
clearTimeout(this._iceGraceTimer);
|
|
685
|
-
}
|
|
686
|
-
this._iceGraceReason = t, this._iceGraceTimer = setTimeout(() => {
|
|
687
|
-
this._iceGraceTimer = null;
|
|
688
|
-
const s = this._iceGraceReason;
|
|
689
|
-
this._iceGraceReason = null;
|
|
690
|
-
const i = this._pc?.iceConnectionState;
|
|
691
|
-
if (!(i === "connected" || i === "completed")) {
|
|
692
|
-
if (s === "disconnected" && i === "disconnected") {
|
|
693
|
-
this._emit("error", {
|
|
694
|
-
source: "webrtc",
|
|
695
|
-
error: new Error(`ICE stuck in 'disconnected' for > ${e}ms`)
|
|
696
|
-
});
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (s === "failed" || i === "failed") {
|
|
700
|
-
const o = new Error("ICE connection failed");
|
|
701
|
-
this._sessionReject && (this._sessionReject(o), this._sessionResolve = null, this._sessionReject = null), this._emit("error", { source: "webrtc", error: o });
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}, e);
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* `disconnected` while the tab is hidden. JS timers are throttled
|
|
708
|
-
* in background tabs (Chrome clamps to ~1 Hz, Safari can pause
|
|
709
|
-
* altogether), so a foreground grace timer would either miss the
|
|
710
|
-
* window or fire long after the connection healed. Wait for the
|
|
711
|
-
* tab to come back, then re-evaluate.
|
|
712
|
-
*/
|
|
713
|
-
_armIceGraceOnVisibility() {
|
|
714
|
-
if (this._pendingVisibilityHandler) return;
|
|
715
|
-
const e = Date.now(), t = () => {
|
|
716
|
-
if (typeof document < "u" && document.hidden || (document.removeEventListener("visibilitychange", t), this._pendingVisibilityHandler = null, !this._pc)) return;
|
|
717
|
-
const s = this._pc.iceConnectionState;
|
|
718
|
-
if (!(s === "connected" || s === "completed")) {
|
|
719
|
-
if (Date.now() - e > Y) {
|
|
720
|
-
const i = new Error(
|
|
721
|
-
"Session expired while tab was backgrounded"
|
|
722
|
-
);
|
|
723
|
-
this._sessionReject && (this._sessionReject(i), this._sessionResolve = null, this._sessionReject = null), this._emit("error", { source: "webrtc", error: i });
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
if (s === "failed") {
|
|
727
|
-
this._scheduleIceGrace(D, "failed");
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
this._scheduleIceGrace(j, "disconnected");
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
document.addEventListener("visibilitychange", t), this._pendingVisibilityHandler = t;
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Install browser-level network listeners and forward them as
|
|
737
|
-
* public `networkOnline` / `networkOffline` / `networkChange`
|
|
738
|
-
* events on this instance. Idempotent: called from
|
|
739
|
-
* `startSession()`, removed by `_uninstallNetworkListeners` on
|
|
740
|
-
* teardown. Reachable only when there's a live `window`
|
|
741
|
-
* (defensive guard for SSR / test environments).
|
|
742
|
-
*
|
|
743
|
-
* `online` / `offline` are semantically about CONNECTIVITY:
|
|
744
|
-
* "does the OS think we can reach the internet". They flip
|
|
745
|
-
* symmetrically.
|
|
746
|
-
*
|
|
747
|
-
* `connection.change` (NetworkInformation API, Chrome / Android
|
|
748
|
-
* WebView only) is semantically about the TRANSPORT: it fires
|
|
749
|
-
* on Wi-Fi → 4G swaps, AP roams, etc. without necessarily going
|
|
750
|
-
* through `offline`. We forward it as its own `networkChange`
|
|
751
|
-
* event rather than aliasing it onto `networkOnline`, so
|
|
752
|
-
* consumers don't have to guess whether they're seeing a real
|
|
753
|
-
* connectivity recovery or a silent transport swap.
|
|
754
|
-
*/
|
|
755
|
-
_installNetworkListeners() {
|
|
756
|
-
if (this._onlineHandler || typeof window > "u") return;
|
|
757
|
-
const e = () => this._emit("networkOnline", {}), t = () => this._emit("networkOffline", {});
|
|
758
|
-
window.addEventListener("online", e), window.addEventListener("offline", t), this._onlineHandler = e, this._offlineHandler = t;
|
|
759
|
-
const s = navigator.connection;
|
|
760
|
-
if (s && typeof s.addEventListener == "function") {
|
|
761
|
-
const i = () => this._emit("networkChange", {
|
|
762
|
-
effectiveType: s.effectiveType,
|
|
763
|
-
downlink: s.downlink,
|
|
764
|
-
rtt: s.rtt,
|
|
765
|
-
saveData: s.saveData
|
|
766
|
-
});
|
|
767
|
-
s.addEventListener("change", i), this._connectionChangeHandler = i;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
/** Counterpart to `_installNetworkListeners`. */
|
|
771
|
-
_uninstallNetworkListeners() {
|
|
772
|
-
typeof window < "u" && (this._onlineHandler && window.removeEventListener("online", this._onlineHandler), this._offlineHandler && window.removeEventListener("offline", this._offlineHandler));
|
|
773
|
-
const e = navigator.connection;
|
|
774
|
-
e && this._connectionChangeHandler && typeof e.removeEventListener == "function" && e.removeEventListener("change", this._connectionChangeHandler), this._onlineHandler = null, this._offlineHandler = null, this._connectionChangeHandler = null;
|
|
775
|
-
}
|
|
776
|
-
// ─── Commands ────────────────────────────────────────────────────────
|
|
777
|
-
/**
|
|
778
|
-
* Atomic raw-units pose update over the data channel. Channels you
|
|
779
|
-
* omit are held at their last commanded value (per-axis, independent).
|
|
780
|
-
*
|
|
781
|
-
* **Head pose is in the WORLD frame.** The daemon's IK splits the
|
|
782
|
-
* requested head world-yaw between body rotation and the stewart
|
|
783
|
-
* platform, subject to the mechanical limit
|
|
784
|
-
* `|head_yaw_world − body_yaw| ≤ 65°`.
|
|
785
|
-
*
|
|
786
|
-
* **If you want the head to FOLLOW the body** (tank-style rotation):
|
|
787
|
-
* a `setTarget({ body_yaw })` on its own does NOT rotate the head —
|
|
788
|
-
* the head's commanded world yaw is unchanged, so its gaze stays
|
|
789
|
-
* pinned in world frame while the body turns under it. To make the
|
|
790
|
-
* head turn with the body, include a `head` matrix in the SAME call
|
|
791
|
-
* with the body-yaw delta added to the head RPY's yaw:
|
|
792
|
-
*
|
|
793
|
-
* ```ts
|
|
794
|
-
* // Body-yaw drag handler: tank-couple the head so it follows.
|
|
795
|
-
* const delta = newBodyDeg - lastCommandedBodyDeg;
|
|
796
|
-
* const nextHeadYaw = lastCommandedHeadYawDeg + delta;
|
|
797
|
-
* robot.setTarget({
|
|
798
|
-
* head: rpyToMatrix(headRoll, headPitch, nextHeadYaw).flat(),
|
|
799
|
-
* body_yaw: degToRad(newBodyDeg),
|
|
800
|
-
* });
|
|
801
|
-
* lastCommandedHeadYawDeg = nextHeadYaw;
|
|
802
|
-
* lastCommandedBodyDeg = newBodyDeg;
|
|
803
|
-
* ```
|
|
804
|
-
*
|
|
805
|
-
* **Baseline must be the last COMMANDED value, not telemetry.** For
|
|
806
|
-
* continuous-input controllers (slider drag, joystick), do not use
|
|
807
|
-
* `state.head` from the `state` event as the baseline for incremental
|
|
808
|
-
* commands — telemetry lags one WebRTC round-trip, so cumulative
|
|
809
|
-
* deltas computed against it stall (every iteration in a rapid drag
|
|
810
|
-
* adds the same `delta` to the same stale baseline → the head fails
|
|
811
|
-
* to keep up). Track the last-commanded RPY in your own buffer.
|
|
812
|
-
*
|
|
813
|
-
* @param head Flat row-major 4×4 matrix (16 finite numbers) in
|
|
814
|
-
* the world frame. Omit to hold the previous head target.
|
|
815
|
-
* @param antennas `[rightRad, leftRad]` (radians). Omit to hold.
|
|
816
|
-
* @param body_yaw Signed radians. Omit to hold.
|
|
817
|
-
* @returns `true` if the command was queued on the data channel,
|
|
818
|
-
* `false` if the channel is not open.
|
|
819
|
-
*/
|
|
820
|
-
setTarget({ head: e, antennas: t, body_yaw: s } = {}) {
|
|
821
|
-
const i = { type: "set_full_target" };
|
|
822
|
-
if (e !== void 0) {
|
|
823
|
-
if (!Array.isArray(e) || e.length !== 16 || !e.every((o) => Number.isFinite(o)))
|
|
824
|
-
throw new TypeError(
|
|
825
|
-
`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}`
|
|
826
|
-
);
|
|
827
|
-
i.head = e;
|
|
828
|
-
}
|
|
829
|
-
if (t !== void 0) {
|
|
830
|
-
if (!Array.isArray(t) || t.length !== 2 || !t.every((o) => Number.isFinite(o)))
|
|
831
|
-
throw new TypeError(
|
|
832
|
-
`setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
|
|
833
|
-
);
|
|
834
|
-
i.antennas = t;
|
|
835
|
-
}
|
|
836
|
-
if (s !== void 0) {
|
|
837
|
-
if (!Number.isFinite(s))
|
|
838
|
-
throw new TypeError(
|
|
839
|
-
`setTarget: body_yaw must be a finite number (radians); got ${s}`
|
|
840
|
-
);
|
|
841
|
-
i.body_yaw = s;
|
|
842
|
-
}
|
|
843
|
-
return this._sendCommand(i);
|
|
844
|
-
}
|
|
845
|
-
gotoTarget({ head: e, antennas: t, body_yaw: s, duration: i }) {
|
|
846
|
-
const o = { type: "goto_target" };
|
|
847
|
-
if (e !== void 0) {
|
|
848
|
-
if (!Array.isArray(e) || e.length !== 16 || !e.every((n) => Number.isFinite(n)))
|
|
849
|
-
throw new TypeError(
|
|
850
|
-
`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}`
|
|
851
|
-
);
|
|
852
|
-
o.head = e;
|
|
853
|
-
}
|
|
854
|
-
if (t !== void 0) {
|
|
855
|
-
if (!Array.isArray(t) || t.length !== 2 || !t.every((n) => Number.isFinite(n)))
|
|
856
|
-
throw new TypeError(
|
|
857
|
-
`gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
|
|
858
|
-
);
|
|
859
|
-
o.antennas = t;
|
|
860
|
-
}
|
|
861
|
-
if (s !== void 0) {
|
|
862
|
-
if (!Number.isFinite(s))
|
|
863
|
-
throw new TypeError(
|
|
864
|
-
`gotoTarget: body_yaw must be a finite number (radians); got ${s}`
|
|
865
|
-
);
|
|
866
|
-
o.body_yaw = s;
|
|
867
|
-
}
|
|
868
|
-
if (!Number.isFinite(i) || i <= 0)
|
|
869
|
-
throw new TypeError(
|
|
870
|
-
`gotoTarget: duration must be a positive finite number (seconds); got ${i}`
|
|
871
|
-
);
|
|
872
|
-
return o.duration = i, this._sendCommand(o);
|
|
873
|
-
}
|
|
874
|
-
setHeadRpyDeg(e, t, s) {
|
|
875
|
-
return this.setTarget({ head: B(e, t, s).flat() });
|
|
876
|
-
}
|
|
877
|
-
setAntennasDeg(e, t) {
|
|
878
|
-
return this.setTarget({ antennas: [R(e), R(t)] });
|
|
879
|
-
}
|
|
880
|
-
setBodyYawDeg(e) {
|
|
881
|
-
return this.setTarget({ body_yaw: R(e) });
|
|
882
|
-
}
|
|
883
|
-
playSound(e) {
|
|
884
|
-
return this._sendCommand({ type: "play_sound", file: e });
|
|
885
|
-
}
|
|
886
|
-
clearIncomingAudio() {
|
|
887
|
-
return this._sendCommand({ type: "clear_incoming_audio" });
|
|
888
|
-
}
|
|
889
|
-
startHeadTracking(e = 1) {
|
|
890
|
-
if (!Number.isFinite(e))
|
|
891
|
-
throw new TypeError(`startHeadTracking: weight must be a finite number; got ${e}`);
|
|
892
|
-
const t = Math.min(Math.max(e, 0), 1);
|
|
893
|
-
return this._sendCommand({
|
|
894
|
-
type: "set_head_tracking",
|
|
895
|
-
enabled: !0,
|
|
896
|
-
weight: t
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
stopHeadTracking() {
|
|
900
|
-
return this._sendCommand({ type: "set_head_tracking", enabled: !1 });
|
|
901
|
-
}
|
|
902
|
-
getTrackedFace() {
|
|
903
|
-
return this._slotRoundtrip(
|
|
904
|
-
() => this._trackedFaceResolve,
|
|
905
|
-
(e) => {
|
|
906
|
-
this._trackedFaceResolve = e;
|
|
907
|
-
},
|
|
908
|
-
{ type: "get_tracked_face" }
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Trigger a PyPI update of the daemon over the data channel. Remote
|
|
913
|
-
* counterpart of `POST /update/start`. The daemon acks then restarts
|
|
914
|
-
* itself once the install finishes, which tears this session down -
|
|
915
|
-
* the caller is expected to reconnect afterwards.
|
|
916
|
-
*
|
|
917
|
-
* Pass `onProgress` to receive `update_progress` events (one per log
|
|
918
|
-
* line of the update job). A *successful* update restarts the daemon
|
|
919
|
-
* before a `done` event can arrive, so treat the session teardown +
|
|
920
|
-
* a successful reconnect as the success signal; `onProgress` will fire
|
|
921
|
-
* with `status: 'failed'` if the install errors before the restart.
|
|
922
|
-
*
|
|
923
|
-
* Returns `false` if the data channel isn't open.
|
|
924
|
-
*/
|
|
925
|
-
startDaemonUpdate({ preRelease: e = !1, onProgress: t } = {}) {
|
|
926
|
-
return t && this._updateProgressSubscribers.add(t), this._sendCommand({ type: "start_update", pre_release: e });
|
|
927
|
-
}
|
|
928
|
-
setMotorMode(e) {
|
|
929
|
-
return this._sendCommand({ type: "set_motor_mode", mode: e });
|
|
930
|
-
}
|
|
931
|
-
setMotorTorque(e, t = null) {
|
|
932
|
-
return this._sendCommand({ type: "set_torque", on: e, ids: t });
|
|
933
|
-
}
|
|
934
|
-
wakeUp({ timeoutMs: e = 8e3 } = {}) {
|
|
935
|
-
return this._sendCommand({ type: "set_motor_mode", mode: "enabled" }), this._sendCommandAwaitCompletion("wake_up", e);
|
|
936
|
-
}
|
|
937
|
-
gotoSleep({ timeoutMs: e = 8e3 } = {}) {
|
|
938
|
-
return this._sendCommandAwaitCompletion("goto_sleep", e);
|
|
939
|
-
}
|
|
940
|
-
_sendCommandAwaitCompletion(e, t) {
|
|
941
|
-
return this._sendCommand({ type: e }) ? new Promise((s, i) => {
|
|
942
|
-
const o = {
|
|
943
|
-
resolve: s,
|
|
944
|
-
reject: i,
|
|
945
|
-
timer: setTimeout(() => {
|
|
946
|
-
const n = this._pendingMotionCompletions[e], a = n.indexOf(o);
|
|
947
|
-
a !== -1 && n.splice(a, 1), i(new Error(`${e} timed out after ${t}ms`));
|
|
948
|
-
}, t)
|
|
949
|
-
};
|
|
950
|
-
this._pendingMotionCompletions[e].push(o);
|
|
951
|
-
}) : Promise.reject(new Error(`${e}: data channel not open`));
|
|
952
|
-
}
|
|
953
|
-
_rejectPendingMotionCompletions(e) {
|
|
954
|
-
for (const t of Object.keys(this._pendingMotionCompletions)) {
|
|
955
|
-
const s = this._pendingMotionCompletions[t];
|
|
956
|
-
for (; s.length; ) {
|
|
957
|
-
const i = s.shift();
|
|
958
|
-
clearTimeout(i.timer), i.reject(e);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
isAwake() {
|
|
963
|
-
const e = this._robotState?.motor_mode;
|
|
964
|
-
return e === "enabled" || e === "gravity_compensation";
|
|
965
|
-
}
|
|
966
|
-
async ensureAwake(e = 1e3) {
|
|
967
|
-
return this._robotState?.motor_mode === void 0 && await new Promise((t) => {
|
|
968
|
-
const s = () => {
|
|
969
|
-
this.removeEventListener("state", s), clearTimeout(i), t();
|
|
970
|
-
}, i = setTimeout(s, e);
|
|
971
|
-
this.addEventListener("state", s), this.requestState();
|
|
972
|
-
}), this.isAwake() || this.wakeUp().catch(() => {
|
|
973
|
-
}), !0;
|
|
974
|
-
}
|
|
975
|
-
getVersion() {
|
|
976
|
-
return new Promise((e, t) => {
|
|
977
|
-
if (!this._dc || this._dc.readyState !== "open") {
|
|
978
|
-
t(new Error("Data channel not open"));
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
this._versionResolve && this._versionResolve(null), this._versionResolve = e, this._sendCommand({ type: "get_version" });
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
getHardwareId() {
|
|
985
|
-
return new Promise((e, t) => {
|
|
986
|
-
if (!this._dc || this._dc.readyState !== "open") {
|
|
987
|
-
t(new Error("Data channel not open"));
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
this._hardwareIdResolve && this._hardwareIdResolve(null), this._hardwareIdResolve = e, this._sendCommand({ type: "get_hardware_id" });
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
getVolume() {
|
|
994
|
-
return this._slotRoundtrip(
|
|
995
|
-
() => this._volumeResolve,
|
|
996
|
-
(e) => {
|
|
997
|
-
this._volumeResolve = e;
|
|
998
|
-
},
|
|
999
|
-
{ type: "get_volume" }
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
setVolume(e) {
|
|
1003
|
-
return this._slotRoundtrip(
|
|
1004
|
-
() => this._volumeResolve,
|
|
1005
|
-
(t) => {
|
|
1006
|
-
this._volumeResolve = t;
|
|
1007
|
-
},
|
|
1008
|
-
{ type: "set_volume", volume: L(e) }
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
getMicrophoneVolume() {
|
|
1012
|
-
return this._slotRoundtrip(
|
|
1013
|
-
() => this._micVolumeResolve,
|
|
1014
|
-
(e) => {
|
|
1015
|
-
this._micVolumeResolve = e;
|
|
1016
|
-
},
|
|
1017
|
-
{ type: "get_microphone_volume" }
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
setMicrophoneVolume(e) {
|
|
1021
|
-
return this._slotRoundtrip(
|
|
1022
|
-
() => this._micVolumeResolve,
|
|
1023
|
-
(t) => {
|
|
1024
|
-
this._micVolumeResolve = t;
|
|
1025
|
-
},
|
|
1026
|
-
{ type: "set_microphone_volume", volume: L(e) }
|
|
1027
|
-
);
|
|
1028
|
-
}
|
|
1029
|
-
applyAudioConfig(e, { verify: t = !0 } = {}) {
|
|
1030
|
-
return this._slotRoundtrip(
|
|
1031
|
-
() => this._applyAudioConfigResolve,
|
|
1032
|
-
(s) => {
|
|
1033
|
-
this._applyAudioConfigResolve = s;
|
|
1034
|
-
},
|
|
1035
|
-
{ type: "apply_audio_config", config: e, verify: t }
|
|
1036
|
-
).then((s) => s === !0);
|
|
1037
|
-
}
|
|
1038
|
-
readAudioParameter(e) {
|
|
1039
|
-
return this._slotRoundtrip(
|
|
1040
|
-
() => this._readAudioParameterResolve,
|
|
1041
|
-
(t) => {
|
|
1042
|
-
this._readAudioParameterResolve = t;
|
|
1043
|
-
},
|
|
1044
|
-
{ type: "read_audio_parameter", name: e }
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Internal: send a command and await the matching daemon response in a
|
|
1049
|
-
* named single-resolver slot. Used by the volume helpers and the
|
|
1050
|
-
* XVF3800 audio-config helpers — every one of them has a strict
|
|
1051
|
-
* request/response shape where a single in-flight call per slot is
|
|
1052
|
-
* sufficient. If a previous request on the same slot is still
|
|
1053
|
-
* pending when a new one comes in, the older promise is resolved to
|
|
1054
|
-
* `null` so its caller doesn't hang forever.
|
|
1055
|
-
*
|
|
1056
|
-
* Slot access is passed in as getter/setter closures rather than a
|
|
1057
|
-
* key into `this`: that keeps the helper fully generic-checked (T is
|
|
1058
|
-
* inferred from the slot's resolver type at each call site) with no
|
|
1059
|
-
* indexed-property casts.
|
|
1060
|
-
*/
|
|
1061
|
-
_slotRoundtrip(e, t, s) {
|
|
1062
|
-
return new Promise((i, o) => {
|
|
1063
|
-
if (!this._dc || this._dc.readyState !== "open") {
|
|
1064
|
-
o(new Error("Data channel not open"));
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
const n = e();
|
|
1068
|
-
n && n(null), t(i), this._sendCommand(s);
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
sendRaw(e) {
|
|
1072
|
-
return this._sendCommand(e);
|
|
1073
|
-
}
|
|
1074
|
-
subscribeLogs({ onLine: e, onError: t }) {
|
|
1075
|
-
if (typeof e != "function")
|
|
1076
|
-
throw new TypeError("subscribeLogs: onLine callback is required");
|
|
1077
|
-
const s = { onLine: e, onError: t }, i = this._logSubscribers.size === 0;
|
|
1078
|
-
this._logSubscribers.add(s), i && this._sendCommand({ type: "subscribe_logs" });
|
|
1079
|
-
let o = !1;
|
|
1080
|
-
return () => {
|
|
1081
|
-
o || (o = !0, this._logSubscribers.delete(s), this._logSubscribers.size === 0 && this._sendCommand({ type: "unsubscribe_logs" }));
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
requestState() {
|
|
1085
|
-
return this._sendCommand({ type: "get_state" });
|
|
1086
|
-
}
|
|
1087
|
-
// ─── Audio ───────────────────────────────────────────────────────────
|
|
1088
|
-
setAudioMuted(e) {
|
|
1089
|
-
this._audioMuted = e, this._videoElement && (this._videoElement.muted = e);
|
|
1090
|
-
}
|
|
1091
|
-
setMicMuted(e) {
|
|
1092
|
-
this._micMuted = e, this._micStream && this._micStream.getAudioTracks().forEach((t) => {
|
|
1093
|
-
t.enabled = !e;
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
// ─── Video helper ────────────────────────────────────────────────────
|
|
1097
|
-
attachVideo(e) {
|
|
1098
|
-
this._videoElement = e, e.muted = this._audioMuted;
|
|
1099
|
-
const t = (i) => {
|
|
1100
|
-
const o = i;
|
|
1101
|
-
e.srcObject = o.detail.stream, e.playsInline = !0, "requestVideoFrameCallback" in e && this._startLatencyMonitor(e);
|
|
1102
|
-
}, s = () => {
|
|
1103
|
-
e.srcObject = null;
|
|
1104
|
-
};
|
|
1105
|
-
return this.addEventListener("videoTrack", t), this.addEventListener("sessionStopped", s), () => {
|
|
1106
|
-
this.removeEventListener("videoTrack", t), this.removeEventListener("sessionStopped", s), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), e.srcObject = null, this._videoElement = null;
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
// ─── Daemon-side recorded-move playback ──────────────────────────────
|
|
1110
|
-
async playMove(e, {
|
|
1111
|
-
audioBlob: t = null,
|
|
1112
|
-
audioLeadMs: s = -100,
|
|
1113
|
-
description: i = "move",
|
|
1114
|
-
encoding: o = "gzip+base64",
|
|
1115
|
-
playFrequency: n = 100,
|
|
1116
|
-
initialGotoDuration: a = 0,
|
|
1117
|
-
startTimeoutMs: c = 8e3,
|
|
1118
|
-
onProgress: l = () => {
|
|
1119
|
-
},
|
|
1120
|
-
onStarted: h = () => {
|
|
1121
|
-
}
|
|
1122
|
-
} = {}) {
|
|
1123
|
-
if (!this._dc || this._dc.readyState !== "open")
|
|
1124
|
-
throw new Error("data channel not open");
|
|
1125
|
-
if (!e?.time?.length || !e?.set_target_data?.length)
|
|
1126
|
-
throw new Error("playMove: motion must have time + set_target_data");
|
|
1127
|
-
const d = U();
|
|
1128
|
-
this._activeMoveUploadId = d;
|
|
1129
|
-
const m = {
|
|
1130
|
-
description: i,
|
|
1131
|
-
time: e.time,
|
|
1132
|
-
set_target_data: e.set_target_data
|
|
1133
|
-
}, f = JSON.stringify(m);
|
|
1134
|
-
let p, v;
|
|
1135
|
-
o === "gzip+base64" && J() ? (p = await X(f), v = "gzip+base64") : (p = f, v = "json");
|
|
1136
|
-
const g = Math.ceil(p.length / y) || 1;
|
|
1137
|
-
l({
|
|
1138
|
-
phase: "starting",
|
|
1139
|
-
sent: 0,
|
|
1140
|
-
total: g,
|
|
1141
|
-
bytes: p.length,
|
|
1142
|
-
encoding: v
|
|
1143
|
-
}), this._sendCommand({
|
|
1144
|
-
type: "upload_move_start",
|
|
1145
|
-
upload_id: d,
|
|
1146
|
-
total_chunks: g,
|
|
1147
|
-
description: i,
|
|
1148
|
-
encoding: v
|
|
1149
|
-
});
|
|
1150
|
-
for (let u = 0; u < g; u++) {
|
|
1151
|
-
this._dc.bufferedAmount > A && await this._awaitDataChannelDrain();
|
|
1152
|
-
const _ = u * y;
|
|
1153
|
-
this._sendCommand({
|
|
1154
|
-
type: "upload_move_chunk",
|
|
1155
|
-
upload_id: d,
|
|
1156
|
-
chunk_index: u,
|
|
1157
|
-
chunk: p.slice(_, _ + y)
|
|
1158
|
-
}), l({ phase: "upload", sent: u + 1, total: g });
|
|
1159
|
-
}
|
|
1160
|
-
if (this._sendCommand({ type: "upload_move_finish", upload_id: d }), l({ phase: "uploaded", sent: g, total: g }), t) {
|
|
1161
|
-
const u = new Uint8Array(await t.arrayBuffer()), _ = E(u), b = Math.ceil(_.length / y) || 1;
|
|
1162
|
-
l({
|
|
1163
|
-
phase: "audio-starting",
|
|
1164
|
-
sent: 0,
|
|
1165
|
-
total: b,
|
|
1166
|
-
bytes: _.length
|
|
1167
|
-
}), this._sendCommand({
|
|
1168
|
-
type: "upload_audio_start",
|
|
1169
|
-
upload_id: d,
|
|
1170
|
-
total_chunks: b,
|
|
1171
|
-
encoding: P(t),
|
|
1172
|
-
description: i
|
|
1173
|
-
});
|
|
1174
|
-
for (let I = 0; I < b; I++) {
|
|
1175
|
-
this._dc.bufferedAmount > A && await this._awaitDataChannelDrain();
|
|
1176
|
-
const T = I * y;
|
|
1177
|
-
this._sendCommand({
|
|
1178
|
-
type: "upload_audio_chunk",
|
|
1179
|
-
upload_id: d,
|
|
1180
|
-
chunk_index: I,
|
|
1181
|
-
chunk: _.slice(T, T + y)
|
|
1182
|
-
}), l({ phase: "audio-upload", sent: I + 1, total: b });
|
|
1183
|
-
}
|
|
1184
|
-
this._sendCommand({ type: "upload_audio_finish", upload_id: d }), l({ phase: "audio-uploaded", sent: b, total: b });
|
|
1185
|
-
}
|
|
1186
|
-
this._sendCommand({
|
|
1187
|
-
type: "play_uploaded_move",
|
|
1188
|
-
upload_id: d,
|
|
1189
|
-
play_frequency: n,
|
|
1190
|
-
initial_goto_duration: a,
|
|
1191
|
-
audio_lead_ms: s
|
|
1192
|
-
});
|
|
1193
|
-
let w;
|
|
1194
|
-
try {
|
|
1195
|
-
w = await this._waitForBroadcast(
|
|
1196
|
-
(u) => u?.type === "play_uploaded_move" && u?.upload_id === d && (u.started === !0 || typeof u.error == "string"),
|
|
1197
|
-
{ timeoutMs: c, debugLabel: "play_uploaded_move started" }
|
|
1198
|
-
);
|
|
1199
|
-
} catch (u) {
|
|
1200
|
-
throw new Error(
|
|
1201
|
-
`Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${u.message}`
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
if (typeof w.error == "string")
|
|
1205
|
-
throw new Error(`play_uploaded_move: ${w.error}`);
|
|
1206
|
-
try {
|
|
1207
|
-
h({
|
|
1208
|
-
duration_s: w.duration_s,
|
|
1209
|
-
has_audio: w.has_audio === !0
|
|
1210
|
-
});
|
|
1211
|
-
} catch (u) {
|
|
1212
|
-
console.warn("playMove.onStarted threw:", u);
|
|
1213
|
-
}
|
|
1214
|
-
l({ phase: "playing", duration_s: w.duration_s });
|
|
1215
|
-
const S = await this._waitForBroadcast(
|
|
1216
|
-
(u) => u?.type === "play_uploaded_move" && u?.upload_id === d && (u.finished === !0 || u.cancelled === !0 || typeof u.error == "string"),
|
|
1217
|
-
{
|
|
1218
|
-
timeoutMs: (w.duration_s + 30) * 1e3,
|
|
1219
|
-
debugLabel: "play_uploaded_move final"
|
|
1220
|
-
}
|
|
1221
|
-
);
|
|
1222
|
-
return this._activeMoveUploadId === d && (this._activeMoveUploadId = null), S;
|
|
1223
|
-
}
|
|
1224
|
-
cancelMove(e = null) {
|
|
1225
|
-
const t = e ?? this._activeMoveUploadId;
|
|
1226
|
-
return t ? this._sendCommand({ type: "cancel_move", upload_id: t }) : !1;
|
|
1227
|
-
}
|
|
1228
|
-
async uploadAudio(e, { description: t = "audio", onProgress: s = () => {
|
|
1229
|
-
} } = {}) {
|
|
1230
|
-
if (!this._dc || this._dc.readyState !== "open")
|
|
1231
|
-
throw new Error("data channel not open");
|
|
1232
|
-
if (!(e instanceof Blob))
|
|
1233
|
-
throw new TypeError("uploadAudio: expected a Blob");
|
|
1234
|
-
const i = U(), o = new Uint8Array(await e.arrayBuffer()), n = E(o), a = Math.ceil(n.length / y) || 1;
|
|
1235
|
-
s({ phase: "audio-starting", sent: 0, total: a, bytes: n.length }), this._sendCommand({
|
|
1236
|
-
type: "upload_audio_start",
|
|
1237
|
-
upload_id: i,
|
|
1238
|
-
total_chunks: a,
|
|
1239
|
-
encoding: P(e),
|
|
1240
|
-
description: t
|
|
1241
|
-
});
|
|
1242
|
-
for (let c = 0; c < a; c++) {
|
|
1243
|
-
this._dc.bufferedAmount > A && await this._awaitDataChannelDrain();
|
|
1244
|
-
const l = c * y;
|
|
1245
|
-
this._sendCommand({
|
|
1246
|
-
type: "upload_audio_chunk",
|
|
1247
|
-
upload_id: i,
|
|
1248
|
-
chunk_index: c,
|
|
1249
|
-
chunk: n.slice(l, l + y)
|
|
1250
|
-
}), s({ phase: "audio-upload", sent: c + 1, total: a });
|
|
1251
|
-
}
|
|
1252
|
-
return this._sendCommand({ type: "upload_audio_finish", upload_id: i }), s({ phase: "audio-uploaded", sent: a, total: a }), i;
|
|
1253
|
-
}
|
|
1254
|
-
async playUploadedAudio(e, { timeoutMs: t = 8e3 } = {}) {
|
|
1255
|
-
if (!this._dc || this._dc.readyState !== "open")
|
|
1256
|
-
throw new Error("data channel not open");
|
|
1257
|
-
const s = this._waitForBroadcast(
|
|
1258
|
-
(o) => o?.type === "play_uploaded_audio" && o?.upload_id === e && (o.started === !0 || typeof o.error == "string"),
|
|
1259
|
-
{ timeoutMs: t, debugLabel: "play_uploaded_audio started" }
|
|
1260
|
-
);
|
|
1261
|
-
this._sendCommand({ type: "play_uploaded_audio", upload_id: e });
|
|
1262
|
-
const i = await s;
|
|
1263
|
-
if (typeof i.error == "string") throw new Error(i.error);
|
|
1264
|
-
return this._activeAudioUploadId = e, i;
|
|
1265
|
-
}
|
|
1266
|
-
cancelAudio(e = null) {
|
|
1267
|
-
const t = e ?? this._activeAudioUploadId;
|
|
1268
|
-
return t ? (this._activeAudioUploadId === t && (this._activeAudioUploadId = null), this._sendCommand({ type: "cancel_audio", upload_id: t })) : !1;
|
|
1269
|
-
}
|
|
1270
|
-
// ─── Private ─────────────────────────────────────────────────────────
|
|
1271
|
-
_emit(e, t) {
|
|
1272
|
-
this.dispatchEvent(new CustomEvent(e, { detail: t }));
|
|
1273
|
-
}
|
|
1274
|
-
_waitForBroadcast(e, { timeoutMs: t = 5e3, debugLabel: s = "" } = {}) {
|
|
1275
|
-
return new Promise((i, o) => {
|
|
1276
|
-
const n = {
|
|
1277
|
-
predicate: e,
|
|
1278
|
-
resolve: i,
|
|
1279
|
-
timer: setTimeout(() => {
|
|
1280
|
-
const a = this._broadcastWaiters.indexOf(n);
|
|
1281
|
-
a !== -1 && this._broadcastWaiters.splice(a, 1), o(new Error(`broadcast timeout (${t} ms): ${s}`));
|
|
1282
|
-
}, t)
|
|
1283
|
-
};
|
|
1284
|
-
this._broadcastWaiters.push(n);
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
async _awaitDataChannelDrain() {
|
|
1288
|
-
for (; this._dc && this._dc.bufferedAmount > z; )
|
|
1289
|
-
if (await new Promise((e) => setTimeout(e, 30)), !this._dc || this._dc.readyState !== "open")
|
|
1290
|
-
throw new Error("data channel closed mid-upload");
|
|
1291
|
-
}
|
|
1292
|
-
async _sendToServer(e) {
|
|
1293
|
-
if (!this._token) throw new Error("No token — authenticate() first");
|
|
1294
|
-
try {
|
|
1295
|
-
const t = await fetch(`${this._signalingUrl}/send`, {
|
|
1296
|
-
method: "POST",
|
|
1297
|
-
headers: {
|
|
1298
|
-
"Content-Type": "application/json",
|
|
1299
|
-
Authorization: `Bearer ${this._token}`
|
|
1300
|
-
},
|
|
1301
|
-
body: JSON.stringify(e)
|
|
1302
|
-
});
|
|
1303
|
-
if (!t.ok) {
|
|
1304
|
-
let s = "";
|
|
1305
|
-
try {
|
|
1306
|
-
s = await t.text();
|
|
1307
|
-
} catch {
|
|
1308
|
-
}
|
|
1309
|
-
return console.warn(
|
|
1310
|
-
`[reachy-mini] /send rejected (${t.status}) for type=${e?.type}; body=${s || "<empty>"}`
|
|
1311
|
-
), null;
|
|
1312
|
-
}
|
|
1313
|
-
return await t.json();
|
|
1314
|
-
} catch (t) {
|
|
1315
|
-
return console.error("Send error:", t), null;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
_sendCommand(e) {
|
|
1319
|
-
return !this._dc || this._dc.readyState !== "open" ? !1 : (this._dc.send(JSON.stringify(e)), !0);
|
|
1320
|
-
}
|
|
1321
|
-
_checkSessionReady() {
|
|
1322
|
-
this._iceConnected && this._dcOpen && this._sessionResolve && (this._state = "streaming", this.requestState(), this._stateRefreshInterval = setInterval(() => this.requestState(), 500), this._emit("streaming", { sessionId: this._sessionId, robotId: this._selectedRobotId }), this._sessionResolve(), this._sessionResolve = null, this._sessionReject = null);
|
|
1323
|
-
}
|
|
1324
|
-
async _handleSignalingMessage(e) {
|
|
1325
|
-
switch (e.type) {
|
|
1326
|
-
case "welcome":
|
|
1327
|
-
break;
|
|
1328
|
-
case "list":
|
|
1329
|
-
this._robots = e.producers || [], this._emit("robotsChanged", { robots: this._robots }), this._maybeAutoStart();
|
|
1330
|
-
break;
|
|
1331
|
-
case "peerStatusChanged": {
|
|
1332
|
-
const t = await this._sendToServer({ type: "list" });
|
|
1333
|
-
t?.producers && (this._robots = t.producers, this._emit("robotsChanged", { robots: this._robots }), this._maybeAutoStart());
|
|
1334
|
-
break;
|
|
1335
|
-
}
|
|
1336
|
-
case "sessionStarted":
|
|
1337
|
-
this._sessionId = e.sessionId ?? null;
|
|
1338
|
-
break;
|
|
1339
|
-
case "sessionRejected":
|
|
1340
|
-
this._failSessionRejected(e);
|
|
1341
|
-
break;
|
|
1342
|
-
case "endSession":
|
|
1343
|
-
this._handleEndSession(e);
|
|
1344
|
-
break;
|
|
1345
|
-
case "peer":
|
|
1346
|
-
this._handlePeerMessage(e);
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
_handleEndSession(e) {
|
|
1351
|
-
const t = e.reason, s = 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;
|
|
1352
|
-
if (this._sessionReject) {
|
|
1353
|
-
const i = new Error(
|
|
1354
|
-
s || `Session ended before it could start: ${t || "unknown reason"}`
|
|
1355
|
-
);
|
|
1356
|
-
i.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;
|
|
1357
|
-
const o = this._sessionReject;
|
|
1358
|
-
this._sessionResolve = null, this._sessionReject = null, o(i);
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
this._state === "streaming" && (this._emit("sessionStopped", {
|
|
1362
|
-
reason: t || "remote_end",
|
|
1363
|
-
message: s
|
|
1364
|
-
}), this.stopSession().catch(() => {
|
|
1365
|
-
}));
|
|
1366
|
-
}
|
|
1367
|
-
async _handlePeerMessage(e) {
|
|
1368
|
-
if (this._pc)
|
|
1369
|
-
try {
|
|
1370
|
-
if (e.sdp) {
|
|
1371
|
-
const t = e.sdp;
|
|
1372
|
-
if (t.type === "offer") {
|
|
1373
|
-
const i = W(t.sdp);
|
|
1374
|
-
if (this._micSupported = i, this._emit("micSupported", { supported: i }), i && this._micStream)
|
|
1375
|
-
for (const n of this._micStream.getAudioTracks())
|
|
1376
|
-
this._pc.addTrack(n, this._micStream);
|
|
1377
|
-
await this._pc.setRemoteDescription(new RTCSessionDescription(t));
|
|
1378
|
-
const o = await this._pc.createAnswer();
|
|
1379
|
-
await this._pc.setLocalDescription(o), await this._sendToServer({
|
|
1380
|
-
type: "peer",
|
|
1381
|
-
sessionId: this._sessionId,
|
|
1382
|
-
sdp: { type: "answer", sdp: o.sdp }
|
|
1383
|
-
});
|
|
1384
|
-
} else
|
|
1385
|
-
await this._pc.setRemoteDescription(new RTCSessionDescription(t));
|
|
1386
|
-
const s = this._pendingRemoteIce;
|
|
1387
|
-
if (s && s.length) {
|
|
1388
|
-
this._pendingRemoteIce = [];
|
|
1389
|
-
for (const i of s)
|
|
1390
|
-
try {
|
|
1391
|
-
await this._pc.addIceCandidate(new RTCIceCandidate(i));
|
|
1392
|
-
} catch (o) {
|
|
1393
|
-
console.warn("[reachy-mini] buffered ICE candidate rejected:", o);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
if (e.ice) {
|
|
1398
|
-
if (!e.ice.candidate) return;
|
|
1399
|
-
this._pc.remoteDescription ? await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)) : (this._pendingRemoteIce || (this._pendingRemoteIce = []), this._pendingRemoteIce.push(e.ice));
|
|
1400
|
-
}
|
|
1401
|
-
} catch (t) {
|
|
1402
|
-
console.error("WebRTC error:", t), this._emit("error", { source: "webrtc", error: t });
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
_handleRobotMessage(e) {
|
|
1406
|
-
if ("version" in e && this._versionResolve) {
|
|
1407
|
-
this._versionResolve(e.version), this._versionResolve = null;
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
if ("hardware_id" in e && this._hardwareIdResolve) {
|
|
1411
|
-
this._hardwareIdResolve(e.hardware_id), this._hardwareIdResolve = null;
|
|
1412
|
-
return;
|
|
1413
|
-
}
|
|
1414
|
-
if (e.command === "get_volume" || e.command === "set_volume") {
|
|
1415
|
-
this._volumeResolve && (this._volumeResolve(e.status === "error" ? null : e.volume), this._volumeResolve = null);
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
if (e.command === "get_microphone_volume" || e.command === "set_microphone_volume") {
|
|
1419
|
-
this._micVolumeResolve && (this._micVolumeResolve(e.status === "error" ? null : e.volume), this._micVolumeResolve = null);
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
if (e.command === "apply_audio_config") {
|
|
1423
|
-
this._applyAudioConfigResolve && (this._applyAudioConfigResolve(e.error ? !1 : !!e.applied), this._applyAudioConfigResolve = null);
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
if (e.command === "read_audio_parameter") {
|
|
1427
|
-
this._readAudioParameterResolve && (this._readAudioParameterResolve(
|
|
1428
|
-
e.error ? null : e.values ?? null
|
|
1429
|
-
), this._readAudioParameterResolve = null);
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
if (e.command === "get_tracked_face") {
|
|
1433
|
-
this._trackedFaceResolve && (this._trackedFaceResolve(
|
|
1434
|
-
e.face_target ?? null
|
|
1435
|
-
), this._trackedFaceResolve = null);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
if ((e.command === "wake_up" || e.command === "goto_sleep") && this._pendingMotionCompletions && this._pendingMotionCompletions[e.command]) {
|
|
1439
|
-
const t = this._pendingMotionCompletions[e.command];
|
|
1440
|
-
if (e.completed === !0 && t.length > 0) {
|
|
1441
|
-
const s = t.shift();
|
|
1442
|
-
clearTimeout(s.timer), s.resolve();
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (e.error && t.length > 0) {
|
|
1446
|
-
const s = t.shift();
|
|
1447
|
-
clearTimeout(s.timer), s.reject(new Error(`${e.command}: ${e.error}`));
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
if (e.type === "log_line") {
|
|
1452
|
-
for (const t of this._logSubscribers)
|
|
1453
|
-
try {
|
|
1454
|
-
t.onLine({ timestamp: e.timestamp, line: e.line });
|
|
1455
|
-
} catch (s) {
|
|
1456
|
-
console.error("subscribeLogs onLine threw:", s);
|
|
1457
|
-
}
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
if (e.type === "log_stream_error") {
|
|
1461
|
-
for (const t of this._logSubscribers)
|
|
1462
|
-
if (typeof t.onError == "function")
|
|
1463
|
-
try {
|
|
1464
|
-
t.onError(e.error);
|
|
1465
|
-
} catch (s) {
|
|
1466
|
-
console.error("subscribeLogs onError threw:", s);
|
|
1467
|
-
}
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
if (e.command === "start_update") {
|
|
1471
|
-
if (typeof e.error == "string") {
|
|
1472
|
-
const t = { status: "failed", error: e.error };
|
|
1473
|
-
for (const s of this._updateProgressSubscribers)
|
|
1474
|
-
try {
|
|
1475
|
-
s(t);
|
|
1476
|
-
} catch (i) {
|
|
1477
|
-
console.error("startDaemonUpdate onProgress threw:", i);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
if (e.type === "update_progress") {
|
|
1483
|
-
const t = {
|
|
1484
|
-
status: e.status,
|
|
1485
|
-
line: typeof e.line == "string" ? e.line : void 0,
|
|
1486
|
-
error: typeof e.error == "string" ? e.error : void 0
|
|
1487
|
-
};
|
|
1488
|
-
for (const s of this._updateProgressSubscribers)
|
|
1489
|
-
try {
|
|
1490
|
-
s(t);
|
|
1491
|
-
} catch (i) {
|
|
1492
|
-
console.error("startDaemonUpdate onProgress threw:", i);
|
|
1493
|
-
}
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
if (e.state) {
|
|
1497
|
-
const t = e.state;
|
|
1498
|
-
t.head_pose && (this._robotState.head = t.head_pose.flat()), t.antennas && (this._robotState.antennas = [t.antennas[0], t.antennas[1]]), typeof t.body_yaw == "number" && (this._robotState.body_yaw = t.body_yaw), t.motor_mode && (this._robotState.motor_mode = t.motor_mode), typeof t.is_move_running == "boolean" && (this._robotState.is_move_running = t.is_move_running), t.face_target && (this._robotState.face_target = t.face_target), this._emit("state", { ...this._robotState });
|
|
1499
|
-
}
|
|
1500
|
-
if (e.error && this._emit("error", { source: "robot", error: e.error }), this._broadcastWaiters.length > 0)
|
|
1501
|
-
for (let t = this._broadcastWaiters.length - 1; t >= 0; t--) {
|
|
1502
|
-
const s = this._broadcastWaiters[t];
|
|
1503
|
-
if (s.predicate(e)) {
|
|
1504
|
-
this._broadcastWaiters.splice(t, 1), clearTimeout(s.timer), s.resolve(e);
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
/** Snap video playback to live edge if buffered lag exceeds 0.5 s. */
|
|
1510
|
-
_startLatencyMonitor(e) {
|
|
1511
|
-
this._latencyMonitorId && clearInterval(this._latencyMonitorId), this._latencyMonitorId = setInterval(() => {
|
|
1512
|
-
if (!e.srcObject || e.paused) return;
|
|
1513
|
-
const t = e.buffered;
|
|
1514
|
-
if (t.length > 0) {
|
|
1515
|
-
const s = t.end(t.length - 1), i = s - e.currentTime;
|
|
1516
|
-
i > 0.5 && (console.log(`Latency correction: was ${i.toFixed(2)}s behind`), e.currentTime = s - 0.1);
|
|
1517
|
-
}
|
|
1518
|
-
}, 2e3);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
export {
|
|
1522
|
-
K as R
|
|
1523
|
-
};
|