realtime-avatar 0.24.1 → 0.26.0
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/README.md +78 -0
- package/dist/browser.d.ts +2 -87
- package/dist/browser.js +6 -1
- package/dist/{chunk-EODL5S3A.js → chunk-HMESE54B.js} +46 -10
- package/dist/chunk-XJPGQMEW.js +161 -0
- package/dist/express.d.ts +1 -1
- package/dist/express.js +5 -3
- package/dist/hono.d.ts +1 -1
- package/dist/hono.js +2 -2
- package/dist/index.d.ts +24 -1
- package/dist/index.js +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.js +2 -2
- package/dist/playout-delay-CtRZdtsj.d.ts +168 -0
- package/dist/{proxy-client-DlFXaXtn.d.ts → proxy-client-C8M_G_Kk.d.ts} +168 -18
- package/dist/react-native.d.ts +3 -3
- package/dist/react-native.js +385 -40
- package/dist/react.d.ts +75 -14
- package/dist/react.js +576 -97
- package/dist/server.js +1 -1
- package/dist/tanstack-start.d.ts +1 -1
- package/dist/tanstack-start.js +2 -2
- package/dist/tools.d.ts +17 -1
- package/dist/tools.js +28 -8
- package/dist/{types-CpqMHna3.d.ts → types-C7B7jOx6.d.ts} +7 -0
- package/package.json +1 -1
- package/dist/chunk-4PWWEERK.js +0 -87
- package/dist/playout-delay-DWOc8iCl.d.ts +0 -65
package/dist/react-native.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AudioSession, LiveKitRoom, VideoTrack } from '@livekit/react-native';
|
|
2
2
|
export { AudioSession, VideoTrack, registerGlobals } from '@livekit/react-native';
|
|
3
3
|
import { useState, useRef, useEffect, useCallback, createElement, useMemo } from 'react';
|
|
4
|
-
import { useLocalParticipant, useConnectionState, useTranscriptions, useMaybeRoomContext, useVoiceAssistant, isTrackReference, useRoomContext, useChat } from '@livekit/components-react';
|
|
4
|
+
import { useLocalParticipant, useConnectionState, useTranscriptions, useMaybeRoomContext, useVoiceAssistant, isTrackReference, useRoomContext, useChat, useAudioPlayback } from '@livekit/components-react';
|
|
5
5
|
export { useChat, useConnectionState, useLocalParticipant, useRoomContext, useTrackToggle, useTranscriptions, useVoiceAssistant } from '@livekit/components-react';
|
|
6
|
-
import { ConnectionState, isRemoteTrack, VideoQuality, ConnectionQuality, RoomEvent, TrackEvent, Track, ConnectionError, ConnectionErrorReason, DisconnectReason, createLocalVideoTrack, Room } from 'livekit-client';
|
|
6
|
+
import { ConnectionState, isRemoteTrack, VideoQuality, ConnectionQuality, RoomEvent, TrackEvent, Track, ConnectionError, ConnectionErrorReason, ParticipantEvent, DisconnectReason, createLocalVideoTrack, Room } from 'livekit-client';
|
|
7
7
|
export { DisconnectReason, Room, RoomEvent, Track } from 'livekit-client';
|
|
8
8
|
import { Platform, StyleSheet, Animated, Image, View, Text } from 'react-native';
|
|
9
9
|
import { z } from 'zod';
|
|
@@ -414,7 +414,13 @@ function normalizeError(input, overrides) {
|
|
|
414
414
|
};
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
// ../http-client/src/errors.ts
|
|
418
|
+
function isRequestId(value) {
|
|
419
|
+
return typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
|
|
420
|
+
}
|
|
421
|
+
|
|
417
422
|
// ../client/src/errors.ts
|
|
423
|
+
var TIMEOUT_CODE = "upstream_timeout";
|
|
418
424
|
var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
|
|
419
425
|
constructor(message, status, body, response, metadata = {}) {
|
|
420
426
|
super(message);
|
|
@@ -426,6 +432,7 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
|
|
|
426
432
|
this.retryable = metadata.retryable;
|
|
427
433
|
this.billingUrl = metadata.billingUrl;
|
|
428
434
|
this.rawMessage = metadata.rawMessage ?? null;
|
|
435
|
+
this.requestId = metadata.requestId;
|
|
429
436
|
}
|
|
430
437
|
status;
|
|
431
438
|
body;
|
|
@@ -435,6 +442,8 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
|
|
|
435
442
|
billingUrl;
|
|
436
443
|
/** Debug detail returned by the API. Do not render directly in user-facing UI. */
|
|
437
444
|
rawMessage;
|
|
445
|
+
/** The platform's correlation ID for this attempt. Quote it to support. */
|
|
446
|
+
requestId;
|
|
438
447
|
get isBillingRequired() {
|
|
439
448
|
return this.status === 402 || this.code === "insufficient_credits" || this.code === "spend_limit_exceeded";
|
|
440
449
|
}
|
|
@@ -443,14 +452,31 @@ var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
|
|
|
443
452
|
const body = contentType.includes("application/json") ? await response.clone().json().catch(() => null) : await response.clone().text().catch(() => "");
|
|
444
453
|
const rawMessage = (extractErrorMessage(body) ?? response.statusText) || null;
|
|
445
454
|
const metadata = extractErrorMetadata(body, response.status, rawMessage);
|
|
455
|
+
const bodyRequestId = readRecord(body)?.requestId;
|
|
456
|
+
const headerRequestId = response.headers.get("x-request-id");
|
|
446
457
|
return new _RealtimeAvatarApiError(
|
|
447
458
|
metadata.message,
|
|
448
459
|
response.status,
|
|
449
460
|
body,
|
|
450
461
|
response,
|
|
451
|
-
|
|
462
|
+
{
|
|
463
|
+
...metadata,
|
|
464
|
+
requestId: isRequestId(bodyRequestId) ? bodyRequestId : isRequestId(headerRequestId) ? headerRequestId : void 0
|
|
465
|
+
}
|
|
452
466
|
);
|
|
453
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* A request that got no answer before its deadline. The status is the one the vocabulary pairs
|
|
470
|
+
* with the code, so `normalizeRealtimeAvatarError` reads it back as the same failure.
|
|
471
|
+
*/
|
|
472
|
+
static timeout(timeoutMs) {
|
|
473
|
+
const { message, retryable, statuses: [status] } = ERROR_SEMANTICS[TIMEOUT_CODE];
|
|
474
|
+
return new _RealtimeAvatarApiError(message, status, null, null, {
|
|
475
|
+
code: TIMEOUT_CODE,
|
|
476
|
+
retryable,
|
|
477
|
+
rawMessage: `No response within ${timeoutMs}ms.`
|
|
478
|
+
});
|
|
479
|
+
}
|
|
454
480
|
};
|
|
455
481
|
var RealtimeAvatarCapacityError = class extends Error {
|
|
456
482
|
constructor(message, busy) {
|
|
@@ -672,6 +698,11 @@ function useLiveKitAvatarGrant(input) {
|
|
|
672
698
|
});
|
|
673
699
|
const stateRef = useRef(state);
|
|
674
700
|
stateRef.current = state;
|
|
701
|
+
const wantedRef = useRef(active);
|
|
702
|
+
wantedRef.current = active;
|
|
703
|
+
useEffect(() => () => {
|
|
704
|
+
wantedRef.current = false;
|
|
705
|
+
}, []);
|
|
675
706
|
const [tabVisible, setTabVisible] = useState(
|
|
676
707
|
() => typeof document === "undefined" || document.visibilityState === "visible"
|
|
677
708
|
);
|
|
@@ -712,6 +743,8 @@ function useLiveKitAvatarGrant(input) {
|
|
|
712
743
|
if (cancelled) {
|
|
713
744
|
if (result.status !== "busy") {
|
|
714
745
|
void client.releaseLiveKitSession(result.grant.session_id, "superseded");
|
|
746
|
+
} else if (!wantedRef.current && result.busy.queue_ticket_id) {
|
|
747
|
+
void client.releaseLiveKitQueueTicket(result.busy.queue_ticket_id, "unmount");
|
|
715
748
|
}
|
|
716
749
|
return;
|
|
717
750
|
}
|
|
@@ -795,6 +828,9 @@ function sameCapacityState(a, b) {
|
|
|
795
828
|
return true;
|
|
796
829
|
}
|
|
797
830
|
}
|
|
831
|
+
function publishesMicrophoneByDefault(grant) {
|
|
832
|
+
return grant?.stt_mode === "server";
|
|
833
|
+
}
|
|
798
834
|
var capacityErrorFromBusy = (busy) => {
|
|
799
835
|
return new RealtimeAvatarCapacityError(busy.message, busy);
|
|
800
836
|
};
|
|
@@ -2120,6 +2156,169 @@ function createConnectionHistoryCollector(input) {
|
|
|
2120
2156
|
return { enqueue, flush, dispose };
|
|
2121
2157
|
}
|
|
2122
2158
|
|
|
2159
|
+
// ../browser/src/microphone.ts
|
|
2160
|
+
var HINTS = {
|
|
2161
|
+
"insecure-origin": "The browser only exposes microphones on a secure origin. Serve the page over https, or open it on http://localhost \u2014 a LAN address like http://192.168.1.5 will not do.",
|
|
2162
|
+
"no-answer": "The permission prompt is probably still open, or was dismissed without an answer. Look for it in the address bar, answer it, and try again.",
|
|
2163
|
+
"denied-by-browser": "This site is blocked from using the microphone. Click the icon at the left of the address bar, set Microphone to Allow, and reload.",
|
|
2164
|
+
"denied-by-os": "The operating system is blocking the browser itself, so no site can record. On macOS: System Settings > Privacy & Security > Microphone, enable your browser, then RESTART it \u2014 the change does not apply to a running browser.",
|
|
2165
|
+
"no-device": "No microphone is available. Connect one, and if you are on Bluetooth headphones check they are in a mode that has a microphone.",
|
|
2166
|
+
"device-in-use": "Another application or browser tab is holding the microphone. Close it \u2014 including any call from this page you did not hang up \u2014 and try again.",
|
|
2167
|
+
"device-lost": "The microphone stopped during the call: it was unplugged, or access to it was turned off. Reconnect it or allow access again, then turn the microphone back on.",
|
|
2168
|
+
unknown: "Try again, and if it persists include the message above in your report."
|
|
2169
|
+
};
|
|
2170
|
+
function isOperatingSystemDenial(message) {
|
|
2171
|
+
return /\bby system\b|system permission/i.test(message);
|
|
2172
|
+
}
|
|
2173
|
+
function classify(error) {
|
|
2174
|
+
if (error instanceof TypeError && /mediaDevices|undefined/i.test(error.message)) {
|
|
2175
|
+
return { reason: "insecure-origin", message: error.message };
|
|
2176
|
+
}
|
|
2177
|
+
const name = typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
|
|
2178
|
+
const message = typeof error === "object" && error !== null && "message" in error && error.message ? String(error.message) : String(error);
|
|
2179
|
+
switch (name) {
|
|
2180
|
+
case "NotAllowedError":
|
|
2181
|
+
case "PermissionDeniedError":
|
|
2182
|
+
return {
|
|
2183
|
+
reason: isOperatingSystemDenial(message) ? "denied-by-os" : "denied-by-browser",
|
|
2184
|
+
message
|
|
2185
|
+
};
|
|
2186
|
+
case "NotFoundError":
|
|
2187
|
+
case "DevicesNotFoundError":
|
|
2188
|
+
case "OverconstrainedError":
|
|
2189
|
+
case "ConstraintNotSatisfiedError":
|
|
2190
|
+
return { reason: "no-device", message };
|
|
2191
|
+
case "NotReadableError":
|
|
2192
|
+
case "TrackStartError":
|
|
2193
|
+
case "AbortError":
|
|
2194
|
+
return { reason: "device-in-use", message };
|
|
2195
|
+
case "SecurityError":
|
|
2196
|
+
return { reason: "insecure-origin", message };
|
|
2197
|
+
default:
|
|
2198
|
+
return { reason: "unknown", message };
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
function describeMicrophoneFailure(error) {
|
|
2202
|
+
const { reason, message } = classify(error);
|
|
2203
|
+
return { reason, message, hint: HINTS[reason] };
|
|
2204
|
+
}
|
|
2205
|
+
function microphoneLost(message) {
|
|
2206
|
+
return { reason: "device-lost", message, hint: HINTS["device-lost"] };
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// ../client/src/react/call-media.ts
|
|
2210
|
+
var DEVICE_LOST_MESSAGE = "The microphone track ended during the call.";
|
|
2211
|
+
function callMicrophoneFrom(facts, intent) {
|
|
2212
|
+
if (!facts.wanted) return { status: "off" };
|
|
2213
|
+
const { publication } = facts;
|
|
2214
|
+
if (publication) {
|
|
2215
|
+
if (publication.ended) {
|
|
2216
|
+
return publication.muted ? { status: "unavailable", ...microphoneLost(DEVICE_LOST_MESSAGE) } : { status: "pending" };
|
|
2217
|
+
}
|
|
2218
|
+
return publication.muted ? { status: "muted" } : { status: "on" };
|
|
2219
|
+
}
|
|
2220
|
+
if (intent.muted) return { status: "muted" };
|
|
2221
|
+
if (facts.enabling) return { status: "pending" };
|
|
2222
|
+
const error = facts.deviceError;
|
|
2223
|
+
if (error) {
|
|
2224
|
+
const problem = describeMicrophoneFailure(error);
|
|
2225
|
+
const blocked = problem.reason === "denied-by-browser" || problem.reason === "denied-by-os" || problem.reason === "insecure-origin";
|
|
2226
|
+
return blocked ? { status: "blocked", ...problem } : { status: "unavailable", ...problem };
|
|
2227
|
+
}
|
|
2228
|
+
return facts.connected ? { status: "pending" } : { status: "off" };
|
|
2229
|
+
}
|
|
2230
|
+
function microphoneFacts(room, wanted, connected, enabling, staleError) {
|
|
2231
|
+
const publication = room.localParticipant.getTrackPublication(Track.Source.Microphone);
|
|
2232
|
+
const track = publication?.track;
|
|
2233
|
+
return {
|
|
2234
|
+
wanted,
|
|
2235
|
+
connected,
|
|
2236
|
+
enabling,
|
|
2237
|
+
publication: track ? { muted: publication.isMuted, ended: track.mediaStreamTrack?.readyState === "ended" } : null,
|
|
2238
|
+
deviceError: room.localParticipant.lastMicrophoneError !== staleError && room.localParticipant.lastMicrophoneError || null
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
function useCallMedia(room, options) {
|
|
2242
|
+
const { setMicrophoneFacts, setAudioPlayback, registerMediaControls, microphoneWanted, connectionState, callKey } = options;
|
|
2243
|
+
const connected = connectionState === ConnectionState.Connected;
|
|
2244
|
+
const scope = useMemo(
|
|
2245
|
+
() => ({ room, callKey, stale: room.localParticipant.lastMicrophoneError }),
|
|
2246
|
+
[room, callKey]
|
|
2247
|
+
);
|
|
2248
|
+
const [enabling, setEnabling] = useState(false);
|
|
2249
|
+
const [version, setVersion] = useState(0);
|
|
2250
|
+
const sinksRef = useRef({ setMicrophoneFacts, setAudioPlayback });
|
|
2251
|
+
sinksRef.current = { setMicrophoneFacts, setAudioPlayback };
|
|
2252
|
+
useEffect(() => {
|
|
2253
|
+
if (!setMicrophoneFacts) return;
|
|
2254
|
+
const bump = () => setVersion((value) => value + 1);
|
|
2255
|
+
const participant = room.localParticipant;
|
|
2256
|
+
let watched = null;
|
|
2257
|
+
const watchTrack = () => {
|
|
2258
|
+
watched?.off();
|
|
2259
|
+
const track = participant.getTrackPublication(Track.Source.Microphone)?.track;
|
|
2260
|
+
if (!track) {
|
|
2261
|
+
watched = null;
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
track.on(TrackEvent.Ended, bump).on(TrackEvent.Restarted, bump).on(TrackEvent.Muted, bump).on(TrackEvent.Unmuted, bump);
|
|
2265
|
+
watched = { off: () => {
|
|
2266
|
+
track.off(TrackEvent.Ended, bump).off(TrackEvent.Restarted, bump).off(TrackEvent.Muted, bump).off(TrackEvent.Unmuted, bump);
|
|
2267
|
+
} };
|
|
2268
|
+
};
|
|
2269
|
+
const onPublications = () => {
|
|
2270
|
+
watchTrack();
|
|
2271
|
+
bump();
|
|
2272
|
+
};
|
|
2273
|
+
room.on(RoomEvent.MediaDevicesError, bump).on(RoomEvent.LocalTrackPublished, onPublications).on(RoomEvent.LocalTrackUnpublished, onPublications);
|
|
2274
|
+
participant.on(ParticipantEvent.TrackMuted, bump).on(ParticipantEvent.TrackUnmuted, bump);
|
|
2275
|
+
watchTrack();
|
|
2276
|
+
return () => {
|
|
2277
|
+
watched?.off();
|
|
2278
|
+
room.off(RoomEvent.MediaDevicesError, bump).off(RoomEvent.LocalTrackPublished, onPublications).off(RoomEvent.LocalTrackUnpublished, onPublications);
|
|
2279
|
+
participant.off(ParticipantEvent.TrackMuted, bump).off(ParticipantEvent.TrackUnmuted, bump);
|
|
2280
|
+
};
|
|
2281
|
+
}, [room, setMicrophoneFacts]);
|
|
2282
|
+
useEffect(() => {
|
|
2283
|
+
sinksRef.current.setMicrophoneFacts?.(microphoneFacts(room, microphoneWanted, connected, enabling, scope.stale));
|
|
2284
|
+
}, [room, microphoneWanted, connected, enabling, version, scope]);
|
|
2285
|
+
const { canPlayAudio } = useAudioPlayback(room);
|
|
2286
|
+
useEffect(() => {
|
|
2287
|
+
sinksRef.current.setAudioPlayback?.(!canPlayAudio ? "blocked" : connected ? "allowed" : "unknown");
|
|
2288
|
+
}, [canPlayAudio, connected]);
|
|
2289
|
+
const unlockTriedRef = useRef(null);
|
|
2290
|
+
const connecting = connectionState === ConnectionState.Connecting || connected;
|
|
2291
|
+
useEffect(() => {
|
|
2292
|
+
if (!setAudioPlayback || !connecting || unlockTriedRef.current === room) return;
|
|
2293
|
+
unlockTriedRef.current = room;
|
|
2294
|
+
void room.startAudio().catch(() => void 0);
|
|
2295
|
+
}, [room, setAudioPlayback, connecting]);
|
|
2296
|
+
const controls = useMemo(() => ({
|
|
2297
|
+
startAudio: async () => {
|
|
2298
|
+
try {
|
|
2299
|
+
await room.startAudio();
|
|
2300
|
+
} catch {
|
|
2301
|
+
}
|
|
2302
|
+
return room.canPlaybackAudio;
|
|
2303
|
+
},
|
|
2304
|
+
setMicrophoneEnabled: async (enabled) => {
|
|
2305
|
+
if (enabled) setEnabling(true);
|
|
2306
|
+
try {
|
|
2307
|
+
await room.localParticipant.setMicrophoneEnabled(enabled);
|
|
2308
|
+
} catch {
|
|
2309
|
+
} finally {
|
|
2310
|
+
if (enabled) setEnabling(false);
|
|
2311
|
+
setVersion((value) => value + 1);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}), [room]);
|
|
2315
|
+
useEffect(() => {
|
|
2316
|
+
if (!registerMediaControls) return;
|
|
2317
|
+
registerMediaControls(controls);
|
|
2318
|
+
return () => registerMediaControls(null);
|
|
2319
|
+
}, [controls, registerMediaControls]);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2123
2322
|
// ../client/src/react/session-lifecycle.ts
|
|
2124
2323
|
function phaseForReason(reason) {
|
|
2125
2324
|
switch (reason) {
|
|
@@ -2280,10 +2479,13 @@ function useSessionLifecycle(input) {
|
|
|
2280
2479
|
const reconnectPolicyRef = useRef(reconnectPolicy);
|
|
2281
2480
|
reconnectPolicyRef.current = reconnectPolicy;
|
|
2282
2481
|
const connectWatchdogMs = typeof connectWatchdogSeconds === "number" && Number.isFinite(connectWatchdogSeconds) ? Math.max(0, Math.floor(connectWatchdogSeconds)) * 1e3 : DEFAULT_CONNECT_WATCHDOG_SECONDS * 1e3;
|
|
2482
|
+
const [userEnded, setUserEnded] = useState(false);
|
|
2483
|
+
const userEndedRef = useRef(false);
|
|
2484
|
+
const phaseRef = useRef({ kind: "idle" });
|
|
2283
2485
|
const grantState = useLiveKitAvatarGrant({
|
|
2284
2486
|
client,
|
|
2285
2487
|
session,
|
|
2286
|
-
active,
|
|
2488
|
+
active: active && !userEnded,
|
|
2287
2489
|
autoRetryBusy,
|
|
2288
2490
|
requestOptions
|
|
2289
2491
|
});
|
|
@@ -2300,6 +2502,8 @@ function useSessionLifecycle(input) {
|
|
|
2300
2502
|
refreshRef.current = grantState.refresh;
|
|
2301
2503
|
const releaseRef = useRef(grantState.release);
|
|
2302
2504
|
releaseRef.current = grantState.release;
|
|
2505
|
+
const clearRef = useRef(grantState.clear);
|
|
2506
|
+
clearRef.current = grantState.clear;
|
|
2303
2507
|
const capacityRef = useRef(capacity);
|
|
2304
2508
|
capacityRef.current = capacity;
|
|
2305
2509
|
const refreshBaselineCapacityRef = useRef(null);
|
|
@@ -2326,13 +2530,16 @@ function useSessionLifecycle(input) {
|
|
|
2326
2530
|
refreshBaselineCapacityRef.current = null;
|
|
2327
2531
|
attemptRef.current = 0;
|
|
2328
2532
|
setAttempt(0);
|
|
2329
|
-
setRecovery({ kind: "connected" });
|
|
2533
|
+
setRecovery(userEndedRef.current ? { kind: "ended", reason: "user" } : { kind: "connected" });
|
|
2330
2534
|
setConnected(false);
|
|
2331
2535
|
setAgentPresentState(false);
|
|
2332
2536
|
lastActivityRef.current = Date.now();
|
|
2333
2537
|
}, [clearTimer]);
|
|
2334
2538
|
useEffect(() => {
|
|
2335
|
-
if (
|
|
2539
|
+
if (active) return;
|
|
2540
|
+
userEndedRef.current = false;
|
|
2541
|
+
setUserEnded(false);
|
|
2542
|
+
reset();
|
|
2336
2543
|
}, [active, reset]);
|
|
2337
2544
|
const markActivity = useCallback(() => {
|
|
2338
2545
|
lastActivityRef.current = Date.now();
|
|
@@ -2363,6 +2570,7 @@ function useSessionLifecycle(input) {
|
|
|
2363
2570
|
leaveRoomRef.current?.();
|
|
2364
2571
|
}, [clearTimer]);
|
|
2365
2572
|
const onConnected = useCallback(() => {
|
|
2573
|
+
if (userEndedRef.current) return;
|
|
2366
2574
|
clearTimer();
|
|
2367
2575
|
connectedRef.current = true;
|
|
2368
2576
|
manualReconnectPendingRef.current = false;
|
|
@@ -2375,7 +2583,7 @@ function useSessionLifecycle(input) {
|
|
|
2375
2583
|
}, [clearTimer]);
|
|
2376
2584
|
const onDisconnected = useCallback(
|
|
2377
2585
|
(reason) => {
|
|
2378
|
-
if (!active) return;
|
|
2586
|
+
if (!active || userEndedRef.current) return;
|
|
2379
2587
|
const action = disconnectAction(reason, connectedRef.current);
|
|
2380
2588
|
if (action === "noop") return;
|
|
2381
2589
|
if (action === "reset") {
|
|
@@ -2400,7 +2608,7 @@ function useSessionLifecycle(input) {
|
|
|
2400
2608
|
);
|
|
2401
2609
|
const onConnectionError = useCallback(
|
|
2402
2610
|
(error) => {
|
|
2403
|
-
if (!active || !isRecoverableConnectionError(error)) return;
|
|
2611
|
+
if (!active || userEndedRef.current || !isRecoverableConnectionError(error)) return;
|
|
2404
2612
|
connectedRef.current = false;
|
|
2405
2613
|
setConnected(false);
|
|
2406
2614
|
setAgentPresentState(false);
|
|
@@ -2413,6 +2621,9 @@ function useSessionLifecycle(input) {
|
|
|
2413
2621
|
},
|
|
2414
2622
|
[active]
|
|
2415
2623
|
);
|
|
2624
|
+
useEffect(() => {
|
|
2625
|
+
if (capacity.kind === "error" && recovery.kind === "connected") manualReconnectPendingRef.current = false;
|
|
2626
|
+
}, [capacity, recovery]);
|
|
2416
2627
|
useEffect(() => {
|
|
2417
2628
|
if (recovery.kind !== "refreshing" || capacity.kind !== "error") return;
|
|
2418
2629
|
const baseline = refreshBaselineCapacityRef.current;
|
|
@@ -2422,7 +2633,7 @@ function useSessionLifecycle(input) {
|
|
|
2422
2633
|
}, [capacity, recovery]);
|
|
2423
2634
|
const onConnectionStateChange = useCallback(
|
|
2424
2635
|
(state) => {
|
|
2425
|
-
if (!active) return;
|
|
2636
|
+
if (!active || userEndedRef.current) return;
|
|
2426
2637
|
if (state === "connected") {
|
|
2427
2638
|
onConnected();
|
|
2428
2639
|
return;
|
|
@@ -2470,8 +2681,30 @@ function useSessionLifecycle(input) {
|
|
|
2470
2681
|
}, step2.delayMs);
|
|
2471
2682
|
return clearTimer;
|
|
2472
2683
|
}, [recovery.kind, active, attempt, clearTimer]);
|
|
2684
|
+
const end = useCallback(() => {
|
|
2685
|
+
if (!active || userEndedRef.current || phaseRef.current.kind === "ended") return;
|
|
2686
|
+
userEndedRef.current = true;
|
|
2687
|
+
clearTimer();
|
|
2688
|
+
connectedRef.current = false;
|
|
2689
|
+
manualReconnectPendingRef.current = false;
|
|
2690
|
+
refreshBaselineCapacityRef.current = null;
|
|
2691
|
+
setUserEnded(true);
|
|
2692
|
+
setConnected(false);
|
|
2693
|
+
setAgentPresentState(false);
|
|
2694
|
+
setRecovery({ kind: "ended", reason: "user" });
|
|
2695
|
+
clearRef.current();
|
|
2696
|
+
leaveRoomRef.current?.();
|
|
2697
|
+
}, [active, clearTimer]);
|
|
2473
2698
|
const reconnect = useCallback(() => {
|
|
2474
|
-
if (!active
|
|
2699
|
+
if (!active) return;
|
|
2700
|
+
if (userEndedRef.current) {
|
|
2701
|
+
userEndedRef.current = false;
|
|
2702
|
+
setUserEnded(false);
|
|
2703
|
+
reset();
|
|
2704
|
+
manualReconnectPendingRef.current = true;
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
if (manualReconnectPendingRef.current || recovery.kind === "in-place-reconnecting") return;
|
|
2475
2708
|
manualReconnectPendingRef.current = true;
|
|
2476
2709
|
clearTimer();
|
|
2477
2710
|
attemptRef.current = 0;
|
|
@@ -2480,7 +2713,7 @@ function useSessionLifecycle(input) {
|
|
|
2480
2713
|
releaseRef.current("superseded");
|
|
2481
2714
|
setRecovery({ kind: "refreshing", attempt: 0 });
|
|
2482
2715
|
refreshRef.current();
|
|
2483
|
-
}, [active, clearTimer, recovery.kind]);
|
|
2716
|
+
}, [active, clearTimer, recovery.kind, reset]);
|
|
2484
2717
|
const endIdleRef = useRef(endIdle);
|
|
2485
2718
|
endIdleRef.current = endIdle;
|
|
2486
2719
|
useEffect(() => {
|
|
@@ -2512,6 +2745,7 @@ function useSessionLifecycle(input) {
|
|
|
2512
2745
|
() => lifecyclePhaseFrom({ capacity, recovery, idle, connected, agentPresent }),
|
|
2513
2746
|
[capacity, recovery, idle, connected, agentPresent]
|
|
2514
2747
|
);
|
|
2748
|
+
phaseRef.current = phase;
|
|
2515
2749
|
const onBehaviorChangeRef = useRef(onBehaviorChange);
|
|
2516
2750
|
onBehaviorChangeRef.current = onBehaviorChange;
|
|
2517
2751
|
const lastBehaviorRef = useRef(null);
|
|
@@ -2537,6 +2771,7 @@ function useSessionLifecycle(input) {
|
|
|
2537
2771
|
stayConnected,
|
|
2538
2772
|
markActivity,
|
|
2539
2773
|
reconnect,
|
|
2774
|
+
end,
|
|
2540
2775
|
onConnected,
|
|
2541
2776
|
onDisconnected,
|
|
2542
2777
|
onConnectionError,
|
|
@@ -2555,6 +2790,7 @@ function useSessionLifecycle(input) {
|
|
|
2555
2790
|
stayConnected,
|
|
2556
2791
|
markActivity,
|
|
2557
2792
|
reconnect,
|
|
2793
|
+
end,
|
|
2558
2794
|
onConnected,
|
|
2559
2795
|
onDisconnected,
|
|
2560
2796
|
onConnectionError,
|
|
@@ -2569,7 +2805,8 @@ function useSessionLifecycle(input) {
|
|
|
2569
2805
|
function SessionLifecycleRoomBridge({
|
|
2570
2806
|
lifecycle,
|
|
2571
2807
|
onConnectionDetailsChange,
|
|
2572
|
-
onMediaModeChange
|
|
2808
|
+
onMediaModeChange,
|
|
2809
|
+
microphone
|
|
2573
2810
|
}) {
|
|
2574
2811
|
const {
|
|
2575
2812
|
onConnectionStateChange,
|
|
@@ -2580,12 +2817,23 @@ function SessionLifecycleRoomBridge({
|
|
|
2580
2817
|
registerDataPublisher,
|
|
2581
2818
|
registerTurnSender,
|
|
2582
2819
|
setTurnState,
|
|
2583
|
-
setMedia
|
|
2820
|
+
setMedia,
|
|
2821
|
+
setMicrophoneFacts,
|
|
2822
|
+
setAudioPlayback,
|
|
2823
|
+
registerMediaControls
|
|
2584
2824
|
} = lifecycle;
|
|
2585
2825
|
const connectionState = useConnectionState();
|
|
2586
2826
|
const assistant = useVoiceAssistant();
|
|
2587
2827
|
const transcriptions = useTranscriptions();
|
|
2588
2828
|
const room = useRoomContext();
|
|
2829
|
+
useCallMedia(room, {
|
|
2830
|
+
setMicrophoneFacts,
|
|
2831
|
+
setAudioPlayback,
|
|
2832
|
+
registerMediaControls,
|
|
2833
|
+
microphoneWanted: microphone ?? publishesMicrophoneByDefault(lifecycle.grant),
|
|
2834
|
+
connectionState,
|
|
2835
|
+
callKey: lifecycle.grant?.session_id
|
|
2836
|
+
});
|
|
2589
2837
|
const mediaModeCallbackRef = useRef(onMediaModeChange);
|
|
2590
2838
|
mediaModeCallbackRef.current = onMediaModeChange;
|
|
2591
2839
|
const mediaModeEnabled = onMediaModeChange !== void 0 && lifecycle.grant !== null;
|
|
@@ -2859,6 +3107,8 @@ function resolveEndReason(workerLabel, innerReason) {
|
|
|
2859
3107
|
return "idle";
|
|
2860
3108
|
case "error":
|
|
2861
3109
|
return "failed";
|
|
3110
|
+
case "user":
|
|
3111
|
+
return "user_ended";
|
|
2862
3112
|
default:
|
|
2863
3113
|
return "disconnected";
|
|
2864
3114
|
}
|
|
@@ -2914,6 +3164,16 @@ function useRealtimeSession(input) {
|
|
|
2914
3164
|
const [turn, setTurn] = useState("quiet");
|
|
2915
3165
|
const [media, setMediaState] = useState({ video: "connecting", audio: "silent" });
|
|
2916
3166
|
const [behavior, setBehavior] = useState(null);
|
|
3167
|
+
const [microphoneFacts2, setMicrophoneFactsState] = useState({
|
|
3168
|
+
wanted: false,
|
|
3169
|
+
connected: false,
|
|
3170
|
+
enabling: false,
|
|
3171
|
+
publication: null,
|
|
3172
|
+
deviceError: null
|
|
3173
|
+
});
|
|
3174
|
+
const [microphoneMuted, setMicrophoneMuted] = useState(false);
|
|
3175
|
+
const [audioPlayback, setAudioPlaybackState] = useState("unknown");
|
|
3176
|
+
const mediaControlsRef = useRef(null);
|
|
2917
3177
|
const lastBehaviorRef = useRef(null);
|
|
2918
3178
|
const lastLabeledEndReasonRef = useRef(null);
|
|
2919
3179
|
const pendingClipsRef = useRef(/* @__PURE__ */ new Map());
|
|
@@ -3020,6 +3280,31 @@ function useRealtimeSession(input) {
|
|
|
3020
3280
|
const setMedia = useCallback((next) => {
|
|
3021
3281
|
setMediaState((prev) => prev.video === next.video && prev.audio === next.audio ? prev : next);
|
|
3022
3282
|
}, []);
|
|
3283
|
+
const microphoneFactsRef = useRef(microphoneFacts2);
|
|
3284
|
+
microphoneFactsRef.current = microphoneFacts2;
|
|
3285
|
+
const setMicrophoneFacts = useCallback((facts) => setMicrophoneFactsState(facts), []);
|
|
3286
|
+
const setAudioPlayback = useCallback((playback) => setAudioPlaybackState(playback), []);
|
|
3287
|
+
const registerMediaControls = useCallback((controls) => {
|
|
3288
|
+
mediaControlsRef.current = controls;
|
|
3289
|
+
}, []);
|
|
3290
|
+
const startAudio = useCallback(async () => {
|
|
3291
|
+
const controls = mediaControlsRef.current;
|
|
3292
|
+
return controls ? controls.startAudio() : false;
|
|
3293
|
+
}, []);
|
|
3294
|
+
const callEnded = lifecycle.phase.kind === "ended";
|
|
3295
|
+
const callEndedRef = useRef(callEnded);
|
|
3296
|
+
callEndedRef.current = callEnded;
|
|
3297
|
+
const setMicrophoneEnabled = useCallback(async (enabled) => {
|
|
3298
|
+
if (callEndedRef.current) return;
|
|
3299
|
+
setMicrophoneMuted(!enabled);
|
|
3300
|
+
if (!microphoneFactsRef.current.connected) return;
|
|
3301
|
+
await mediaControlsRef.current?.setMicrophoneEnabled(enabled);
|
|
3302
|
+
}, []);
|
|
3303
|
+
const microphone = useMemo(
|
|
3304
|
+
() => callEnded ? { status: "off" } : callMicrophoneFrom(microphoneFacts2, { muted: microphoneMuted }),
|
|
3305
|
+
[callEnded, microphoneFacts2, microphoneMuted]
|
|
3306
|
+
);
|
|
3307
|
+
const callAudioPlayback = callEnded ? "unknown" : audioPlayback;
|
|
3023
3308
|
useEffect(() => {
|
|
3024
3309
|
if (phaseKind === "idle" || phaseKind === "ended") return;
|
|
3025
3310
|
const timer = window.setInterval(() => {
|
|
@@ -3164,11 +3449,23 @@ function useRealtimeSession(input) {
|
|
|
3164
3449
|
void sendResolvedTurn(last.text, last.attributes, last.id).catch(() => void 0);
|
|
3165
3450
|
}, [sendResolvedTurn]);
|
|
3166
3451
|
const end = useCallback((reason) => {
|
|
3452
|
+
if (phaseKind === "ended") return;
|
|
3167
3453
|
if (reason) lastLabeledEndReasonRef.current = reason;
|
|
3168
3454
|
lastTurnRef.current = null;
|
|
3169
3455
|
requestGracefulClose();
|
|
3170
|
-
lifecycle.
|
|
3171
|
-
}, [lifecycle, requestGracefulClose]);
|
|
3456
|
+
lifecycle.end();
|
|
3457
|
+
}, [lifecycle, phaseKind, requestGracefulClose]);
|
|
3458
|
+
const active = inner.active ?? true;
|
|
3459
|
+
useEffect(() => {
|
|
3460
|
+
if (active) return;
|
|
3461
|
+
lastLabeledEndReasonRef.current = null;
|
|
3462
|
+
setMicrophoneMuted(false);
|
|
3463
|
+
}, [active]);
|
|
3464
|
+
const innerReconnect = lifecycle.reconnect;
|
|
3465
|
+
const reconnect = useCallback(() => {
|
|
3466
|
+
if (phaseKind === "ended") lastLabeledEndReasonRef.current = null;
|
|
3467
|
+
innerReconnect();
|
|
3468
|
+
}, [innerReconnect, phaseKind]);
|
|
3172
3469
|
const performAction = useCallback(
|
|
3173
3470
|
(actionId, opts) => {
|
|
3174
3471
|
const requestId = newTurnId();
|
|
@@ -3214,6 +3511,7 @@ function useRealtimeSession(input) {
|
|
|
3214
3511
|
approachingFiredRef.current = false;
|
|
3215
3512
|
creditsLowFiredRef.current = false;
|
|
3216
3513
|
lastTurnRef.current = null;
|
|
3514
|
+
setMicrophoneMuted(false);
|
|
3217
3515
|
lifecycle.reset();
|
|
3218
3516
|
}, [lifecycle]);
|
|
3219
3517
|
return useMemo(
|
|
@@ -3227,6 +3525,12 @@ function useRealtimeSession(input) {
|
|
|
3227
3525
|
endsAt,
|
|
3228
3526
|
graceWindow,
|
|
3229
3527
|
media,
|
|
3528
|
+
microphone,
|
|
3529
|
+
audioPlayback: callAudioPlayback,
|
|
3530
|
+
startAudio,
|
|
3531
|
+
setMicrophoneEnabled,
|
|
3532
|
+
microphoneMuted,
|
|
3533
|
+
reconnect,
|
|
3230
3534
|
lifecycle,
|
|
3231
3535
|
sendClosingTurn,
|
|
3232
3536
|
requestGracefulClose,
|
|
@@ -3242,6 +3546,9 @@ function useRealtimeSession(input) {
|
|
|
3242
3546
|
registerTurnSender,
|
|
3243
3547
|
setTurnState,
|
|
3244
3548
|
setMedia,
|
|
3549
|
+
setMicrophoneFacts,
|
|
3550
|
+
setAudioPlayback,
|
|
3551
|
+
registerMediaControls,
|
|
3245
3552
|
reset
|
|
3246
3553
|
}),
|
|
3247
3554
|
[
|
|
@@ -3251,6 +3558,12 @@ function useRealtimeSession(input) {
|
|
|
3251
3558
|
endsAt,
|
|
3252
3559
|
graceWindow,
|
|
3253
3560
|
media,
|
|
3561
|
+
microphone,
|
|
3562
|
+
callAudioPlayback,
|
|
3563
|
+
startAudio,
|
|
3564
|
+
setMicrophoneEnabled,
|
|
3565
|
+
microphoneMuted,
|
|
3566
|
+
reconnect,
|
|
3254
3567
|
sendClosingTurn,
|
|
3255
3568
|
requestGracefulClose,
|
|
3256
3569
|
extend,
|
|
@@ -3265,44 +3578,74 @@ function useRealtimeSession(input) {
|
|
|
3265
3578
|
registerTurnSender,
|
|
3266
3579
|
setTurnState,
|
|
3267
3580
|
setMedia,
|
|
3581
|
+
setMicrophoneFacts,
|
|
3582
|
+
setAudioPlayback,
|
|
3583
|
+
registerMediaControls,
|
|
3268
3584
|
reset
|
|
3269
3585
|
]
|
|
3270
3586
|
);
|
|
3271
3587
|
}
|
|
3272
3588
|
|
|
3589
|
+
// ../http-client/src/retry.ts
|
|
3590
|
+
var PROXY_CLIENT_TIMEOUT_MS = 6e4;
|
|
3591
|
+
|
|
3273
3592
|
// ../client/src/proxy-client.ts
|
|
3274
|
-
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
3275
3593
|
var normalize = (url) => url.replace(/\/+$/, "");
|
|
3276
3594
|
function createProxyClient(options) {
|
|
3277
3595
|
const base = normalize(options.proxyUrl);
|
|
3278
3596
|
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
3279
|
-
const timeoutMs = options.timeoutMs ??
|
|
3280
|
-
const
|
|
3281
|
-
|
|
3282
|
-
const timer = AbortSignal.timeout(timeoutMs);
|
|
3283
|
-
return caller ? AbortSignal.any([caller, timer]) : timer;
|
|
3284
|
-
};
|
|
3597
|
+
const timeoutMs = options.timeoutMs ?? PROXY_CLIENT_TIMEOUT_MS;
|
|
3598
|
+
const credentials = options.credentials;
|
|
3599
|
+
const withCredentials = credentials === void 0 ? {} : { credentials };
|
|
3285
3600
|
const post = async (path, body, request) => {
|
|
3286
3601
|
if (!doFetch) throw new Error("realtime-avatar: no fetch available \u2014 pass one via `fetch`.");
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3602
|
+
const caller = request?.signal;
|
|
3603
|
+
const timer = timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0;
|
|
3604
|
+
try {
|
|
3605
|
+
return await doFetch(`${base}${path}`, {
|
|
3606
|
+
method: "POST",
|
|
3607
|
+
...withCredentials,
|
|
3608
|
+
headers: { "content-type": "application/json", ...request?.headers ?? {} },
|
|
3609
|
+
body: JSON.stringify(body),
|
|
3610
|
+
signal: timer && caller ? AbortSignal.any([caller, timer]) : timer ?? caller
|
|
3611
|
+
});
|
|
3612
|
+
} catch (cause) {
|
|
3613
|
+
if (timer?.aborted && !caller?.aborted) throw RealtimeAvatarApiError.timeout(timeoutMs);
|
|
3614
|
+
throw cause;
|
|
3615
|
+
}
|
|
3293
3616
|
};
|
|
3294
|
-
const
|
|
3617
|
+
const pageHide = (body) => {
|
|
3618
|
+
const page = globalThis.location?.href;
|
|
3619
|
+
let target;
|
|
3620
|
+
try {
|
|
3621
|
+
target = new URL(`${base}/end`, page);
|
|
3622
|
+
} catch {
|
|
3623
|
+
return false;
|
|
3624
|
+
}
|
|
3625
|
+
const payload = JSON.stringify(body);
|
|
3295
3626
|
const send = globalThis.navigator?.sendBeacon?.bind(globalThis.navigator);
|
|
3296
|
-
|
|
3297
|
-
|
|
3627
|
+
const sameOrigin = page !== void 0 && target.origin === new URL(page).origin;
|
|
3628
|
+
if (send && (sameOrigin || credentials === "include")) {
|
|
3629
|
+
if (send(target.href, new Blob([payload], { type: "application/json" }))) return true;
|
|
3630
|
+
}
|
|
3631
|
+
if (!doFetch) return false;
|
|
3632
|
+
try {
|
|
3633
|
+
void doFetch(target.href, {
|
|
3634
|
+
method: "POST",
|
|
3635
|
+
keepalive: true,
|
|
3636
|
+
...withCredentials,
|
|
3637
|
+
headers: { "content-type": "application/json" },
|
|
3638
|
+
body: payload
|
|
3639
|
+
}).catch(() => void 0);
|
|
3640
|
+
return true;
|
|
3641
|
+
} catch {
|
|
3642
|
+
return false;
|
|
3643
|
+
}
|
|
3298
3644
|
};
|
|
3299
3645
|
return {
|
|
3300
3646
|
async createLiveKitSessionOrBusy(input, requestOptions) {
|
|
3301
|
-
const
|
|
3302
|
-
|
|
3303
|
-
{ avatarId: input.avatarId, mode: input.mode },
|
|
3304
|
-
requestOptions
|
|
3305
|
-
);
|
|
3647
|
+
const connect = { avatarId: input.avatarId, mode: input.mode };
|
|
3648
|
+
const response = await post("/connect", connect, requestOptions);
|
|
3306
3649
|
if (response.status === 429) {
|
|
3307
3650
|
const value = await response.clone().json().catch(() => null);
|
|
3308
3651
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -3320,7 +3663,8 @@ function createProxyClient(options) {
|
|
|
3320
3663
|
async releaseLiveKitSession(sessionId, reason, requestOptions) {
|
|
3321
3664
|
if (!sessionId) return false;
|
|
3322
3665
|
try {
|
|
3323
|
-
const
|
|
3666
|
+
const body = { session_id: sessionId, reason };
|
|
3667
|
+
const response = await post("/end", body, requestOptions);
|
|
3324
3668
|
return response.ok;
|
|
3325
3669
|
} catch {
|
|
3326
3670
|
return false;
|
|
@@ -3328,12 +3672,13 @@ function createProxyClient(options) {
|
|
|
3328
3672
|
},
|
|
3329
3673
|
releaseLiveKitSessionBeacon(sessionId, reason) {
|
|
3330
3674
|
if (!sessionId) return false;
|
|
3331
|
-
return
|
|
3675
|
+
return pageHide({ session_id: sessionId, reason: reason ?? "page_hide" });
|
|
3332
3676
|
},
|
|
3333
3677
|
async releaseLiveKitQueueTicket(queueTicketId, reason, requestOptions) {
|
|
3334
3678
|
if (!queueTicketId) return false;
|
|
3335
3679
|
try {
|
|
3336
|
-
const
|
|
3680
|
+
const body = { queue_ticket_id: queueTicketId, reason };
|
|
3681
|
+
const response = await post("/end", body, requestOptions);
|
|
3337
3682
|
return response.ok;
|
|
3338
3683
|
} catch {
|
|
3339
3684
|
return false;
|
|
@@ -3341,7 +3686,7 @@ function createProxyClient(options) {
|
|
|
3341
3686
|
},
|
|
3342
3687
|
releaseLiveKitQueueTicketBeacon(queueTicketId, reason) {
|
|
3343
3688
|
if (!queueTicketId) return false;
|
|
3344
|
-
return
|
|
3689
|
+
return pageHide({ queue_ticket_id: queueTicketId, reason: reason ?? "page_hide" });
|
|
3345
3690
|
}
|
|
3346
3691
|
};
|
|
3347
3692
|
}
|