@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.
- package/dist/shared/assistant/avatar/live/LiveAvatarStage.js +2 -3
- package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +2 -3
- package/dist/shared/assistant/avatar/live/avatarPorts.d.ts +9 -0
- package/dist/shared/assistant/avatar/live/avatarTransport.d.ts +25 -3
- package/dist/shared/assistant/avatar/live/avatarTransport.js +29 -2
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +22 -4
- package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +113 -81
- package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +15 -1
- package/dist/shared/assistant/avatar/live/liveAvatarController.js +204 -186
- package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.js +6 -1
- package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +1 -1
- package/dist/shared-assistant.js +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
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
|
|
227
|
-
*
|
|
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
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
36
|
-
let i = n(), a =
|
|
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 &&
|
|
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
|
|
56
|
+
function A() {
|
|
52
57
|
let e = null;
|
|
53
58
|
return () => {
|
|
54
|
-
let t = typeof location > "u" ? void 0 : location.href, n =
|
|
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:
|
|
62
|
+
client: p({
|
|
58
63
|
baseUrl: n,
|
|
59
|
-
getAuthorization:
|
|
60
|
-
getWorkspaceAuthorization:
|
|
64
|
+
getAuthorization: O,
|
|
65
|
+
getWorkspaceAuthorization: k
|
|
61
66
|
})
|
|
62
67
|
}), e.client);
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
|
-
var
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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
|
|
108
|
+
function I(e) {
|
|
104
109
|
if (!(typeof window > "u")) return (t) => (window.addEventListener(e, t), () => window.removeEventListener(e, t));
|
|
105
110
|
}
|
|
106
|
-
function
|
|
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
|
|
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
|
|
134
|
-
let t = e.preferences ??
|
|
135
|
-
client: e.client ??
|
|
136
|
-
openRoom: e.openRoom ??
|
|
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) =>
|
|
140
|
-
createAudioElement: n.createAudioElement ??
|
|
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 ?? (() =>
|
|
146
|
-
now:
|
|
147
|
-
newIdempotencyKey: n.newIdempotencyKey ?? (() =>
|
|
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
|
-
...
|
|
152
|
-
...
|
|
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
|
-
}),
|
|
162
|
-
let i =
|
|
163
|
-
avatarVoices:
|
|
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 :
|
|
168
|
-
},
|
|
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 =
|
|
205
|
+
return n.audioOutput = S(k.createOutput(), () => {
|
|
174
206
|
n.voice = e.voice;
|
|
175
|
-
}, (e) =>
|
|
176
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
200
|
-
|
|
201
|
-
}), n = c, r.audioOutput &&=
|
|
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:
|
|
231
|
-
} :
|
|
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__ */
|
|
244
|
-
controller:
|
|
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: () =>
|
|
253
|
-
voiceLive: (e) =>
|
|
284
|
+
switchOff: () => k.switchOff(),
|
|
285
|
+
voiceLive: (e) => k.setVoiceLive(e)
|
|
254
286
|
},
|
|
255
287
|
tab: {
|
|
256
288
|
offered: {
|
|
257
|
-
subscribe: (e) =>
|
|
258
|
-
getSnapshot: () =>
|
|
289
|
+
subscribe: (e) => k.subscribe(e),
|
|
290
|
+
getSnapshot: () => k.getSnapshot().tabOffered
|
|
259
291
|
},
|
|
260
292
|
ask: {
|
|
261
|
-
subscribe: (e) =>
|
|
262
|
-
getSnapshot: () =>
|
|
293
|
+
subscribe: (e) => k.subscribe(e),
|
|
294
|
+
getSnapshot: () => g(k.getSnapshot(), O())
|
|
263
295
|
},
|
|
264
|
-
setOpen: (e) =>
|
|
265
|
-
heldByVoice: () =>
|
|
266
|
-
View: (t) => /* @__PURE__ */ C
|
|
296
|
+
setOpen: (e) => k.setTabOpen(e),
|
|
297
|
+
heldByVoice: () => k.getSnapshot().heldByVoice,
|
|
298
|
+
View: (t) => /* @__PURE__ */ w(C, {
|
|
267
299
|
...t,
|
|
268
|
-
controller:
|
|
269
|
-
avatarVoices:
|
|
270
|
-
voiceStore:
|
|
300
|
+
controller: k,
|
|
301
|
+
avatarVoices: V,
|
|
302
|
+
voiceStore: U,
|
|
271
303
|
...e.avatarNames ? { avatarNames: e.avatarNames } : {},
|
|
272
|
-
...
|
|
304
|
+
...H ? { avatarImages: H } : {},
|
|
273
305
|
...e.defaultAvatarVoiceMode ? { defaultAvatarVoiceMode: e.defaultAvatarVoiceMode } : {},
|
|
274
|
-
...
|
|
306
|
+
...G ? { minutesPurchase: G } : {}
|
|
275
307
|
})
|
|
276
308
|
},
|
|
277
|
-
controller:
|
|
278
|
-
dispose: () =>
|
|
309
|
+
controller: k,
|
|
310
|
+
dispose: () => k.dispose()
|
|
279
311
|
};
|
|
280
312
|
}
|
|
281
313
|
//#endregion
|
|
282
|
-
export {
|
|
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 (
|
|
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. */
|