@pollen-robotics/reachy-mini-sdk 1.8.0 → 1.8.1

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