realtime-avatar 0.7.3 → 0.8.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 CHANGED
@@ -140,7 +140,7 @@ Browser — these never can:
140
140
  | --- | --- |
141
141
  | `realtime-avatar/react` | `AvatarCall`, `useAvatarCall`, `useRealtimeSession`, `useSessionLifecycle` |
142
142
  | `realtime-avatar/react-native` | The same surface for Expo / React Native |
143
- | `realtime-avatar/browser` | `enableMicrophone`, `attachRemoteAudio` — no React |
143
+ | `realtime-avatar/browser` | `enableMicrophone`, `attachRemoteAudio`, `prepareAvatarRoom` — no React |
144
144
  | `realtime-avatar/tools` | `attachAvatarTools` — the browser tool plane |
145
145
 
146
146
  Every adapter takes the same two hooks: `authorize` gates the request, `session` decides the
@@ -169,6 +169,16 @@ start" is one sentence covering six causes with different fixes — and one of t
169
169
  system denial, cannot be fixed from the address bar and needs the browser restarted.
170
170
  `attachRemoteAudio` attaches into the DOM *before* `connect`, which is what stops a track
171
171
  arriving mid-connect from being lost on a fast connection.
172
+ `prepareAvatarRoom(room)` — one call after `new Room()` — gives every track the room subscribes
173
+ the same 0.5s receiver cushion the React surface applies on its own: the avatar crosses the public
174
+ internet, a shallow buffer freezes on every lost packet, and a deeper one recovers it before
175
+ playout (measured at ~5% loss: ~11fps with multi-second freezes → a steady 25fps). It covers audio
176
+ and video alike so the pair stays lip-locked, and `attachRemoteAudio` calls it for you, so a page
177
+ that follows the pattern above never has to know the knob exists. `applyPlayoutDelay(track)` is the
178
+ per-track primitive underneath. Until these lived here the cushion was only reachable through
179
+ `/react`, which is why every plain-DOM page — this repo's demos included — froze that way; and in
180
+ React it was applied only by `AvatarVideoSurface`, so `RealtimeAvatarLiveKitRoom` now applies it
181
+ room-wide to whatever you render inside it (`playoutDelaySeconds={false}` opts out).
172
182
 
173
183
  `attachAvatarTools` runs your functions in the page. Nothing is executed on the platform, and
174
184
  a tool has **2.5 seconds** to answer before the call gives up on it and tells her it failed.
package/dist/browser.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { D as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, P as PlayoutDelayTarget, a as applyAvatarPlayoutDelay, b as applyPlayoutDelay } from './playout-delay-DWOc8iCl.js';
2
+
1
3
  /**
2
4
  * Turning the microphone on, and saying what went wrong when it does not.
3
5
  *
@@ -118,6 +120,14 @@ interface AudioCapableRoom {
118
120
  off(event: string, listener: (...args: never[]) => void): unknown;
119
121
  }
120
122
  interface AttachRemoteAudioOptions {
123
+ /**
124
+ * Receiver playout cushion applied to EVERY remote track the room subscribes (video
125
+ * included, so the pair stays lip-locked) via `prepareAvatarRoom`. Default 0.5s — the
126
+ * measured freeze-free depth. `false` leaves the room untouched, for a page that owns
127
+ * its own buffering. Not undone by `detach()`: a shallower buffer mid-call only brings
128
+ * the freezes back.
129
+ */
130
+ playoutDelaySeconds?: number | false;
121
131
  /**
122
132
  * Where to park the audio elements. Defaults to `document.body`.
123
133
  *
@@ -162,6 +172,37 @@ interface RemoteAudioAttachment {
162
172
  */
163
173
  declare function attachRemoteAudio(room: AudioCapableRoom, options?: AttachRemoteAudioOptions): RemoteAudioAttachment;
164
174
 
175
+ /** The sliver of a livekit-client `Room` this needs. */
176
+ interface PreparableRoom {
177
+ on(event: string, listener: (...args: never[]) => void): unknown;
178
+ off(event: string, listener: (...args: never[]) => void): unknown;
179
+ /** Read once, so tracks subscribed BEFORE this call are covered too. Optional on purpose. */
180
+ remoteParticipants?: Iterable<[unknown, PreparableParticipant]> | {
181
+ values(): Iterable<PreparableParticipant>;
182
+ };
183
+ }
184
+ /** The sliver of a livekit-client `RemoteParticipant` this needs. */
185
+ interface PreparableParticipant {
186
+ trackPublications?: Iterable<[unknown, {
187
+ track?: unknown;
188
+ }]> | {
189
+ values(): Iterable<{
190
+ track?: unknown;
191
+ }>;
192
+ };
193
+ }
194
+ interface PrepareAvatarRoomOptions {
195
+ /** Receiver cushion in seconds for every remote track. Default 0.5 — see `./playout-delay.ts`. */
196
+ playoutDelaySeconds?: number;
197
+ }
198
+ interface PreparedAvatarRoom {
199
+ /** Stop applying the cushion to tracks subscribed from now on. Idempotent. */
200
+ detach(): void;
201
+ /** The delay currently applied to new tracks. */
202
+ readonly playoutDelaySeconds: number;
203
+ }
204
+ declare function prepareAvatarRoom(room: PreparableRoom, options?: PrepareAvatarRoomOptions): PreparedAvatarRoom;
205
+
165
206
  /**
166
207
  * Ping-pong (boomerang) playback for idle/ambient clips.
167
208
  *
@@ -200,4 +241,4 @@ type BoomerangPlayback = {
200
241
  */
201
242
  declare function attachBoomerangPlayback(video: HTMLVideoElement, options?: BoomerangPlaybackOptions): BoomerangPlayback;
202
243
 
203
- export { type AttachRemoteAudioOptions, type AttachableTrack, type AudioCapableRoom, type BoomerangPlayback, type BoomerangPlaybackOptions, type EnableMicrophoneOptions, type MicrophoneCapableRoom, type MicrophoneFailureReason, type MicrophoneResult, type RemoteAudioAttachment, attachBoomerangPlayback, attachRemoteAudio, enableMicrophone };
244
+ export { type AttachRemoteAudioOptions, type AttachableTrack, type AudioCapableRoom, type BoomerangPlayback, type BoomerangPlaybackOptions, type EnableMicrophoneOptions, type MicrophoneCapableRoom, type MicrophoneFailureReason, type MicrophoneResult, type PreparableParticipant, type PreparableRoom, type PrepareAvatarRoomOptions, type PreparedAvatarRoom, type RemoteAudioAttachment, attachBoomerangPlayback, attachRemoteAudio, enableMicrophone, prepareAvatarRoom };
package/dist/browser.js CHANGED
@@ -75,14 +75,86 @@ async function enableMicrophone(room, options = {}) {
75
75
  }
76
76
  }
77
77
 
78
+ // ../browser/src/playout-delay.ts
79
+ var DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS = 0.5;
80
+ function applyPlayoutDelay(track, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
81
+ const target = playoutDelayTarget(track);
82
+ if (!target) return false;
83
+ target.setPlayoutDelay(Math.max(0, delaySeconds));
84
+ return true;
85
+ }
86
+ function applyAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
87
+ const delay = Math.max(0, delaySeconds);
88
+ applyPlayoutDelay(videoTrack, delay);
89
+ applyPlayoutDelay(audioTrack, delay);
90
+ }
91
+ function playoutDelayTarget(track) {
92
+ const candidate = track;
93
+ return typeof candidate?.setPlayoutDelay === "function" ? candidate : void 0;
94
+ }
95
+
96
+ // ../browser/src/prepare-room.ts
97
+ var TRACK_SUBSCRIBED = "trackSubscribed";
98
+ var prepared = /* @__PURE__ */ new WeakMap();
99
+ function prepareAvatarRoom(room, options = {}) {
100
+ const requested = options.playoutDelaySeconds;
101
+ const existing = prepared.get(room);
102
+ if (existing) {
103
+ if (requested !== void 0 && requested !== existing.delay) {
104
+ existing.delay = requested;
105
+ applyToExisting(room, requested);
106
+ }
107
+ return existing.handle;
108
+ }
109
+ const state = {
110
+ delay: requested ?? DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS,
111
+ listener: ((track) => {
112
+ const current = prepared.get(room);
113
+ applyPlayoutDelay(track, current ? current.delay : DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS);
114
+ }),
115
+ handle: {
116
+ get playoutDelaySeconds() {
117
+ return prepared.get(room)?.delay ?? state.delay;
118
+ },
119
+ detach() {
120
+ const live = prepared.get(room);
121
+ if (!live) return;
122
+ room.off(TRACK_SUBSCRIBED, live.listener);
123
+ prepared.delete(room);
124
+ }
125
+ }
126
+ };
127
+ prepared.set(room, state);
128
+ room.on(TRACK_SUBSCRIBED, state.listener);
129
+ applyToExisting(room, state.delay);
130
+ return state.handle;
131
+ }
132
+ function applyToExisting(room, delay) {
133
+ for (const participant of values(room.remoteParticipants)) {
134
+ for (const publication of values(participant?.trackPublications)) {
135
+ if (publication?.track) applyPlayoutDelay(publication.track, delay);
136
+ }
137
+ }
138
+ }
139
+ function values(source) {
140
+ if (!source) return [];
141
+ if (Symbol.iterator in source) {
142
+ return Array.from(source, ([, value]) => value);
143
+ }
144
+ return source.values();
145
+ }
146
+
78
147
  // ../browser/src/remote-audio.ts
79
148
  var AUDIO_PLAYBACK_CHANGED = "audioPlaybackChanged";
80
- var TRACK_SUBSCRIBED = "trackSubscribed";
149
+ var TRACK_SUBSCRIBED2 = "trackSubscribed";
81
150
  var AUDIO_KIND = "audio";
82
151
  function attachRemoteAudio(room, options = {}) {
83
152
  const container = options.container ?? document.body;
84
153
  const elements = /* @__PURE__ */ new Set();
85
154
  let detached = false;
155
+ if (options.playoutDelaySeconds !== false) {
156
+ prepareAvatarRoom(room, { playoutDelaySeconds: options.playoutDelaySeconds });
157
+ }
86
158
  const publishPlaybackState = () => {
87
159
  if (detached) return;
88
160
  options.onPlaybackBlocked?.(room.canPlaybackAudio ? null : () => room.startAudio());
@@ -95,14 +167,14 @@ function attachRemoteAudio(room, options = {}) {
95
167
  elements.add(element);
96
168
  publishPlaybackState();
97
169
  };
98
- room.on(TRACK_SUBSCRIBED, onTrackSubscribed);
170
+ room.on(TRACK_SUBSCRIBED2, onTrackSubscribed);
99
171
  room.on(AUDIO_PLAYBACK_CHANGED, publishPlaybackState);
100
172
  publishPlaybackState();
101
173
  return {
102
174
  detach() {
103
175
  if (detached) return;
104
176
  detached = true;
105
- room.off(TRACK_SUBSCRIBED, onTrackSubscribed);
177
+ room.off(TRACK_SUBSCRIBED2, onTrackSubscribed);
106
178
  room.off(AUDIO_PLAYBACK_CHANGED, publishPlaybackState);
107
179
  for (const element of elements) element.remove();
108
180
  elements.clear();
@@ -169,4 +241,4 @@ function now() {
169
241
  return typeof performance !== "undefined" ? performance.now() : Date.now();
170
242
  }
171
243
 
172
- export { attachBoomerangPlayback, attachRemoteAudio, enableMicrophone };
244
+ export { DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, applyAvatarPlayoutDelay, applyPlayoutDelay, attachBoomerangPlayback, attachRemoteAudio, enableMicrophone, prepareAvatarRoom };
@@ -43,7 +43,7 @@ function isTransient(cause) {
43
43
 
44
44
  // ../http-client/src/client.ts
45
45
  var DEFAULT_BASE_URL = "https://realtimeavatar.ai/api/v1";
46
- var SDK_VERSION = "0.7.3";
46
+ var SDK_VERSION = "0.8.0";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -1,4 +1,4 @@
1
- import { RealtimeAvatar, isQueued, RealtimeAvatarHttpError } from './chunk-2V333KRZ.js';
1
+ import { RealtimeAvatar, isQueued, RealtimeAvatarHttpError } from './chunk-JNRRH2G7.js';
2
2
 
3
3
  // ../proxy/src/config.ts
4
4
  var json = (body, status = 200) => new Response(JSON.stringify(body), {
package/dist/express.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-NKCoqM8E.js';
2
- import './types-ZfY6oxQY.js';
1
+ import { P as ProxyConfig } from './types-QoMHpR1h.js';
2
+ import './types-D_aIKv4P.js';
3
3
 
4
4
  type Expressish = {
5
5
  method: string;
package/dist/express.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-OMOEUVO5.js';
2
- import './chunk-2V333KRZ.js';
1
+ import { createProxyHandler } from './chunk-TOXLWFQY.js';
2
+ import './chunk-JNRRH2G7.js';
3
3
 
4
4
  // ../proxy/src/express.ts
5
5
  function realtimeAvatarExpress(config) {
package/dist/hono.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-NKCoqM8E.js';
2
- import './types-ZfY6oxQY.js';
1
+ import { P as ProxyConfig } from './types-QoMHpR1h.js';
2
+ import './types-D_aIKv4P.js';
3
3
 
4
4
  /**
5
5
  * Hono (and anything else built on Fetch handlers — Workers, Bun, Deno).
package/dist/hono.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-OMOEUVO5.js';
2
- import './chunk-2V333KRZ.js';
1
+ import { createProxyHandler } from './chunk-TOXLWFQY.js';
2
+ import './chunk-JNRRH2G7.js';
3
3
 
4
4
  // ../proxy/src/hono.ts
5
5
  function realtimeAvatarHono(config) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as CallPolicy, C as CallMode, S as StartCallResult, E as EndCallOptions, A as Avatar, b as AvatarUpdate, c as AvatarSourceSwap, d as ClipDeclaration, e as ClipLibraryUpdate, L as LoopRedirect, f as ClipLibrary, g as ClipSyncResult, h as AssetKind, i as Asset, j as ListSessionsOptions, U as UsageSessionPage, k as UsageSession, l as CreditBalance, T as TranscriptPayload } from './types-ZfY6oxQY.js';
2
- export { m as AvatarClip, n as CallConnection, o as CallQueued, p as ClipLibraryPlan, q as ClipSource, r as ContextMessage, s as EndCallReason, V as VideoPolicy, t as VideoState, u as isQueued } from './types-ZfY6oxQY.js';
1
+ import { a as CallPolicy, C as CallMode, S as StartCallResult, E as EndCallOptions, A as Avatar, b as AvatarUpdate, c as AvatarSourceSwap, d as ClipDeclaration, e as ClipLibraryUpdate, L as LoopRedirect, f as ClipLibrary, g as ClipSyncResult, h as AssetKind, i as Asset, j as ListSessionsOptions, U as UsageSessionPage, k as UsageSession, l as CreditBalance, T as TranscriptPayload } from './types-D_aIKv4P.js';
2
+ export { m as AvatarClip, n as CallConnection, o as CallQueued, p as ClipLibraryPlan, q as ClipSource, r as ContextMessage, s as EndCallReason, V as VideoPolicy, t as VideoState, u as isQueued } from './types-D_aIKv4P.js';
3
3
 
4
4
  interface RealtimeAvatarOptions {
5
5
  /** `tic_live_…` or `tic_test_…`. Server-side only — never ship this to a browser. */
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, isQueued, verifyTranscript } from './chunk-2V333KRZ.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, isQueued, verifyTranscript } from './chunk-JNRRH2G7.js';
package/dist/nextjs.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-NKCoqM8E.js';
2
- import './types-ZfY6oxQY.js';
1
+ import { P as ProxyConfig } from './types-QoMHpR1h.js';
2
+ import './types-D_aIKv4P.js';
3
3
 
4
4
  /**
5
5
  * App Router. Mount at `app/api/realtime-avatar/[...path]/route.ts`:
package/dist/nextjs.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-OMOEUVO5.js';
2
- import './chunk-2V333KRZ.js';
1
+ import { createProxyHandler } from './chunk-TOXLWFQY.js';
2
+ import './chunk-JNRRH2G7.js';
3
3
 
4
4
  // ../proxy/src/nextjs.ts
5
5
  function createRealtimeAvatarRoute(config) {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Receiver-side playout delay (SECONDS) for the avatar media tracks.
3
+ *
4
+ * The avatar is published from a GPU datacenter and reaches viewers over the
5
+ * public internet, where a few percent of packet loss is normal. A shallow
6
+ * receiver buffer stalls (freezes) on every loss while it waits for a
7
+ * retransmit; a deeper buffer recovers the lost packets BEFORE playout, so the
8
+ * viewer sees smooth 25fps instead of freezing. Measured at ~5% loss this took
9
+ * the stream from ~11fps with multi-second freezes to a steady 25fps with zero
10
+ * freezes.
11
+ *
12
+ * 0.5s is applied via the native `RemoteTrack.setPlayoutDelay`, which sets the
13
+ * receiver's `playoutDelayHint` — the same Chromium jitter-buffer knob the old
14
+ * hand-rolled `jitterBufferTarget` reached into (the spec renamed
15
+ * playoutDelayHint→jitterBufferTarget; both influence the same buffer depth),
16
+ * but typed and SDK-owned so there is no cast or feature-probe. The hint is NOT
17
+ * free at the open: the receiver holds the FIRST video frame toward the target
18
+ * (~250-450ms of measured TTFF), a deliberate cost paid once for the freeze-free
19
+ * steady state above. The open-small-then-ramp variant that tried to dodge that
20
+ * cost was shipped, failed twice (the rVFC ramp trigger is throttled on the
21
+ * opacity:0-mounted element, stranding the shallow buffer), and was deliberately
22
+ * removed in 2262e4c7b — do not re-propose it. No-op on browsers that don't
23
+ * support the hint (the SDK warns and moves on).
24
+ *
25
+ * This lives in the vanilla browser package, not the React one, because the
26
+ * React surface applies it for you and a plain-DOM page has to be given it.
27
+ * Until this file existed the helper was only reachable through
28
+ * `realtime-avatar/react`, so every plain-DOM adopter — every one of this repo's
29
+ * own demos included — ran with the browser's default shallow buffer and froze on
30
+ * every lost packet. Nobody should have to know this knob exists: `prepareAvatarRoom`
31
+ * (and `attachRemoteAudio`, which calls it) applies it to every track the room
32
+ * subscribes, and `RealtimeAvatarLiveKitRoom` does the same for React. This file is
33
+ * the one place the number and its reasoning live.
34
+ * Structural like its siblings: no `livekit-client` import, no version pin.
35
+ */
36
+ declare const DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS = 0.5;
37
+ /**
38
+ * A track that can take a native playout-delay hint — livekit-client's subscribed
39
+ * `RemoteTrack`. Duck-typed on the method rather than `instanceof`, so a
40
+ * placeholder or local track is skipped and the helper is testable with a fake.
41
+ */
42
+ interface PlayoutDelayTarget {
43
+ setPlayoutDelay(delayInSeconds: number): void;
44
+ }
45
+ /**
46
+ * Apply the receiver playout delay to ONE subscribed track.
47
+ *
48
+ * Call it for every track `RoomEvent.TrackSubscribed` hands you, audio and video
49
+ * alike, before or after `attach()` — WebRTC syncs the pair to the LARGER hint,
50
+ * so applying it to one stream and not the other lets the lips lag the voice.
51
+ * Returns `true` when the hint was applied and `false` when the object cannot take
52
+ * one (a local track, a placeholder, a not-yet-subscribed publication), so a
53
+ * caller can log the miss without a try/catch. The delay is floored at 0 so a
54
+ * negative input can never throw.
55
+ */
56
+ declare function applyPlayoutDelay(track: unknown, delaySeconds?: number): boolean;
57
+ /**
58
+ * Apply the SAME native playout delay to the avatar's audio + video tracks
59
+ * (kept equal so the a/v sync stays lip-locked). Pure and exported so the clamp
60
+ * and the both-tracks-equal invariant are unit-testable without a DOM; the React
61
+ * hook `useAvatarPlayoutDelay` is a thin effect wrapper around this.
62
+ */
63
+ declare function applyAvatarPlayoutDelay(videoTrack: unknown, audioTrack: unknown, delaySeconds?: number): void;
64
+
65
+ export { DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS as D, type PlayoutDelayTarget as P, applyAvatarPlayoutDelay as a, applyPlayoutDelay as b };
@@ -740,31 +740,7 @@ declare function useCallTranscript(): CallTranscript;
740
740
  * (needs `useLocalParticipant`), e.g. under {@link RealtimeAvatarLiveKitRoom}.
741
741
  */
742
742
  declare function useReleaseMicLeaseOnTrackEnded(token: symbol): void;
743
- /**
744
- * Receiver-side playout delay (SECONDS) for the avatar media tracks.
745
- *
746
- * The avatar is published from a GPU datacenter and reaches viewers over the
747
- * public internet, where a few percent of packet loss is normal. A shallow
748
- * receiver buffer stalls (freezes) on every loss while it waits for a
749
- * retransmit; a deeper buffer recovers the lost packets BEFORE playout, so the
750
- * viewer sees smooth 25fps instead of freezing. Measured at ~5% loss this took
751
- * the stream from ~11fps with multi-second freezes to a steady 25fps with zero
752
- * freezes.
753
- *
754
- * 0.5s is applied via the native {@link RemoteTrack.setPlayoutDelay}, which sets
755
- * the receiver's `playoutDelayHint` — the same Chromium jitter-buffer knob the
756
- * old hand-rolled `jitterBufferTarget` reached into (the spec renamed
757
- * playoutDelayHint→jitterBufferTarget; both influence the same buffer depth),
758
- * but typed and SDK-owned so there is no cast or feature-probe. The hint is NOT
759
- * free at the open: the receiver holds the FIRST video frame toward the target
760
- * (~250-450ms of measured TTFF), a deliberate cost paid once for the freeze-free
761
- * steady state above. The open-small-then-ramp variant that tried to dodge that
762
- * cost was shipped, failed twice (the rVFC ramp trigger is throttled on the
763
- * opacity:0-mounted element, stranding the shallow buffer), and was deliberately
764
- * removed in 2262e4c7b — do not re-propose it. No-op on browsers that don't
765
- * support the hint (the SDK warns and moves on).
766
- */
767
- declare const DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS = 0.5;
743
+
768
744
  /**
769
745
  * Sets the avatar AUDIO and VIDEO tracks' native playout delay so the stream has
770
746
  * a real de-jitter / loss-recovery cushion for smooth, freeze-free playback.
@@ -861,6 +837,14 @@ type RealtimeAvatarLiveKitRoomProps = Omit<LiveKitRoomProps, "serverUrl" | "toke
861
837
  /** Passed directly to LiveKitRoom. */
862
838
  options?: LiveKitRoomProps["options"];
863
839
  renderRoomAudio?: boolean;
840
+ /**
841
+ * Receiver playout cushion applied to EVERY remote track this room subscribes — so
842
+ * whatever you render inside (a bare `<VideoTrack>`, your own `<video>`, or
843
+ * {@link AvatarVideoSurface}) gets the freeze-free buffer, not only the SDK's own
844
+ * surface. Default {@link DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS}. `false` opts out
845
+ * for a consumer that owns its own buffering.
846
+ */
847
+ playoutDelaySeconds?: number | false;
864
848
  children?: ReactNode;
865
849
  };
866
850
  /** Thin typed bridge from a Realtime Avatar grant into LiveKitRoom. */
@@ -1633,4 +1617,4 @@ interface ProxyClientOptions {
1633
1617
  }
1634
1618
  declare function createProxyClient(options: ProxyClientOptions): AvatarSessionClient;
1635
1619
 
1636
- export { type QualityCap as $, type AvatarVideoFit as A, type BehaviorSnapshot as B, type CallTranscript as C, DEFAULT_APPROACHING_END_LEAD_SECONDS as D, type EndReason as E, type FishTtsModel as F, type Governor as G, type GovernorState as H, type GraceWindowClosedEvent as I, type GraceWindowOpenEvent as J, type GraceWindowState as K, type LiveKitSessionGrant as L, type IdleWarningEvent as M, type InboundRtpCursor as N, type InboundRtpReading as O, type KnownBehaviorState as P, type LLMProvider as Q, type LLMSelection as R, type LiveKitAvatarGrantState as S, type LiveKitAvatarGrantStatus as T, type LiveKitCapacityState as U, type LiveKitConnectionStatus as V, type LiveKitSessionRequest as W, type LiveKitSessionStartResult as X, MAX_RECONNECT_ATTEMPTS as Y, MAX_SESSION_INSTRUCTIONS_CHARS as Z, type ProxyClientOptions as _, AdaptivePlayoutController as a, RECONNECT_BACKOFF_MS as a0, type RealtimeAvatarRequestOptions as a1, type RealtimeSessionApi as a2, type RealtimeSessionMedia as a3, type RealtimeSessionRoomSinks as a4, type ReconnectPolicy as a5, type ReconnectingEvent as a6, type RecoveryState as a7, type RetryStep as a8, type SessionBehavior as a9, splitCallTranscript as aA, useAvatarAdaptivePlayoutDelay as aB, useAvatarPlayoutDelay as aC, useAvatarQualityGovernor as aD, useCallTranscript as aE, useLiveKitAvatarGrant as aF, useLiveTrackProducing as aG, useMicLease as aH, useRealtimeSession as aI, useReleaseMicLeaseOnTrackEnded as aJ, useSessionLifecycle as aK, AvatarVideoSurface as aL, type AvatarVideoSurfaceProps as aM, type LivePlaybackKeeper as aN, MIC_LEASE_ENDED_TIMEOUT_MS as aO, type MicLease as aP, type PlayableVideoElement as aQ, RealtimeAvatarCapacityError as aR, RealtimeAvatarLiveKitRoom as aS, type RealtimeAvatarLiveKitRoomProps as aT, shouldReplayPendingTurn as aU, type SessionClip as aa, type SessionClocks as ab, type SessionEndReason as ac, type SessionLifecycleApi as ad, type SessionLifecyclePhase as ae, type SessionLifecyclePhaseKind as af, SessionLifecycleRoomBridge as ag, type SessionLifecycleRoomBridgeProps as ah, type SurfaceLayers as ai, type TurnState as aj, type TurnTimeoutEvent as ak, type UseAvatarQualityGovernorInput as al, type UseLiveKitAvatarGrantInput as am, type UseRealtimeSessionInput as an, type UseSessionLifecycleInput as ao, type VoiceSpec as ap, type VoiceSpecInput as aq, capacityErrorFromBusy as ar, capacityStateFromGrant as as, createProxyClient as at, isNativeLiveTrackSubscribed as au, knownBehaviorStates as av, mapTurnState as aw, readInboundRtp as ax, sessionBehaviorSchema as ay, sessionClipSchema as az, type AdaptivePlayoutDecision as b, type AdaptivePlayoutOptions as c, type AdaptivePlayoutSample as d, type ApproachingEndEvent as e, type ApproachingEndReason as f, type AvatarSessionClient as g, type CallTranscriptSegment as h, type CartesiaTtsModel as i, type ClipResult as j, type ClosingTurnResult as k, type CreditsLowEvent as l, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS as m, DEFAULT_CREDITS_LOW_LEAD_SECONDS as n, DEFAULT_GOVERNOR_CONFIG as o, DEFAULT_GRACE_CEILING_SECONDS as p, DEFAULT_GRACE_WINDOW_LEAD_SECONDS as q, DEFAULT_IDLE_SECONDS as r, DEFAULT_IDLE_WARN_LEAD_SECONDS as s, DEFAULT_TURN_TIMEOUT_SECONDS as t, type EndedEvent as u, type ExtendResult as v, type FreezeReadingFn as w, type GovernorAction as x, type GovernorConfig as y, type GovernorSignal as z };
1620
+ export { RECONNECT_BACKOFF_MS as $, type AvatarVideoFit as A, type BehaviorSnapshot as B, type CallTranscript as C, DEFAULT_APPROACHING_END_LEAD_SECONDS as D, type EndReason as E, type FishTtsModel as F, type Governor as G, type GraceWindowClosedEvent as H, type GraceWindowOpenEvent as I, type GraceWindowState as J, type IdleWarningEvent as K, type LiveKitSessionGrant as L, type InboundRtpCursor as M, type InboundRtpReading as N, type KnownBehaviorState as O, type LLMProvider as P, type LLMSelection as Q, type LiveKitAvatarGrantState as R, type LiveKitAvatarGrantStatus as S, type LiveKitCapacityState as T, type LiveKitConnectionStatus as U, type LiveKitSessionRequest as V, type LiveKitSessionStartResult as W, MAX_RECONNECT_ATTEMPTS as X, MAX_SESSION_INSTRUCTIONS_CHARS as Y, type ProxyClientOptions as Z, type QualityCap as _, AdaptivePlayoutController as a, type RealtimeAvatarRequestOptions as a0, type RealtimeSessionApi as a1, type RealtimeSessionMedia as a2, type RealtimeSessionRoomSinks as a3, type ReconnectPolicy as a4, type ReconnectingEvent as a5, type RecoveryState as a6, type RetryStep as a7, type SessionBehavior as a8, type SessionClip as a9, useAvatarAdaptivePlayoutDelay as aA, useAvatarPlayoutDelay as aB, useAvatarQualityGovernor as aC, useCallTranscript as aD, useLiveKitAvatarGrant as aE, useLiveTrackProducing as aF, useMicLease as aG, useRealtimeSession as aH, useReleaseMicLeaseOnTrackEnded as aI, useSessionLifecycle as aJ, AvatarVideoSurface as aK, type AvatarVideoSurfaceProps as aL, type LivePlaybackKeeper as aM, MIC_LEASE_ENDED_TIMEOUT_MS as aN, type MicLease as aO, type PlayableVideoElement as aP, RealtimeAvatarCapacityError as aQ, RealtimeAvatarLiveKitRoom as aR, type RealtimeAvatarLiveKitRoomProps as aS, shouldReplayPendingTurn as aT, type SessionClocks as aa, type SessionEndReason as ab, type SessionLifecycleApi as ac, type SessionLifecyclePhase as ad, type SessionLifecyclePhaseKind as ae, SessionLifecycleRoomBridge as af, type SessionLifecycleRoomBridgeProps as ag, type SurfaceLayers as ah, type TurnState as ai, type TurnTimeoutEvent as aj, type UseAvatarQualityGovernorInput as ak, type UseLiveKitAvatarGrantInput as al, type UseRealtimeSessionInput as am, type UseSessionLifecycleInput as an, type VoiceSpec as ao, type VoiceSpecInput as ap, capacityErrorFromBusy as aq, capacityStateFromGrant as ar, createProxyClient as as, isNativeLiveTrackSubscribed as at, knownBehaviorStates as au, mapTurnState as av, readInboundRtp as aw, sessionBehaviorSchema as ax, sessionClipSchema as ay, splitCallTranscript as az, type AdaptivePlayoutDecision as b, type AdaptivePlayoutOptions as c, type AdaptivePlayoutSample as d, type ApproachingEndEvent as e, type ApproachingEndReason as f, type AvatarSessionClient as g, type CallTranscriptSegment as h, type CartesiaTtsModel as i, type ClipResult as j, type ClosingTurnResult as k, type CreditsLowEvent as l, DEFAULT_CREDITS_LOW_LEAD_SECONDS as m, DEFAULT_GOVERNOR_CONFIG as n, DEFAULT_GRACE_CEILING_SECONDS as o, DEFAULT_GRACE_WINDOW_LEAD_SECONDS as p, DEFAULT_IDLE_SECONDS as q, DEFAULT_IDLE_WARN_LEAD_SECONDS as r, DEFAULT_TURN_TIMEOUT_SECONDS as s, type EndedEvent as t, type ExtendResult as u, type FreezeReadingFn as v, type GovernorAction as w, type GovernorConfig as x, type GovernorSignal as y, type GovernorState as z };
@@ -1,10 +1,11 @@
1
1
  import { LiveKitRoomProps } from '@livekit/react-native';
2
2
  export { AudioSession, VideoTrack, VideoTrackProps, registerGlobals } from '@livekit/react-native';
3
3
  import { ReactNode, ReactElement } from 'react';
4
- import { L as LiveKitSessionGrant, A as AvatarVideoFit } from './proxy-client-BzblQMvg.js';
5
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarSessionClient, B as BehaviorSnapshot, C as CallTranscript, h as CallTranscriptSegment, i as CartesiaTtsModel, j as ClipResult, k as ClosingTurnResult, l as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, m as DEFAULT_AVATAR_PLAYOUT_DELAY_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, Y as MAX_RECONNECT_ATTEMPTS, Z as MAX_SESSION_INSTRUCTIONS_CHARS, _ as ProxyClientOptions, $ as QualityCap, a0 as RECONNECT_BACKOFF_MS, 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, ah as SessionLifecycleRoomBridgeProps, 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, au as isNativeLiveTrackSubscribed, av as knownBehaviorStates, aw as mapTurnState, ax as readInboundRtp, ay as sessionBehaviorSchema, az as sessionClipSchema, aA as splitCallTranscript, aB as useAvatarAdaptivePlayoutDelay, aC as useAvatarPlayoutDelay, aD as useAvatarQualityGovernor, aE as useCallTranscript, aF as useLiveKitAvatarGrant, aG as useLiveTrackProducing, aH as useMicLease, aI as useRealtimeSession, aJ as useReleaseMicLeaseOnTrackEnded, aK as useSessionLifecycle } from './proxy-client-BzblQMvg.js';
4
+ import { L as LiveKitSessionGrant, A as AvatarVideoFit } from './proxy-client-DDqYRz-_.js';
5
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, g as AvatarSessionClient, B as BehaviorSnapshot, C as CallTranscript, h as CallTranscriptSegment, i as CartesiaTtsModel, j as ClipResult, k as ClosingTurnResult, l as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, m as DEFAULT_CREDITS_LOW_LEAD_SECONDS, n as DEFAULT_GOVERNOR_CONFIG, o as DEFAULT_GRACE_CEILING_SECONDS, p as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, q as DEFAULT_IDLE_SECONDS, r as DEFAULT_IDLE_WARN_LEAD_SECONDS, s as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, t as EndedEvent, u as ExtendResult, F as FishTtsModel, v as FreezeReadingFn, G as Governor, w as GovernorAction, x as GovernorConfig, y as GovernorSignal, z as GovernorState, H as GraceWindowClosedEvent, I as GraceWindowOpenEvent, J as GraceWindowState, K as IdleWarningEvent, M as InboundRtpCursor, N as InboundRtpReading, O as KnownBehaviorState, P as LLMProvider, Q as LLMSelection, R as LiveKitAvatarGrantState, S as LiveKitAvatarGrantStatus, T as LiveKitCapacityState, U as LiveKitConnectionStatus, V as LiveKitSessionRequest, W as LiveKitSessionStartResult, X as MAX_RECONNECT_ATTEMPTS, Y as MAX_SESSION_INSTRUCTIONS_CHARS, Z as ProxyClientOptions, _ as QualityCap, $ as RECONNECT_BACKOFF_MS, a0 as RealtimeAvatarRequestOptions, a1 as RealtimeSessionApi, a2 as RealtimeSessionMedia, a3 as RealtimeSessionRoomSinks, a4 as ReconnectPolicy, a5 as ReconnectingEvent, a6 as RecoveryState, a7 as RetryStep, a8 as SessionBehavior, a9 as SessionClip, aa as SessionClocks, ab as SessionEndReason, ac as SessionLifecycleApi, ad as SessionLifecyclePhase, ae as SessionLifecyclePhaseKind, af as SessionLifecycleRoomBridge, ag as SessionLifecycleRoomBridgeProps, ah as SurfaceLayers, ai as TurnState, aj as TurnTimeoutEvent, ak as UseAvatarQualityGovernorInput, al as UseLiveKitAvatarGrantInput, am as UseRealtimeSessionInput, an as UseSessionLifecycleInput, ao as VoiceSpec, ap as VoiceSpecInput, aq as capacityErrorFromBusy, ar as capacityStateFromGrant, as as createProxyClient, at as isNativeLiveTrackSubscribed, au as knownBehaviorStates, av as mapTurnState, aw as readInboundRtp, ax as sessionBehaviorSchema, ay as sessionClipSchema, az as splitCallTranscript, aA as useAvatarAdaptivePlayoutDelay, aB as useAvatarPlayoutDelay, aC as useAvatarQualityGovernor, aD as useCallTranscript, aE as useLiveKitAvatarGrant, aF as useLiveTrackProducing, aG as useMicLease, aH as useRealtimeSession, aI as useReleaseMicLeaseOnTrackEnded, aJ as useSessionLifecycle } from './proxy-client-DDqYRz-_.js';
6
6
  import { StyleProp, ViewStyle } from 'react-native';
7
7
  export { useChat, useConnectionState, useLocalParticipant, useRoomContext, useTrackToggle, useTranscriptions, useVoiceAssistant } from '@livekit/components-react';
8
+ export { D as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS } from './playout-delay-DWOc8iCl.js';
8
9
  export { DisconnectReason, RemoteAudioTrack, RemoteTrack, RemoteVideoTrack, Room, RoomEvent, SendTextOptions, Track } from 'livekit-client';
9
10
  import 'zod';
10
11
 
@@ -130,6 +130,24 @@ function timeoutSetter(fn, ms) {
130
130
  return () => clearTimeout(id);
131
131
  }
132
132
 
133
+ // ../browser/src/playout-delay.ts
134
+ var DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS = 0.5;
135
+ function applyPlayoutDelay(track, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
136
+ const target = playoutDelayTarget(track);
137
+ if (!target) return false;
138
+ target.setPlayoutDelay(Math.max(0, delaySeconds));
139
+ return true;
140
+ }
141
+ function applyAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
142
+ const delay = Math.max(0, delaySeconds);
143
+ applyPlayoutDelay(videoTrack, delay);
144
+ applyPlayoutDelay(audioTrack, delay);
145
+ }
146
+ function playoutDelayTarget(track) {
147
+ const candidate = track;
148
+ return typeof candidate?.setPlayoutDelay === "function" ? candidate : void 0;
149
+ }
150
+
133
151
  // ../client/src/errors.ts
134
152
  var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
135
153
  constructor(message, status, body, response, metadata = {}) {
@@ -327,7 +345,6 @@ function useReleaseMicLeaseOnTrackEnded(token) {
327
345
  };
328
346
  }, [localParticipant, token, localParticipant?.audioTrackPublications.size]);
329
347
  }
330
- var DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS = 0.5;
331
348
  function useAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
332
349
  const videoMediaTrack = videoTrack?.publication?.track;
333
350
  const audioMediaTrack = audioTrack?.publication?.track;
@@ -335,15 +352,6 @@ function useAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AV
335
352
  applyAvatarPlayoutDelay(videoMediaTrack, audioMediaTrack, delaySeconds);
336
353
  }, [videoMediaTrack, audioMediaTrack, delaySeconds]);
337
354
  }
338
- function applyAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
339
- const delay = Math.max(0, delaySeconds);
340
- playoutDelayTarget(videoTrack)?.setPlayoutDelay(delay);
341
- playoutDelayTarget(audioTrack)?.setPlayoutDelay(delay);
342
- }
343
- function playoutDelayTarget(track) {
344
- const candidate = track;
345
- return typeof candidate?.setPlayoutDelay === "function" ? candidate : void 0;
346
- }
347
355
  function resolveReleaseTarget(heldSessionId, queueTicketId) {
348
356
  if (heldSessionId) return { kind: "session", sessionId: heldSessionId };
349
357
  if (queueTicketId) return { kind: "ticket", queueTicketId };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
- import { g as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-BzblQMvg.js';
3
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, aL as AvatarVideoSurface, aM as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, h as CallTranscriptSegment, i as CartesiaTtsModel, j as ClipResult, k as ClosingTurnResult, l as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, m as DEFAULT_AVATAR_PLAYOUT_DELAY_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, aN as LivePlaybackKeeper, Y as MAX_RECONNECT_ATTEMPTS, Z as MAX_SESSION_INSTRUCTIONS_CHARS, aO as MIC_LEASE_ENDED_TIMEOUT_MS, aP as MicLease, aQ as PlayableVideoElement, _ as ProxyClientOptions, $ as QualityCap, a0 as RECONNECT_BACKOFF_MS, aR as RealtimeAvatarCapacityError, aS as RealtimeAvatarLiveKitRoom, aT 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, ah as SessionLifecycleRoomBridgeProps, 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, aU as shouldReplayPendingTurn, aA as splitCallTranscript, aB as useAvatarAdaptivePlayoutDelay, aC as useAvatarPlayoutDelay, aD as useAvatarQualityGovernor, aE as useCallTranscript, aF as useLiveKitAvatarGrant, aH as useMicLease, aI as useRealtimeSession, aJ as useReleaseMicLeaseOnTrackEnded, aK as useSessionLifecycle } from './proxy-client-BzblQMvg.js';
2
+ import { g as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-DDqYRz-_.js';
3
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, aK as AvatarVideoSurface, aL as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, h as CallTranscriptSegment, i as CartesiaTtsModel, j as ClipResult, k as ClosingTurnResult, l as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, m as DEFAULT_CREDITS_LOW_LEAD_SECONDS, n as DEFAULT_GOVERNOR_CONFIG, o as DEFAULT_GRACE_CEILING_SECONDS, p as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, q as DEFAULT_IDLE_SECONDS, r as DEFAULT_IDLE_WARN_LEAD_SECONDS, s as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, t as EndedEvent, u as ExtendResult, F as FishTtsModel, v as FreezeReadingFn, G as Governor, w as GovernorAction, x as GovernorConfig, y as GovernorSignal, z as GovernorState, H as GraceWindowClosedEvent, I as GraceWindowOpenEvent, J as GraceWindowState, K as IdleWarningEvent, M as InboundRtpCursor, N as InboundRtpReading, O as KnownBehaviorState, P as LLMProvider, Q as LLMSelection, R as LiveKitAvatarGrantState, S as LiveKitAvatarGrantStatus, T as LiveKitCapacityState, U as LiveKitConnectionStatus, V as LiveKitSessionRequest, W as LiveKitSessionStartResult, aM as LivePlaybackKeeper, X as MAX_RECONNECT_ATTEMPTS, Y as MAX_SESSION_INSTRUCTIONS_CHARS, aN as MIC_LEASE_ENDED_TIMEOUT_MS, aO as MicLease, aP as PlayableVideoElement, Z as ProxyClientOptions, _ as QualityCap, $ as RECONNECT_BACKOFF_MS, aQ as RealtimeAvatarCapacityError, aR as RealtimeAvatarLiveKitRoom, aS as RealtimeAvatarLiveKitRoomProps, a0 as RealtimeAvatarRequestOptions, a1 as RealtimeSessionApi, a2 as RealtimeSessionMedia, a3 as RealtimeSessionRoomSinks, a4 as ReconnectPolicy, a5 as ReconnectingEvent, a6 as RecoveryState, a7 as RetryStep, a8 as SessionBehavior, a9 as SessionClip, aa as SessionClocks, ab as SessionEndReason, ac as SessionLifecycleApi, ad as SessionLifecyclePhase, ae as SessionLifecyclePhaseKind, af as SessionLifecycleRoomBridge, ag as SessionLifecycleRoomBridgeProps, ah as SurfaceLayers, ai as TurnState, aj as TurnTimeoutEvent, ak as UseAvatarQualityGovernorInput, al as UseLiveKitAvatarGrantInput, am as UseRealtimeSessionInput, an as UseSessionLifecycleInput, ao as VoiceSpec, ap as VoiceSpecInput, aq as capacityErrorFromBusy, ar as capacityStateFromGrant, as as createProxyClient, au as knownBehaviorStates, av as mapTurnState, aw as readInboundRtp, ax as sessionBehaviorSchema, ay as sessionClipSchema, aT as shouldReplayPendingTurn, az as splitCallTranscript, aA as useAvatarAdaptivePlayoutDelay, aB as useAvatarPlayoutDelay, aC as useAvatarQualityGovernor, aD as useCallTranscript, aE as useLiveKitAvatarGrant, aG as useMicLease, aH as useRealtimeSession, aI as useReleaseMicLeaseOnTrackEnded, aJ as useSessionLifecycle } from './proxy-client-DDqYRz-_.js';
4
+ export { D as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS } from './playout-delay-DWOc8iCl.js';
4
5
  import '@livekit/components-react';
5
6
  import 'livekit-client';
6
7
  import 'zod';
package/dist/react.js CHANGED
@@ -89,6 +89,24 @@ function timeoutSetter(fn, ms) {
89
89
  return () => clearTimeout(id);
90
90
  }
91
91
 
92
+ // ../browser/src/playout-delay.ts
93
+ var DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS = 0.5;
94
+ function applyPlayoutDelay(track, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
95
+ const target = playoutDelayTarget(track);
96
+ if (!target) return false;
97
+ target.setPlayoutDelay(Math.max(0, delaySeconds));
98
+ return true;
99
+ }
100
+ function applyAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
101
+ const delay = Math.max(0, delaySeconds);
102
+ applyPlayoutDelay(videoTrack, delay);
103
+ applyPlayoutDelay(audioTrack, delay);
104
+ }
105
+ function playoutDelayTarget(track) {
106
+ const candidate = track;
107
+ return typeof candidate?.setPlayoutDelay === "function" ? candidate : void 0;
108
+ }
109
+
92
110
  // ../client/src/errors.ts
93
111
  var RealtimeAvatarApiError = class _RealtimeAvatarApiError extends Error {
94
112
  constructor(message, status, body, response, metadata = {}) {
@@ -286,7 +304,6 @@ function useReleaseMicLeaseOnTrackEnded(token) {
286
304
  };
287
305
  }, [localParticipant, token, localParticipant?.audioTrackPublications.size]);
288
306
  }
289
- var DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS = 0.5;
290
307
  function useAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
291
308
  const videoMediaTrack = videoTrack?.publication?.track;
292
309
  const audioMediaTrack = audioTrack?.publication?.track;
@@ -294,15 +311,6 @@ function useAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AV
294
311
  applyAvatarPlayoutDelay(videoMediaTrack, audioMediaTrack, delaySeconds);
295
312
  }, [videoMediaTrack, audioMediaTrack, delaySeconds]);
296
313
  }
297
- function applyAvatarPlayoutDelay(videoTrack, audioTrack, delaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS) {
298
- const delay = Math.max(0, delaySeconds);
299
- playoutDelayTarget(videoTrack)?.setPlayoutDelay(delay);
300
- playoutDelayTarget(audioTrack)?.setPlayoutDelay(delay);
301
- }
302
- function playoutDelayTarget(track) {
303
- const candidate = track;
304
- return typeof candidate?.setPlayoutDelay === "function" ? candidate : void 0;
305
- }
306
314
  function resolveReleaseTarget(heldSessionId, queueTicketId) {
307
315
  if (heldSessionId) return { kind: "session", sessionId: heldSessionId };
308
316
  if (queueTicketId) return { kind: "ticket", queueTicketId };
@@ -540,6 +548,24 @@ function sameCapacityState(a, b) {
540
548
  return true;
541
549
  }
542
550
  }
551
+ function AvatarPlayoutCushion({ seconds }) {
552
+ const room = useRoomContext();
553
+ useEffect(() => {
554
+ const onSubscribed = (track) => {
555
+ applyPlayoutDelay(track, seconds);
556
+ };
557
+ for (const participant of room.remoteParticipants.values()) {
558
+ for (const publication of participant.trackPublications.values()) {
559
+ applyPlayoutDelay(publication.track, seconds);
560
+ }
561
+ }
562
+ room.on(RoomEvent.TrackSubscribed, onSubscribed);
563
+ return () => {
564
+ room.off(RoomEvent.TrackSubscribed, onSubscribed);
565
+ };
566
+ }, [room, seconds]);
567
+ return null;
568
+ }
543
569
  function RealtimeAvatarLiveKitRoom(props) {
544
570
  const {
545
571
  grant,
@@ -548,11 +574,13 @@ function RealtimeAvatarLiveKitRoom(props) {
548
574
  video = false,
549
575
  options,
550
576
  renderRoomAudio = true,
577
+ playoutDelaySeconds = DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS,
551
578
  children,
552
579
  ...roomProps
553
580
  } = props;
554
581
  const shouldConnect = Boolean(grant && connect);
555
582
  const roomAudio = renderRoomAudio ? createElement(RoomAudioRenderer, { key: "room-audio" }) : null;
583
+ const cushion = playoutDelaySeconds === false ? null : createElement(AvatarPlayoutCushion, { key: "playout-cushion", seconds: playoutDelaySeconds });
556
584
  const roomOptions = { adaptiveStream: false, dynacast: true, ...options };
557
585
  return createElement(
558
586
  LiveKitRoom,
@@ -565,7 +593,7 @@ function RealtimeAvatarLiveKitRoom(props) {
565
593
  video,
566
594
  options: roomOptions
567
595
  },
568
- createElement(Fragment, null, children, roomAudio)
596
+ createElement(Fragment, null, cushion, children, roomAudio)
569
597
  );
570
598
  }
571
599
  var capacityErrorFromBusy = (busy) => {
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, RealtimeAvatarOptions, StartCallOptions, verifyTranscript } from './index.js';
2
- export { i as Asset, h as AssetKind, A as Avatar, m as AvatarClip, c as AvatarSourceSwap, b as AvatarUpdate, n as CallConnection, C as CallMode, a as CallPolicy, o as CallQueued, d as ClipDeclaration, f as ClipLibrary, p as ClipLibraryPlan, e as ClipLibraryUpdate, q as ClipSource, g as ClipSyncResult, r as ContextMessage, l as CreditBalance, E as EndCallOptions, s as EndCallReason, j as ListSessionsOptions, L as LoopRedirect, S as StartCallResult, T as TranscriptPayload, k as UsageSession, U as UsageSessionPage, V as VideoPolicy, t as VideoState, u as isQueued } from './types-ZfY6oxQY.js';
2
+ export { i as Asset, h as AssetKind, A as Avatar, m as AvatarClip, c as AvatarSourceSwap, b as AvatarUpdate, n as CallConnection, C as CallMode, a as CallPolicy, o as CallQueued, d as ClipDeclaration, f as ClipLibrary, p as ClipLibraryPlan, e as ClipLibraryUpdate, q as ClipSource, g as ClipSyncResult, r as ContextMessage, l as CreditBalance, E as EndCallOptions, s as EndCallReason, j as ListSessionsOptions, L as LoopRedirect, S as StartCallResult, T as TranscriptPayload, k as UsageSession, U as UsageSessionPage, V as VideoPolicy, t as VideoState, u as isQueued } from './types-D_aIKv4P.js';
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, isQueued, verifyTranscript } from './chunk-2V333KRZ.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, isQueued, verifyTranscript } from './chunk-JNRRH2G7.js';
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-NKCoqM8E.js';
2
- import './types-ZfY6oxQY.js';
1
+ import { P as ProxyConfig } from './types-QoMHpR1h.js';
2
+ import './types-D_aIKv4P.js';
3
3
 
4
4
  /**
5
5
  * TanStack Start. Mount at `routes/api/realtime-avatar/$.ts` — the trailing `$` is Start's
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-OMOEUVO5.js';
2
- import './chunk-2V333KRZ.js';
1
+ import { createProxyHandler } from './chunk-TOXLWFQY.js';
2
+ import './chunk-JNRRH2G7.js';
3
3
 
4
4
  // src/tanstack-start.ts
5
5
  function realtimeAvatarServerRoute(config) {
@@ -585,12 +585,24 @@ interface components {
585
585
  Error: {
586
586
  /** @description Human-readable message. Not a stable contract — switch on `code` or `status`. */
587
587
  error: string;
588
- /** @description Repeats the HTTP status in the body. Absent on unhandled 500s. */
589
- status?: number;
590
- /** @description Machine-readable discriminator, present where the failure has one — billing, clip-library and loop refusals all carry it. Switch on this rather than on `error`, whose wording is not a contract. The set is open and grows with the API; the codes a given endpoint can return are documented with that endpoint at https://realtimeavatar.ai/docs/api-reference. */
591
- code?: string;
588
+ /** @description Repeats the HTTP status in the body. Always present. */
589
+ status: number;
590
+ /** @description Machine-readable discriminator. ALWAYS present: a failure that names its own — billing, clip-library and loop refusals, `insufficient_scope` — carries that, and everything else carries the status's default (`unauthorized`, `not_found`, `rate_limited`, …). Switch on this rather than on `error`, whose wording is not a contract. The set is open and grows with the API; the codes a given endpoint can return are documented with that endpoint at https://realtimeavatar.ai/docs/api-reference. */
591
+ code: string;
592
+ /**
593
+ * Format: uri
594
+ * @description Where to read how to resolve a failure of this class — the authentication guide for 401/403, the API reference otherwise. Always present.
595
+ */
596
+ documentation: string;
592
597
  /** @description Set when the failure is worth retrying as-is. */
593
598
  retryable?: boolean;
599
+ /**
600
+ * @description Present on `403 insufficient_scope` — the one scope this operation needed and the key did not carry. Repeated in the `WWW-Authenticate` challenge as `scope=`.
601
+ * @enum {string}
602
+ */
603
+ requiredScope?: "*" | "api_keys:write" | "credits:read" | "avatars:read" | "avatars:write" | "realtime:write" | "usage:read" | "usage:write";
604
+ /** @description Present alongside `requiredScope` — the scopes the presented key does carry. */
605
+ grantedScopes?: ("*" | "api_keys:write" | "credits:read" | "avatars:read" | "avatars:write" | "realtime:write" | "usage:read" | "usage:write")[];
594
606
  /** @description Present on 402 — where to send the user to top up. */
595
607
  billingUrl?: string;
596
608
  } & {
@@ -1,4 +1,4 @@
1
- import { C as CallMode, a as CallPolicy } from './types-ZfY6oxQY.js';
1
+ import { C as CallMode, a as CallPolicy } from './types-D_aIKv4P.js';
2
2
 
3
3
  /** The three things a caller can ask the proxy to do. Gate on these, not on URLs. */
4
4
  type ProxyOperation = "connect" | "end" | "avatars" | "credits";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtime-avatar",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Realtime Avatar SDK. `realtime-avatar` and the route adapters hold your API key and are server-only; `realtime-avatar/react`, `/browser` and `/tools` never can — importing a server entry into a browser build throws at bundle time.",
5
5
  "license": "MIT",
6
6
  "author": "The Influence Company",