eager-kite-60809
12/21/2025, 4:24 PMawait api.LiveKitAPI().start_room_composite_egress(req) would fail silently when called in livekit agent when credentials are misconfigured. Is this expected? Shouldnt it throw an unauthorised error? Anyone also encountering the same?
I was following this guide from https://docs.livekit.io/deploy/observability/data/#record-audio-or-video
Code:
from livekit import api
file_outputs = [
api.EncodedFileOutput(
file_type=api.EncodedFileType.OGG,
filepath=filepath,
azure=api.AzureBlobUpload(
account_name=account_name,
account_key=account_key,
container_name=container_name,
),
)
]
req = api.RoomCompositeEgressRequest(room_name=ctx.room.name, audio_only=True, file_outputs=file_outputs, audio_mixing="DUAL_CHANNEL_AGENT")
lkapi = api.LiveKitAPI()
try:
await lkapi.egress.start_room_composite_egress(req)
except Exception as e:
print(e)abundant-magician-17307
12/22/2025, 2:01 AMambitious-london-8261
12/22/2025, 6:28 AMminiature-soccer-13249
12/22/2025, 8:03 AMglamorous-garden-97700
12/23/2025, 4:24 PMexport async function startRoomRecording(roomName: string): Promise<void> {
const egressClient = getEgressClient();
const filepath = `${roomName}.mp4`;
const fileOutput = new EncodedFileOutput({
filepath,
output: {
case: "s3",
value: new S3Upload({
endpoint: R2_ENDPOINT,
accessKey: R2_KEY_ID,
secret: R2_KEY_SECRET,
region: R2_REGION,
bucket: R2_BUCKET,
forcePathStyle: true,
}),
},
});
await egressClient.startRoomCompositeEgress(roomName, fileOutput, {
layout: "grid",
});
}
Questions:
1. Is the black screen expected behavior when no video tracks are published?
2. Is there a way to force a default tile / placeholder in recordings?
3. Do we need a custom layout or video track injection for this use case?
Everything else in our recording flow works perfectly — this is the only blocker.
Thanks in advance 🙏colossal-bear-85704
12/25/2025, 4:54 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_ADMINcuddly-cartoon-47334
12/27/2025, 10:58 PMfancy-florist-18763
02/12/2026, 8:09 PMstrong-hydrogen-34992
02/13/2026, 4:14 PM2026-02-13T07:05:59.764Z INFO egress pipeline/watch.go:115 pipeline received EOS {"nodeID": "NE_JGn2CW6uEUET", "handlerID": "EGH_DCTYA66WgMtY", "clusterID": "", "egressID": "EG_qfzmKVSQR9wb"}
2026-02-13T07:06:00.127Z INFO egress info/io.go:269 egress_complete {"nodeID": "NE_JGn2CW6uEUET", "clusterID": "", "egressID": "EG_qfzmKVSQR9wb", "requestType": "room_composite", "outputType": "file", "error": "", "code": 0, "details": "End reason: Source closed"}
2026-02-13T07:24:38.141Z INFO egress server/server_rpc.go:59 request received {"nodeID": "NE_JGn2CW6uEUET", "clusterID": "", "egressID": "EG_dz3JMaE3cvex"}
2026-02-13T07:24:38.141Z INFO egress server/server_rpc.go:69 request validated {"nodeID": "NE_JGn2CW6uEUET", "clusterID": "", "egressID": "EG_dz3JMaE3cvex", "requestType": "room_composite", "sourceType": "EGRESS_SOURCE_TYPE_WEB", "outputType": "file", "room": "study_698dc93713cf9c6e82b1a8e3_participant_698dcb4d13cf9c6e82b1a8e6_e0d60f", "request": {"RoomComposite":{"room_name":"study_698dc93713cf9c6e82b1a8e3_participant_698dcb4d13cf9c6e82b1a8e6_e0d60f","layout":"grid-dark","Output":null,"Options":null,"file_outputs":[{"file_type":1,"filepath":"study_698dc93713cf9c6e82b1a8e3_participant_698dcb4d13cf9c6e82b1a8e6_e0d60f/media","Output":{"Azure":{"account_name":"{account_name}","account_key":"{account_key}","container_name":"interview"}}}]}}}
2026-02-13T07:24:38.156Z INFO egress redis/redis.go:144 connecting to redis {"nodeID": "NE_JGn2CW6uEUET", "handlerID": "EGH_iSDJGyGmoBXM", "clusterID": "", "egressID": "EG_dz3JMaE3cvex", "simple": true, "addr": "localhost:6379"}
I am running egress, livekit and redis on a single machine. Attached is my init script for all the docker containers.lively-winter-88668
02/16/2026, 10:32 AMHi everyone, please be aware that we have a new community forum at community.livekit.io and the plan is to move support for this channel into that new forum. I understand such a move will not happen overnight, but LiveKit staff will start to prioritise the new forum when answering community support questions. Please see full details at https://livekit-users.slack.com/archives/C01KZ61HZRR/p1770299394024089, which is also the best place to ask questions and raise concerns.
broad-teacher-94403
02/16/2026, 5:59 PMbland-cpu-63716
02/17/2026, 7:30 AMonst startRecording = async (req, res) => { console.log("========== START RECORDING API CALLED =========="); console.log("Request Body:", req.body); console.log("Auth Type:", req.authType); console.log("User ID:", req.userId); console.log("Host ID:", req.hostId); console.log("Login ID:", req.login_id); try { const role = req.authType; console.log("Role Detected:", role); console.log("Fetching AWS Config..."); const config = await getAWSConfig(); console.log("AWS Config Loaded:", { livekitHost: config.livekitHost, apiKey: config.livekitApiKey ? "Present" : "Missing", bucket: config.bucket, region: config.region, }); console.log("Creating Egress Client..."); const egressClient = new EgressClient( config.livekitHost.replace("wss://", "https://"), config.livekitApiKey, config.livekitSecret ); console.log("Egress Client Created Successfully"); let hostId; let participantsToRecord = []; console.log("Checking Role Logic..."); if (role === 4) { console.log("Role is HOST (4)"); hostId = req.userId?.toString(); participantsToRecord = [host_${hostId}]; console.log("Host Participants:", participantsToRecord); } else if (role === 6) { console.log("Role is USER (6)"); hostId = req.hostId?.toString(); const userIdentity = req.login_id; const hostIdentity = "roothost"; participantsToRecord = [hostIdentity, userIdentity]; console.log("User Participants:", participantsToRecord); } else { console.log("Unauthorized Role:", role); return res.status(403).json({ success: false, message: "Recording not allowed for this role", }); } const roomName = call_${hostId}; console.log("Room Name:", roomName); console.log("Checking if recording already exists..."); console.log("Active Recordings Store:", activeRecordings.host); // Make sure the per-host store exists activeRecordings.host[hostId] = activeRecordings.host[hostId] || {}; // Now check per-user if (activeRecordings.host[hostId][req.userId?.toString()]) { console.log("Recording already running for user:", req.userId); return res.status(400).json({ success: false, message: "Recording already running for this user", }); } let filePath; if (role === 4) { filePath = recordings/host/${hostId}/${uuidv4()}.mp4; console.log("Host Recording Path:", filePath); } else { const userId = req.userId.toString(); filePath = recordings/user/${hostId}/${userId}_${uuidv4()}.mp4; console.log("User Recording Path:", filePath); } console.log("Preparing S3 File Output..."); const fileOutput = new DirectFileOutput({ filepath: filePath, // filepath: recordings/composite_${uuidv4()}.ogg, output: { case: "s3", value: new S3Upload({ accessKey: config.accessKey, secret: config.secret, bucket: config.bucket, region: config.region, }), }, }); console.log("Starting Room Composite Egress..."); console.log("Egress Options:", { layout: "grid", audioOnly: false, participants: participantsToRecord, }); const requestedUsers = [req.login_id,"roothost"]; // ["101", "204"] const audioTrackIds = []; const roomService = await getRoomService(); const participants = await roomService.listParticipants(roomName); for (const p of participants) { if (!requestedUsers.includes(p.identity)) continue; for (const pub of p.tracks) { console.log(pub,"pub") // MICROPHONE audio track // if (pub.source === 1 && pub.sid && !pub.muted) { audioTrackIds.push(pub.sid); // } } } console.log("Collected Audio Track IDs:", audioTrackIds); const egressRequest = { roomName, // include roomName here audioTrackIds, // array of strings file: fileOutput, // S3 or local output layout: "grid" // optional for video }; // const egress = await egressClient.startRoomCompositeEgress( // roomName, // fileOutput, // { // layout: "grid", // audioOnly: true, // audioTrackSelector: { // participants: requestedUsers // participant identities, NOT track SIDs // } // } // ); const egress = await egressClient.startTrackCompositeEgress(roomName, { audioTrackIds: [audioTrackIds], // only this user's track file: fileOutput }); console.log("Track composite egress started:", egress.egressId); // Ensure egressId exists before storing if (!egress?.egressId) { // wait for egress to be fully initialized if SDK provides an event await new Promise((resolve, reject) => { egressClient.once('egress_started', (event) => { if (event.egressId) { resolve(event.egressId); } else { reject(new Error("Egress started but egressId not found")); } }); // Optional: add timeout to reject after 10s setTimeout(() => reject(new Error("Egress start timed out")), 10000); }).then((id) => { egress.egressId = id; }); } console.log("✅ TRACK EGRESS STARTED:", egress.egressId); console.log("✅ EGRESS STARTED SUCCESSFULLY"); console.log("Egress ID:", egress.egressId); // activeRecordings.host[hostId] = egress.egressId; // Store egressId per host AND per user activeRecordings.host[hostId] = activeRecordings.host[hostId] || {}; activeRecordings.host[hostId][req.userId.toString()] = egress.egressId; console.log("Active Recording Updated:", activeRecordings.host); console.log("========== START RECORDING SUCCESS =========="); return res.json({ success: true, message: "Recording started", egressId: egress.egressId, filePath, }); } catch (error) { console.error("========== START RECORDING ERROR =========="); console.error("Error Message:", error.message); console.error("Stack:", error.stack); return res.status(500).json({ success: false, message: error.message, }); } };gorgeous-elephant-19666
02/17/2026, 6:59 PMlively-winter-88668
02/18/2026, 2:19 PMHi everyone, please be aware that we have a new community forum at community.livekit.io and the plan is to move support for this channel into that new forum. I understand such a move will not happen overnight, but LiveKit staff will start to prioritise the new forum when answering community support questions. Please see full details at https://livekit-users.slack.com/archives/C01KZ61HZRR/p1770299394024089, which is also the best place to ask questions and raise concerns.
melodic-kangaroo-88838
02/18/2026, 6:34 PMeager-pager-29628
02/20/2026, 10:00 AMlively-winter-88668
02/20/2026, 10:22 AMHi everyone, please be aware that we have a new community forum at community.livekit.io and the plan is to move support for this channel into that new forum. Please see full details here. The Slack community will remain, but we will do housekeeping of the technical channels after that date.
most-agency-42791
03/22/2026, 12:41 PMbrainy-stone-78705
03/23/2026, 2:39 PMbusy-fish-13948
03/23/2026, 6:10 PMlively-terabyte-73999
03/27/2026, 6:06 AMable-printer-77739
04/01/2026, 10:35 PMrapid-forest-54057
04/10/2026, 5:31 PMbillions-king-25562
04/11/2026, 7:57 PMfierce-vase-70658
04/16/2026, 8:12 AMalert-ghost-21188
04/20/2026, 5:48 AMearly-ability-54324
04/20/2026, 6:21 AMbreezy-toddler-90951
05/09/2026, 4:30 PMpolite-oil-10264
05/11/2026, 5:12 PMlively-winter-88668
05/27/2026, 8:43 AM