Debugging audio/video sync issues in LiveKit publishing
Learn how to identify and fix audio/video synchronization drift issues when publishing to LiveKit, including root causes and best practices.
Last Updated:
If you've experienced audio and video drift apart during publishing, with audio often lagging behind video by 150–300ms, this article explains the potential causes, common pitfalls, and best practices to minimize A/V sync issues.
Symptoms
- Client-side playback shows noticeable desynchronization between audio and video.
- Server-side monitoring shows
last_video_time - last_audio_timeconsistently positive (>150ms, sometimes ~300ms). - Once drift begins, it does not self-correct.
Root Causes
Several factors can contribute to A/V sync drift:
Queue Size Too Large
- Large
AVSynchronizerbuffer (queue_size_ms) can introduce significant lag. - Defaulting to values like
1500msallows too much buildup.
Blocking Behavior Between Audio and Video Pushes
- If both audio and video frames are pushed from the same task sequentially, a delay in one stream (e.g., encoding or I/O stall) blocks the other.
- This leads to drift since one media type is consistently delayed in entering the sync pipeline.
Capture Time & Duration Mismatch
- Incorrectly assigning
capture_time_nsor misaligneddurationvalues can skew synchronization.
Best Practices for Fixing Drift
Use a Smaller Synchronizer Queue
- Configure
AVSynchronizer(queue_size_ms=200)or similar. - Smaller buffers reduce lag and make sync corrections faster.
Push Audio & Video from Separate Tasks
- Avoid sequential pushes in a single task:
# Problematic: sequential pushfor audio_frame, video_frame in frames: push(audio_frame) push(video_frame)
# Recommended: independent tasksasyncio.create_task(push(audio_frame))asyncio.create_task(push(video_frame))
- This prevents one stream from blocking the other.
Verify Capture Timestamps
- Ensure
frame.capture_time_nsand durations are assigned consistently. - Mismatched or artificially skewed values will accumulate drift.
Example: Publishing Code (Before vs After)
Before (sequential push, large queue):
self._av_sync = AVSynchronizer(queue_size_ms=1500)
for audio_frame, video_frame in frames: await self._av_sync.push(audio_frame, ...) await self._av_sync.push(video_frame, ...)
After (parallel push, smaller queue):
self._av_sync = AVSynchronizer(queue_size_ms=200)
for audio_frame, video_frame in frames: asyncio.create_task(self._av_sync.push(audio_frame, ...)) asyncio.create_task(self._av_sync.push(video_frame, ...))
Key Takeaways
- A/V drift often comes from queue size misconfiguration or blocking frame pushes.
- Keep the synchronizer buffer small (≈200ms).
- Push audio and video in parallel tasks to avoid blocking.
- Monitor ΔA/V (
last_video_time – last_audio_time) logs to validate improvements.
Read related documentation
Find more WebRTC Transport guides
CreateRoom() is taking too long
Learn why CreateRoom() API calls can be slow and how to avoid explicit room creation by leveraging automatic room creation on participant join.
Diagnosing Connection Errors with the Connection Test Utility
Learn how to use the Connection Test utility to diagnose and troubleshoot connectivity problems with LiveKit Cloud, including WebRTC, WebSocket, and TURN connectivity checks.