@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.
Files changed (128) hide show
  1. package/APP_CREATION_GUIDE.md +98 -0
  2. package/dist/lib/central-listener.d.ts +71 -0
  3. package/dist/lib/central-listener.d.ts.map +1 -0
  4. package/dist/lib/central-listener.js +277 -0
  5. package/dist/lib/central-listener.js.map +1 -0
  6. package/dist/lib/central-listener.test.d.ts +2 -0
  7. package/dist/lib/central-listener.test.d.ts.map +1 -0
  8. package/dist/lib/central-listener.test.js +378 -0
  9. package/dist/lib/central-listener.test.js.map +1 -0
  10. package/dist/lib/dc-watchdog.test.d.ts +2 -0
  11. package/dist/lib/dc-watchdog.test.d.ts.map +1 -0
  12. package/dist/lib/dc-watchdog.test.js +154 -0
  13. package/dist/lib/dc-watchdog.test.js.map +1 -0
  14. package/dist/lib/ensure-awake.test.d.ts +2 -0
  15. package/dist/lib/ensure-awake.test.d.ts.map +1 -0
  16. package/dist/lib/ensure-awake.test.js +117 -0
  17. package/dist/lib/ensure-awake.test.js.map +1 -0
  18. package/dist/lib/ice-grace.test.d.ts +2 -0
  19. package/dist/lib/ice-grace.test.d.ts.map +1 -0
  20. package/dist/lib/ice-grace.test.js +211 -0
  21. package/dist/lib/ice-grace.test.js.map +1 -0
  22. package/dist/lib/imu.test.d.ts +2 -0
  23. package/dist/lib/imu.test.d.ts.map +1 -0
  24. package/dist/lib/imu.test.js +78 -0
  25. package/dist/lib/imu.test.js.map +1 -0
  26. package/dist/lib/logger.d.ts +38 -0
  27. package/dist/lib/logger.d.ts.map +1 -0
  28. package/dist/lib/logger.js +81 -0
  29. package/dist/lib/logger.js.map +1 -0
  30. package/dist/lib/pending-replies.d.ts +127 -0
  31. package/dist/lib/pending-replies.d.ts.map +1 -0
  32. package/dist/lib/pending-replies.js +255 -0
  33. package/dist/lib/pending-replies.js.map +1 -0
  34. package/dist/lib/pending-replies.test.d.ts +2 -0
  35. package/dist/lib/pending-replies.test.d.ts.map +1 -0
  36. package/dist/lib/pending-replies.test.js +217 -0
  37. package/dist/lib/pending-replies.test.js.map +1 -0
  38. package/dist/lib/reachy-mini.d.ts +202 -88
  39. package/dist/lib/reachy-mini.d.ts.map +1 -1
  40. package/dist/lib/reachy-mini.js +651 -566
  41. package/dist/lib/reachy-mini.js.map +1 -1
  42. package/dist/lib/redial.test.d.ts +2 -0
  43. package/dist/lib/redial.test.d.ts.map +1 -0
  44. package/dist/lib/redial.test.js +206 -0
  45. package/dist/lib/redial.test.js.map +1 -0
  46. package/dist/lib/request.test.d.ts +2 -0
  47. package/dist/lib/request.test.d.ts.map +1 -0
  48. package/dist/lib/request.test.js +90 -0
  49. package/dist/lib/request.test.js.map +1 -0
  50. package/dist/lib/session-supervisor.d.ts +201 -0
  51. package/dist/lib/session-supervisor.d.ts.map +1 -0
  52. package/dist/lib/session-supervisor.js +520 -0
  53. package/dist/lib/session-supervisor.js.map +1 -0
  54. package/dist/lib/token-store.d.ts +50 -0
  55. package/dist/lib/token-store.d.ts.map +1 -0
  56. package/dist/lib/token-store.js +120 -0
  57. package/dist/lib/token-store.js.map +1 -0
  58. package/dist/lib/token-store.test.d.ts +2 -0
  59. package/dist/lib/token-store.test.d.ts.map +1 -0
  60. package/dist/lib/token-store.test.js +82 -0
  61. package/dist/lib/token-store.test.js.map +1 -0
  62. package/dist/lib/types.d.ts +280 -6
  63. package/dist/lib/types.d.ts.map +1 -1
  64. package/dist/lib/url-helpers.d.ts.map +1 -1
  65. package/dist/lib/url-helpers.js +3 -1
  66. package/dist/lib/url-helpers.js.map +1 -1
  67. package/dist/lib/version.d.ts +2 -0
  68. package/dist/lib/version.d.ts.map +1 -0
  69. package/dist/lib/version.js +5 -0
  70. package/dist/lib/version.js.map +1 -0
  71. package/dist/reachy-mini-sdk.d.ts +15 -1
  72. package/dist/reachy-mini-sdk.d.ts.map +1 -1
  73. package/dist/reachy-mini-sdk.js +12 -0
  74. package/dist/reachy-mini-sdk.js.map +1 -1
  75. package/host/dist/ReachyHost.d.ts.map +1 -1
  76. package/host/dist/chunks/centralRest-uXNMidlL.js +2315 -0
  77. package/host/dist/chunks/index-Beu4GNzi.js +821 -0
  78. package/host/dist/chunks/{mountHost-DjE5zE9R.js → mountHost-BTTF4Uu9.js} +24933 -27488
  79. package/host/dist/components/ConnectingView.d.ts.map +1 -1
  80. package/host/dist/components/DaemonUpdateGate.d.ts +65 -0
  81. package/host/dist/components/DaemonUpdateGate.d.ts.map +1 -0
  82. package/host/dist/components/ErrorView.d.ts.map +1 -1
  83. package/host/dist/components/HostErrorBoundary.d.ts +30 -0
  84. package/host/dist/components/HostErrorBoundary.d.ts.map +1 -0
  85. package/host/dist/components/LeavingView.d.ts.map +1 -1
  86. package/host/dist/components/PickerView.d.ts +16 -14
  87. package/host/dist/components/PickerView.d.ts.map +1 -1
  88. package/host/dist/components/PostOAuthSplash.d.ts +14 -9
  89. package/host/dist/components/PostOAuthSplash.d.ts.map +1 -1
  90. package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
  91. package/host/dist/components/SignInView.d.ts.map +1 -1
  92. package/host/dist/components/TopBar.d.ts +5 -6
  93. package/host/dist/components/TopBar.d.ts.map +1 -1
  94. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
  95. package/host/dist/embed/index.d.ts +68 -5
  96. package/host/dist/embed/index.d.ts.map +1 -1
  97. package/host/dist/entry/auto.js +2 -2
  98. package/host/dist/entry/embed.d.ts +5 -4
  99. package/host/dist/entry/embed.d.ts.map +1 -1
  100. package/host/dist/entry/embed.js +5 -4
  101. package/host/dist/hooks/useHostBridge.d.ts +35 -1
  102. package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
  103. package/host/dist/hooks/useLatestDaemonVersion.d.ts +7 -0
  104. package/host/dist/hooks/useLatestDaemonVersion.d.ts.map +1 -0
  105. package/host/dist/hooks/useOAuth.d.ts +18 -0
  106. package/host/dist/hooks/useOAuth.d.ts.map +1 -1
  107. package/host/dist/hooks/useRobots.d.ts +0 -2
  108. package/host/dist/hooks/useRobots.d.ts.map +1 -1
  109. package/host/dist/index.d.ts +2 -2
  110. package/host/dist/index.d.ts.map +1 -1
  111. package/host/dist/index.js +11 -10
  112. package/host/dist/lib/daemonRelease.d.ts +84 -0
  113. package/host/dist/lib/daemonRelease.d.ts.map +1 -0
  114. package/host/dist/lib/protocol.d.ts +141 -7
  115. package/host/dist/lib/protocol.d.ts.map +1 -1
  116. package/host/dist/lib/rebootWatch.d.ts +51 -0
  117. package/host/dist/lib/rebootWatch.d.ts.map +1 -0
  118. package/host/dist/lib/sdk-types.d.ts +1 -1
  119. package/host/dist/lib/sdk-types.d.ts.map +1 -1
  120. package/host/dist/lib/sdkStaleness.d.ts +78 -0
  121. package/host/dist/lib/sdkStaleness.d.ts.map +1 -0
  122. package/host/dist/lib/settings.d.ts +16 -5
  123. package/host/dist/lib/settings.d.ts.map +1 -1
  124. package/package.json +11 -4
  125. package/host/dist/chunks/index-lMs2sCXG.js +0 -439
  126. package/host/dist/chunks/reachy-mini-CdsdQLIU.js +0 -1523
  127. package/host/dist/lib/centralListener.d.ts +0 -73
  128. 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
- };