little-kite-91953
04/08/2026, 6:05 AMDate.now() elapsed time from when agentState transitions to 'speaking'. This works fine under normal conditions but drifts when audio playback stutters — the wall clock keeps ticking while audio buffers, so captions race ahead of what the user hears.
What we’ve tried:
HTMLAudioElement.currentTime on the element from track.attachedElements[0] — for WebRTC MediaStream sources, this is effectively another wall clock. It doesn’t stall during jitter buffer underruns.
• We’re now looking at using RTCInboundRtpStreamStats.concealedSamples (via RemoteAudioTrack.getRTCStatsReport()) to compute cumulative stutter duration and subtract it from the wall clock.
Questions:
Is there a better way to get the actual audio playout position from the LiveKit client SDK? Something like “seconds of audio actually emitted to the speaker” rather than wall-clock time?
Is the concealedSamples delta approach sound for compensating the playback timer, or are there gotchas we should know about (e.g., does playout speed adjustment via insertedSamplesForDeceleration also need to be accounted for)?
1. We’re using sync_transcription=False on the agent side so we can do our own word-level reveal. Is there any server-side mechanism that could help — e.g., a data channel message when audio frames are actually sent, or a way to get the server’s playout schedule?tall-airline-16756
04/08/2026, 6:06 AMtall-airline-16756
04/08/2026, 6:06 AMSpeechHandle.wait_for_playout() exist internally but their timing is not exposed to clients by default.
Practical implementation guidance
1. Compute an adjusted playout position:
◦ Track when the agent starts producing audio (your agentState → 'speaking' timestamp).
◦ Periodically call RemoteAudioTrack.getRTCStatsReport() and read inbound audio stats.
◦ Convert samples to seconds using the sample rate:
▪︎ stutterSeconds = delta(concealedSamples) / sampleRate
▪︎ decelSeconds = delta(insertedSamplesForDeceleration) / sampleRate (subtract)
▪︎ accelSeconds = delta(removedSamplesForAcceleration) / sampleRate (add)
◦ AdjustedPlayout = wallClockElapsed - cumulativeStutterSeconds - cumulativeDecelSeconds + cumulativeAccelSeconds
1. Polling cadence and deltas:
◦ Poll stats at a modest interval (e.g., 250–500 ms). Use deltas between successive reports to compute changes.
◦ Use cumulative counters (stats are monotonic) to avoid missed samples if a poll is delayed.
1. Sample rate and mapping:
◦ Use the actual codec/sampleRate from the inbound track stats (or know your TTS sample rate) for accurate conversion.
◦ Align TTS word timestamps to the same timebase as your start-of-playout event.
1. Edge cases and gotchas:
◦ concealedSamples covers loss/underrun concealment, but some platforms may implement concealment differently; test on target browsers/devices.
◦ Jitter buffer increases can delay first-sample playout after start; initial offset should be measured per session.
◦ If users change volume or route audio to Bluetooth/headset, that doesn’t affect these counters but can change perceived latency—keep UX tolerant to small offsets.
◦ If you see insertedSamplesForDeceleration / removedSamplesForAcceleration in your stats, include them as above to correct for clock compensation.
◦ HTMLAudioElement.currentTime is not reliable for WebRTC MediaStream playout under jitter — don’t use it for word-level sync.
1. If you need absolute confirmation from the agent/server:
◦ Because there’s no built-in server-side event that tells the client “frame X has been emitted to speakers,” implement a custom signal:
▪︎ Have the agent send data messages at key points (e.g., on playout start, on TTS chunk boundaries, or on playout complete) from the server/agent when it calls its internal playout-complete hooks.
▪︎ Combine server-sent playout events with client-side RTC stats to get robust sync (server timestamps give authoritative schedule; client stats correct for network/runtime deviations).
Next step I can help with
• If you’d like, I can provide a concise code sketch showing:
◦ how to poll getRTCStatsReport() and compute the adjusted playout time, or
◦ an example data-message schema for agent->client playout events to pair with the client-side corrections.
Which would you prefer?
Sources: client-sdk-js | livekit/livekit_composite | DeepWiki | Why WebRTC beats WebSockets for realtime voice AI | LiveKit | How to detect when an agent has finished speaking | LiveKit, with interactive elements