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