@burdenoff/fe-libs 2026.1024.4 → 2026.1026.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.
@@ -34,14 +34,13 @@ var m = "inline-flex h-8 items-center gap-1.5 rounded-full border border-border-
34
34
  children: [
35
35
  /* @__PURE__ */ f("video", {
36
36
  ref: _.attachVideo,
37
- className: `size-full object-cover ${A.videoReady ? "" : "invisible"}`,
37
+ className: `size-full object-cover ${A.videoReady && !A.videoBlocked ? "" : "invisible"}`,
38
38
  "aria-label": x.stageLabel,
39
- autoPlay: !0,
40
39
  muted: !0,
41
40
  playsInline: !0,
42
41
  "data-testid": "assistant-live-avatar-video"
43
42
  }),
44
- A.videoReady ? null : /* @__PURE__ */ f("p", {
43
+ A.videoReady && !A.videoBlocked || j === "ready" && A.videoBlocked ? null : /* @__PURE__ */ f("p", {
45
44
  className: "absolute inset-0 flex items-center justify-center p-3 text-center text-xs text-text-muted",
46
45
  children: j === "ending" ? x.stateEnding : j === "connecting" ? x.stageWaiting : x.stateStarting
47
46
  }),
@@ -102,9 +102,8 @@ var h = "text-xs font-medium text-text-secondary", g = "text-xs text-text-muted"
102
102
  children: [
103
103
  P ? /* @__PURE__ */ l("video", {
104
104
  ref: i.attachVideo,
105
- className: `size-full object-cover ${j.videoReady ? "" : "invisible"}`,
105
+ className: `size-full object-cover ${j.videoReady && !j.videoBlocked ? "" : "invisible"}`,
106
106
  "aria-label": C.stageLabel,
107
- autoPlay: !0,
108
107
  muted: !0,
109
108
  playsInline: !0,
110
109
  "data-testid": "assistant-avatar-tab-video"
@@ -115,7 +114,7 @@ var h = "text-xs font-medium text-text-secondary", g = "text-xs text-text-muted"
115
114
  src: X,
116
115
  onError: ze
117
116
  }) : null,
118
- P && j.videoReady ? null : /* @__PURE__ */ l("p", {
117
+ P && j.videoReady && !j.videoBlocked || N === "ready" && j.videoBlocked ? null : /* @__PURE__ */ l("p", {
119
118
  className: X === null ? "absolute inset-0 flex items-center justify-center p-3 text-center text-sm text-text-secondary" : "absolute inset-x-0 bottom-0 bg-bg-elevated/90 px-3 py-1.5 text-center text-xs text-text-secondary",
120
119
  "data-testid": "assistant-avatar-tab-frame-text",
121
120
  children: Ie
@@ -63,4 +63,13 @@ export interface AvatarRoom {
63
63
  startAudio(): Promise<void>;
64
64
  /** Whether the browser currently lets remote audio play (false until a user gesture while autoplay is blocked). */
65
65
  canPlaybackAudio(): boolean;
66
+ /**
67
+ * ★ BOFF-9696: the browser refused (false) or allowed again (true) the avatar's VIDEO. iOS in Low Power Mode refuses even a muted
68
+ * inline video until a user gesture. Optional: a room without it never reports a refused video.
69
+ */
70
+ onVideoPlaybackChanged?(handler: (canPlay: boolean) => void): AvatarUnsubscribe;
71
+ /** Start the refused video after a user gesture: the video twin of startAudio. */
72
+ startVideo?(): Promise<void>;
73
+ /** Whether the browser currently lets the remote video play. */
74
+ canPlaybackVideo?(): boolean;
66
75
  }
@@ -51,6 +51,11 @@ export type AvatarTransportEvent = {
51
51
  readonly type: 'audioPlayback';
52
52
  readonly canPlay: boolean;
53
53
  }
54
+ /** ★ BOFF-9696: the browser refused (or allowed again) the avatar's video, e.g. iOS in Low Power Mode until a user gesture. */
55
+ | {
56
+ readonly type: 'videoPlayback';
57
+ readonly canPlay: boolean;
58
+ }
54
59
  /** Result of the readiness probe at connect (see AvatarReadinessProbeOptions). */
55
60
  | {
56
61
  readonly type: 'readiness';
@@ -168,6 +173,8 @@ export declare class LiveKitAvatarTransport {
168
173
  private videoPresent;
169
174
  private degraded;
170
175
  private audioElement;
176
+ /** ★ BOFF-9696 (S1): the browser paused the avatar's audio element on its own (see watchAudioElement). */
177
+ private elementPaused;
171
178
  private disposed;
172
179
  private rejectReady;
173
180
  private mediaReady;
@@ -218,15 +225,30 @@ export declare class LiveKitAvatarTransport {
218
225
  get transportState(): AvatarTransportState;
219
226
  /** The readiness probe's result once connect() has resolved; null before. */
220
227
  get readiness(): AvatarReadinessStatus | null;
221
- /** True while the browser blocks the avatar's audio (autoplay policy): resumeAudio() from a user gesture is needed. */
228
+ /**
229
+ * True while the browser blocks the avatar's audio: by autoplay policy, or by pausing its element on its own (an interruption, the lock
230
+ * screen). resumeAudio() from a user gesture is needed.
231
+ */
222
232
  get audioBlocked(): boolean;
233
+ /** ★ BOFF-9696: true while the browser refuses the avatar's video (resumeVideo() from a user gesture plays it). */
234
+ get videoBlocked(): boolean;
223
235
  subscribe(handler: (event: AvatarTransportEvent) => void): AvatarUnsubscribe;
224
236
  private emit;
225
237
  /**
226
- * Resume audio playback. Call it synchronously from the user's click on the Avatar toggle (a user gesture is required by
227
- * the browser's autoplay policy), and again from a "tap to enable sound" control when audioPlayback reports canPlay=false.
238
+ * Resume audio playback. Call it synchronously from a user gesture (the browser's autoplay policy requires one): today the
239
+ * "tap to enable sound" control (`controller.resumeAudio()`), shown when audioPlayback reports canPlay=false.
228
240
  */
229
241
  resumeAudio(): Promise<void>;
242
+ /** ★ BOFF-9696: play a refused video. Call it synchronously from a user gesture, like resumeAudio(). */
243
+ resumeVideo(): Promise<void>;
244
+ /**
245
+ * ★ BOFF-9696 (S1): an element the browser PAUSED on its own while the avatar is on (an interruption such as a call or Siri, the lock
246
+ * screen, or WebKit pausing it when it is unmuted). The SDK only learns of a blocked playback from a failed play(), so without this the
247
+ * avatar would lip-sync with no voice and no way back. The pause reports the sound blocked; the existing tap (resumeAudio: the SDK's
248
+ * startAudio plays every attached element) plays it again, and its 'playing' reports it back. Only while ready and fed: a release
249
+ * (srcObject null) or a teardown is not a block.
250
+ */
251
+ private watchAudioElement;
230
252
  /** Mute or unmute the avatar's audio element instantly (used to silence output on cancel before the provider confirms). */
231
253
  setOutputMuted(muted: boolean): void;
232
254
  connect(params: AvatarConnectParams): Promise<void>;
@@ -65,6 +65,7 @@ var p = class {
65
65
  videoPresent = !1;
66
66
  degraded = !1;
67
67
  audioElement = null;
68
+ elementPaused = !1;
68
69
  disposed = !1;
69
70
  rejectReady = null;
70
71
  mediaReady = !1;
@@ -85,7 +86,10 @@ var p = class {
85
86
  return this.readinessStatus;
86
87
  }
87
88
  get audioBlocked() {
88
- return this.state === "ready" && !this.options.room.canPlaybackAudio();
89
+ return this.state === "ready" && (!this.options.room.canPlaybackAudio() || this.elementPaused);
90
+ }
91
+ get videoBlocked() {
92
+ return this.state === "ready" && this.options.room.canPlaybackVideo?.() === !1;
89
93
  }
90
94
  subscribe(e) {
91
95
  return this.listeners.add(e), () => {
@@ -100,6 +104,26 @@ var p = class {
100
104
  async resumeAudio() {
101
105
  await this.options.room.startAudio();
102
106
  }
107
+ async resumeVideo() {
108
+ await this.options.room.startVideo?.();
109
+ }
110
+ watchAudioElement(e) {
111
+ if (e === null || typeof e.addEventListener != "function") return;
112
+ let t = () => {
113
+ this.disposed || this.state !== "ready" || e.srcObject === null || this.elementPaused || (this.elementPaused = !0, this.emit({
114
+ type: "audioPlayback",
115
+ canPlay: !1
116
+ }));
117
+ }, n = () => {
118
+ this.elementPaused && (this.elementPaused = !1, this.emit({
119
+ type: "audioPlayback",
120
+ canPlay: this.options.room.canPlaybackAudio()
121
+ }));
122
+ };
123
+ e.addEventListener("pause", t), e.addEventListener("playing", n), this.unsubscribes.push(() => {
124
+ e.removeEventListener("pause", t), e.removeEventListener("playing", n);
125
+ });
126
+ }
103
127
  setOutputMuted(e) {
104
128
  this.audioElement && (this.audioElement.muted = e);
105
129
  }
@@ -107,7 +131,7 @@ var p = class {
107
131
  if (this.state !== "idle") throw Error("transport already used");
108
132
  this.state = "connecting";
109
133
  let { room: t, avatarIdentity: i } = this.options;
110
- this.audioElement = e.audioElement ?? null, t.registerRpc(n, (e, t) => this.handleFinished(e, t)), t.registerRpc(r, (e) => this.handleStarted(e)), this.unsubscribes.push(t.onParticipantConnected((e) => {
134
+ this.audioElement = e.audioElement ?? null, this.watchAudioElement(this.audioElement), t.registerRpc(n, (e, t) => this.handleFinished(e, t)), t.registerRpc(r, (e) => this.handleStarted(e)), this.unsubscribes.push(t.onParticipantConnected((e) => {
111
135
  e === i && (this.avatarPresent = !0, this.checkReady());
112
136
  }), t.onParticipantDisconnected((e) => {
113
137
  e === i && (this.avatarPresent = !1, this.videoPresent = !1, this.avatarDepartures += 1, this.owedFinishes = 0, this.endAllSegments(), this.emit({ type: "avatarLeft" }));
@@ -122,6 +146,9 @@ var p = class {
122
146
  }), t.onAudioPlaybackChanged((e) => this.emit({
123
147
  type: "audioPlayback",
124
148
  canPlay: e
149
+ }))), t.onVideoPlaybackChanged && this.unsubscribes.push(t.onVideoPlaybackChanged((e) => this.emit({
150
+ type: "videoPlayback",
151
+ canPlay: e
125
152
  })));
126
153
  try {
127
154
  await t.connect(e.url, e.token);
@@ -58,7 +58,7 @@ export interface LiveAvatarSeamOptions {
58
58
  readonly avatarVoiceStore?: LiveAvatarVoiceStore;
59
59
  readonly scheduler?: AvatarScheduler;
60
60
  readonly browserSupported?: () => boolean;
61
- readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'isPageVisible' | 'subscribePageVisible' | 'subscribePageShow' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
61
+ readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'subscribePageHidden' | 'endOnHide' | 'subscribeUserGesture' | 'isPageVisible' | 'subscribePageVisible' | 'subscribePageShow' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
62
62
  }
63
63
  /** The seam, plus what a host or a test may need of it. */
64
64
  export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
@@ -71,9 +71,12 @@ export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
71
71
  dispose(): void;
72
72
  }
73
73
  /**
74
- * Whether this browser may show the avatar. Desktop Chrome first (GATE1B §10: no other browser or phone is offered until it is tested,
75
- * Chromium builds included, other Chromium-based browsers not), with the platform pieces the avatar needs: WebRTC, Web Audio (the level
76
- * meter and the MP3 decode) and MediaStream.
74
+ * Whether this browser can show the avatar: a check of what the avatar needs, on every browser and phone (BOFF-9696, the operator's
75
+ * decision of 2026-10-25; until then desktop Chrome only, GATE1B §10). The pieces: WebRTC (the room), an AudioContext (the level meter),
76
+ * MediaStream, and an OfflineAudioContext at the reply audio's rate (the MP3 decode makes one: avatarAudioDecode.ts). All four are
77
+ * unprefixed: Safari shipped the unprefixed audio contexts together (14.1), and the prefixed offline context's callback-only decode
78
+ * could not serve the avatar anyway. The user agent is never read: a browser that has these runs the avatar, one that lacks any cannot.
79
+ * Where autoplay needs a tap for sound (Safari and every iOS browser), the sound-blocked state asks for it.
77
80
  */
78
81
  export declare function isLiveAvatarBrowserSupported(scope?: typeof globalThis): boolean;
79
82
  /** The `Authorization` the hosted TTS requests carry (the host's `getHeaders`), else the stored access token. Nothing else is read. */
@@ -91,4 +94,19 @@ export declare function liveAvatarWorkspaceAuthorization(options: {
91
94
  readonly fresh: boolean;
92
95
  readonly refused?: string;
93
96
  }): Promise<string | null>;
97
+ /**
98
+ * ★ BOFF-9696 (S5): a touch-first device (a phone or tablet without a hovering pointer), where a hidden page ends the session. A
99
+ * capability query, never the user agent. Exported for tests only.
100
+ */
101
+ export declare function isTouchFirstDevice(scope?: {
102
+ readonly matchMedia?: (query: string) => {
103
+ readonly matches: boolean;
104
+ };
105
+ }): boolean;
106
+ /**
107
+ * ★ BOFF-9696: the person's taps, clicks and keys anywhere on the page, in the capture phase so no handler can stop them first. Each
108
+ * is a user gesture, inside which a browser lets a refused video start (iOS unlocks media on `touchend` and `click`). Only trusted
109
+ * events count: a script's synthetic click is no gesture. Exported for tests only.
110
+ */
111
+ export declare function documentUserGesture(doc?: Pick<Document, 'addEventListener' | 'removeEventListener'> | null): ((listener: () => void) => () => void) | undefined;
94
112
  export declare function createLiveAvatarSeam(options?: LiveAvatarSeamOptions): LiveAvatarSeamHandle;
@@ -4,36 +4,41 @@ import { readSpeechEngineConfig as a } from "../../speech/createSpeechEngine.js"
4
4
  import { useAvatarPreferencesStore as o } from "../avatarPreferences.js";
5
5
  import { realAvatarScheduler as s } from "./avatarPorts.js";
6
6
  import { startAvatarAudioMeter as c } from "./avatarAudioMeter.js";
7
- import { decodeMp3ToPcm16k as l } from "./avatarAudioDecode.js";
8
- import { avatarApiBaseFromTtsEndpoint as u, avatarIdempotencyKey as d, createAvatarSessionClient as f } from "./avatarSessionClient.js";
9
- import { createLiveAvatarController as p } from "./liveAvatarController.js";
10
- import { openLiveKitAvatarRoom as m } from "./liveKitAvatarRoom.js";
11
- import { liveAvatarAskState as h } from "./liveAvatarStatus.js";
12
- import { LiveAvatarStageView as g } from "./LiveAvatarStage.js";
13
- import { sanitizeAvatarImages as _ } from "./avatarFaces.js";
14
- import { avatarSpeakerFor as v, hostedAvatarVoice as y, liveAvatarVoicesFrom as b, restoreVoiceWhenOutputLeaves as x } from "./avatarVoice.js";
15
- import { LiveAvatarTabView as S } from "./LiveAvatarTabView.js";
16
- import { jsx as C } from "react/jsx-runtime";
7
+ import { AVATAR_SAMPLE_RATE as l } from "./avatarPcm16.js";
8
+ import { decodeMp3ToPcm16k as u } from "./avatarAudioDecode.js";
9
+ import { avatarApiBaseFromTtsEndpoint as d, avatarIdempotencyKey as f, createAvatarSessionClient as p } from "./avatarSessionClient.js";
10
+ import { createLiveAvatarController as m } from "./liveAvatarController.js";
11
+ import { openLiveKitAvatarRoom as h } from "./liveKitAvatarRoom.js";
12
+ import { liveAvatarAskState as g } from "./liveAvatarStatus.js";
13
+ import { LiveAvatarStageView as _ } from "./LiveAvatarStage.js";
14
+ import { sanitizeAvatarImages as v } from "./avatarFaces.js";
15
+ import { avatarSpeakerFor as y, hostedAvatarVoice as b, liveAvatarVoicesFrom as x, restoreVoiceWhenOutputLeaves as S } from "./avatarVoice.js";
16
+ import { LiveAvatarTabView as C } from "./LiveAvatarTabView.js";
17
+ import { jsx as w } from "react/jsx-runtime";
17
18
  //#region src/shared/assistant/avatar/live/createLiveAvatarSeam.tsx
18
- function w(e = globalThis) {
19
- let t = e;
20
- if (typeof t.RTCPeerConnection != "function" || typeof t.OfflineAudioContext != "function" || typeof t.AudioContext != "function" || typeof t.MediaStream != "function") return !1;
21
- let n = t.navigator, r = n?.userAgentData;
22
- if (r?.brands !== void 0) {
23
- let e = r.brands.map((e) => e.brand);
24
- return r.mobile === !1 && e.some((e) => e === "Google Chrome" || e === "Chromium") && !e.some((e) => /Edge|Opera|Brave|Vivaldi|Samsung/i.test(e));
19
+ var T = /* @__PURE__ */ new WeakMap();
20
+ function E(e = globalThis) {
21
+ let t = T.get(e);
22
+ if (t !== void 0) return t;
23
+ let n = D(e);
24
+ return T.set(e, n), n;
25
+ }
26
+ function D(e) {
27
+ if (typeof e.RTCPeerConnection != "function" || typeof e.AudioContext != "function" || typeof e.MediaStream != "function" || typeof e.OfflineAudioContext != "function") return !1;
28
+ try {
29
+ return new e.OfflineAudioContext(1, 1, l), !0;
30
+ } catch {
31
+ return !1;
25
32
  }
26
- let i = n?.userAgent ?? "";
27
- return /\b(?:Chrome|Chromium)\//.test(i) && !/\b(?:Edg|OPR|SamsungBrowser)\/|Mobile|Android|iPhone|iPad/.test(i);
28
33
  }
29
- function T() {
34
+ function O() {
30
35
  let e = a().getHeaders?.() ?? {};
31
36
  for (let [t, n] of Object.entries(e)) if (t.toLowerCase() === "authorization" && typeof n == "string" && n.trim() !== "") return n.trim();
32
37
  let n = t();
33
38
  return n ? `Bearer ${n}` : null;
34
39
  }
35
- async function E(t) {
36
- let i = n(), a = T(), o = a?.replace(/^Bearer\s+/i, "") ?? null;
40
+ async function k(t) {
41
+ let i = n(), a = O(), o = a?.replace(/^Bearer\s+/i, "") ?? null;
37
42
  if (!i || !o) return null;
38
43
  let s = t.refused?.replace(/^Bearer\s+/i, ""), c = await e({
39
44
  url: r("workspace"),
@@ -42,38 +47,38 @@ async function E(t) {
42
47
  currentQuery: "liveAvatarSession",
43
48
  ...t.fresh ? { issuanceGuard: {
44
49
  identityKey: "liveAvatarSession",
45
- isCurrent: () => n() === i && T() === a,
50
+ isCurrent: () => n() === i && O() === a,
46
51
  accepts: (e, t) => e !== s && t === i
47
52
  } } : {}
48
53
  });
49
54
  return c ? `Bearer ${c}` : null;
50
55
  }
51
- function D() {
56
+ function A() {
52
57
  let e = null;
53
58
  return () => {
54
- let t = typeof location > "u" ? void 0 : location.href, n = u(a().endpoint, t);
59
+ let t = typeof location > "u" ? void 0 : location.href, n = d(a().endpoint, t);
55
60
  return n === null ? null : (e?.base !== n && (e = {
56
61
  base: n,
57
- client: f({
62
+ client: p({
58
63
  baseUrl: n,
59
- getAuthorization: T,
60
- getWorkspaceAuthorization: E
64
+ getAuthorization: O,
65
+ getWorkspaceAuthorization: k
61
66
  })
62
67
  }), e.client);
63
68
  };
64
69
  }
65
- var O = {
70
+ var j = {
66
71
  getState: () => o.getState(),
67
72
  setAvatarEnabled: (e) => o.getState().setAvatarEnabled(e),
68
73
  setAvatarId: (e) => o.getState().setAvatarId(e),
69
74
  subscribe: (e) => o.subscribe(() => e())
70
- }, k = {
75
+ }, M = {
71
76
  getState: () => o.getState(),
72
77
  subscribe: (e) => o.subscribe(() => e()),
73
78
  setVoiceMode: (e) => o.getState().setVoiceMode(e),
74
79
  setAvatarSpeaker: (e) => o.getState().setAvatarSpeaker(e)
75
80
  };
76
- function A(e, t) {
81
+ function N(e, t) {
77
82
  let n = (e) => {
78
83
  i(e) || t();
79
84
  };
@@ -90,24 +95,32 @@ function A(e, t) {
90
95
  cancel: () => e.cancel()
91
96
  };
92
97
  }
93
- function j(e) {
98
+ function P(e) {
94
99
  if (e === void 0) return;
95
100
  let { access: t } = e;
96
101
  return { access: () => t.call(e) };
97
102
  }
98
- function M() {
103
+ function F() {
99
104
  if (typeof document > "u") return null;
100
105
  let e = document.createElement("audio");
101
106
  return e.autoplay = !0, e.hidden = !0, e.setAttribute("data-testid", "assistant-live-avatar-audio"), document.body.appendChild(e), e;
102
107
  }
103
- function N(e) {
108
+ function I(e) {
104
109
  if (!(typeof window > "u")) return (t) => (window.addEventListener(e, t), () => window.removeEventListener(e, t));
105
110
  }
106
- function P() {
111
+ function L() {
107
112
  if (typeof document > "u" || typeof document.addEventListener != "function") return;
108
113
  let e = () => document.visibilityState !== "hidden";
109
114
  return {
110
115
  isVisible: e,
116
+ onHidden: (t) => {
117
+ let n = () => {
118
+ e() || t();
119
+ };
120
+ return document.addEventListener("visibilitychange", n), () => {
121
+ document.removeEventListener("visibilitychange", n);
122
+ };
123
+ },
111
124
  onShown: (t) => {
112
125
  let n = () => {
113
126
  e() && t();
@@ -120,7 +133,23 @@ function P() {
120
133
  }
121
134
  };
122
135
  }
123
- function F() {
136
+ function R(e = globalThis) {
137
+ return typeof e.matchMedia == "function" && e.matchMedia("(hover: none) and (pointer: coarse)").matches;
138
+ }
139
+ function z(e = typeof document > "u" ? null : document) {
140
+ if (!(e === null || typeof e.addEventListener != "function")) return (t) => {
141
+ let n = (e) => {
142
+ e.isTrusted && t();
143
+ };
144
+ return e.addEventListener("touchend", n, {
145
+ capture: !0,
146
+ passive: !0
147
+ }), e.addEventListener("click", n, !0), e.addEventListener("keydown", n, !0), () => {
148
+ e.removeEventListener("touchend", n, { capture: !0 }), e.removeEventListener("click", n, !0), e.removeEventListener("keydown", n, !0);
149
+ };
150
+ };
151
+ }
152
+ function B() {
124
153
  if (!(typeof window > "u" || typeof document > "u")) return (e) => {
125
154
  let t = window, n = document, r = () => {
126
155
  n.visibilityState === "visible" && e();
@@ -130,26 +159,29 @@ function F() {
130
159
  };
131
160
  };
132
161
  }
133
- function I(e = {}) {
134
- let t = e.preferences ?? O, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? N("burdenoff:workspace-changed"), i = n.subscribePageHide ?? N("pagehide"), a = P(), o = n.isPageVisible ?? a?.isVisible, u = n.subscribePageVisible ?? a?.onShown, f = n.subscribePageShow ?? F(), T = n.now ?? (() => Date.now()), E = p({
135
- client: e.client ?? D(),
136
- openRoom: e.openRoom ?? m,
162
+ function V(e = {}) {
163
+ let t = e.preferences ?? j, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? I("burdenoff:workspace-changed"), i = n.subscribePageHide ?? I("pagehide"), a = L(), o = n.isPageVisible ?? a?.isVisible, l = n.subscribePageVisible ?? a?.onShown, d = n.subscribePageHidden ?? a?.onHidden, p = n.endOnHide ?? R, T = n.subscribeUserGesture ?? z(), D = n.subscribePageShow ?? B(), O = n.now ?? (() => Date.now()), k = m({
164
+ client: e.client ?? A(),
165
+ openRoom: e.openRoom ?? h,
137
166
  scheduler: e.scheduler ?? s,
138
167
  preferences: t,
139
- decode: n.decode ?? ((e, t) => l(e, { signal: t })),
140
- createAudioElement: n.createAudioElement ?? M,
168
+ decode: n.decode ?? ((e, t) => u(e, { signal: t })),
169
+ createAudioElement: n.createAudioElement ?? F,
141
170
  releaseAudioElement: n.releaseAudioElement ?? ((e) => {
142
171
  e.srcObject = null, e.remove();
143
172
  }),
144
173
  startMeter: n.startMeter ?? ((e, t) => typeof MediaStream > "u" ? null : c(new MediaStream([e]), t)),
145
- browserSupported: e.browserSupported ?? (() => w()),
146
- now: T,
147
- newIdempotencyKey: n.newIdempotencyKey ?? (() => d()),
174
+ browserSupported: e.browserSupported ?? (() => E()),
175
+ now: O,
176
+ newIdempotencyKey: n.newIdempotencyKey ?? (() => f()),
148
177
  ...r ? { subscribeWorkspaceChange: r } : {},
149
178
  ...i ? { subscribePageHide: i } : {},
150
179
  ...o ? { isPageVisible: o } : {},
151
- ...u ? { subscribePageVisible: u } : {},
152
- ...f ? { subscribePageShow: f } : {},
180
+ ...l ? { subscribePageVisible: l } : {},
181
+ ...d ? { subscribePageHidden: d } : {},
182
+ endOnHide: p,
183
+ ...T ? { subscribeUserGesture: T } : {},
184
+ ...D ? { subscribePageShow: D } : {},
153
185
  ...n.idleMs === void 0 ? {} : { idleMs: n.idleMs },
154
186
  ...n.endBeforeDeadlineMs === void 0 ? {} : { endBeforeDeadlineMs: n.endBeforeDeadlineMs },
155
187
  ...n.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: n.connectTimeoutMs },
@@ -158,34 +190,34 @@ function I(e = {}) {
158
190
  ...n.stallMs === void 0 ? {} : { stallMs: n.stallMs },
159
191
  ...n.readinessProbe === void 0 ? {} : { readinessProbe: n.readinessProbe },
160
192
  ...e.onLog ? { onLog: e.onLog } : {}
161
- }), I = b(e.avatarVoices), L = e.avatarImages === void 0 ? null : _(e.avatarImages), R = e.avatarVoiceStore ?? k, z = () => R.getState(), B = j(e.minutesPurchase), V = (n, r) => {
162
- let i = v(z(), {
163
- avatarVoices: I,
193
+ }), V = x(e.avatarVoices), H = e.avatarImages === void 0 ? null : v(e.avatarImages), U = e.avatarVoiceStore ?? M, W = () => U.getState(), G = P(e.minutesPurchase), K = (n, r) => {
194
+ let i = y(W(), {
195
+ avatarVoices: V,
164
196
  avatarId: t.getState().avatarId,
165
197
  ...e.defaultAvatarVoiceMode ? { hostDefault: e.defaultAvatarVoiceMode } : {}
166
198
  });
167
- return i === null ? null : y(i, n, r.listVoices());
168
- }, H = (e, t) => {
199
+ return i === null ? null : b(i, n, r.listVoices());
200
+ }, q = (e, t) => {
169
201
  let n = {
170
202
  ...e,
171
203
  voice: t
172
204
  };
173
- return n.audioOutput = x(E.createOutput(), () => {
205
+ return n.audioOutput = S(k.createOutput(), () => {
174
206
  n.voice = e.voice;
175
- }, (e) => E.subscribe(() => {
176
- E.getSnapshot().phase !== "ready" && e();
207
+ }, (e) => k.subscribe(() => {
208
+ k.getSnapshot().phase !== "ready" && e();
177
209
  })), n;
178
210
  };
179
211
  return {
180
212
  decorateSpeechEngine: (e) => {
181
213
  let t = (t) => {
182
- let n = V(t.lang, e), r = n === null ? t : {
214
+ let n = K(t.lang, e), r = n === null ? t : {
183
215
  ...t,
184
216
  voice: n
185
217
  };
186
218
  return {
187
219
  voice: n,
188
- target: E.route(r, e)
220
+ target: k.route(r, e)
189
221
  };
190
222
  }, n = null, r = () => {
191
223
  let e = n;
@@ -196,9 +228,9 @@ function I(e = {}) {
196
228
  }, l = () => o ? !1 : (o = !0, c(), !0), u = () => {
197
229
  !a || !l() || (e.cancel(), i?.onError?.("output-left"));
198
230
  };
199
- s = E.subscribe(() => {
200
- E.getSnapshot().phase !== "ready" && u();
201
- }), n = c, r.audioOutput &&= A(r.audioOutput, u), r.fallback = !1, e.speak(t, r, {
231
+ s = k.subscribe(() => {
232
+ k.getSnapshot().phase !== "ready" && u();
233
+ }), n = c, r.audioOutput &&= N(r.audioOutput, u), r.fallback = !1, e.speak(t, r, {
202
234
  ...i,
203
235
  onEnd: () => {
204
236
  l() && i?.onEnd?.();
@@ -227,8 +259,8 @@ function I(e = {}) {
227
259
  }
228
260
  let l = s === null ? {
229
261
  ...a,
230
- audioOutput: E.createOutput()
231
- } : H(a, s);
262
+ audioOutput: k.createOutput()
263
+ } : q(a, s);
232
264
  if (a.avatarOnly === !0) {
233
265
  i(n, l, o);
234
266
  return;
@@ -240,8 +272,8 @@ function I(e = {}) {
240
272
  }
241
273
  };
242
274
  },
243
- Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ C(g, {
244
- controller: E,
275
+ Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ w(_, {
276
+ controller: k,
245
277
  expanded: t,
246
278
  fallback: n,
247
279
  ...e.avatarNames ? { avatarNames: e.avatarNames } : {}
@@ -249,34 +281,34 @@ function I(e = {}) {
249
281
  enabled: {
250
282
  subscribe: (e) => t.subscribe(e),
251
283
  getSnapshot: () => t.getState().avatarEnabled,
252
- switchOff: () => E.switchOff(),
253
- voiceLive: (e) => E.setVoiceLive(e)
284
+ switchOff: () => k.switchOff(),
285
+ voiceLive: (e) => k.setVoiceLive(e)
254
286
  },
255
287
  tab: {
256
288
  offered: {
257
- subscribe: (e) => E.subscribe(e),
258
- getSnapshot: () => E.getSnapshot().tabOffered
289
+ subscribe: (e) => k.subscribe(e),
290
+ getSnapshot: () => k.getSnapshot().tabOffered
259
291
  },
260
292
  ask: {
261
- subscribe: (e) => E.subscribe(e),
262
- getSnapshot: () => h(E.getSnapshot(), T())
293
+ subscribe: (e) => k.subscribe(e),
294
+ getSnapshot: () => g(k.getSnapshot(), O())
263
295
  },
264
- setOpen: (e) => E.setTabOpen(e),
265
- heldByVoice: () => E.getSnapshot().heldByVoice,
266
- View: (t) => /* @__PURE__ */ C(S, {
296
+ setOpen: (e) => k.setTabOpen(e),
297
+ heldByVoice: () => k.getSnapshot().heldByVoice,
298
+ View: (t) => /* @__PURE__ */ w(C, {
267
299
  ...t,
268
- controller: E,
269
- avatarVoices: I,
270
- voiceStore: R,
300
+ controller: k,
301
+ avatarVoices: V,
302
+ voiceStore: U,
271
303
  ...e.avatarNames ? { avatarNames: e.avatarNames } : {},
272
- ...L ? { avatarImages: L } : {},
304
+ ...H ? { avatarImages: H } : {},
273
305
  ...e.defaultAvatarVoiceMode ? { defaultAvatarVoiceMode: e.defaultAvatarVoiceMode } : {},
274
- ...B ? { minutesPurchase: B } : {}
306
+ ...G ? { minutesPurchase: G } : {}
275
307
  })
276
308
  },
277
- controller: E,
278
- dispose: () => E.dispose()
309
+ controller: k,
310
+ dispose: () => k.dispose()
279
311
  };
280
312
  }
281
313
  //#endregion
282
- export { I as createLiveAvatarSeam, w as isLiveAvatarBrowserSupported, T as liveAvatarAuthorization, E as liveAvatarWorkspaceAuthorization };
314
+ export { V as createLiveAvatarSeam, z as documentUserGesture, E as isLiveAvatarBrowserSupported, R as isTouchFirstDevice, O as liveAvatarAuthorization, k as liveAvatarWorkspaceAuthorization };
@@ -93,6 +93,11 @@ export interface LiveAvatarSnapshot {
93
93
  readonly audioBlocked: boolean;
94
94
  /** The avatar's video has arrived. */
95
95
  readonly videoReady: boolean;
96
+ /**
97
+ * ★ BOFF-9696: the browser refuses to play the avatar's video (iOS in Low Power Mode, until a user gesture): the views show the stage's
98
+ * placeholder instead of a frozen face. The sound tap (`resumeAudio`) plays it too, and so does the person's next tap or key on the page.
99
+ */
100
+ readonly videoBlocked: boolean;
96
101
  /**
97
102
  * ★ BOFF-9446: the minutes block of the latest availability answer (the server sends one in `enforce` only), or null: no answer with
98
103
  * one yet (today's answers never have one), an answer with none since, or cleared by a workspace switch or a "not offered" answer.
@@ -151,7 +156,7 @@ export interface LiveAvatarControllerDeps {
151
156
  readonly releaseAudioElement: (element: HTMLAudioElement) => void;
152
157
  /** A level meter on the avatar's received audio; null when none can be built (the output then cannot prove silence). */
153
158
  readonly startMeter: (track: MediaStreamTrack, onChange: (audible: boolean) => void) => AvatarMeterHandle | null;
154
- /** The browser can show the avatar at all (desktop Chromium first: GATE1B §10). */
159
+ /** The browser can show the avatar at all: what it needs is there (any browser or phone since BOFF-9696). */
155
160
  readonly browserSupported: () => boolean;
156
161
  /**
157
162
  * Wall-clock milliseconds: a refusal's retry time (`retryAt`), its lock and its re-check (`recheckRetry`); also the fallback for
@@ -161,6 +166,15 @@ export interface LiveAvatarControllerDeps {
161
166
  readonly newIdempotencyKey: () => string;
162
167
  readonly subscribeWorkspaceChange?: (listener: () => void) => () => void;
163
168
  readonly subscribePageHide?: (listener: () => void) => () => void;
169
+ /**
170
+ * ★ BOFF-9696 (S5): the page was hidden (its document hidden). A phone hides a page without `pagehide` when the person switches apps or
171
+ * locks the screen, and freezes its timers then, so a session left on there would run on unseen until its idle rule or deadline.
172
+ */
173
+ readonly subscribePageHidden?: (listener: () => void) => () => void;
174
+ /** ★ BOFF-9696 (S5): whether a hidden page ends the session at once (touch devices); absent or false: only `pagehide` does. */
175
+ readonly endOnHide?: () => boolean;
176
+ /** ★ BOFF-9696: the person's next tap or key anywhere on the page (a user gesture), once: it plays a refused video. */
177
+ readonly subscribeUserGesture?: (listener: () => void) => () => void;
164
178
  /** ★ BOFF-9446: the page is shown now; no quiet minutes question is asked while it is hidden. Absent: always shown. */
165
179
  readonly isPageVisible?: () => boolean;
166
180
  /** ★ BOFF-9446: called when the page is shown again (after it was hidden): the minutes are asked again then, quietly. */