realtime-avatar 0.22.0 → 0.23.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/dist/react.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { RegisteredTool, ToolRegistration } from './tools.js';
2
- import { ah as SessionLifecycleRoomBridgeProps, h as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-6ndCkiio.js';
3
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, aM as AvatarVideoSurface, aN as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, u as EndedEvent, v as ExtendResult, F as FishTtsModel, w as FreezeReadingFn, G as Governor, x as GovernorAction, y as GovernorConfig, z as GovernorSignal, H as GovernorState, I as GraceWindowClosedEvent, J as GraceWindowOpenEvent, K as GraceWindowState, M as IdleWarningEvent, N as InboundRtpCursor, O as InboundRtpReading, P as KnownBehaviorState, Q as LLMProvider, R as LLMSelection, S as LiveKitAvatarGrantState, T as LiveKitAvatarGrantStatus, U as LiveKitCapacityState, V as LiveKitConnectionStatus, W as LiveKitSessionRequest, X as LiveKitSessionStartResult, aO as LivePlaybackKeeper, Y as MAX_RECONNECT_ATTEMPTS, Z as MAX_SESSION_INSTRUCTIONS_CHARS, aP as MIC_LEASE_ENDED_TIMEOUT_MS, aQ as MicLease, aR as NETWORK_QUALITY_POLICY, aS as NetworkQualityStatus, aT as PlayableVideoElement, _ as ProxyClientOptions, $ as QualityCap, a0 as RECONNECT_BACKOFF_MS, aU as RealtimeAvatarCapacityError, aV as RealtimeAvatarLiveKitRoom, aW as RealtimeAvatarLiveKitRoomProps, a1 as RealtimeAvatarRequestOptions, a2 as RealtimeSessionApi, a3 as RealtimeSessionMedia, a4 as RealtimeSessionRoomSinks, a5 as ReconnectPolicy, a6 as ReconnectingEvent, a7 as RecoveryState, a8 as RetryStep, a9 as SessionBehavior, aa as SessionClip, ab as SessionClocks, ac as SessionEndReason, ad as SessionLifecycleApi, ae as SessionLifecyclePhase, af as SessionLifecyclePhaseKind, ag as SessionLifecycleRoomBridge, ai as SurfaceLayers, aj as TurnState, ak as TurnTimeoutEvent, al as UseAvatarQualityGovernorInput, am as UseLiveKitAvatarGrantInput, an as UseRealtimeSessionInput, ao as UseSessionLifecycleInput, ap as VoiceSpec, aq as VoiceSpecInput, ar as capacityErrorFromBusy, as as capacityStateFromGrant, at as createProxyClient, av as knownBehaviorStates, aw as mapTurnState, ax as readInboundRtp, ay as sessionBehaviorSchema, az as sessionClipSchema, aX as shouldReplayPendingTurn, aA as splitCallTranscript, aB as useAvatarAdaptivePlayoutDelay, aC as useAvatarCamera, aD as useAvatarPlayoutDelay, aE as useAvatarQualityGovernor, aF as useCallTranscript, aG as useLiveKitAvatarGrant, aI as useMicLease, aJ as useRealtimeSession, aK as useReleaseMicLeaseOnTrackEnded, aL as useSessionLifecycle } from './proxy-client-6ndCkiio.js';
2
+ import { aj as SessionLifecycleRoomBridgeProps, h as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-DlFXaXtn.js';
3
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarConnectionDetails, aQ as AvatarVideoSurface, aR as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, i as CallTranscriptSegment, j as CartesiaTtsModel, k as ClipResult, l as ClosingTurnResult, m as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, n as DEFAULT_CREDITS_LOW_LEAD_SECONDS, o as DEFAULT_GOVERNOR_CONFIG, p as DEFAULT_GRACE_CEILING_SECONDS, q as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, r as DEFAULT_IDLE_SECONDS, s as DEFAULT_IDLE_WARN_LEAD_SECONDS, t as DEFAULT_TURN_TIMEOUT_SECONDS, u as DeclaredInputSource, E as EndReason, v as EndedEvent, w as ExtendResult, F as FishTtsModel, x as FreezeReadingFn, G as Governor, y as GovernorAction, z as GovernorConfig, H as GovernorSignal, I as GovernorState, J as GraceWindowClosedEvent, K as GraceWindowOpenEvent, M as GraceWindowState, N as IdleWarningEvent, O as InboundRtpCursor, P as InboundRtpReading, Q as KnownBehaviorState, R as LLMProvider, S as LLMSelection, T as LiveKitAvatarGrantState, U as LiveKitAvatarGrantStatus, V as LiveKitCapacityState, W as LiveKitConnectionStatus, X as LiveKitSessionRequest, Y as LiveKitSessionStartResult, aS as LivePlaybackKeeper, Z as MAX_RECONNECT_ATTEMPTS, _ as MAX_SESSION_INSTRUCTIONS_CHARS, aT as MIC_LEASE_ENDED_TIMEOUT_MS, aU as MicLease, aV as NETWORK_QUALITY_POLICY, aW as NetworkQualityStatus, aX as PlayableVideoElement, $ as ProxyClientOptions, a0 as QualityCap, a1 as RECONNECT_BACKOFF_MS, aY as RealtimeAvatarCapacityError, aZ as RealtimeAvatarLiveKitRoom, a_ as RealtimeAvatarLiveKitRoomProps, a2 as RealtimeAvatarRequestOptions, a3 as RealtimeSessionApi, a4 as RealtimeSessionMedia, a5 as RealtimeSessionRoomSinks, a6 as ReconnectPolicy, a7 as ReconnectingEvent, a8 as RecoveryState, a9 as RetryStep, aa as SendTurnOptions, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, ak as SurfaceLayers, al as TranscriptSender, am as TranscriptSenderOptions, an as TurnState, ao as TurnTimeoutEvent, ap as UseAvatarQualityGovernorInput, aq as UseLiveKitAvatarGrantInput, ar as UseRealtimeSessionInput, as as UseSessionLifecycleInput, at as VoiceSpec, au as VoiceSpecInput, av as capacityErrorFromBusy, aw as capacityStateFromGrant, ax as createProxyClient, az as knownBehaviorStates, aA as mapTurnState, aB as readInboundRtp, aC as sessionBehaviorSchema, aD as sessionClipSchema, a$ as shouldReplayPendingTurn, aE as splitCallTranscript, aF as useAvatarAdaptivePlayoutDelay, aG as useAvatarCamera, aH as useAvatarPlayoutDelay, aI as useAvatarQualityGovernor, aJ as useCallTranscript, aK as useLiveKitAvatarGrant, aM as useMicLease, aN as useRealtimeSession, aO as useReleaseMicLeaseOnTrackEnded, aP as useSessionLifecycle } from './proxy-client-DlFXaXtn.js';
4
4
  import { ReactNode } from 'react';
5
+ import * as react_jsx_runtime from 'react/jsx-runtime';
6
+ import { R as RecordingArtifact } from './recording-DjswTx5B.js';
5
7
  export { D as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS } from './playout-delay-DWOc8iCl.js';
6
8
  import 'zod';
7
9
  import 'livekit-client';
@@ -129,4 +131,14 @@ declare function useAvatarCall(props: AvatarCallProps): {
129
131
  /** One component: a live character on screen. */
130
132
  declare function AvatarCall(props: AvatarCallProps): ReactNode;
131
133
 
132
- export { AvatarCall, type AvatarCallEndReason, type AvatarCallHandle, type AvatarCallProps, type AvatarCallStatus, AvatarSessionClient, AvatarVideoFit, DEFAULT_AVATAR_UNSTABLE_STALL_MS, SessionLifecycleRoomBridgeProps, useAvatarCall, useCharacterTools };
134
+ type RecordingPlaybackAsset = {
135
+ recording: RecordingArtifact;
136
+ url: string;
137
+ };
138
+ /** Each participant file carries only its own voice; one clock drives both files. */
139
+ declare function RecordingPlayer({ assets, className }: {
140
+ assets: readonly RecordingPlaybackAsset[];
141
+ className?: string;
142
+ }): react_jsx_runtime.JSX.Element;
143
+
144
+ export { AvatarCall, type AvatarCallEndReason, type AvatarCallHandle, type AvatarCallProps, type AvatarCallStatus, AvatarSessionClient, AvatarVideoFit, DEFAULT_AVATAR_UNSTABLE_STALL_MS, type RecordingPlaybackAsset, RecordingPlayer, SessionLifecycleRoomBridgeProps, useAvatarCall, useCharacterTools };
package/dist/react.js CHANGED
@@ -3,6 +3,7 @@ import { useRef, useState, useEffect, useCallback, useMemo, createElement, Fragm
3
3
  import { useRoomContext, useConnectionState, useLocalParticipant, useTranscriptions, RoomAudioRenderer, LiveKitRoom, useMaybeRoomContext, useVoiceAssistant, VideoTrack, useChat } from '@livekit/components-react';
4
4
  import { ConnectionState, isRemoteTrack, VideoQuality, ConnectionQuality, RoomEvent, TrackEvent, Track, ConnectionError, ConnectionErrorReason, DisconnectReason, createLocalVideoTrack, Room } from 'livekit-client';
5
5
  import { z } from 'zod';
6
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
7
 
7
8
  // ../client/src/react/use-character-tools.ts
8
9
  var invocationSchema = z.object({
@@ -2119,15 +2120,22 @@ var AVATAR_VIDEO_LIVE_WRAP = "absolute inset-0 z-20 size-full transition-opacity
2119
2120
  var AVATAR_VIDEO_BADGE = "absolute top-2 right-2 z-30 inline-flex items-center gap-1.5 rounded-md bg-black/55 px-1.5 py-0.5 font-mono text-[10px] text-white/85 backdrop-blur-sm";
2120
2121
  var AVATAR_VIDEO_BADGE_DOT = "size-1.5 rounded-full bg-emerald-400";
2121
2122
  var RECORDED_MEDIA_MODES = ["audio", "video", "audio_video"];
2122
- var recordingModeSchema = z.enum(["off", ...RECORDED_MEDIA_MODES]).describe("Server-owned recording policy; omitted means off. Audio includes user and avatar audio.");
2123
+ var recordingModeSchema = z.enum(["off", ...RECORDED_MEDIA_MODES, "participants"]).describe("Server-owned recording policy; omitted means off. Legacy audio includes user and avatar audio. participants saves one continuous audio/video file per participant, without mixing their voices.");
2123
2124
  var RECORDING_STATUSES = ["pending", "recording", "processing", "ready", "failed", "expired"];
2124
2125
  z.enum(RECORDING_STATUSES);
2126
+ var recordingParticipantSchema = z.object({
2127
+ participantIdentity: z.string().min(1).max(160),
2128
+ role: z.enum(["user", "avatar"])
2129
+ }).strict();
2125
2130
  var recordingMetadataSchema = z.object({
2126
2131
  sessionId: z.string().min(1),
2127
2132
  recordingId: z.string().min(1),
2128
- mode: recordingModeSchema.exclude(["off"]),
2133
+ mode: z.enum(RECORDED_MEDIA_MODES),
2129
2134
  createdAt: z.string().datetime({ offset: true }),
2130
- retainedUntil: z.string().datetime({ offset: true })
2135
+ retainedUntil: z.string().datetime({ offset: true }),
2136
+ participant: recordingParticipantSchema.optional(),
2137
+ mediaStartedAt: z.string().datetime({ offset: true }).optional(),
2138
+ mediaEndedAt: z.string().datetime({ offset: true }).optional()
2131
2139
  });
2132
2140
  var recordingArtifactSchema = z.discriminatedUnion("status", [
2133
2141
  recordingMetadataSchema.extend({ status: z.enum(["pending", "recording", "processing", "expired"]) }).strict(),
@@ -2434,6 +2442,8 @@ z.object({
2434
2442
  }).strict();
2435
2443
  z.object({
2436
2444
  recording: recordingArtifactSchema.optional(),
2445
+ recording_mode: recordingModeSchema.optional(),
2446
+ recordings: z.array(recordingArtifactSchema).max(2).optional(),
2437
2447
  connection_history: connectionHistoryGrantSchema.optional(),
2438
2448
  status: z.literal("ready").default("ready"),
2439
2449
  session_id: z.string().min(1),
@@ -2454,6 +2464,11 @@ var RTA_LIFECYCLE_TOPIC = "rta.lifecycle";
2454
2464
  var RTA_TURN_INSTRUCTIONS_ATTR = "rta.turn_instructions";
2455
2465
  var RTA_CLOSING_TURN_ATTR = "rta.closing_turn";
2456
2466
  var RTA_TURN_ID_ATTR = "rta.turn_id";
2467
+ var RTA_RETRY_OF_TURN_ID_ATTR = "rta.retry_of_turn_id";
2468
+ var RTA_INPUT_SOURCE_VERSION_ATTR = "rta.input_source_version";
2469
+ var RTA_OBSERVED_INPUT_SOURCE_ATTR = "rta.observed_input_source";
2470
+ var RTA_DECLARED_INPUT_SOURCE_ATTR = "rta.declared_input_source";
2471
+ var RTA_INPUT_SOURCE_DECLARATION_SCOPE_ATTR = "rta.input_source_declaration_scope";
2457
2472
  var sessionEndReasonSchema = z.enum([
2458
2473
  "user_ended",
2459
2474
  "session_cap",
@@ -3394,6 +3409,11 @@ var DEFAULT_CREDITS_LOW_LEAD_SECONDS = 300;
3394
3409
  function positive(value, fallbackSeconds) {
3395
3410
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallbackSeconds;
3396
3411
  }
3412
+ function validateInputSource(value) {
3413
+ if (value !== void 0 && value !== "text" && value !== "client_stt") {
3414
+ throw new TypeError('inputSource must be "text" or "client_stt"');
3415
+ }
3416
+ }
3397
3417
  function useRealtimeSession(input) {
3398
3418
  const {
3399
3419
  session,
@@ -3451,7 +3471,8 @@ function useRealtimeSession(input) {
3451
3471
  const turnSenderRef = useRef(null);
3452
3472
  const dataPublisherRef = useRef(null);
3453
3473
  const lastTurnRef = useRef(null);
3454
- const [clockTick, setClockTick] = useState(0);
3474
+ const turnSequenceRef = useRef(0);
3475
+ const [, setClockTick] = useState(0);
3455
3476
  const endsAt = endsAtFrom({
3456
3477
  serverStartedAtUnixMs: serverClock?.startedAtUnixMs ?? null,
3457
3478
  maxSessionSeconds: serverClock?.maxSessionSeconds ?? null
@@ -3473,8 +3494,8 @@ function useRealtimeSession(input) {
3473
3494
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
3474
3495
  } catch {
3475
3496
  }
3476
- return `t-${Date.now()}-${clockTick}`;
3477
- }, [clockTick]);
3497
+ return `t-${Date.now()}-${++turnSequenceRef.current}`;
3498
+ }, []);
3478
3499
  const onLifecycleData = useCallback((frame) => {
3479
3500
  const parsed = lifecycleServerFrameSchema.safeParse(frame);
3480
3501
  if (!parsed.success) return;
@@ -3574,8 +3595,8 @@ function useRealtimeSession(input) {
3574
3595
  if (gw === "open" || gw === "delivering") stayConnected();
3575
3596
  }
3576
3597
  const turnInfo = lastTurnRef.current;
3577
- if (turnInfo && now - turnInfo.sentAt >= turnTimeoutMs) {
3578
- lastTurnRef.current = null;
3598
+ if (turnInfo && turnInfo.sentAt !== null && now - turnInfo.sentAt >= turnTimeoutMs) {
3599
+ turnInfo.sentAt = null;
3579
3600
  cbRef.current.onTurnTimeout?.({ turnId: turnInfo.id });
3580
3601
  }
3581
3602
  setClockTick((value) => value + 1);
@@ -3604,6 +3625,7 @@ function useRealtimeSession(input) {
3604
3625
  }, [phaseKind, lifecycle.phase, lifecycle.attempt]);
3605
3626
  useEffect(() => {
3606
3627
  if (phaseKind === "ended") {
3628
+ lastTurnRef.current = null;
3607
3629
  if (!endedFiredRef.current) {
3608
3630
  endedFiredRef.current = true;
3609
3631
  const inner2 = lifecycle.phase.kind === "ended" ? lifecycle.phase.reason : void 0;
@@ -3640,23 +3662,47 @@ function useRealtimeSession(input) {
3640
3662
  setGraceWindow({ kind: "closed" });
3641
3663
  return { ok: true };
3642
3664
  }, []);
3643
- const sendTurn = useCallback(async (text, opts) => {
3665
+ const sendResolvedTurn = useCallback(async (text, attributes, retryOfTurnId) => {
3644
3666
  const trimmed = text.trim();
3645
3667
  const sender = turnSenderRef.current;
3646
3668
  if (!sender || !trimmed) return;
3647
3669
  const turnId = newTurnId();
3648
- lastTurnRef.current = { text: trimmed, opts, id: turnId, sentAt: Date.now() };
3670
+ const outgoing = { ...attributes, [RTA_TURN_ID_ATTR]: turnId };
3671
+ if (retryOfTurnId) outgoing[RTA_RETRY_OF_TURN_ID_ATTR] = retryOfTurnId;
3672
+ lastTurnRef.current = { text: trimmed, attributes: { ...outgoing }, id: turnId, sentAt: Date.now() };
3649
3673
  lifecycle.markActivity();
3650
- const attributes = opts?.instructions ? { [RTA_TURN_INSTRUCTIONS_ATTR]: opts.instructions } : void 0;
3651
- await sender(trimmed, attributes ? { attributes } : void 0);
3674
+ await sender(trimmed, { attributes: outgoing });
3652
3675
  }, [lifecycle, newTurnId]);
3676
+ const sendInput = useCallback(async (text, opts, adapterSource) => {
3677
+ const turnSource = opts?.inputSource;
3678
+ validateInputSource(turnSource);
3679
+ const declaredSource = turnSource ?? adapterSource;
3680
+ const attributes = {
3681
+ [RTA_INPUT_SOURCE_VERSION_ATTR]: "1",
3682
+ [RTA_OBSERVED_INPUT_SOURCE_ATTR]: "text"
3683
+ };
3684
+ if (declaredSource !== void 0) {
3685
+ attributes[RTA_DECLARED_INPUT_SOURCE_ATTR] = declaredSource;
3686
+ attributes[RTA_INPUT_SOURCE_DECLARATION_SCOPE_ATTR] = turnSource !== void 0 ? "turn" : "adapter";
3687
+ }
3688
+ if (opts?.instructions) attributes[RTA_TURN_INSTRUCTIONS_ATTR] = opts.instructions;
3689
+ await sendResolvedTurn(text, attributes);
3690
+ }, [sendResolvedTurn]);
3691
+ const sendTurn = useCallback((text, opts) => sendInput(text, opts), [sendInput]);
3692
+ const createTranscriptSender = useCallback((opts) => {
3693
+ const source = opts?.inputSource;
3694
+ validateInputSource(source);
3695
+ const boundSource = source ?? "client_stt";
3696
+ return (text, turnOpts) => sendInput(text, turnOpts, boundSource);
3697
+ }, [sendInput]);
3653
3698
  const retryTurn = useCallback(() => {
3654
3699
  const last = lastTurnRef.current;
3655
3700
  if (!last) return;
3656
- void sendTurn(last.text, last.opts);
3657
- }, [sendTurn]);
3701
+ void sendResolvedTurn(last.text, last.attributes, last.id).catch(() => void 0);
3702
+ }, [sendResolvedTurn]);
3658
3703
  const end = useCallback((reason) => {
3659
3704
  if (reason) lastLabeledEndReasonRef.current = reason;
3705
+ lastTurnRef.current = null;
3660
3706
  requestGracefulClose();
3661
3707
  lifecycle.reset();
3662
3708
  }, [lifecycle, requestGracefulClose]);
@@ -3723,6 +3769,7 @@ function useRealtimeSession(input) {
3723
3769
  requestGracefulClose,
3724
3770
  extend,
3725
3771
  sendTurn,
3772
+ createTranscriptSender,
3726
3773
  retryTurn,
3727
3774
  end,
3728
3775
  behavior,
@@ -3745,6 +3792,7 @@ function useRealtimeSession(input) {
3745
3792
  requestGracefulClose,
3746
3793
  extend,
3747
3794
  sendTurn,
3795
+ createTranscriptSender,
3748
3796
  retryTurn,
3749
3797
  end,
3750
3798
  behavior,
@@ -3921,4 +3969,224 @@ function createProxyClient(options) {
3921
3969
  };
3922
3970
  }
3923
3971
 
3924
- export { AdaptivePlayoutController, AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_AVATAR_UNSTABLE_STALL_MS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, MIC_LEASE_ENDED_TIMEOUT_MS, NETWORK_QUALITY_POLICY, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCall, useAvatarCamera, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useCharacterTools, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
3972
+ // ../browser/src/recording-playback.ts
3973
+ function recordingTimeline(recordings) {
3974
+ if (new Set(recordings.map((recording) => recording.sessionId)).size > 1) throw new Error("Recordings belong to different sessions");
3975
+ if (new Set(recordings.map((recording) => recording.recordingId)).size !== recordings.length) throw new Error("Duplicate recording assets");
3976
+ const participants = recordings.filter((recording) => recording.participant && recording.status === "ready");
3977
+ if (new Set(participants.map((recording) => recording.participant?.role)).size !== participants.length) throw new Error("Duplicate participant recordings");
3978
+ const entries = participants.map((recording) => {
3979
+ if (recording.status !== "ready" || !recording.mediaStartedAt || !recording.mediaEndedAt || recording.durationMs === null) {
3980
+ throw new Error("Recording media timing is unavailable");
3981
+ }
3982
+ const start = Date.parse(recording.mediaStartedAt);
3983
+ const end = Math.min(Date.parse(recording.mediaEndedAt), start + recording.durationMs);
3984
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) throw new Error("Recording media timing is invalid");
3985
+ return { recording, start, end };
3986
+ });
3987
+ if (!entries.length) return null;
3988
+ const originMs = Math.min(...entries.map((entry) => entry.start));
3989
+ return {
3990
+ originMs,
3991
+ durationMs: Math.max(...entries.map((entry) => entry.end)) - originMs,
3992
+ entries: entries.map(({ recording, start, end }) => ({ recording, startMs: start - originMs, endMs: end - originMs }))
3993
+ };
3994
+ }
3995
+ function recordingPositions(timeline, positionMs) {
3996
+ return timeline.entries.map((entry) => ({
3997
+ recordingId: entry.recording.recordingId,
3998
+ active: positionMs >= entry.startMs && positionMs < entry.endMs,
3999
+ timeSeconds: Math.max(0, Math.min(positionMs, entry.endMs) - entry.startMs) / 1e3
4000
+ }));
4001
+ }
4002
+ function exhausted(element, seconds) {
4003
+ return Number.isFinite(element.duration) && seconds >= element.duration || element.ended && seconds >= element.currentTime;
4004
+ }
4005
+ function RecordingPlayer({ assets, className }) {
4006
+ const timeline = useMemo(() => {
4007
+ try {
4008
+ return recordingTimeline(assets.map((asset) => asset.recording));
4009
+ } catch {
4010
+ return null;
4011
+ }
4012
+ }, [assets]);
4013
+ const media = useRef(/* @__PURE__ */ new Map());
4014
+ const position = useRef(0);
4015
+ const anchor = useRef(null);
4016
+ const [displayPosition, setDisplayPosition] = useState(0);
4017
+ const [playing, setPlaying] = useState(false);
4018
+ const [buffering, setBuffering] = useState(false);
4019
+ const [error, setError] = useState("");
4020
+ const sourceKey = assets.map((asset) => `${asset.recording.recordingId}:${asset.url}`).join("|");
4021
+ useEffect(() => {
4022
+ if (!timeline) return;
4023
+ const sessionId = timeline.entries[0].recording.sessionId;
4024
+ if (anchor.current?.sessionId === sessionId) {
4025
+ position.current = Math.min(timeline.durationMs, Math.max(0, position.current + anchor.current.originMs - timeline.originMs));
4026
+ } else if (anchor.current) {
4027
+ position.current = 0;
4028
+ setPlaying(false);
4029
+ }
4030
+ anchor.current = { sessionId, originMs: timeline.originMs };
4031
+ setDisplayPosition(position.current);
4032
+ let cancelled = false;
4033
+ let frame = 0;
4034
+ let last = performance.now();
4035
+ let painted = last;
4036
+ const starting = /* @__PURE__ */ new Set();
4037
+ const stopMedia = () => {
4038
+ for (const element of media.current.values()) element.pause();
4039
+ };
4040
+ const fail = () => {
4041
+ if (cancelled) return;
4042
+ stopMedia();
4043
+ setPlaying(false);
4044
+ setError("Playback is unavailable. Refresh access and try again.");
4045
+ };
4046
+ const update = (now) => {
4047
+ if (cancelled) return;
4048
+ const proposed = Math.min(timeline.durationMs, position.current + (playing ? now - last : 0));
4049
+ last = now;
4050
+ const states = recordingPositions(timeline, proposed);
4051
+ const waiting = playing && states.some((state) => {
4052
+ const element = media.current.get(state.recordingId);
4053
+ return state.active && (!element || !exhausted(element, state.timeSeconds) && (element.readyState < 2 || element.seeking));
4054
+ });
4055
+ setBuffering(waiting);
4056
+ if (!waiting) position.current = proposed;
4057
+ for (const state of recordingPositions(timeline, position.current)) {
4058
+ const element = media.current.get(state.recordingId);
4059
+ if (!element) continue;
4060
+ if (!state.active || exhausted(element, state.timeSeconds)) {
4061
+ element.pause();
4062
+ continue;
4063
+ }
4064
+ if (element.readyState >= 1 && Math.abs(element.currentTime - state.timeSeconds) > 0.08) {
4065
+ element.currentTime = state.timeSeconds;
4066
+ }
4067
+ if (!playing || waiting) {
4068
+ element.pause();
4069
+ continue;
4070
+ }
4071
+ if (element.paused && !starting.has(state.recordingId)) {
4072
+ starting.add(state.recordingId);
4073
+ void element.play().catch((cause) => {
4074
+ if (!(cause instanceof Error && cause.name === "AbortError")) fail();
4075
+ }).finally(() => {
4076
+ starting.delete(state.recordingId);
4077
+ });
4078
+ }
4079
+ }
4080
+ if (now - painted >= 100 || !playing || position.current >= timeline.durationMs) {
4081
+ setDisplayPosition(position.current);
4082
+ painted = now;
4083
+ }
4084
+ if (position.current >= timeline.durationMs) {
4085
+ stopMedia();
4086
+ setPlaying(false);
4087
+ }
4088
+ frame = requestAnimationFrame(update);
4089
+ };
4090
+ if (playing) frame = requestAnimationFrame(update);
4091
+ else {
4092
+ stopMedia();
4093
+ setBuffering(false);
4094
+ }
4095
+ return () => {
4096
+ cancelled = true;
4097
+ cancelAnimationFrame(frame);
4098
+ stopMedia();
4099
+ };
4100
+ }, [timeline, playing, sourceKey]);
4101
+ const activeIds = new Set(timeline ? recordingPositions(timeline, displayPosition).filter((state) => state.active).map((state) => state.recordingId) : []);
4102
+ const seek = (value) => {
4103
+ position.current = value;
4104
+ setDisplayPosition(value);
4105
+ if (timeline) for (const state of recordingPositions(timeline, value)) {
4106
+ const element = media.current.get(state.recordingId);
4107
+ if (!element) continue;
4108
+ if (element.readyState >= 1) element.currentTime = state.timeSeconds;
4109
+ if (!state.active) element.pause();
4110
+ }
4111
+ };
4112
+ const toggle = () => {
4113
+ setError("");
4114
+ if (playing) {
4115
+ setPlaying(false);
4116
+ return;
4117
+ }
4118
+ if (!timeline) return;
4119
+ if (position.current >= timeline.durationMs) seek(0);
4120
+ for (const state of recordingPositions(timeline, position.current)) {
4121
+ const element = media.current.get(state.recordingId);
4122
+ if (state.active && element && !exhausted(element, state.timeSeconds)) {
4123
+ if (element.readyState >= 1) element.currentTime = state.timeSeconds;
4124
+ void element.play().catch((cause) => {
4125
+ if (cause instanceof Error && cause.name === "AbortError") return;
4126
+ for (const current of media.current.values()) current.pause();
4127
+ setPlaying(false);
4128
+ setError("Your browser could not play this recording. Try again or download the original.");
4129
+ });
4130
+ }
4131
+ }
4132
+ setPlaying(true);
4133
+ };
4134
+ return /* @__PURE__ */ jsxs("div", { className, children: [
4135
+ /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 240px), 1fr))", gap: 12 }, children: assets.map(({ recording, url }) => {
4136
+ if (!recording.participant) return null;
4137
+ const label = recording.participant.role === "user" ? "User" : "Character";
4138
+ const ref = (element) => {
4139
+ if (element) media.current.set(recording.recordingId, element);
4140
+ else media.current.delete(recording.recordingId);
4141
+ };
4142
+ const onError = () => {
4143
+ for (const current of media.current.values()) current.pause();
4144
+ setPlaying(false);
4145
+ setError("A recording could not load. Refresh access and try again.");
4146
+ };
4147
+ return recording.mode !== "audio" ? /* @__PURE__ */ jsxs("figure", { hidden: !activeIds.has(recording.recordingId), style: { margin: 0 }, children: [
4148
+ /* @__PURE__ */ jsx("figcaption", { children: label }),
4149
+ /* @__PURE__ */ jsx(
4150
+ "video",
4151
+ {
4152
+ ref,
4153
+ src: url,
4154
+ playsInline: true,
4155
+ preload: "auto",
4156
+ onError,
4157
+ hidden: !activeIds.has(recording.recordingId),
4158
+ style: { width: "100%", maxHeight: 360, objectFit: "contain" }
4159
+ }
4160
+ ),
4161
+ !activeIds.has(recording.recordingId) ? /* @__PURE__ */ jsx("p", { children: "No recording at this time." }) : null
4162
+ ] }, recording.recordingId) : /* @__PURE__ */ jsx("audio", { ref, src: url, preload: "auto", onError, "aria-label": `${label} audio` }, recording.recordingId);
4163
+ }) }),
4164
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
4165
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: toggle, disabled: !timeline, style: { minHeight: 44, minWidth: 44 }, children: playing ? "Pause" : "Play" }),
4166
+ /* @__PURE__ */ jsx(
4167
+ "input",
4168
+ {
4169
+ type: "range",
4170
+ "aria-label": "Call playback position",
4171
+ min: 0,
4172
+ max: timeline?.durationMs ?? 0,
4173
+ step: 100,
4174
+ value: displayPosition,
4175
+ "aria-valuetext": `${Math.floor(displayPosition / 1e3)} seconds`,
4176
+ disabled: !timeline,
4177
+ onChange: (event) => seek(Number(event.currentTarget.value)),
4178
+ style: { minHeight: 44, flex: 1 }
4179
+ }
4180
+ ),
4181
+ /* @__PURE__ */ jsxs("span", { children: [
4182
+ Math.floor(displayPosition / 1e3),
4183
+ " / ",
4184
+ Math.ceil((timeline?.durationMs ?? 0) / 1e3),
4185
+ " s"
4186
+ ] })
4187
+ ] }),
4188
+ /* @__PURE__ */ jsx("p", { role: "status", children: error || (!timeline ? "Media timing is unavailable. Individual recordings can still be downloaded." : buffering ? "Buffering\u2026" : "") })
4189
+ ] });
4190
+ }
4191
+
4192
+ export { AdaptivePlayoutController, AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_AVATAR_UNSTABLE_STALL_MS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, MIC_LEASE_ENDED_TIMEOUT_MS, NETWORK_QUALITY_POLICY, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, RecordingPlayer, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCall, useAvatarCamera, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useCharacterTools, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
@@ -0,0 +1,198 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const recordingModeSchema: z.ZodEnum<{
4
+ audio: "audio";
5
+ video: "video";
6
+ audio_video: "audio_video";
7
+ off: "off";
8
+ participants: "participants";
9
+ }>;
10
+ type RecordingMode = z.infer<typeof recordingModeSchema>;
11
+ declare const recordingArtifactStatusSchema: z.ZodEnum<{
12
+ ready: "ready";
13
+ pending: "pending";
14
+ recording: "recording";
15
+ processing: "processing";
16
+ failed: "failed";
17
+ expired: "expired";
18
+ }>;
19
+ type RecordingArtifactStatus = z.infer<typeof recordingArtifactStatusSchema>;
20
+ declare const recordingParticipantSchema: z.ZodObject<{
21
+ participantIdentity: z.ZodString;
22
+ role: z.ZodEnum<{
23
+ user: "user";
24
+ avatar: "avatar";
25
+ }>;
26
+ }, z.core.$strict>;
27
+ type RecordingParticipant = z.infer<typeof recordingParticipantSchema>;
28
+ declare const recordingArtifactSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
29
+ sessionId: z.ZodString;
30
+ recordingId: z.ZodString;
31
+ mode: z.ZodEnum<{
32
+ audio: "audio";
33
+ video: "video";
34
+ audio_video: "audio_video";
35
+ }>;
36
+ createdAt: z.ZodString;
37
+ retainedUntil: z.ZodString;
38
+ participant: z.ZodOptional<z.ZodObject<{
39
+ participantIdentity: z.ZodString;
40
+ role: z.ZodEnum<{
41
+ user: "user";
42
+ avatar: "avatar";
43
+ }>;
44
+ }, z.core.$strict>>;
45
+ mediaStartedAt: z.ZodOptional<z.ZodString>;
46
+ mediaEndedAt: z.ZodOptional<z.ZodString>;
47
+ status: z.ZodEnum<{
48
+ pending: "pending";
49
+ recording: "recording";
50
+ processing: "processing";
51
+ expired: "expired";
52
+ }>;
53
+ }, z.core.$strict>, z.ZodObject<{
54
+ sessionId: z.ZodString;
55
+ recordingId: z.ZodString;
56
+ mode: z.ZodEnum<{
57
+ audio: "audio";
58
+ video: "video";
59
+ audio_video: "audio_video";
60
+ }>;
61
+ createdAt: z.ZodString;
62
+ retainedUntil: z.ZodString;
63
+ participant: z.ZodOptional<z.ZodObject<{
64
+ participantIdentity: z.ZodString;
65
+ role: z.ZodEnum<{
66
+ user: "user";
67
+ avatar: "avatar";
68
+ }>;
69
+ }, z.core.$strict>>;
70
+ mediaStartedAt: z.ZodOptional<z.ZodString>;
71
+ mediaEndedAt: z.ZodOptional<z.ZodString>;
72
+ status: z.ZodLiteral<"ready">;
73
+ mediaType: z.ZodEnum<{
74
+ "audio/mp4": "audio/mp4";
75
+ "video/mp4": "video/mp4";
76
+ }>;
77
+ sizeBytes: z.ZodNumber;
78
+ durationMs: z.ZodNullable<z.ZodNumber>;
79
+ }, z.core.$strict>, z.ZodObject<{
80
+ sessionId: z.ZodString;
81
+ recordingId: z.ZodString;
82
+ mode: z.ZodEnum<{
83
+ audio: "audio";
84
+ video: "video";
85
+ audio_video: "audio_video";
86
+ }>;
87
+ createdAt: z.ZodString;
88
+ retainedUntil: z.ZodString;
89
+ participant: z.ZodOptional<z.ZodObject<{
90
+ participantIdentity: z.ZodString;
91
+ role: z.ZodEnum<{
92
+ user: "user";
93
+ avatar: "avatar";
94
+ }>;
95
+ }, z.core.$strict>>;
96
+ mediaStartedAt: z.ZodOptional<z.ZodString>;
97
+ mediaEndedAt: z.ZodOptional<z.ZodString>;
98
+ status: z.ZodLiteral<"failed">;
99
+ errorCode: z.ZodEnum<{
100
+ recording_failed: "recording_failed";
101
+ recording_unavailable: "recording_unavailable";
102
+ }>;
103
+ }, z.core.$strict>], "status">;
104
+ type RecordingArtifact = z.infer<typeof recordingArtifactSchema>;
105
+ declare const listRecordingsQuerySchema: z.ZodObject<{
106
+ sessionId: z.ZodOptional<z.ZodString>;
107
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
108
+ cursor: z.ZodOptional<z.ZodString>;
109
+ }, z.core.$strict>;
110
+ type ListRecordingsQuery = Partial<z.infer<typeof listRecordingsQuerySchema>>;
111
+ declare const listRecordingsResponseSchema: z.ZodObject<{
112
+ data: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
113
+ sessionId: z.ZodString;
114
+ recordingId: z.ZodString;
115
+ mode: z.ZodEnum<{
116
+ audio: "audio";
117
+ video: "video";
118
+ audio_video: "audio_video";
119
+ }>;
120
+ createdAt: z.ZodString;
121
+ retainedUntil: z.ZodString;
122
+ participant: z.ZodOptional<z.ZodObject<{
123
+ participantIdentity: z.ZodString;
124
+ role: z.ZodEnum<{
125
+ user: "user";
126
+ avatar: "avatar";
127
+ }>;
128
+ }, z.core.$strict>>;
129
+ mediaStartedAt: z.ZodOptional<z.ZodString>;
130
+ mediaEndedAt: z.ZodOptional<z.ZodString>;
131
+ status: z.ZodEnum<{
132
+ pending: "pending";
133
+ recording: "recording";
134
+ processing: "processing";
135
+ expired: "expired";
136
+ }>;
137
+ }, z.core.$strict>, z.ZodObject<{
138
+ sessionId: z.ZodString;
139
+ recordingId: z.ZodString;
140
+ mode: z.ZodEnum<{
141
+ audio: "audio";
142
+ video: "video";
143
+ audio_video: "audio_video";
144
+ }>;
145
+ createdAt: z.ZodString;
146
+ retainedUntil: z.ZodString;
147
+ participant: z.ZodOptional<z.ZodObject<{
148
+ participantIdentity: z.ZodString;
149
+ role: z.ZodEnum<{
150
+ user: "user";
151
+ avatar: "avatar";
152
+ }>;
153
+ }, z.core.$strict>>;
154
+ mediaStartedAt: z.ZodOptional<z.ZodString>;
155
+ mediaEndedAt: z.ZodOptional<z.ZodString>;
156
+ status: z.ZodLiteral<"ready">;
157
+ mediaType: z.ZodEnum<{
158
+ "audio/mp4": "audio/mp4";
159
+ "video/mp4": "video/mp4";
160
+ }>;
161
+ sizeBytes: z.ZodNumber;
162
+ durationMs: z.ZodNullable<z.ZodNumber>;
163
+ }, z.core.$strict>, z.ZodObject<{
164
+ sessionId: z.ZodString;
165
+ recordingId: z.ZodString;
166
+ mode: z.ZodEnum<{
167
+ audio: "audio";
168
+ video: "video";
169
+ audio_video: "audio_video";
170
+ }>;
171
+ createdAt: z.ZodString;
172
+ retainedUntil: z.ZodString;
173
+ participant: z.ZodOptional<z.ZodObject<{
174
+ participantIdentity: z.ZodString;
175
+ role: z.ZodEnum<{
176
+ user: "user";
177
+ avatar: "avatar";
178
+ }>;
179
+ }, z.core.$strict>>;
180
+ mediaStartedAt: z.ZodOptional<z.ZodString>;
181
+ mediaEndedAt: z.ZodOptional<z.ZodString>;
182
+ status: z.ZodLiteral<"failed">;
183
+ errorCode: z.ZodEnum<{
184
+ recording_failed: "recording_failed";
185
+ recording_unavailable: "recording_unavailable";
186
+ }>;
187
+ }, z.core.$strict>], "status">>;
188
+ nextCursor: z.ZodNullable<z.ZodString>;
189
+ }, z.core.$strict>;
190
+ type ListRecordingsResponse = z.infer<typeof listRecordingsResponseSchema>;
191
+ declare const recordingAccessResponseSchema: z.ZodObject<{
192
+ recordingId: z.ZodString;
193
+ url: z.ZodString;
194
+ expiresAt: z.ZodString;
195
+ }, z.core.$strict>;
196
+ type RecordingAccessResponse = z.infer<typeof recordingAccessResponseSchema>;
197
+
198
+ export { type ListRecordingsQuery as L, type RecordingArtifact as R, type ListRecordingsResponse as a, type RecordingAccessResponse as b, type RecordingArtifactStatus as c, type RecordingMode as d, type RecordingParticipant as e, listRecordingsResponseSchema as f, recordingArtifactSchema as g, recordingArtifactStatusSchema as h, recordingModeSchema as i, recordingParticipantSchema as j, listRecordingsQuerySchema as l, recordingAccessResponseSchema as r };