@pollen-robotics/reachy-mini-sdk 1.7.3 → 1.8.0-main.54c2dcd

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 (120) hide show
  1. package/APP_CREATION_GUIDE.md +1795 -0
  2. package/README.md +65 -5
  3. package/dist/animation/distance.d.ts +87 -0
  4. package/dist/animation/distance.d.ts.map +1 -0
  5. package/dist/animation/distance.js +140 -0
  6. package/dist/animation/distance.js.map +1 -0
  7. package/dist/animation/index.d.ts +34 -0
  8. package/dist/animation/index.d.ts.map +1 -0
  9. package/dist/animation/index.js +33 -0
  10. package/dist/animation/index.js.map +1 -0
  11. package/dist/animation/pose.d.ts +40 -0
  12. package/dist/animation/pose.d.ts.map +1 -0
  13. package/dist/animation/pose.js +15 -0
  14. package/dist/animation/pose.js.map +1 -0
  15. package/dist/animation/presets.d.ts +85 -0
  16. package/dist/animation/presets.d.ts.map +1 -0
  17. package/dist/animation/presets.js +82 -0
  18. package/dist/animation/presets.js.map +1 -0
  19. package/dist/animation/safe-return.d.ts +90 -0
  20. package/dist/animation/safe-return.d.ts.map +1 -0
  21. package/dist/animation/safe-return.js +123 -0
  22. package/dist/animation/safe-return.js.map +1 -0
  23. package/dist/lib/math.d.ts +23 -0
  24. package/dist/lib/math.d.ts.map +1 -0
  25. package/dist/lib/math.js +40 -0
  26. package/dist/lib/math.js.map +1 -0
  27. package/dist/lib/reachy-mini.d.ts +193 -0
  28. package/dist/lib/reachy-mini.d.ts.map +1 -0
  29. package/dist/lib/reachy-mini.js +1495 -0
  30. package/dist/lib/reachy-mini.js.map +1 -0
  31. package/dist/lib/types.d.ts +384 -0
  32. package/dist/lib/types.d.ts.map +1 -0
  33. package/dist/lib/types.js +9 -0
  34. package/dist/lib/types.js.map +1 -0
  35. package/dist/lib/upload-helpers.d.ts +18 -0
  36. package/dist/lib/upload-helpers.d.ts.map +1 -0
  37. package/dist/lib/upload-helpers.js +44 -0
  38. package/dist/lib/upload-helpers.js.map +1 -0
  39. package/dist/lib/url-helpers.d.ts +63 -0
  40. package/dist/lib/url-helpers.d.ts.map +1 -0
  41. package/dist/lib/url-helpers.js +153 -0
  42. package/dist/lib/url-helpers.js.map +1 -0
  43. package/dist/reachy-mini-sdk.d.ts +69 -0
  44. package/dist/reachy-mini-sdk.d.ts.map +1 -0
  45. package/dist/reachy-mini-sdk.js +68 -0
  46. package/dist/reachy-mini-sdk.js.map +1 -0
  47. package/host/README.md +157 -0
  48. package/host/dist/ReachyHost.d.ts +40 -0
  49. package/host/dist/ReachyHost.d.ts.map +1 -0
  50. package/host/dist/assets/index.d.ts +16 -0
  51. package/host/dist/assets/index.d.ts.map +1 -0
  52. package/host/dist/chunks/index-CyLPysJS.js +400 -0
  53. package/host/dist/chunks/mountHost-zdJ99_oZ.js +48392 -0
  54. package/host/dist/chunks/reachy-mini-BV9nAnC1.js +1283 -0
  55. package/host/dist/components/ConnectingView.d.ts +13 -0
  56. package/host/dist/components/ConnectingView.d.ts.map +1 -0
  57. package/host/dist/components/EmbedFrame.d.ts +16 -0
  58. package/host/dist/components/EmbedFrame.d.ts.map +1 -0
  59. package/host/dist/components/ErrorView.d.ts +21 -0
  60. package/host/dist/components/ErrorView.d.ts.map +1 -0
  61. package/host/dist/components/LeavingView.d.ts +33 -0
  62. package/host/dist/components/LeavingView.d.ts.map +1 -0
  63. package/host/dist/components/PickerView.d.ts +51 -0
  64. package/host/dist/components/PickerView.d.ts.map +1 -0
  65. package/host/dist/components/PostOAuthSplash.d.ts +25 -0
  66. package/host/dist/components/PostOAuthSplash.d.ts.map +1 -0
  67. package/host/dist/components/ReachyHostShell.d.ts +21 -0
  68. package/host/dist/components/ReachyHostShell.d.ts.map +1 -0
  69. package/host/dist/components/SignInView.d.ts +32 -0
  70. package/host/dist/components/SignInView.d.ts.map +1 -0
  71. package/host/dist/components/StepsProgressIndicator.d.ts +41 -0
  72. package/host/dist/components/StepsProgressIndicator.d.ts.map +1 -0
  73. package/host/dist/components/TopBar.d.ts +52 -0
  74. package/host/dist/components/TopBar.d.ts.map +1 -0
  75. package/host/dist/components/WelcomeBackOverlay.d.ts +28 -0
  76. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -0
  77. package/host/dist/embed/index.d.ts +152 -0
  78. package/host/dist/embed/index.d.ts.map +1 -0
  79. package/host/dist/entry/auto.d.ts +12 -0
  80. package/host/dist/entry/auto.d.ts.map +1 -0
  81. package/host/dist/entry/auto.js +6 -0
  82. package/host/dist/entry/embed.d.ts +12 -0
  83. package/host/dist/entry/embed.d.ts.map +1 -0
  84. package/host/dist/entry/embed.js +6 -0
  85. package/host/dist/hooks/useHfProfile.d.ts +7 -0
  86. package/host/dist/hooks/useHfProfile.d.ts.map +1 -0
  87. package/host/dist/hooks/useHostBridge.d.ts +37 -0
  88. package/host/dist/hooks/useHostBridge.d.ts.map +1 -0
  89. package/host/dist/hooks/useOAuth.d.ts +16 -0
  90. package/host/dist/hooks/useOAuth.d.ts.map +1 -0
  91. package/host/dist/hooks/useRobots.d.ts +17 -0
  92. package/host/dist/hooks/useRobots.d.ts.map +1 -0
  93. package/host/dist/hooks/useSdk.d.ts +16 -0
  94. package/host/dist/hooks/useSdk.d.ts.map +1 -0
  95. package/host/dist/index.d.ts +25 -0
  96. package/host/dist/index.d.ts.map +1 -0
  97. package/host/dist/index.js +14 -0
  98. package/host/dist/lib/centralListener.d.ts +73 -0
  99. package/host/dist/lib/centralListener.d.ts.map +1 -0
  100. package/host/dist/lib/centralRest.d.ts +35 -0
  101. package/host/dist/lib/centralRest.d.ts.map +1 -0
  102. package/host/dist/lib/protocol.d.ts +230 -0
  103. package/host/dist/lib/protocol.d.ts.map +1 -0
  104. package/host/dist/lib/protocol.js +47 -0
  105. package/host/dist/lib/sdk-types.d.ts +46 -0
  106. package/host/dist/lib/sdk-types.d.ts.map +1 -0
  107. package/host/dist/lib/settings.d.ts +70 -0
  108. package/host/dist/lib/settings.d.ts.map +1 -0
  109. package/host/dist/lib/signalingUrl.d.ts +28 -0
  110. package/host/dist/lib/signalingUrl.d.ts.map +1 -0
  111. package/host/dist/lib/theme.d.ts +4 -0
  112. package/host/dist/lib/theme.d.ts.map +1 -0
  113. package/host/dist/lib/themeMode.d.ts +9 -0
  114. package/host/dist/lib/themeMode.d.ts.map +1 -0
  115. package/host/dist/lib/tokens.d.ts +64 -0
  116. package/host/dist/lib/tokens.d.ts.map +1 -0
  117. package/host/dist/mountHost.d.ts +36 -0
  118. package/host/dist/mountHost.d.ts.map +1 -0
  119. package/package.json +79 -11
  120. package/reachy-mini-sdk.js +0 -2013
@@ -0,0 +1,1283 @@
1
+ var L = Object.defineProperty, D = (n, e, t) => e in n ? L(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t, A = (n, e, t) => (D(n, typeof e != "symbol" ? e + "" : e, t), t), P = "https://huggingface.co";
2
+ async function C(n, e) {
3
+ var t, s;
4
+ const o = new $(n.url, n.status, (t = n.headers.get("X-Request-Id")) != null ? t : void 0);
5
+ o.message = `Api error with status ${o.statusCode}`;
6
+ const i = [`URL: ${o.url}`, o.requestId ? `Request ID: ${o.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
+ o.message = r.error || r.message || o.message, o.data = r;
10
+ } else
11
+ o.data = { message: await n.text() };
12
+ throw o.message += `. ${i}`, o;
13
+ }
14
+ var $ = class extends Error {
15
+ constructor(n, e, t, s) {
16
+ super(s), A(this, "statusCode"), A(this, "url"), A(this, "requestId"), A(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), [o, i] = [s.get("error"), s.get("error_description")];
36
+ if (o)
37
+ throw new Error(`${o}: ${i}`);
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 = P, 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: 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"), !v.ok)
77
+ throw await C(v);
78
+ const w = await v.json(), g = new Date(Date.now() + w.expires_in * 1e3), S = await fetch(p.userinfo_endpoint, {
79
+ headers: {
80
+ Authorization: `Bearer ${w.access_token}`
81
+ }
82
+ });
83
+ if (!S.ok)
84
+ throw await C(S);
85
+ const u = await S.json();
86
+ return {
87
+ accessToken: w.access_token,
88
+ accessTokenExpiresAt: g,
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: w.scope
110
+ };
111
+ }
112
+ async function O(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 N(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 || P, o = `${new URL(s).origin}/.well-known/openid-configuration`, i = await fetch(o, {
125
+ headers: {
126
+ Accept: "application/json"
127
+ }
128
+ });
129
+ if (!i.ok)
130
+ throw await C(i);
131
+ const r = await i.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 F(n, e, t) {
157
+ const s = R(n), o = R(e), i = R(t), r = Math.cos(i), a = Math.sin(i), l = Math.cos(o), c = Math.sin(o), 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 B() {
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", o = e.get("hf_token_expires"), i = o && !Number.isNaN(new Date(o).getTime()) ? o : 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", i);
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 V() {
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 q(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 y = 12 * 1024, k = 1 * 1024 * 1024, H = 512 * 1024;
216
+ function W() {
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 z(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 j(n) {
238
+ const e = Math.round(Number(n) || 0);
239
+ return Math.max(0, Math.min(100, e));
240
+ }
241
+ class J extends EventTarget {
242
+ // ─── Config ──────────────────────────────────────────────────────────
243
+ _signalingUrl;
244
+ _clientId;
245
+ _appName;
246
+ _videoJitterBufferTargetMs;
247
+ _autoStartFromUrl;
248
+ _autoStartAttempted;
249
+ // ─── Public-ish state mirrors ────────────────────────────────────────
250
+ _state = "disconnected";
251
+ _robots = [];
252
+ _robotState = {};
253
+ _preselectedRobotId;
254
+ // ─── Auth ────────────────────────────────────────────────────────────
255
+ _token = null;
256
+ _username = null;
257
+ _tokenExpires = null;
258
+ // ─── Signaling ───────────────────────────────────────────────────────
259
+ _sseAbortController = null;
260
+ // ─── WebRTC ──────────────────────────────────────────────────────────
261
+ _pc = null;
262
+ _dc = null;
263
+ _sessionId = null;
264
+ _selectedRobotId = null;
265
+ _pendingRemoteIce = [];
266
+ // ─── Audio ───────────────────────────────────────────────────────────
267
+ _micStream = null;
268
+ _micMuted = !0;
269
+ _audioMuted = !0;
270
+ _micSupported = !1;
271
+ // ─── Timers ──────────────────────────────────────────────────────────
272
+ _latencyMonitorId = null;
273
+ _stateRefreshInterval = null;
274
+ // ─── Single-slot promise resolvers ───────────────────────────────────
275
+ _versionResolve = null;
276
+ _hardwareIdResolve = null;
277
+ _volumeResolve = null;
278
+ _micVolumeResolve = null;
279
+ // applyAudioConfig() / readAudioParameter() share the same single-slot
280
+ // pattern as the volume helpers. Separate slots so the two can be
281
+ // in-flight concurrently without collision.
282
+ _applyAudioConfigResolve = null;
283
+ _readAudioParameterResolve = null;
284
+ // ─── Log subscribers ─────────────────────────────────────────────────
285
+ _logSubscribers = /* @__PURE__ */ new Set();
286
+ // ─── Broadcast waiters (playMove / playUploadedAudio) ────────────────
287
+ _broadcastWaiters = [];
288
+ // ─── Active upload ids for no-arg cancels ────────────────────────────
289
+ _activeMoveUploadId = null;
290
+ _activeAudioUploadId = null;
291
+ // ─── Session promise plumbing ────────────────────────────────────────
292
+ _sessionResolve = null;
293
+ _sessionReject = null;
294
+ _iceConnected = !1;
295
+ _dcOpen = !1;
296
+ // ─── Motion completion plumbing (wake_up / goto_sleep) ───────────────
297
+ _pendingMotionCompletions = {
298
+ wake_up: [],
299
+ goto_sleep: []
300
+ };
301
+ // ─── Video element ───────────────────────────────────────────────────
302
+ _videoElement = null;
303
+ constructor(e = {}) {
304
+ super(), this._signalingUrl = e.signalingUrl || "https://pollen-robotics-reachy-mini-central.hf.space", this._clientId = e.clientId || null, this._appName = e.appName || "unknown", this._videoJitterBufferTargetMs = e.videoJitterBufferTargetMs ?? 0, this._autoStartFromUrl = e.autoStartFromUrl === !0, this._autoStartAttempted = !1, this._preselectedRobotId = V();
305
+ }
306
+ // ─── Read-only properties ────────────────────────────────────────────
307
+ get state() {
308
+ return this._state;
309
+ }
310
+ get robots() {
311
+ return this._robots;
312
+ }
313
+ get robotState() {
314
+ return this._robotState;
315
+ }
316
+ get username() {
317
+ return this._username;
318
+ }
319
+ get isAuthenticated() {
320
+ return !!this._token;
321
+ }
322
+ get micSupported() {
323
+ return this._micSupported;
324
+ }
325
+ get micMuted() {
326
+ return this._micMuted;
327
+ }
328
+ get audioMuted() {
329
+ return this._audioMuted;
330
+ }
331
+ get preselectedRobotId() {
332
+ return this._preselectedRobotId;
333
+ }
334
+ get isEmbedded() {
335
+ return this._preselectedRobotId !== null;
336
+ }
337
+ /**
338
+ * Internal: try to honour the `autoStartFromUrl` constructor
339
+ * option. Called from the signaling-message handler after every
340
+ * `robotsChanged` emit, so a robot that comes online after the
341
+ * SDK is already `connected` still triggers the auto-start.
342
+ */
343
+ _maybeAutoStart() {
344
+ if (!this._autoStartFromUrl || this._autoStartAttempted || !this._preselectedRobotId || this._state !== "connected" || !this._robots.find((s) => s.id === this._preselectedRobotId)) return;
345
+ this._autoStartAttempted = !0;
346
+ const t = this._preselectedRobotId;
347
+ setTimeout(() => {
348
+ this._state === "connected" && this.startSession(t).catch((s) => {
349
+ console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:", s);
350
+ });
351
+ }, 0);
352
+ }
353
+ // ─── Auth ────────────────────────────────────────────────────────────
354
+ async authenticate() {
355
+ try {
356
+ B();
357
+ const e = await O();
358
+ if (e)
359
+ return this._username = e.userInfo.preferred_username || e.userInfo.name || null, this._token = e.accessToken, this._tokenExpires = e.accessTokenExpiresAt, sessionStorage.setItem("hf_token", this._token), sessionStorage.setItem("hf_username", this._username ?? ""), sessionStorage.setItem(
360
+ "hf_token_expires",
361
+ typeof this._tokenExpires == "string" ? this._tokenExpires : this._tokenExpires.toISOString()
362
+ ), !0;
363
+ const t = sessionStorage.getItem("hf_token"), s = sessionStorage.getItem("hf_username"), o = sessionStorage.getItem("hf_token_expires");
364
+ return t && s && o && new Date(o) > /* @__PURE__ */ new Date() ? (this._token = t, this._username = s, this._tokenExpires = o, !0) : !1;
365
+ } catch (e) {
366
+ return console.error("Auth error:", e), !1;
367
+ }
368
+ }
369
+ async login() {
370
+ const e = {};
371
+ this._clientId && (e.clientId = this._clientId), window.location.href = await N(e);
372
+ }
373
+ logout() {
374
+ sessionStorage.removeItem("hf_token"), sessionStorage.removeItem("hf_username"), sessionStorage.removeItem("hf_token_expires"), this._username = null, this._tokenExpires = null, this.disconnect();
375
+ }
376
+ // ─── Lifecycle ───────────────────────────────────────────────────────
377
+ async connect(e) {
378
+ if (this._state !== "disconnected") throw new Error("Already connected");
379
+ if (e && (this._token = e), !this._token) throw new Error("No token — call authenticate() first or pass a token");
380
+ this._sseAbortController = new AbortController();
381
+ let t;
382
+ try {
383
+ t = await fetch(
384
+ `${this._signalingUrl}/events`,
385
+ {
386
+ signal: this._sseAbortController.signal,
387
+ headers: { Authorization: `Bearer ${this._token}` }
388
+ }
389
+ );
390
+ } catch (s) {
391
+ throw this._sseAbortController = null, s;
392
+ }
393
+ if (!t.ok)
394
+ throw this._sseAbortController = null, new Error(`HTTP ${t.status}`);
395
+ return new Promise((s, o) => {
396
+ let i = !1;
397
+ const r = t.body.getReader(), a = new TextDecoder();
398
+ let l = "";
399
+ (async () => {
400
+ try {
401
+ for (; ; ) {
402
+ const { done: h, value: d } = await r.read();
403
+ if (h) break;
404
+ l += a.decode(d, { stream: !0 });
405
+ const m = l.split(`
406
+ `);
407
+ l = m.pop() ?? "";
408
+ for (const f of m)
409
+ if (f.startsWith("data:"))
410
+ try {
411
+ const p = JSON.parse(f.slice(5).trim());
412
+ !i && p.type === "welcome" && p.peerId && (i = !0, this._state = "connected", await this._sendToServer({
413
+ type: "setPeerStatus",
414
+ roles: ["listener"],
415
+ meta: { name: this._appName }
416
+ }), this._emit("connected", { peerId: p.peerId }), s()), this._handleSignalingMessage(p);
417
+ } catch {
418
+ }
419
+ }
420
+ } catch (h) {
421
+ if (h.name !== "AbortError" && this._emit("error", { source: "signaling", error: h }), !i) {
422
+ o(h);
423
+ return;
424
+ }
425
+ }
426
+ this._state !== "disconnected" && (this._state = "disconnected", this._emit("disconnected", { reason: "SSE closed" })), i || o(new Error("Connection closed before welcome"));
427
+ })();
428
+ });
429
+ }
430
+ async autoConnect(e = {}) {
431
+ const {
432
+ token: t,
433
+ pickRobot: s,
434
+ autoPickIfSingle: o = !0,
435
+ filterBusy: i = !0,
436
+ wakeOnConnect: r = !0
437
+ } = e;
438
+ if (this._state === "streaming") {
439
+ const l = this._robots?.find((c) => c.id === this._selectedRobotId);
440
+ return {
441
+ robotId: this._selectedRobotId,
442
+ robotName: l?.meta?.name ?? null,
443
+ isEmbedded: this.isEmbedded,
444
+ alreadyStreaming: !0
445
+ };
446
+ }
447
+ const a = this._autoStartFromUrl;
448
+ this._autoStartFromUrl = !1;
449
+ try {
450
+ if (t)
451
+ this._token = t;
452
+ else if (!this._token && !await this.authenticate())
453
+ throw new Error("Not authenticated — call login() or pass a token");
454
+ this._state === "disconnected" && await this.connect();
455
+ let l, c = null;
456
+ if (this.isEmbedded) {
457
+ l = this._preselectedRobotId;
458
+ try {
459
+ await this._waitForRobotInList(l, 5e3);
460
+ } catch {
461
+ }
462
+ c = this._robots?.find((d) => d.id === l)?.meta?.name ?? null;
463
+ } else {
464
+ const h = await this._fetchOwnedRobots({ filterBusy: i });
465
+ if (h.length === 0)
466
+ throw new Error("No reachable robots");
467
+ if (o && h.length === 1 && !h[0].busy)
468
+ l = h[0].id, c = h[0].name;
469
+ else if (s) {
470
+ const d = await s(h);
471
+ if (!d) throw new Error("Robot selection cancelled");
472
+ l = d, c = h.find((m) => m.id === d)?.name ?? null;
473
+ } else
474
+ throw new Error(
475
+ "Multiple robots available — pass a pickRobot callback to autoConnect()"
476
+ );
477
+ }
478
+ if (await this.startSession(l), r && typeof this.ensureAwake == "function")
479
+ try {
480
+ await this.ensureAwake();
481
+ } catch (h) {
482
+ console.warn("[reachy-mini] autoConnect: ensureAwake failed:", h);
483
+ }
484
+ return { robotId: l, robotName: c, isEmbedded: this.isEmbedded };
485
+ } finally {
486
+ this._autoStartFromUrl = a;
487
+ }
488
+ }
489
+ async _fetchOwnedRobots({ filterBusy: e = !0 } = {}) {
490
+ try {
491
+ const t = await fetch(`${this._signalingUrl}/api/robot-status`, {
492
+ headers: { Authorization: `Bearer ${this._token}` }
493
+ });
494
+ if (!t.ok) throw new Error(`HTTP ${t.status}`);
495
+ const s = await t.json(), o = /* @__PURE__ */ new Map();
496
+ for (const i of s.robots || []) {
497
+ if (e && i.busy) continue;
498
+ const r = i.meta?.install_id ?? i.meta?.hardware_id ?? i.peerId;
499
+ o.set(r, {
500
+ id: i.peerId,
501
+ name: i.robotName ?? i.meta?.name ?? null,
502
+ busy: !!i.busy,
503
+ activeApp: i.activeApp ?? null,
504
+ meta: i.meta ?? {},
505
+ lastSeenAgeSeconds: i.last_seen_age_seconds ?? null
506
+ });
507
+ }
508
+ return Array.from(o.values()).sort(
509
+ (i, r) => (i.lastSeenAgeSeconds ?? 1 / 0) - (r.lastSeenAgeSeconds ?? 1 / 0)
510
+ );
511
+ } catch (t) {
512
+ return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:", t), (this._robots || []).map((s) => ({
513
+ id: s.id,
514
+ name: s.meta?.name ?? null,
515
+ busy: !1,
516
+ activeApp: null,
517
+ meta: s.meta ?? {},
518
+ lastSeenAgeSeconds: null
519
+ }));
520
+ }
521
+ }
522
+ _waitForRobotInList(e, t) {
523
+ return this._robots?.find((s) => s.id === e) ? Promise.resolve() : new Promise((s, o) => {
524
+ const i = () => {
525
+ this._robots?.find((a) => a.id === e) && (this.removeEventListener("robotsChanged", i), clearTimeout(r), s());
526
+ }, r = setTimeout(() => {
527
+ this.removeEventListener("robotsChanged", i), o(new Error(`Timeout waiting for robot ${e} in list`));
528
+ }, t);
529
+ this.addEventListener("robotsChanged", i);
530
+ });
531
+ }
532
+ async startSession(e) {
533
+ if (this._state !== "connected") throw new Error("Not connected");
534
+ this._selectedRobotId = e, this._iceConnected = !1, this._dcOpen = !1, this._micSupported = !1, this._pendingRemoteIce = [];
535
+ try {
536
+ const t = window, s = t.AudioContext ?? t.webkitAudioContext;
537
+ if (!s) throw new Error("AudioContext not supported");
538
+ const o = new s(), i = o.createMediaStreamDestination(), r = o.createOscillator(), a = o.createGain();
539
+ a.gain.value = 0, r.connect(a).connect(i), r.start();
540
+ const l = i.stream;
541
+ l.getAudioTracks().forEach((c) => {
542
+ c.enabled = !1;
543
+ }), this._micStream = l, this._micMuted = !0;
544
+ } catch (t) {
545
+ console.warn("Audio sender placeholder setup failed:", t), this._micStream = null;
546
+ }
547
+ return this._pc = new RTCPeerConnection({
548
+ iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
549
+ }), new Promise((t, s) => {
550
+ this._sessionResolve = t, this._sessionReject = s, this._pc.ontrack = (o) => {
551
+ if (o.track.kind === "video") {
552
+ const i = this._videoJitterBufferTargetMs;
553
+ try {
554
+ o.receiver.jitterBufferTarget = i;
555
+ } catch {
556
+ }
557
+ try {
558
+ o.receiver.playoutDelayHint = i / 1e3;
559
+ } catch {
560
+ }
561
+ this._emit("videoTrack", { track: o.track, stream: o.streams[0] });
562
+ }
563
+ }, this._pc.onicecandidate = async (o) => {
564
+ o.candidate && this._sessionId && await this._sendToServer({
565
+ type: "peer",
566
+ sessionId: this._sessionId,
567
+ ice: {
568
+ candidate: o.candidate.candidate,
569
+ sdpMLineIndex: o.candidate.sdpMLineIndex,
570
+ sdpMid: o.candidate.sdpMid
571
+ }
572
+ });
573
+ }, this._pc.oniceconnectionstatechange = () => {
574
+ const o = this._pc?.iceConnectionState;
575
+ if (o)
576
+ if (o === "connected" || o === "completed")
577
+ this._iceConnected = !0, this._checkSessionReady();
578
+ else if (o === "failed") {
579
+ const i = new Error("ICE connection failed");
580
+ this._sessionReject && (this._sessionReject(i), this._sessionResolve = null, this._sessionReject = null), this._emit("error", { source: "webrtc", error: i });
581
+ } else o === "disconnected" && this._emit("error", { source: "webrtc", error: new Error("ICE disconnected") });
582
+ }, this._pc.ondatachannel = (o) => {
583
+ this._dc = o.channel, this._dc.onopen = () => {
584
+ this._dcOpen = !0, this._checkSessionReady();
585
+ }, this._dc.onmessage = (i) => this._handleRobotMessage(JSON.parse(i.data));
586
+ }, this._sendToServer({ type: "startSession", peerId: e }).then((o) => {
587
+ if (o?.type === "sessionRejected") {
588
+ this._failSessionRejected(o);
589
+ return;
590
+ }
591
+ o?.sessionId && (this._sessionId = o.sessionId);
592
+ });
593
+ });
594
+ }
595
+ _failSessionRejected(e) {
596
+ const t = new Error(
597
+ e.reason === "robot_busy" ? `Robot is busy: "${e.activeApp || "another app"}" is already connected` : `Session rejected: ${e.reason || "unknown reason"}`
598
+ );
599
+ if (t.reason = e.reason ?? null, t.activeApp = e.activeApp ?? null, this._pc && (this._pc.close(), this._pc = null), this._micStream && (this._micStream.getTracks().forEach((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) {
600
+ const s = this._sessionReject;
601
+ this._sessionResolve = null, this._sessionReject = null, s(t);
602
+ }
603
+ }
604
+ async stopSession() {
605
+ this._versionResolve && (this._versionResolve(null), this._versionResolve = null), this._hardwareIdResolve && (this._hardwareIdResolve(null), this._hardwareIdResolve = null), this._volumeResolve && (this._volumeResolve(null), this._volumeResolve = null), this._micVolumeResolve && (this._micVolumeResolve(null), this._micVolumeResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._rejectPendingMotionCompletions(new Error("Session stopped")), this._sessionReject && (this._sessionReject(new Error("Session stopped")), this._sessionResolve = null, this._sessionReject = null), this._stateRefreshInterval && (clearInterval(this._stateRefreshInterval), this._stateRefreshInterval = null), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), this._sessionId && await this._sendToServer({ type: "endSession", sessionId: this._sessionId }), this._micStream && (this._micStream.getTracks().forEach((t) => t.stop()), this._micStream = null), this._micMuted = !0, this._micSupported = !1, this._pc && (this._pc.close(), this._pc = null), this._dc && (this._dc.close(), this._dc = null), this._sessionId = null, this._iceConnected = !1, this._dcOpen = !1, this._state === "streaming" && (this._state = "connected", this._emit("sessionStopped", { reason: "user" }));
606
+ }
607
+ disconnect() {
608
+ this._sseAbortController && (this._sseAbortController.abort(), this._sseAbortController = null), this._versionResolve && (this._versionResolve(null), this._versionResolve = null), this._hardwareIdResolve && (this._hardwareIdResolve(null), this._hardwareIdResolve = null), this._volumeResolve && (this._volumeResolve(null), this._volumeResolve = null), this._micVolumeResolve && (this._micVolumeResolve(null), this._micVolumeResolve = null), this._applyAudioConfigResolve && (this._applyAudioConfigResolve(!1), this._applyAudioConfigResolve = null), this._readAudioParameterResolve && (this._readAudioParameterResolve(null), this._readAudioParameterResolve = null), this._logSubscribers.clear(), this._rejectPendingMotionCompletions(new Error("Disconnected")), this._sessionReject && (this._sessionReject(new Error("Disconnected")), this._sessionResolve = null, this._sessionReject = null), this._stateRefreshInterval && (clearInterval(this._stateRefreshInterval), this._stateRefreshInterval = null), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), this._sessionId && this._token && this._sendToServer({ type: "endSession", sessionId: this._sessionId }), this._micStream && (this._micStream.getTracks().forEach((e) => e.stop()), this._micStream = null), this._pc && (this._pc.close(), this._pc = null), this._dc && (this._dc.close(), this._dc = null), this._sessionId = null, this._micMuted = !0, this._micSupported = !1, this._iceConnected = !1, this._dcOpen = !1, this._robots = [], this._state = "disconnected", this._emit("disconnected", { reason: "user" });
609
+ }
610
+ // ─── Commands ────────────────────────────────────────────────────────
611
+ /**
612
+ * Atomic raw-units pose update over the data channel. Channels you
613
+ * omit are held at their last commanded value (per-axis, independent).
614
+ *
615
+ * **Head pose is in the WORLD frame.** The daemon's IK splits the
616
+ * requested head world-yaw between body rotation and the stewart
617
+ * platform, subject to the mechanical limit
618
+ * `|head_yaw_world − body_yaw| ≤ 65°`.
619
+ *
620
+ * **If you want the head to FOLLOW the body** (tank-style rotation):
621
+ * a `setTarget({ body_yaw })` on its own does NOT rotate the head —
622
+ * the head's commanded world yaw is unchanged, so its gaze stays
623
+ * pinned in world frame while the body turns under it. To make the
624
+ * head turn with the body, include a `head` matrix in the SAME call
625
+ * with the body-yaw delta added to the head RPY's yaw:
626
+ *
627
+ * ```ts
628
+ * // Body-yaw drag handler: tank-couple the head so it follows.
629
+ * const delta = newBodyDeg - lastCommandedBodyDeg;
630
+ * const nextHeadYaw = lastCommandedHeadYawDeg + delta;
631
+ * robot.setTarget({
632
+ * head: rpyToMatrix(headRoll, headPitch, nextHeadYaw).flat(),
633
+ * body_yaw: degToRad(newBodyDeg),
634
+ * });
635
+ * lastCommandedHeadYawDeg = nextHeadYaw;
636
+ * lastCommandedBodyDeg = newBodyDeg;
637
+ * ```
638
+ *
639
+ * **Baseline must be the last COMMANDED value, not telemetry.** For
640
+ * continuous-input controllers (slider drag, joystick), do not use
641
+ * `state.head` from the `state` event as the baseline for incremental
642
+ * commands — telemetry lags one WebRTC round-trip, so cumulative
643
+ * deltas computed against it stall (every iteration in a rapid drag
644
+ * adds the same `delta` to the same stale baseline → the head fails
645
+ * to keep up). Track the last-commanded RPY in your own buffer.
646
+ *
647
+ * @param head Flat row-major 4×4 matrix (16 finite numbers) in
648
+ * the world frame. Omit to hold the previous head target.
649
+ * @param antennas `[rightRad, leftRad]` (radians). Omit to hold.
650
+ * @param body_yaw Signed radians. Omit to hold.
651
+ * @returns `true` if the command was queued on the data channel,
652
+ * `false` if the channel is not open.
653
+ */
654
+ setTarget({ head: e, antennas: t, body_yaw: s } = {}) {
655
+ const o = { type: "set_full_target" };
656
+ if (e !== void 0) {
657
+ if (!Array.isArray(e) || e.length !== 16 || !e.every((i) => Number.isFinite(i)))
658
+ throw new TypeError(
659
+ `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}`
660
+ );
661
+ o.head = e;
662
+ }
663
+ if (t !== void 0) {
664
+ if (!Array.isArray(t) || t.length !== 2 || !t.every((i) => Number.isFinite(i)))
665
+ throw new TypeError(
666
+ `setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
667
+ );
668
+ o.antennas = t;
669
+ }
670
+ if (s !== void 0) {
671
+ if (!Number.isFinite(s))
672
+ throw new TypeError(
673
+ `setTarget: body_yaw must be a finite number (radians); got ${s}`
674
+ );
675
+ o.body_yaw = s;
676
+ }
677
+ return this._sendCommand(o);
678
+ }
679
+ gotoTarget({ head: e, antennas: t, body_yaw: s, duration: o }) {
680
+ const i = { type: "goto_target" };
681
+ if (e !== void 0) {
682
+ if (!Array.isArray(e) || e.length !== 16 || !e.every((r) => Number.isFinite(r)))
683
+ throw new TypeError(
684
+ `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}`
685
+ );
686
+ i.head = e;
687
+ }
688
+ if (t !== void 0) {
689
+ if (!Array.isArray(t) || t.length !== 2 || !t.every((r) => Number.isFinite(r)))
690
+ throw new TypeError(
691
+ `gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t) ? `Array(${t.length})` : typeof t}`
692
+ );
693
+ i.antennas = t;
694
+ }
695
+ if (s !== void 0) {
696
+ if (!Number.isFinite(s))
697
+ throw new TypeError(
698
+ `gotoTarget: body_yaw must be a finite number (radians); got ${s}`
699
+ );
700
+ i.body_yaw = s;
701
+ }
702
+ if (!Number.isFinite(o) || o <= 0)
703
+ throw new TypeError(
704
+ `gotoTarget: duration must be a positive finite number (seconds); got ${o}`
705
+ );
706
+ return i.duration = o, this._sendCommand(i);
707
+ }
708
+ setHeadRpyDeg(e, t, s) {
709
+ return this.setTarget({ head: F(e, t, s).flat() });
710
+ }
711
+ setAntennasDeg(e, t) {
712
+ return this.setTarget({ antennas: [R(e), R(t)] });
713
+ }
714
+ setBodyYawDeg(e) {
715
+ return this.setTarget({ body_yaw: R(e) });
716
+ }
717
+ playSound(e) {
718
+ return this._sendCommand({ type: "play_sound", file: e });
719
+ }
720
+ setMotorMode(e) {
721
+ return this._sendCommand({ type: "set_motor_mode", mode: e });
722
+ }
723
+ setMotorTorque(e, t = null) {
724
+ return this._sendCommand({ type: "set_torque", on: e, ids: t });
725
+ }
726
+ wakeUp({ timeoutMs: e = 8e3 } = {}) {
727
+ return this._sendCommand({ type: "set_motor_mode", mode: "enabled" }), this._sendCommandAwaitCompletion("wake_up", e);
728
+ }
729
+ gotoSleep({ timeoutMs: e = 8e3 } = {}) {
730
+ return this._sendCommandAwaitCompletion("goto_sleep", e);
731
+ }
732
+ _sendCommandAwaitCompletion(e, t) {
733
+ return this._sendCommand({ type: e }) ? new Promise((s, o) => {
734
+ const i = {
735
+ resolve: s,
736
+ reject: o,
737
+ timer: setTimeout(() => {
738
+ const r = this._pendingMotionCompletions[e], a = r.indexOf(i);
739
+ a !== -1 && r.splice(a, 1), o(new Error(`${e} timed out after ${t}ms`));
740
+ }, t)
741
+ };
742
+ this._pendingMotionCompletions[e].push(i);
743
+ }) : Promise.reject(new Error(`${e}: data channel not open`));
744
+ }
745
+ _rejectPendingMotionCompletions(e) {
746
+ for (const t of Object.keys(this._pendingMotionCompletions)) {
747
+ const s = this._pendingMotionCompletions[t];
748
+ for (; s.length; ) {
749
+ const o = s.shift();
750
+ clearTimeout(o.timer), o.reject(e);
751
+ }
752
+ }
753
+ }
754
+ isAwake() {
755
+ const e = this._robotState?.motor_mode;
756
+ return e === "enabled" || e === "gravity_compensation";
757
+ }
758
+ async ensureAwake(e = 1e3) {
759
+ return this._robotState?.motor_mode === void 0 && await new Promise((t) => {
760
+ const s = () => {
761
+ this.removeEventListener("state", s), clearTimeout(o), t();
762
+ }, o = setTimeout(s, e);
763
+ this.addEventListener("state", s), this.requestState();
764
+ }), this.isAwake() || this.wakeUp().catch(() => {
765
+ }), !0;
766
+ }
767
+ getVersion() {
768
+ return new Promise((e, t) => {
769
+ if (!this._dc || this._dc.readyState !== "open") {
770
+ t(new Error("Data channel not open"));
771
+ return;
772
+ }
773
+ this._versionResolve && this._versionResolve(null), this._versionResolve = e, this._sendCommand({ type: "get_version" });
774
+ });
775
+ }
776
+ getHardwareId() {
777
+ return new Promise((e, t) => {
778
+ if (!this._dc || this._dc.readyState !== "open") {
779
+ t(new Error("Data channel not open"));
780
+ return;
781
+ }
782
+ this._hardwareIdResolve && this._hardwareIdResolve(null), this._hardwareIdResolve = e, this._sendCommand({ type: "get_hardware_id" });
783
+ });
784
+ }
785
+ getVolume() {
786
+ return this._slotRoundtrip(
787
+ () => this._volumeResolve,
788
+ (e) => {
789
+ this._volumeResolve = e;
790
+ },
791
+ { type: "get_volume" }
792
+ );
793
+ }
794
+ setVolume(e) {
795
+ return this._slotRoundtrip(
796
+ () => this._volumeResolve,
797
+ (t) => {
798
+ this._volumeResolve = t;
799
+ },
800
+ { type: "set_volume", volume: j(e) }
801
+ );
802
+ }
803
+ getMicrophoneVolume() {
804
+ return this._slotRoundtrip(
805
+ () => this._micVolumeResolve,
806
+ (e) => {
807
+ this._micVolumeResolve = e;
808
+ },
809
+ { type: "get_microphone_volume" }
810
+ );
811
+ }
812
+ setMicrophoneVolume(e) {
813
+ return this._slotRoundtrip(
814
+ () => this._micVolumeResolve,
815
+ (t) => {
816
+ this._micVolumeResolve = t;
817
+ },
818
+ { type: "set_microphone_volume", volume: j(e) }
819
+ );
820
+ }
821
+ applyAudioConfig(e, { verify: t = !0 } = {}) {
822
+ return this._slotRoundtrip(
823
+ () => this._applyAudioConfigResolve,
824
+ (s) => {
825
+ this._applyAudioConfigResolve = s;
826
+ },
827
+ { type: "apply_audio_config", config: e, verify: t }
828
+ ).then((s) => s === !0);
829
+ }
830
+ readAudioParameter(e) {
831
+ return this._slotRoundtrip(
832
+ () => this._readAudioParameterResolve,
833
+ (t) => {
834
+ this._readAudioParameterResolve = t;
835
+ },
836
+ { type: "read_audio_parameter", name: e }
837
+ );
838
+ }
839
+ /**
840
+ * Internal: send a command and await the matching daemon response in a
841
+ * named single-resolver slot. Used by the volume helpers and the
842
+ * XVF3800 audio-config helpers — every one of them has a strict
843
+ * request/response shape where a single in-flight call per slot is
844
+ * sufficient. If a previous request on the same slot is still
845
+ * pending when a new one comes in, the older promise is resolved to
846
+ * `null` so its caller doesn't hang forever.
847
+ *
848
+ * Slot access is passed in as getter/setter closures rather than a
849
+ * key into `this`: that keeps the helper fully generic-checked (T is
850
+ * inferred from the slot's resolver type at each call site) with no
851
+ * indexed-property casts.
852
+ */
853
+ _slotRoundtrip(e, t, s) {
854
+ return new Promise((o, i) => {
855
+ if (!this._dc || this._dc.readyState !== "open") {
856
+ i(new Error("Data channel not open"));
857
+ return;
858
+ }
859
+ const r = e();
860
+ r && r(null), t(o), this._sendCommand(s);
861
+ });
862
+ }
863
+ sendRaw(e) {
864
+ return this._sendCommand(e);
865
+ }
866
+ subscribeLogs({ onLine: e, onError: t }) {
867
+ if (typeof e != "function")
868
+ throw new TypeError("subscribeLogs: onLine callback is required");
869
+ const s = { onLine: e, onError: t }, o = this._logSubscribers.size === 0;
870
+ this._logSubscribers.add(s), o && this._sendCommand({ type: "subscribe_logs" });
871
+ let i = !1;
872
+ return () => {
873
+ i || (i = !0, this._logSubscribers.delete(s), this._logSubscribers.size === 0 && this._sendCommand({ type: "unsubscribe_logs" }));
874
+ };
875
+ }
876
+ requestState() {
877
+ return this._sendCommand({ type: "get_state" });
878
+ }
879
+ // ─── Audio ───────────────────────────────────────────────────────────
880
+ setAudioMuted(e) {
881
+ this._audioMuted = e, this._videoElement && (this._videoElement.muted = e);
882
+ }
883
+ setMicMuted(e) {
884
+ this._micMuted = e, this._micStream && this._micStream.getAudioTracks().forEach((t) => {
885
+ t.enabled = !e;
886
+ });
887
+ }
888
+ // ─── Video helper ────────────────────────────────────────────────────
889
+ attachVideo(e) {
890
+ this._videoElement = e, e.muted = this._audioMuted;
891
+ const t = (o) => {
892
+ const i = o;
893
+ e.srcObject = i.detail.stream, e.playsInline = !0, "requestVideoFrameCallback" in e && this._startLatencyMonitor(e);
894
+ }, s = () => {
895
+ e.srcObject = null;
896
+ };
897
+ return this.addEventListener("videoTrack", t), this.addEventListener("sessionStopped", s), () => {
898
+ this.removeEventListener("videoTrack", t), this.removeEventListener("sessionStopped", s), this._latencyMonitorId && (clearInterval(this._latencyMonitorId), this._latencyMonitorId = null), e.srcObject = null, this._videoElement = null;
899
+ };
900
+ }
901
+ // ─── Daemon-side recorded-move playback ──────────────────────────────
902
+ async playMove(e, {
903
+ audioBlob: t = null,
904
+ audioLeadMs: s = -100,
905
+ description: o = "move",
906
+ encoding: i = "gzip+base64",
907
+ playFrequency: r = 100,
908
+ initialGotoDuration: a = 0,
909
+ startTimeoutMs: l = 8e3,
910
+ onProgress: c = () => {
911
+ },
912
+ onStarted: h = () => {
913
+ }
914
+ } = {}) {
915
+ if (!this._dc || this._dc.readyState !== "open")
916
+ throw new Error("data channel not open");
917
+ if (!e?.time?.length || !e?.set_target_data?.length)
918
+ throw new Error("playMove: motion must have time + set_target_data");
919
+ const d = U();
920
+ this._activeMoveUploadId = d;
921
+ const m = {
922
+ description: o,
923
+ time: e.time,
924
+ set_target_data: e.set_target_data
925
+ }, f = JSON.stringify(m);
926
+ let p, v;
927
+ i === "gzip+base64" && W() ? (p = await z(f), v = "gzip+base64") : (p = f, v = "json");
928
+ const w = Math.ceil(p.length / y) || 1;
929
+ c({
930
+ phase: "starting",
931
+ sent: 0,
932
+ total: w,
933
+ bytes: p.length,
934
+ encoding: v
935
+ }), this._sendCommand({
936
+ type: "upload_move_start",
937
+ upload_id: d,
938
+ total_chunks: w,
939
+ description: o,
940
+ encoding: v
941
+ });
942
+ for (let u = 0; u < w; u++) {
943
+ this._dc.bufferedAmount > k && await this._awaitDataChannelDrain();
944
+ const _ = u * y;
945
+ this._sendCommand({
946
+ type: "upload_move_chunk",
947
+ upload_id: d,
948
+ chunk_index: u,
949
+ chunk: p.slice(_, _ + y)
950
+ }), c({ phase: "upload", sent: u + 1, total: w });
951
+ }
952
+ if (this._sendCommand({ type: "upload_move_finish", upload_id: d }), c({ phase: "uploaded", sent: w, total: w }), t) {
953
+ const u = new Uint8Array(await t.arrayBuffer()), _ = E(u), b = Math.ceil(_.length / y) || 1;
954
+ c({
955
+ phase: "audio-starting",
956
+ sent: 0,
957
+ total: b,
958
+ bytes: _.length
959
+ }), this._sendCommand({
960
+ type: "upload_audio_start",
961
+ upload_id: d,
962
+ total_chunks: b,
963
+ encoding: "wav-base64",
964
+ description: o
965
+ });
966
+ for (let I = 0; I < b; I++) {
967
+ this._dc.bufferedAmount > k && await this._awaitDataChannelDrain();
968
+ const T = I * y;
969
+ this._sendCommand({
970
+ type: "upload_audio_chunk",
971
+ upload_id: d,
972
+ chunk_index: I,
973
+ chunk: _.slice(T, T + y)
974
+ }), c({ phase: "audio-upload", sent: I + 1, total: b });
975
+ }
976
+ this._sendCommand({ type: "upload_audio_finish", upload_id: d }), c({ phase: "audio-uploaded", sent: b, total: b });
977
+ }
978
+ this._sendCommand({
979
+ type: "play_uploaded_move",
980
+ upload_id: d,
981
+ play_frequency: r,
982
+ initial_goto_duration: a,
983
+ audio_lead_ms: s
984
+ });
985
+ let g;
986
+ try {
987
+ g = await this._waitForBroadcast(
988
+ (u) => u?.type === "play_uploaded_move" && u?.upload_id === d && (u.started === !0 || typeof u.error == "string"),
989
+ { timeoutMs: l, debugLabel: "play_uploaded_move started" }
990
+ );
991
+ } catch (u) {
992
+ throw new Error(
993
+ `Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${u.message}`
994
+ );
995
+ }
996
+ if (typeof g.error == "string")
997
+ throw new Error(`play_uploaded_move: ${g.error}`);
998
+ try {
999
+ h({
1000
+ duration_s: g.duration_s,
1001
+ has_audio: g.has_audio === !0
1002
+ });
1003
+ } catch (u) {
1004
+ console.warn("playMove.onStarted threw:", u);
1005
+ }
1006
+ c({ phase: "playing", duration_s: g.duration_s });
1007
+ const S = await this._waitForBroadcast(
1008
+ (u) => u?.type === "play_uploaded_move" && u?.upload_id === d && (u.finished === !0 || u.cancelled === !0 || typeof u.error == "string"),
1009
+ {
1010
+ timeoutMs: (g.duration_s + 30) * 1e3,
1011
+ debugLabel: "play_uploaded_move final"
1012
+ }
1013
+ );
1014
+ return this._activeMoveUploadId === d && (this._activeMoveUploadId = null), S;
1015
+ }
1016
+ cancelMove(e = null) {
1017
+ const t = e ?? this._activeMoveUploadId;
1018
+ return t ? this._sendCommand({ type: "cancel_move", upload_id: t }) : !1;
1019
+ }
1020
+ async uploadAudio(e, { description: t = "audio", onProgress: s = () => {
1021
+ } } = {}) {
1022
+ if (!this._dc || this._dc.readyState !== "open")
1023
+ throw new Error("data channel not open");
1024
+ if (!(e instanceof Blob))
1025
+ throw new TypeError("uploadAudio: expected a Blob");
1026
+ const o = U(), i = new Uint8Array(await e.arrayBuffer()), r = E(i), a = Math.ceil(r.length / y) || 1;
1027
+ s({ phase: "audio-starting", sent: 0, total: a, bytes: r.length }), this._sendCommand({
1028
+ type: "upload_audio_start",
1029
+ upload_id: o,
1030
+ total_chunks: a,
1031
+ encoding: "wav-base64",
1032
+ description: t
1033
+ });
1034
+ for (let l = 0; l < a; l++) {
1035
+ this._dc.bufferedAmount > k && await this._awaitDataChannelDrain();
1036
+ const c = l * y;
1037
+ this._sendCommand({
1038
+ type: "upload_audio_chunk",
1039
+ upload_id: o,
1040
+ chunk_index: l,
1041
+ chunk: r.slice(c, c + y)
1042
+ }), s({ phase: "audio-upload", sent: l + 1, total: a });
1043
+ }
1044
+ return this._sendCommand({ type: "upload_audio_finish", upload_id: o }), s({ phase: "audio-uploaded", sent: a, total: a }), o;
1045
+ }
1046
+ async playUploadedAudio(e, { timeoutMs: t = 8e3 } = {}) {
1047
+ if (!this._dc || this._dc.readyState !== "open")
1048
+ throw new Error("data channel not open");
1049
+ const s = this._waitForBroadcast(
1050
+ (i) => i?.type === "play_uploaded_audio" && i?.upload_id === e && (i.started === !0 || typeof i.error == "string"),
1051
+ { timeoutMs: t, debugLabel: "play_uploaded_audio started" }
1052
+ );
1053
+ this._sendCommand({ type: "play_uploaded_audio", upload_id: e });
1054
+ const o = await s;
1055
+ if (typeof o.error == "string") throw new Error(o.error);
1056
+ return this._activeAudioUploadId = e, o;
1057
+ }
1058
+ cancelAudio(e = null) {
1059
+ const t = e ?? this._activeAudioUploadId;
1060
+ return t ? (this._activeAudioUploadId === t && (this._activeAudioUploadId = null), this._sendCommand({ type: "cancel_audio", upload_id: t })) : !1;
1061
+ }
1062
+ // ─── Private ─────────────────────────────────────────────────────────
1063
+ _emit(e, t) {
1064
+ this.dispatchEvent(new CustomEvent(e, { detail: t }));
1065
+ }
1066
+ _waitForBroadcast(e, { timeoutMs: t = 5e3, debugLabel: s = "" } = {}) {
1067
+ return new Promise((o, i) => {
1068
+ const r = {
1069
+ predicate: e,
1070
+ resolve: o,
1071
+ timer: setTimeout(() => {
1072
+ const a = this._broadcastWaiters.indexOf(r);
1073
+ a !== -1 && this._broadcastWaiters.splice(a, 1), i(new Error(`broadcast timeout (${t} ms): ${s}`));
1074
+ }, t)
1075
+ };
1076
+ this._broadcastWaiters.push(r);
1077
+ });
1078
+ }
1079
+ async _awaitDataChannelDrain() {
1080
+ for (; this._dc && this._dc.bufferedAmount > H; )
1081
+ if (await new Promise((e) => setTimeout(e, 30)), !this._dc || this._dc.readyState !== "open")
1082
+ throw new Error("data channel closed mid-upload");
1083
+ }
1084
+ async _sendToServer(e) {
1085
+ if (!this._token) throw new Error("No token — authenticate() first");
1086
+ try {
1087
+ const t = await fetch(`${this._signalingUrl}/send`, {
1088
+ method: "POST",
1089
+ headers: {
1090
+ "Content-Type": "application/json",
1091
+ Authorization: `Bearer ${this._token}`
1092
+ },
1093
+ body: JSON.stringify(e)
1094
+ });
1095
+ if (!t.ok) {
1096
+ let s = "";
1097
+ try {
1098
+ s = await t.text();
1099
+ } catch {
1100
+ }
1101
+ return console.warn(
1102
+ `[reachy-mini] /send rejected (${t.status}) for type=${e?.type}; body=${s || "<empty>"}`
1103
+ ), null;
1104
+ }
1105
+ return await t.json();
1106
+ } catch (t) {
1107
+ return console.error("Send error:", t), null;
1108
+ }
1109
+ }
1110
+ _sendCommand(e) {
1111
+ return !this._dc || this._dc.readyState !== "open" ? !1 : (this._dc.send(JSON.stringify(e)), !0);
1112
+ }
1113
+ _checkSessionReady() {
1114
+ 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);
1115
+ }
1116
+ async _handleSignalingMessage(e) {
1117
+ switch (e.type) {
1118
+ case "welcome":
1119
+ break;
1120
+ case "list":
1121
+ this._robots = e.producers || [], this._emit("robotsChanged", { robots: this._robots }), this._maybeAutoStart();
1122
+ break;
1123
+ case "peerStatusChanged": {
1124
+ const t = await this._sendToServer({ type: "list" });
1125
+ t?.producers && (this._robots = t.producers, this._emit("robotsChanged", { robots: this._robots }), this._maybeAutoStart());
1126
+ break;
1127
+ }
1128
+ case "sessionStarted":
1129
+ this._sessionId = e.sessionId ?? null;
1130
+ break;
1131
+ case "sessionRejected":
1132
+ this._failSessionRejected(e);
1133
+ break;
1134
+ case "endSession":
1135
+ this._handleEndSession(e);
1136
+ break;
1137
+ case "peer":
1138
+ this._handlePeerMessage(e);
1139
+ break;
1140
+ }
1141
+ }
1142
+ _handleEndSession(e) {
1143
+ 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;
1144
+ if (this._sessionReject) {
1145
+ const o = new Error(
1146
+ s || `Session ended before it could start: ${t || "unknown reason"}`
1147
+ );
1148
+ o.reason = t ?? null, this._emit("sessionRejected", { reason: t, activeApp: null }), 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;
1149
+ const i = this._sessionReject;
1150
+ this._sessionResolve = null, this._sessionReject = null, i(o);
1151
+ return;
1152
+ }
1153
+ this._state === "streaming" && (this._emit("sessionStopped", {
1154
+ reason: t || "remote_end",
1155
+ message: s
1156
+ }), this.stopSession().catch(() => {
1157
+ }));
1158
+ }
1159
+ async _handlePeerMessage(e) {
1160
+ if (this._pc)
1161
+ try {
1162
+ if (e.sdp) {
1163
+ const t = e.sdp;
1164
+ if (t.type === "offer") {
1165
+ const o = q(t.sdp);
1166
+ if (this._micSupported = o, this._emit("micSupported", { supported: o }), o && this._micStream)
1167
+ for (const r of this._micStream.getAudioTracks())
1168
+ this._pc.addTrack(r, this._micStream);
1169
+ await this._pc.setRemoteDescription(new RTCSessionDescription(t));
1170
+ const i = await this._pc.createAnswer();
1171
+ await this._pc.setLocalDescription(i), await this._sendToServer({
1172
+ type: "peer",
1173
+ sessionId: this._sessionId,
1174
+ sdp: { type: "answer", sdp: i.sdp }
1175
+ });
1176
+ } else
1177
+ await this._pc.setRemoteDescription(new RTCSessionDescription(t));
1178
+ const s = this._pendingRemoteIce;
1179
+ if (s && s.length) {
1180
+ this._pendingRemoteIce = [];
1181
+ for (const o of s)
1182
+ try {
1183
+ await this._pc.addIceCandidate(new RTCIceCandidate(o));
1184
+ } catch (i) {
1185
+ console.warn("[reachy-mini] buffered ICE candidate rejected:", i);
1186
+ }
1187
+ }
1188
+ }
1189
+ if (e.ice) {
1190
+ if (!e.ice.candidate) return;
1191
+ this._pc.remoteDescription ? await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)) : (this._pendingRemoteIce || (this._pendingRemoteIce = []), this._pendingRemoteIce.push(e.ice));
1192
+ }
1193
+ } catch (t) {
1194
+ console.error("WebRTC error:", t), this._emit("error", { source: "webrtc", error: t });
1195
+ }
1196
+ }
1197
+ _handleRobotMessage(e) {
1198
+ if ("version" in e && this._versionResolve) {
1199
+ this._versionResolve(e.version), this._versionResolve = null;
1200
+ return;
1201
+ }
1202
+ if ("hardware_id" in e && this._hardwareIdResolve) {
1203
+ this._hardwareIdResolve(e.hardware_id), this._hardwareIdResolve = null;
1204
+ return;
1205
+ }
1206
+ if (e.command === "get_volume" || e.command === "set_volume") {
1207
+ this._volumeResolve && (this._volumeResolve(e.status === "error" ? null : e.volume), this._volumeResolve = null);
1208
+ return;
1209
+ }
1210
+ if (e.command === "get_microphone_volume" || e.command === "set_microphone_volume") {
1211
+ this._micVolumeResolve && (this._micVolumeResolve(e.status === "error" ? null : e.volume), this._micVolumeResolve = null);
1212
+ return;
1213
+ }
1214
+ if (e.command === "apply_audio_config") {
1215
+ this._applyAudioConfigResolve && (this._applyAudioConfigResolve(e.error ? !1 : !!e.applied), this._applyAudioConfigResolve = null);
1216
+ return;
1217
+ }
1218
+ if (e.command === "read_audio_parameter") {
1219
+ this._readAudioParameterResolve && (this._readAudioParameterResolve(
1220
+ e.error ? null : e.values ?? null
1221
+ ), this._readAudioParameterResolve = null);
1222
+ return;
1223
+ }
1224
+ if ((e.command === "wake_up" || e.command === "goto_sleep") && this._pendingMotionCompletions && this._pendingMotionCompletions[e.command]) {
1225
+ const t = this._pendingMotionCompletions[e.command];
1226
+ if (e.completed === !0 && t.length > 0) {
1227
+ const s = t.shift();
1228
+ clearTimeout(s.timer), s.resolve();
1229
+ return;
1230
+ }
1231
+ if (e.error && t.length > 0) {
1232
+ const s = t.shift();
1233
+ clearTimeout(s.timer), s.reject(new Error(`${e.command}: ${e.error}`));
1234
+ return;
1235
+ }
1236
+ }
1237
+ if (e.type === "log_line") {
1238
+ for (const t of this._logSubscribers)
1239
+ try {
1240
+ t.onLine({ timestamp: e.timestamp, line: e.line });
1241
+ } catch (s) {
1242
+ console.error("subscribeLogs onLine threw:", s);
1243
+ }
1244
+ return;
1245
+ }
1246
+ if (e.type === "log_stream_error") {
1247
+ for (const t of this._logSubscribers)
1248
+ if (typeof t.onError == "function")
1249
+ try {
1250
+ t.onError(e.error);
1251
+ } catch (s) {
1252
+ console.error("subscribeLogs onError threw:", s);
1253
+ }
1254
+ return;
1255
+ }
1256
+ if (e.state) {
1257
+ const t = e.state;
1258
+ 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), this._emit("state", { ...this._robotState });
1259
+ }
1260
+ if (e.error && this._emit("error", { source: "robot", error: e.error }), this._broadcastWaiters.length > 0)
1261
+ for (let t = this._broadcastWaiters.length - 1; t >= 0; t--) {
1262
+ const s = this._broadcastWaiters[t];
1263
+ if (s.predicate(e)) {
1264
+ this._broadcastWaiters.splice(t, 1), clearTimeout(s.timer), s.resolve(e);
1265
+ return;
1266
+ }
1267
+ }
1268
+ }
1269
+ /** Snap video playback to live edge if buffered lag exceeds 0.5 s. */
1270
+ _startLatencyMonitor(e) {
1271
+ this._latencyMonitorId && clearInterval(this._latencyMonitorId), this._latencyMonitorId = setInterval(() => {
1272
+ if (!e.srcObject || e.paused) return;
1273
+ const t = e.buffered;
1274
+ if (t.length > 0) {
1275
+ const s = t.end(t.length - 1), o = s - e.currentTime;
1276
+ o > 0.5 && (console.log(`Latency correction: was ${o.toFixed(2)}s behind`), e.currentTime = s - 0.1);
1277
+ }
1278
+ }, 2e3);
1279
+ }
1280
+ }
1281
+ export {
1282
+ J as R
1283
+ };