colossal-bear-85704
12/25/2025, 4:57 PMParticipantEgress for Camera + Audio (MP4).
▪︎ 1x ParticipantEgress for Screen Share (Images/Screenshots).
The Issues
In production, I am seeing significant degradation:
1. Random A/V Desync: i don't know the reason and it happens randomly sometimes even with a fast connection. Logs show tracks being ignored due to delays sometimes reaching >1.5 seconds and even more. Sync is rarely recovered.
2. Data Loss: Videos sometimes start with black frames or have no audio at all.
I played around with Egress params but it seems it created other problems ( like sometimes audio keep going off for few seconds and come back, Cf. the egress.yaml config i put at the end of the message )
Below are my configurations. I would really appreciate a feedback in the setup and especially if am doing some trivial mistakes that i can correct immediatly.
Python Code Snippet for the Egresses ( excluding the stop and start logic )
# 1. Camera + Audio Egress
camera_audio_output = EncodedFileOutput(
filepath=camera_storage_path,
file_type=EncodedFileType.MP4,
disable_manifest=True,
)
camera_audio_request = ParticipantEgressRequest(
room_name=room_name,
identity=participant_identity,
screen_share=False, # Camera + audio only
file_outputs=[camera_audio_output],
preset=EncodingOptionsPreset.H264_720P_30
)
# 2. Screen Egress (only if question requires screen recording)
screen_output = ImageOutput(
capture_interval=int(1 / settings.ocr_extraction_fps),
filename_prefix=screen_storage_prefix # e.g. "frame_000000.jpg", "frame_000001.jpg", etc.
)
screen_request = ParticipantEgressRequest(
room_name=room_name,
identity=participant_identity,
screen_share=True, # Record screen share
image_outputs=[screen_output]
)
React Frontend snippet that publishes the streams at the beginning of the interviews
const publishPromises: Promise<void>[] = [];
const userMediaStreamName = 'user-camera-mic';
// Prepare camera + audio with synchronized stream (same stream name for A/V sync)
if (preGrantedStreams.cameraStream) {
const videoTrack = preGrantedStreams.cameraStream.getVideoTracks()[0];
const audioTrack = preGrantedStreams.cameraStream.getAudioTracks()[0];
if (videoTrack && audioTrack) {
// Publish both camera and microphone simultaneously for better synchronization
// Using the same stream name ensures they are placed in the same MediaStream
publishPromises.push(
room.localParticipant.publishTrack(videoTrack, {
name: 'camera',
source: Track.Source.Camera,
stream: userMediaStreamName,
}).then(() => {
console.log('[handleStartInterview] ✓ Camera video track published');
})
);
publishPromises.push(
room.localParticipant.publishTrack(audioTrack, {
name: 'microphone',
source: Track.Source.Microphone,
stream: userMediaStreamName,
}).then(() => {
console.log('[handleStartInterview] ✓ Microphone audio track published');
})
);
} else if (videoTrack) {
publishPromises.push(
room.localParticipant.publishTrack(videoTrack, {
name: 'camera',
source: Track.Source.Camera,
stream: userMediaStreamName,
}).then(() => {
console.log('[handleStartInterview] ✓ Camera video track published');
})
);
}
}
// Prepare screen share
if (preGrantedStreams.screenStream) {
const screenTrack = preGrantedStreams.screenStream.getVideoTracks()[0];
if (screenTrack) {
publishPromises.push(
room.localParticipant.publishTrack(screenTrack, {
name: 'screen_share',
source: Track.Source.ScreenShare,
simulcast: false, // Disable simulcast for faster egress start
}).then(() => {
console.log('[handleStartInterview] ✓ Screen share track published (single quality layer)');
})
);
}
}
// Publish all tracks simultaneously for better synchronization
await Promise.all(publishPromises);
// Mark as started
setIsInterviewStarted(true);
setSetupComplete(true);
showNotification('Interview started successfully!', 'success');
My Current Egress Configuration ( I am quite aware that i might set some params too high ).
redis:
address: <REDIS_ADDR>
storage:
gcp:
bucket: <GCS_BUKCKET>
cpu_cost:
participant_cpu_cost: 1
latency:
rtcp_sender_report_rebase_enabled: true
rtp_max_drift_adjustment: 3s
jitter_buffer_latency: 3s
And last my docker compose for running the live-kit services
services:
caddy:
image: livekit/caddyl4
command: run --config /etc/caddy.yaml --adapter yaml
restart: unless-stopped
network_mode: "host"
volumes:
- ./caddy.yaml:/etc/caddy.yaml
- ./caddy_data:/data
livekit:
image: livekit/livekit-server:latest
command: --config /etc/livekit.yaml
restart: unless-stopped
network_mode: "host"
volumes:
- ./livekit.yaml:/etc/livekit.yaml
redis:
image: redis:7-alpine
command: redis-server /etc/redis.conf
restart: unless-stopped
network_mode: "host"
volumes:
- ./redis.conf:/etc/redis.conf
egress:
image: livekit/egress:latest
restart: unless-stopped
environment:
- EGRESS_CONFIG_FILE=/etc/egress.yaml
network_mode: "host"
volumes:
- ./egress.yaml:/etc/egress.yaml
cap_add:
- CAP_SYS_ADMINtall-airline-16756
12/25/2025, 4:57 PMtall-airline-16756
12/25/2025, 4:57 PMegress.yaml change:
▪︎ jitter_buffer_latency: 3s → 200ms
▪︎ rtp_max_drift_adjustment: 3s → 300ms
◦ Keep rtcp_sender_report_rebase_enabled: true.
◦ Why: 3s buffering causes the large drift and makes egress reject/ignore late frames; lower values drastically reduce observed sync drift and ignored tracks.
1. Ensure egress only starts after tracks are actually publishing
◦ On the server, start egress only after you confirm the participant has published expected tracks (use track_published webhook or verify via API listParticipants/listTracks).
◦ On the client, after publish, wait for the track to have produced a frame (wait-for-first-frame) before starting egress.
◦ Why: If egress starts before the first keyframe/audio packet, MP4s can begin with black frames / silence.
1. Stop frequent stop/start of heavy egress jobs
◦ Don’t stop/start 2 ParticipantEgress jobs per question. Frequent lifecycle churn increases race conditions, CPU spikes, and missed frames.
◦ Instead: use fewer long-running egress jobs that record continuously and split files post-hoc, or use track-level egress that starts/stops much faster and is lighter.
Immediate recommended changes to egress type and workflow
1. Replace ParticipantEgress with track-level egress (TrackComposite/TrackEgress)
◦ ParticipantEgress launches a compositor (headless Chrome) and is CPU/memory heavy. For camera+mic and screen images you do not need the Chrome compositor.
◦ Use TrackCompositeEgress (or TrackEgress APIs) with explicit audio/video track IDs for synchronized recording and ImageOutput for screenshots.
◦ Why: Track-level egress uses GStreamer pipelines (lower CPU, faster startup, more stable) and records directly from tracks which improves sync and avoids browser compositor delays.
1. If you must keep participant-level egress, reduce concurrency
◦ Don’t run two simultaneous ParticipantEgress per question on the same host. If you need both, run them on a separate machine or stagger them.
Infrastructure / capacity fixes
1. Dedicate or scale Egress off the single VM
◦ Your single box (8 vCPU / 16GB) is likely overloaded by repeated egress starts and potential headless Chrome instances.
◦ Run livekit-egress on a separate VM/container host, or use a more powerful host, or LiveKit Cloud for egress. This prevents LiveKit server + egress contention.
1. Monitor resource usage during interviews
◦ Watch CPU, memory, and i/o while an interview runs (docker stats / top / htop). If CPU or memory spikes near saturation when egresses start, that correlates with dropped frames and audio gaps.
Frontend publishing checks
1. Keep using the same MediaStream name for A/V (you already do)
◦ This is correct and helps sync. Continue using stream: user-media for both camera and mic tracks.
1. Avoid unnecessary disabling of simulcast for camera
◦ You set simulcast false for screen share (that may be fine). For camera, allow appropriate encoding options unless proven necessary to disable.
Operational alternatives (if you want simpler, reliable recording)
1. Client-side recording per question
◦ Use MediaRecorder in the client to record each question locally and upload the chunk to GCS. This eliminates server-side egress complexity and prevents server-side encoding contention.
◦ This is a recommended fallback if you do not need server-side composition.
Short checklist you can run right now
• Edit egress.yaml to 200ms / 300ms values and restart egress service.
• Stop Stop/Start pattern: start a single egress, or switch to track-level egress.
• Add server-side check to only start egress after track_published (or verify listTracks).
• Move livekit/egress off your LiveKit server machine if possible or increase host resources.
• Run a production-like test with network jitter to confirm fixes.
If you want, I can:
• Provide example TrackCompositeEgress request payloads (Python) to replace your ParticipantEgress usage.
• Show a small client-side snippet to wait for first frame before signaling the backend to start egress.
• Help you craft a minimal test plan to validate fixes (CPU/memory, simulated jitter, startup timing).
Which of the three follow-ups would you like first: TrackComposite example, client wait-for-first-frame snippet, or a short test plan?
Sources: Debugging Audio/Video Sync Issues in LiveKit Publishing | Universal Egresscolossal-bear-85704
12/25/2025, 5:00 PMredis:
address: <REDIS_ADDR>
storage:
gcp:
bucket: <GCS_BUKCKET>
cpu_cost:
participant_cpu_cost: 0.5tall-airline-16756
12/25/2025, 5:00 PMparticipant_cpu_cost to 0.5 and want to know how that affects the symptoms and what to change.
Short answer
• Setting participant_cpu_cost: 0.5 lowers the CPU budget the egress scheduler assigns per participant. That can cause the egress worker to under-allocate CPU to encoding/compositing work, which increases encoding latency, frame drops, and contributes to A/V drift and black/silent starts. Increase it (or remove the low value) and use other fixes below.
Why this matters (concise)
• Egress tasks (especially ParticipantEgress with a compositor/headless Chrome) require CPU for encoding and I/O. If the scheduler thinks each participant only needs 0.5 CPU, it may pack more concurrent jobs on the host than the machine can handle, causing CPU contention, delayed frames, encoder stalls, and missed keyframes.
• Underprovisioning amplifies issues you already have from jitter buffer and frequent stop/start: slow startups miss first keyframes (black video), audio buffers underflow (no audio), and drift grows.
Concrete recommendations
1. Set participant_cpu_cost to a realistic value
◦ For ParticipantEgress (Chrome compositing): use >= 1.0 (try 1.5 if you still run many concurrent jobs).
◦ For TrackComposite/TrackEgress (GStreamer): 0.5–1.0 may be adequate depending on resolution/bitrate.
◦ Start with participant_cpu_cost: 1.0 and monitor.
1. Combine this with the latency fixes
◦ Set jitter_buffer_latency: 200ms and rtp_max_drift_adjustment: 300ms (as suggested earlier). Both lower latency tolerance and correct CPU allocation are needed together.
1. Prefer track-level egress to reduce CPU needs
◦ Switch from ParticipantEgress to TrackComposite/TrackEgress where possible; those are far lighter and less likely to need a high participant_cpu_cost.
1. Don’t run many concurrent heavy egress jobs on the same host
◦ If you must keep ParticipantEgress, move egress to a dedicated host or increase host CPU/RAM. Monitor (`docker stats`/htop) and ensure CPU isn’t pegged.
1. Use fewer start/stop cycles
◦ Long-running egress or splitting files post-hoc reduces churn and CPU spikes.
Quick checklist to apply now
• Change participant_cpu_cost: 1.0 (or remove the custom value).
• Update egress.yaml jitter settings to 200ms/300ms.
• Restart egress service and run a load test while watching CPU usage.
If you want, I can supply:
• A recommended egress.yaml snippet for Participant vs Track egress, or
• A short plan to test and tune participant_cpu_cost values under load.
Which would you prefer?colossal-bear-85704
12/25/2025, 5:01 PMtall-airline-16756
12/25/2025, 5:01 PMparticipant_cpu_cost plus a minimal egress.yaml snippet and brief rationale.
Recommended value
• participant_cpu_cost: 1.0 — start here for ParticipantEgress on your single-box (8 vCPU / 16GB). Increase to 1.5 if you still run several concurrent heavy egress jobs (Chrome compositor) or see CPU contention.
Why
• 1.0 gives each egress job a realistic CPU budget so the scheduler won’t overcommit the host. Too-low values (0.5) allow too many concurrent jobs, causing encoding stalls, missed keyframes, and A/V drift.
Full minimal egress.yaml snippet to apply (merge with your existing config)
redis:
address: <REDIS_ADDR>
storage:
gcp:
bucket: <GCS_BUCKET>
cpu_cost:
participant_cpu_cost: 1.0
latency:
rtcp_sender_report_rebase_enabled: true
rtp_max_drift_adjustment: 300ms
jitter_buffer_latency: 200ms
Next steps
• Restart the egress service after applying.
• Run a production-like test while monitoring CPU and memory (docker stats / htop). If CPU usage is high and you run many concurrent ParticipantEgress jobs, raise to 1.5 or move egress to a dedicated host and consider switching to track-level egress for lower CPU usage.colossal-bear-85704
12/25/2025, 5:10 PMtall-airline-16756
12/25/2025, 5:11 PM