realtime-avatar 0.5.0 → 0.5.2

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.
@@ -448,6 +448,124 @@ function stableStringify(value) {
448
448
  return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
449
449
  }
450
450
 
451
+ // ../client/src/react/adaptive-playout.ts
452
+ var DEFAULTS = {
453
+ floorSeconds: 0.15,
454
+ ceilingSeconds: 0.5,
455
+ jitterGain: 4,
456
+ lossGain: 5,
457
+ growAlpha: 0.6,
458
+ shrinkAlpha: 0.08,
459
+ hysteresisSeconds: 0.05
460
+ };
461
+ var AdaptivePlayoutController = class {
462
+ opts;
463
+ smoothed;
464
+ applied;
465
+ constructor(options = {}) {
466
+ this.opts = { ...DEFAULTS, ...options };
467
+ this.smoothed = this.opts.ceilingSeconds;
468
+ this.applied = this.opts.ceilingSeconds;
469
+ }
470
+ /** The currently applied depth (what the receivers were last hinted to). */
471
+ get appliedSeconds() {
472
+ return this.applied;
473
+ }
474
+ update(sample) {
475
+ const { floorSeconds, ceilingSeconds, jitterGain, lossGain } = this.opts;
476
+ const jitter = Number.isFinite(sample.jitterSeconds) ? Math.max(0, sample.jitterSeconds) : 0;
477
+ const loss = Number.isFinite(sample.lossFraction) ? Math.min(1, Math.max(0, sample.lossFraction)) : 0;
478
+ const demand = Math.min(
479
+ ceilingSeconds,
480
+ Math.max(floorSeconds, floorSeconds + jitterGain * jitter + lossGain * loss)
481
+ );
482
+ const alpha = demand > this.smoothed ? this.opts.growAlpha : this.opts.shrinkAlpha;
483
+ this.smoothed += alpha * (demand - this.smoothed);
484
+ const hysteresis = this.opts.hysteresisSeconds;
485
+ const clamped = Math.min(ceilingSeconds, Math.max(floorSeconds, this.smoothed));
486
+ const target = clamped - floorSeconds <= hysteresis ? floorSeconds : ceilingSeconds - clamped <= hysteresis ? ceilingSeconds : clamped;
487
+ if (target === this.applied) return { targetSeconds: this.applied, changed: false };
488
+ const atBound = target === floorSeconds || target === ceilingSeconds;
489
+ if (!atBound && Math.abs(target - this.applied) <= hysteresis) {
490
+ return { targetSeconds: this.applied, changed: false };
491
+ }
492
+ this.applied = target;
493
+ return { targetSeconds: this.applied, changed: true };
494
+ }
495
+ };
496
+ function readInboundRtp(reports, previous) {
497
+ for (const report of reports) {
498
+ if (report["type"] !== "inbound-rtp") continue;
499
+ const jitterSeconds = typeof report["jitter"] === "number" ? report["jitter"] : 0;
500
+ const packetsLost = typeof report["packetsLost"] === "number" ? report["packetsLost"] : 0;
501
+ const packetsReceived = typeof report["packetsReceived"] === "number" ? report["packetsReceived"] : 0;
502
+ const dLost = Math.max(0, packetsLost - (previous?.packetsLost ?? packetsLost));
503
+ const dReceived = Math.max(0, packetsReceived - (previous?.packetsReceived ?? packetsReceived));
504
+ const denominator = dLost + dReceived;
505
+ return {
506
+ sample: {
507
+ jitterSeconds,
508
+ lossFraction: denominator > 0 ? dLost / denominator : 0
509
+ },
510
+ cursor: { packetsLost, packetsReceived }
511
+ };
512
+ }
513
+ return void 0;
514
+ }
515
+
516
+ // ../client/src/react/use-adaptive-playout.ts
517
+ function useAvatarAdaptivePlayoutDelay(videoTrack, audioTrack, enabled = false, options) {
518
+ const ceiling = options?.ceilingSeconds ?? DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS;
519
+ const [appliedSeconds, setAppliedSeconds] = useState(ceiling);
520
+ const videoMediaTrack = videoTrack?.publication?.track;
521
+ const audioMediaTrack = audioTrack?.publication?.track;
522
+ useEffect(() => {
523
+ if (!enabled || !videoMediaTrack && !audioMediaTrack) return;
524
+ const receivers = [videoMediaTrack, audioMediaTrack].map((t) => t?.receiver).filter(
525
+ (r) => typeof r?.getStats === "function"
526
+ );
527
+ if (receivers.length === 0) return;
528
+ const controller = new AdaptivePlayoutController(options);
529
+ const cursors = /* @__PURE__ */ new Map();
530
+ let closed = false;
531
+ let inFlight = false;
532
+ const tick = async () => {
533
+ if (closed || inFlight) return;
534
+ inFlight = true;
535
+ try {
536
+ let worst;
537
+ for (let i = 0; i < receivers.length; i++) {
538
+ const stats = await receivers[i].getStats();
539
+ if (closed || typeof stats?.values !== "function") continue;
540
+ const reading = readInboundRtp(stats.values(), cursors.get(i));
541
+ if (!reading) continue;
542
+ cursors.set(i, reading.cursor);
543
+ worst = {
544
+ jitterSeconds: Math.max(worst?.jitterSeconds ?? 0, reading.sample.jitterSeconds),
545
+ lossFraction: Math.max(worst?.lossFraction ?? 0, reading.sample.lossFraction)
546
+ };
547
+ }
548
+ if (closed || !worst) return;
549
+ const decision = controller.update(worst);
550
+ if (decision.changed) {
551
+ applyAvatarPlayoutDelay(videoMediaTrack, audioMediaTrack, decision.targetSeconds);
552
+ setAppliedSeconds(decision.targetSeconds);
553
+ }
554
+ } catch {
555
+ } finally {
556
+ inFlight = false;
557
+ }
558
+ };
559
+ const interval = setInterval(() => void tick(), 1e3);
560
+ return () => {
561
+ closed = true;
562
+ clearInterval(interval);
563
+ setAppliedSeconds(ceiling);
564
+ };
565
+ }, [enabled, videoMediaTrack, audioMediaTrack, options, ceiling]);
566
+ return appliedSeconds;
567
+ }
568
+
451
569
  // ../client/src/react/quality-governor.ts
452
570
  var DEFAULT_GOVERNOR_CONFIG = {
453
571
  openingCap: "low",
@@ -778,10 +896,12 @@ function AvatarVideoSurface(props) {
778
896
  style,
779
897
  children,
780
898
  showLiveBadge = true,
899
+ adaptivePlayout = false,
781
900
  testID
782
901
  } = props;
783
902
  const { videoTrack, audioTrack } = useVoiceAssistant();
784
903
  useAvatarPlayoutDelay(videoTrack, audioTrack);
904
+ useAvatarAdaptivePlayoutDelay(videoTrack, audioTrack, adaptivePlayout);
785
905
  const connectionState = useConnectionState();
786
906
  const connected = connectionState === ConnectionState.Connected;
787
907
  const trackProducing = useLiveTrackProducing(
@@ -2273,4 +2393,4 @@ function createProxyClient(options) {
2273
2393
  };
2274
2394
  }
2275
2395
 
2276
- export { AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, 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, RECONNECT_BACKOFF_MS, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, isNativeLiveTrackSubscribed, knownBehaviorStates, mapTurnState, sessionBehaviorSchema, sessionClipSchema, splitCallTranscript, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useLiveKitAvatarGrant, useLiveTrackProducing, useMicLease, useRealtimeAvatarAudioSession, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
2396
+ export { AdaptivePlayoutController, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, 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, RECONNECT_BACKOFF_MS, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, isNativeLiveTrackSubscribed, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useLiveKitAvatarGrant, useLiveTrackProducing, useMicLease, useRealtimeAvatarAudioSession, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
- import { A as AvatarSessionClient, a as AvatarVideoFit } from './proxy-client-Dm-O0jNn.js';
3
- export { b as ApproachingEndEvent, c as ApproachingEndReason, d as AvatarVideoSurface, e as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, f as CallTranscriptSegment, g as CartesiaTtsModel, h as ClipResult, i as ClosingTurnResult, j as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, k as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, l as DEFAULT_CREDITS_LOW_LEAD_SECONDS, m as DEFAULT_GOVERNOR_CONFIG, n as DEFAULT_GRACE_CEILING_SECONDS, o as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, p as DEFAULT_IDLE_SECONDS, q as DEFAULT_IDLE_WARN_LEAD_SECONDS, r as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, s as EndedEvent, t as ExtendResult, F as FishTtsModel, u as FreezeReadingFn, G as Governor, v as GovernorAction, w as GovernorConfig, x as GovernorSignal, y as GovernorState, z as GraceWindowClosedEvent, H as GraceWindowOpenEvent, I as GraceWindowState, J as IdleWarningEvent, K as KnownBehaviorState, L as LLMProvider, M as LLMSelection, N as LiveKitAvatarGrantState, O as LiveKitAvatarGrantStatus, P as LiveKitCapacityState, Q as LiveKitConnectionStatus, R as LiveKitSessionRequest, S as LiveKitSessionStartResult, T as LivePlaybackKeeper, U as MAX_RECONNECT_ATTEMPTS, V as MAX_SESSION_INSTRUCTIONS_CHARS, W as MIC_LEASE_ENDED_TIMEOUT_MS, X as MicLease, Y as PlayableVideoElement, Z as ProxyClientOptions, _ as QualityCap, $ as RECONNECT_BACKOFF_MS, a0 as RealtimeAvatarCapacityError, a1 as RealtimeAvatarLiveKitRoom, a2 as RealtimeAvatarLiveKitRoomProps, a3 as RealtimeAvatarRequestOptions, a4 as RealtimeSessionApi, a5 as RealtimeSessionMedia, a6 as RealtimeSessionRoomSinks, a7 as ReconnectPolicy, a8 as ReconnectingEvent, a9 as RecoveryState, aa as RetryStep, ab as SessionBehavior, ac as SessionClip, ad as SessionClocks, ae as SessionEndReason, af as SessionLifecycleApi, ag as SessionLifecyclePhase, ah as SessionLifecyclePhaseKind, ai as SessionLifecycleRoomBridge, aj as SessionLifecycleRoomBridgeProps, ak as SurfaceLayers, al as TurnState, am as TurnTimeoutEvent, an as UseAvatarQualityGovernorInput, ao as UseLiveKitAvatarGrantInput, ap as UseRealtimeSessionInput, aq as UseSessionLifecycleInput, ar as VoiceSpec, as as VoiceSpecInput, at as capacityErrorFromBusy, au as capacityStateFromGrant, av as createProxyClient, aw as knownBehaviorStates, ax as mapTurnState, ay as sessionBehaviorSchema, az as sessionClipSchema, aA as shouldReplayPendingTurn, aB as splitCallTranscript, aC as useAvatarPlayoutDelay, aD as useAvatarQualityGovernor, aE as useCallTranscript, aF as useLiveKitAvatarGrant, aG as useMicLease, aH as useRealtimeSession, aI as useReleaseMicLeaseOnTrackEnded, aJ as useSessionLifecycle } from './proxy-client-Dm-O0jNn.js';
2
+ import { A as AvatarSessionClient, a as AvatarVideoFit } from './proxy-client-BIaUo0az.js';
3
+ export { b as AdaptivePlayoutController, c as AdaptivePlayoutDecision, d as AdaptivePlayoutOptions, e as AdaptivePlayoutSample, f as ApproachingEndEvent, g as ApproachingEndReason, h as AvatarVideoSurface, i as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, j as CallTranscriptSegment, k as CartesiaTtsModel, l as ClipResult, m as ClosingTurnResult, n as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, o as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, p as DEFAULT_CREDITS_LOW_LEAD_SECONDS, q as DEFAULT_GOVERNOR_CONFIG, r as DEFAULT_GRACE_CEILING_SECONDS, s as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, t as DEFAULT_IDLE_SECONDS, u as DEFAULT_IDLE_WARN_LEAD_SECONDS, v as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, w as EndedEvent, x as ExtendResult, F as FishTtsModel, y as FreezeReadingFn, G as Governor, z as GovernorAction, H as GovernorConfig, I as GovernorSignal, J as GovernorState, K as GraceWindowClosedEvent, L 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, Z as LivePlaybackKeeper, _ as MAX_RECONNECT_ATTEMPTS, $ as MAX_SESSION_INSTRUCTIONS_CHARS, a0 as MIC_LEASE_ENDED_TIMEOUT_MS, a1 as MicLease, a2 as PlayableVideoElement, a3 as ProxyClientOptions, a4 as QualityCap, a5 as RECONNECT_BACKOFF_MS, a6 as RealtimeAvatarCapacityError, a7 as RealtimeAvatarLiveKitRoom, a8 as RealtimeAvatarLiveKitRoomProps, a9 as RealtimeAvatarRequestOptions, aa as RealtimeSessionApi, ab as RealtimeSessionMedia, ac as RealtimeSessionRoomSinks, ad as ReconnectPolicy, ae as ReconnectingEvent, af as RecoveryState, ag as RetryStep, ah as SessionBehavior, ai as SessionClip, aj as SessionClocks, ak as SessionEndReason, al as SessionLifecycleApi, am as SessionLifecyclePhase, an as SessionLifecyclePhaseKind, ao as SessionLifecycleRoomBridge, ap as SessionLifecycleRoomBridgeProps, aq as SurfaceLayers, ar as TurnState, as as TurnTimeoutEvent, at as UseAvatarQualityGovernorInput, au as UseLiveKitAvatarGrantInput, av as UseRealtimeSessionInput, aw as UseSessionLifecycleInput, ax as VoiceSpec, ay as VoiceSpecInput, az as capacityErrorFromBusy, aA as capacityStateFromGrant, aB as createProxyClient, aC as knownBehaviorStates, aD as mapTurnState, aE as readInboundRtp, aF as sessionBehaviorSchema, aG as sessionClipSchema, aH as shouldReplayPendingTurn, aI as splitCallTranscript, aJ as useAvatarAdaptivePlayoutDelay, aK as useAvatarPlayoutDelay, aL as useAvatarQualityGovernor, aM as useCallTranscript, aN as useLiveKitAvatarGrant, aO as useMicLease, aP as useRealtimeSession, aQ as useReleaseMicLeaseOnTrackEnded, aR as useSessionLifecycle } from './proxy-client-BIaUo0az.js';
4
4
  import '@livekit/components-react';
5
5
  import 'livekit-client';
6
6
  import 'zod';
package/dist/react.js CHANGED
@@ -435,6 +435,124 @@ function stableStringify(value) {
435
435
  return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
436
436
  }
437
437
 
438
+ // ../client/src/react/adaptive-playout.ts
439
+ var DEFAULTS = {
440
+ floorSeconds: 0.15,
441
+ ceilingSeconds: 0.5,
442
+ jitterGain: 4,
443
+ lossGain: 5,
444
+ growAlpha: 0.6,
445
+ shrinkAlpha: 0.08,
446
+ hysteresisSeconds: 0.05
447
+ };
448
+ var AdaptivePlayoutController = class {
449
+ opts;
450
+ smoothed;
451
+ applied;
452
+ constructor(options = {}) {
453
+ this.opts = { ...DEFAULTS, ...options };
454
+ this.smoothed = this.opts.ceilingSeconds;
455
+ this.applied = this.opts.ceilingSeconds;
456
+ }
457
+ /** The currently applied depth (what the receivers were last hinted to). */
458
+ get appliedSeconds() {
459
+ return this.applied;
460
+ }
461
+ update(sample) {
462
+ const { floorSeconds, ceilingSeconds, jitterGain, lossGain } = this.opts;
463
+ const jitter = Number.isFinite(sample.jitterSeconds) ? Math.max(0, sample.jitterSeconds) : 0;
464
+ const loss = Number.isFinite(sample.lossFraction) ? Math.min(1, Math.max(0, sample.lossFraction)) : 0;
465
+ const demand = Math.min(
466
+ ceilingSeconds,
467
+ Math.max(floorSeconds, floorSeconds + jitterGain * jitter + lossGain * loss)
468
+ );
469
+ const alpha = demand > this.smoothed ? this.opts.growAlpha : this.opts.shrinkAlpha;
470
+ this.smoothed += alpha * (demand - this.smoothed);
471
+ const hysteresis = this.opts.hysteresisSeconds;
472
+ const clamped = Math.min(ceilingSeconds, Math.max(floorSeconds, this.smoothed));
473
+ const target = clamped - floorSeconds <= hysteresis ? floorSeconds : ceilingSeconds - clamped <= hysteresis ? ceilingSeconds : clamped;
474
+ if (target === this.applied) return { targetSeconds: this.applied, changed: false };
475
+ const atBound = target === floorSeconds || target === ceilingSeconds;
476
+ if (!atBound && Math.abs(target - this.applied) <= hysteresis) {
477
+ return { targetSeconds: this.applied, changed: false };
478
+ }
479
+ this.applied = target;
480
+ return { targetSeconds: this.applied, changed: true };
481
+ }
482
+ };
483
+ function readInboundRtp(reports, previous) {
484
+ for (const report of reports) {
485
+ if (report["type"] !== "inbound-rtp") continue;
486
+ const jitterSeconds = typeof report["jitter"] === "number" ? report["jitter"] : 0;
487
+ const packetsLost = typeof report["packetsLost"] === "number" ? report["packetsLost"] : 0;
488
+ const packetsReceived = typeof report["packetsReceived"] === "number" ? report["packetsReceived"] : 0;
489
+ const dLost = Math.max(0, packetsLost - (previous?.packetsLost ?? packetsLost));
490
+ const dReceived = Math.max(0, packetsReceived - (previous?.packetsReceived ?? packetsReceived));
491
+ const denominator = dLost + dReceived;
492
+ return {
493
+ sample: {
494
+ jitterSeconds,
495
+ lossFraction: denominator > 0 ? dLost / denominator : 0
496
+ },
497
+ cursor: { packetsLost, packetsReceived }
498
+ };
499
+ }
500
+ return void 0;
501
+ }
502
+
503
+ // ../client/src/react/use-adaptive-playout.ts
504
+ function useAvatarAdaptivePlayoutDelay(videoTrack, audioTrack, enabled = false, options) {
505
+ const ceiling = options?.ceilingSeconds ?? DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS;
506
+ const [appliedSeconds, setAppliedSeconds] = useState(ceiling);
507
+ const videoMediaTrack = videoTrack?.publication?.track;
508
+ const audioMediaTrack = audioTrack?.publication?.track;
509
+ useEffect(() => {
510
+ if (!enabled || !videoMediaTrack && !audioMediaTrack) return;
511
+ const receivers = [videoMediaTrack, audioMediaTrack].map((t) => t?.receiver).filter(
512
+ (r) => typeof r?.getStats === "function"
513
+ );
514
+ if (receivers.length === 0) return;
515
+ const controller = new AdaptivePlayoutController(options);
516
+ const cursors = /* @__PURE__ */ new Map();
517
+ let closed = false;
518
+ let inFlight = false;
519
+ const tick = async () => {
520
+ if (closed || inFlight) return;
521
+ inFlight = true;
522
+ try {
523
+ let worst;
524
+ for (let i = 0; i < receivers.length; i++) {
525
+ const stats = await receivers[i].getStats();
526
+ if (closed || typeof stats?.values !== "function") continue;
527
+ const reading = readInboundRtp(stats.values(), cursors.get(i));
528
+ if (!reading) continue;
529
+ cursors.set(i, reading.cursor);
530
+ worst = {
531
+ jitterSeconds: Math.max(worst?.jitterSeconds ?? 0, reading.sample.jitterSeconds),
532
+ lossFraction: Math.max(worst?.lossFraction ?? 0, reading.sample.lossFraction)
533
+ };
534
+ }
535
+ if (closed || !worst) return;
536
+ const decision = controller.update(worst);
537
+ if (decision.changed) {
538
+ applyAvatarPlayoutDelay(videoMediaTrack, audioMediaTrack, decision.targetSeconds);
539
+ setAppliedSeconds(decision.targetSeconds);
540
+ }
541
+ } catch {
542
+ } finally {
543
+ inFlight = false;
544
+ }
545
+ };
546
+ const interval = setInterval(() => void tick(), 1e3);
547
+ return () => {
548
+ closed = true;
549
+ clearInterval(interval);
550
+ setAppliedSeconds(ceiling);
551
+ };
552
+ }, [enabled, videoMediaTrack, audioMediaTrack, options, ceiling]);
553
+ return appliedSeconds;
554
+ }
555
+
438
556
  // ../client/src/react/quality-governor.ts
439
557
  var DEFAULT_GOVERNOR_CONFIG = {
440
558
  openingCap: "low",
@@ -706,11 +824,23 @@ function AvatarVideoSurface(props) {
706
824
  className,
707
825
  style,
708
826
  children,
709
- showLiveBadge = true
827
+ showLiveBadge = true,
828
+ adaptivePlayout = false,
829
+ onPlayoutDelayChange
710
830
  } = props;
711
831
  const testId = props["data-testid"];
712
832
  const { videoTrack, audioTrack } = useVoiceAssistant();
713
833
  useAvatarPlayoutDelay(videoTrack, audioTrack);
834
+ const playoutDelaySeconds = useAvatarAdaptivePlayoutDelay(
835
+ videoTrack,
836
+ audioTrack,
837
+ adaptivePlayout
838
+ );
839
+ const onPlayoutDelayChangeRef = useRef(onPlayoutDelayChange);
840
+ onPlayoutDelayChangeRef.current = onPlayoutDelayChange;
841
+ useEffect(() => {
842
+ onPlayoutDelayChangeRef.current?.(playoutDelaySeconds);
843
+ }, [playoutDelaySeconds]);
714
844
  const connectionState = useConnectionState();
715
845
  const connected = connectionState === ConnectionState.Connected;
716
846
  const trackProducing = useLiveTrackProducing(videoTrack);
@@ -2578,4 +2708,4 @@ function createProxyClient(options) {
2578
2708
  };
2579
2709
  }
2580
2710
 
2581
- export { AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, 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, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarCall, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
2711
+ export { AdaptivePlayoutController, AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, 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, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCall, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
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, l as AvatarClip, c as AvatarSourceSwap, b as AvatarUpdate, m as CallConnection, C as CallMode, a as CallPolicy, n as CallQueued, d as ClipDeclaration, f as ClipLibrary, o as ClipLibraryPlan, e as ClipLibraryUpdate, p as ClipSource, g as ClipSyncResult, q as ContextMessage, k as CreditBalance, E as EndCallOptions, r as EndCallReason, L as ListSessionsOptions, S as StartCallResult, T as TranscriptPayload, j as UsageSession, U as UsageSessionPage, V as VideoPolicy, s as VideoState, t as isQueued } from './types-DLxz6uFE.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-BeGuvyra.js';
package/dist/server.js CHANGED
@@ -43,7 +43,7 @@ var RealtimeAvatarHttpError = class extends RealtimeAvatarError {
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.5.0";
46
+ var SDK_VERSION = "0.5.2";
47
47
  var RealtimeAvatar = class {
48
48
  #apiKey;
49
49
  #baseUrl;
@@ -157,10 +157,15 @@ var RealtimeAvatar = class {
157
157
  /**
158
158
  * Register a character from a looping clip you host.
159
159
  *
160
- * Use a VIDEO source for anything that will be called live. An avatar built from a still
161
- * image reaches `ready`, mints calls, and publishes a BLACK track — the status and the
162
- * track dimensions both look fine, only the pixels are wrong. Image sources are for
163
- * offline lipsync renders.
160
+ * @deprecated CLOSED to new callers — this answers `422` unless your tenant was already
161
+ * creating from video, in which case it keeps working and your existing avatars are
162
+ * untouched. Use {@link createAvatarFromImage}: one still, and the platform renders the
163
+ * resting loop and the motion library from it.
164
+ *
165
+ * The reason it closed is not arbitrary. Every clip has to start and end on ONE rest pose
166
+ * or a state switch reads as a jump, and the platform can only guarantee that when it
167
+ * rendered the loop and the clips from the same portrait. A supplied video cannot honour
168
+ * it, so the lane could never be made to look right.
164
169
  */
165
170
  async createAvatarFromVideo(input) {
166
171
  const asset = await this.createRemoteAsset({ kind: "video", remoteUrl: input.videoUrl });
@@ -173,12 +178,41 @@ var RealtimeAvatar = class {
173
178
  metadata: input.metadata
174
179
  });
175
180
  }
181
+ /**
182
+ * Register a character from ONE still image. The platform generates everything moving:
183
+ * the resting loop she idles in, then a starter motion library rendered against her rest
184
+ * pose. No footage, no clip URLs, nothing to shoot.
185
+ *
186
+ * `motionPrompt` directs the RESTING LOOP — the video she plays when nothing else is
187
+ * happening — and it is the only chance to direct it, because there is no API today that
188
+ * re-generates a loop after creation (see {@link updateAvatar} for the one thing that can
189
+ * be re-pointed). Describe a small closed arc that returns to where it started: "settles
190
+ * into frame, breathes gently, a slow blink". Omit it and the house default is used.
191
+ *
192
+ * Creation returns while the avatar is still `preprocessing`; poll {@link getAvatar} until
193
+ * it leaves that state. The loop is load-bearing, so a failure there settles `failed` with
194
+ * a readable error — the motion library is not, and a library failure degrades to
195
+ * loop-only rather than demoting the character.
196
+ */
197
+ async createAvatarFromImage(input) {
198
+ const asset = await this.createRemoteAsset({ kind: "image", remoteUrl: input.imageUrl });
199
+ return this.createAvatar({
200
+ displayName: input.displayName,
201
+ sourceKind: "image",
202
+ sourceAssetId: asset.id,
203
+ motionPrompt: input.motionPrompt,
204
+ voice: input.voice,
205
+ settings: input.settings,
206
+ metadata: input.metadata
207
+ });
208
+ }
176
209
  async createAvatar(input) {
177
210
  const body = {
178
211
  displayName: input.displayName,
179
212
  sourceKind: input.sourceKind,
180
213
  sourceAssetId: input.sourceAssetId
181
214
  };
215
+ if (input.motionPrompt !== void 0) body.motionPrompt = input.motionPrompt;
182
216
  if (input.voice !== void 0) body.voice = input.voice;
183
217
  if (input.settings !== void 0) body.settings = input.settings;
184
218
  if (input.metadata !== void 0) body.metadata = input.metadata;
@@ -191,7 +225,14 @@ var RealtimeAvatar = class {
191
225
  async getAvatar(avatarId) {
192
226
  return toAvatar(await this.#json(await this.#request("GET", `/avatars/${avatarId}`)));
193
227
  }
194
- /** Re-point what an avatar already is. `defaultVoiceId: null` clears the default voice. */
228
+ /**
229
+ * Re-point what an avatar already is. `defaultVoiceId: null` clears the default voice.
230
+ *
231
+ * Cheap metadata only. The three things that cost a render have their own methods, because
232
+ * putting them in a patch alongside a rename would hide minutes of GPU work behind a field:
233
+ * {@link retimeAnchor} moves the rest frame, {@link swapSource} replaces the footage, and
234
+ * the resting loop is re-directed by description at `PUT /v1/avatars/{id}/loop`.
235
+ */
195
236
  async updateAvatar(avatarId, patch) {
196
237
  return toAvatar(
197
238
  await this.#json(await this.#request("PATCH", `/avatars/${avatarId}`, { json: patch }))
@@ -274,6 +315,94 @@ var RealtimeAvatar = class {
274
315
  )
275
316
  );
276
317
  }
318
+ /**
319
+ * Re-direct the RESTING LOOP — the video she plays when nothing else is happening — from
320
+ * a new one-sentence description.
321
+ *
322
+ * Not a clip, and this is the distinction integrations get wrong: a clip with
323
+ * `role: "idle"` is a variant spliced OVER the loop, and declaring one never changes what
324
+ * she rests in. This is the only thing that does.
325
+ *
326
+ * `202`, because the render takes minutes. Three properties, all measured against a real
327
+ * render rather than asserted:
328
+ *
329
+ * - **She never goes dark.** She stays `ready` and keeps serving her previous loop for the
330
+ * entire render — returned as `servingUrl` — then the swap publishes in one step.
331
+ * - **Your clip library is untouched.** Clips render against the portrait, not against the
332
+ * loop, so a re-direct re-queues nothing and does not move `revision`.
333
+ * - **It bills once**, at the rendering model's rate, per re-direct.
334
+ *
335
+ * Refusals worth telling apart: `409 loop_pending` (one is already in flight — wait) and
336
+ * `422 loop_not_generatable` (a grandfathered video-sourced avatar has no portrait to
337
+ * re-animate — terminal, do not retry).
338
+ */
339
+ async setLoop(avatarId, loop) {
340
+ return await this.#json(
341
+ await this.#request("PUT", `/avatars/${avatarId}/loop`, { json: { motionPrompt: loop.motionPrompt } })
342
+ );
343
+ }
344
+ /**
345
+ * Block until a loop re-direct settles, and throw if it did not take.
346
+ *
347
+ * `setLoop` returns on ACCEPTANCE; the render runs for minutes afterwards. Every caller
348
+ * therefore writes the same polling loop, and the obvious version of it never terminates
349
+ * on failure — a failed re-direct leaves her `ready` (she is still serving the old loop)
350
+ * and writes nothing to `error`. `idleVideoStatus` is the only field that moves, which is
351
+ * why this exists rather than a doc line telling you to poll.
352
+ *
353
+ * Race-free without a baseline: the platform commits `queued` before `setLoop` returns, so
354
+ * by the time you can call this the status has already left `ready`.
355
+ *
356
+ * Resolves with the settled avatar — `sourceAssetId` is now the new loop. Throws on a
357
+ * failed render and on timeout; a timeout is not a failure, so re-poll or call again.
358
+ */
359
+ async waitForLoop(avatarId, options = {}) {
360
+ const timeoutMs = options.timeoutMs ?? 20 * 6e4;
361
+ const pollMs = Math.max(1e3, options.pollMs ?? 1e4);
362
+ const deadline = Date.now() + timeoutMs;
363
+ for (; ; ) {
364
+ const avatar = await this.getAvatar(avatarId);
365
+ if (avatar.idleVideoStatus === "failed") {
366
+ throw new RealtimeAvatarError(
367
+ `Loop re-direct failed for ${avatarId}. She is still serving her previous loop \u2014 nothing was lost, and you can send another description.`
368
+ );
369
+ }
370
+ if (avatar.idleVideoStatus === "ready" || avatar.idleVideoStatus === "none") return avatar;
371
+ if (Date.now() >= deadline) {
372
+ throw new RealtimeAvatarError(
373
+ `Loop re-direct for ${avatarId} was still ${avatar.idleVideoStatus} after ${Math.round(timeoutMs / 1e3)}s. This is a timeout, not a failure \u2014 the render may still land.`
374
+ );
375
+ }
376
+ await sleep(pollMs);
377
+ }
378
+ }
379
+ /**
380
+ * Block until every clip in the library has stopped moving.
381
+ *
382
+ * Settled means no row is `queued` or `generating` — NOT that every row is `ready`.
383
+ * Waiting for all-`ready` is the intuitive version and it hangs forever: a clip rejected
384
+ * by pose validation settles `failed`, which is terminal. So this returns the library
385
+ * with the failures in it and lets you decide; a partial library is a legitimate outcome
386
+ * and the rest of it is already serving.
387
+ *
388
+ * Throws only on timeout.
389
+ */
390
+ async waitForClips(avatarId, options = {}) {
391
+ const timeoutMs = options.timeoutMs ?? 20 * 6e4;
392
+ const pollMs = Math.max(1e3, options.pollMs ?? 1e4);
393
+ const deadline = Date.now() + timeoutMs;
394
+ for (; ; ) {
395
+ const library = await this.listClips(avatarId);
396
+ const moving = library.data.filter((c) => c.status === "queued" || c.status === "generating");
397
+ if (moving.length === 0) return library;
398
+ if (Date.now() >= deadline) {
399
+ throw new RealtimeAvatarError(
400
+ `${moving.length} clip(s) on ${avatarId} were still rendering after ${Math.round(timeoutMs / 1e3)}s: ${moving.map((c) => c.clipId).join(", ")}. This is a timeout, not a failure.`
401
+ );
402
+ }
403
+ await sleep(pollMs);
404
+ }
405
+ }
277
406
  /** The avatar's clip library: every non-retired clip, plus revision, anchor and eligibility. */
278
407
  async listClips(avatarId) {
279
408
  return this.#clipEnvelope(
@@ -475,7 +604,13 @@ function toAvatar(raw) {
475
604
  sourceAssetId: a.sourceAssetId ? String(a.sourceAssetId) : null,
476
605
  // Carried because it is the ONLY channel a failed source swap has: she stays `ready`
477
606
  // and serving, and this says why the re-shoot did not take.
478
- error: a.error ? String(a.error) : null
607
+ error: a.error ? String(a.error) : null,
608
+ // The loop lane's terminal signal, and the reason it is here: a re-direct that fails
609
+ // leaves `status` on `ready` (she is still serving the old loop, which is the whole
610
+ // design) and writes nothing to `error`. Without this field a caller polling after
611
+ // `setLoop` has NO way to distinguish "still rendering" from "gave up", and waits
612
+ // forever. queued → generating → ready | failed.
613
+ idleVideoStatus: a.idleVideoStatus ?? "none"
479
614
  };
480
615
  }
481
616
  function toAsset(raw) {
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-B45GgjrV.js';
2
- import './types-DLxz6uFE.js';
1
+ import { P as ProxyConfig } from './types-CHrhuhYb.js';
2
+ import './types-BeGuvyra.js';
3
3
 
4
4
  /**
5
5
  * TanStack Start. Mount at `routes/api/realtime-avatar/$.ts` — the trailing `$` is Start's