Starting egress before making an outbound call, ma...
# ask-ai
g
Starting egress before making an outbound call, makes it sound like the agent is not synchronized when the caller connects. How do I fix this so egress is properly recording what goes on in the call.
t
Thanks for reaching out, Aumesh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I understand you're seeing desynchronization when you start egress before an outbound SIP call is answered, and you want recordings to properly capture the full call. Short answer: start egress only after the outbound call is connected (or use Auto‑Egress tied to your dispatch rules). Starting egress before the caller answers means the recorder can join too early and miss or misalign parts of the conversation. What to do 1. Start the outbound call with the call API set to wait until answered, then start egress after you receive confirmation the call is connected. ◦ Example flow: i. Dispatch the agent into the room. ii. Create the SIP participant with
wait_until_answered: true
. iii. When the call is answered, start room composite egress to record. 1. If you want recording to start automatically when the external participant joins, use Auto‑Egress on your dispatch rule for SIP outbound calls. That will trigger recording at the moment the call connects so egress and the live call are synchronized. Why this fixes it • If egress joins before the remote party, recordings may start with gaps or audio only from the agent; starting egress after connection ensures the recorder sees both sides from the same point in time. If you’d like, tell me: • Which SDK/language you’re using (Python, JS, etc.), and • Whether you prefer manual control or Auto‑Egress tied to dispatch rules. I can then provide a short, concrete code example for your setup or the exact dispatch rule configuration to enable Auto‑Egress. Sources: examples/bank-ivr | livekit/livekit_composite | DeepWiki | Python SDKs | livekit/livekit_composite | DeepWiki | Why is there extra audio at the end of my recordings of an agent session? |...
g
The issue is if egress fails to start I want to make sure that we never dial the caller. Can you give me both examples for manual egress and auto-egress setup
t
Thanks — I understand you need two clear options: (1) a manual egress flow that guarantees you never dial the caller unless egress recording has started successfully, and (2) how auto‑egress behaves and what it can/can’t do for that requirement. Below are concise, actionable examples and recommended flow for each. 1) Manual egress (recommended when you must NOT dial unless recording started) • Core idea: start egress first, confirm it started (API response or webhook), and only then create the SIP participant (dial). If egress fails to start, do not dial. Example (Python, simplified flow) 1. Dispatch agent and ensure agent is in the room. 2. Start egress (call the room composite start API). 3. Wait for confirmation that egress started (API success or listen for
egress_started
webhook). 4. If confirmation received → create SIP participant with
wait_until_answered: true
. 5. If egress failed → abort dialing and handle error. Python sketch:
Copy code
# 1) Start egress
egress_resp = await livekit_api.egress.start_room_composite_egress(
    StartRoomCompositeEgressRequest(
        room_name=room_name,
        output=RoomCompositeEgressRequest(
            file=EncodedFileOutput(
                file_type=EncodedFileType.MP4,
                filepath=f"recordings/{room_name}.mp4",
                # s3 config...
            )
        )
    )
)

# 2) Confirm egress started (either from egress_resp or from webhook)
if egress_started:   # true only after confirmation
    # 3) Dial
    await livekit_api.sip.create_sip_participant(
        CreateSIPParticipantRequest(
            room_name=room_name,
            sip_trunk_id=sip_trunk_id,
            sip_call_to=phone_number,
            participant_identity=f"caller_{phone_number}",
            wait_until_answered=True,
        )
    )
else:
    # Do not dial — log/alert and clean up
    pass
Notes and hardening: • Use the egress API response where it includes an egress id and status. For maximum reliability, treat the API start response as tentative and rely on the
egress_started
webhook event to confirm the recorder is actively running. • Implement a timeout: if no
egress_started
event arrives within N seconds, treat as failure and do not dial. • If your process must be atomic, keep the dial attempted only after webhook confirmation; this ensures the remote party is never called if egress fails. 2) Auto‑Egress (what it does and its limitation for your use case) • Core idea: configure the SIP dispatch rule to include egress configuration. When the SIP participant/dispatch rule runs and the call connects, LiveKit will automatically start egress for that room. • Important constraint: Auto‑Egress does not give you a way to block the outbound dial if egress fails. Recording is started automatically when the call connects; if egress fails to start, the call may still have been placed. Therefore auto‑egress cannot guarantee “never dial unless recording started.” Example dispatch rule snippet (JSON)
Copy code
{
  "dispatch_rule": {
    "trunk_ids": [
      "TR_XXX"
    ],
    "rule": {
      "dispatchRuleIndividual": {
        "roomPrefix": "call-"
      }
    },
    "room_config": {
      "egress": {
        "room_composite": {
          "outputs": [
            {
              "file": {
                "filepath": "recordings/{{.Room}}.mp4",
                "s3": {
                  "bucket": "your-bucket"
                }
              }
            }
          ]
        }
      }
    }
  }
}
• Apply via your dispatch configuration method (CLI/management API). • Behavior: when an outbound SIP call routed by that rule connects, LiveKit will create the room and start room composite egress automatically. Recommendation summary • Use manual egress control if your requirement is strict: do not dial unless you have firm confirmation that egress is actively recording. • Use webhook confirmation (listen for
egress_started
) rather than only the API start response for the strongest guarantee. • Use auto‑egress only if you prefer an automated, simpler setup and you accept that a dial may occur even if egress later fails. If you’d like, tell me: • Which SDK/language you want a complete working code sample for (JS/Node, Python, or Go). • Whether you already have webhook handling in place (so I can show the exact webhook event to wait for and sample handling logic). I’ll provide a ready-to-run example tailored to your stack (including webhook handling and timeouts) so you can enforce “do not dial unless egress confirmed.”
g
I have done it the manual egress way, but I face audio synchronization errors (meaning the recording shows the agent starting much earlier than it actual does).
t
Thanks — I hear you: you used the manual-e gress flow to avoid dialing unless recording started, but the resulting recordings show the agent “starting” earlier than they actually do. Let’s diagnose and fix the synchronization so recordings accurately reflect call timing. A few targeted questions so I can give a precise fix: 1. Which SDK/language are you using (Python, Node, Go, or other)? 2. Exactly when do you call start egress today — on room creation, when the agent joins, right after dispatch, or after the SIP call is created? 3. Which egress API are you using:
start_room_composite_egress
,
start_track_composite_egress
, or another? 4. Do you rely on the API start response to confirm egress, or do you wait for a webhook/event (for example
egress_started
)? 5. Are you using
wait_until_answered=True
when creating the SIP participant? 6. Can you share the small snippet that triggers egress and the snippet that creates the SIP participant (sanitized keys/IDs is fine)? Meanwhile, common causes and concrete fixes to try now • Cause: egress is starting before the SIP participant is fully connected (or before the agent’s media is actually flowing). That can make the recorder capture early silence or pre-call agent audio, which appears as the agent starting early. ◦ Fix: Start egress only after you have event confirmation that both relevant participants are present and actively publishing audio: ▪︎ Wait for the SIP participant’s connected/joined event (or
wait_until_answered=True
plus
participant_joined
webhook) and the agent’s
participant_joined
event, then start egress. ▪︎ Prefer using the
egress_started
webhook as the definitive confirmation of active recording rather than only the immediate start API response. • Cause: Using room composite egress can capture a hidden recorder participant join time that doesn’t align with when tracks begin sending audio. ◦ Fix: Try
track_composite_egress
or explicitly select the agent’s and caller’s tracks so recording starts relative to actual track timestamps. • Cause: Race conditions between starting egress and the agent’s publishing of audio (agent appears earlier in the recording timeline). ◦ Fixes: ▪︎ Add an explicit short verification step after participants join: check participant state (is_publishing audio track) before starting egress. ▪︎ Use a small startup buffer: only start egress once both sides have an active audio track (or after receiving a few milliseconds of audio-level activity). • Reliability tip: Use webhooks + timeout ◦ Wait for the
participant_joined
(or equivalent) webhook for both agent and SIP caller, then start egress. If no webhook within your timeout, abort dialing or retry egress. ◦ If you already start egress first to guard dialing, keep egress start but only dial after receiving
egress_started
confirmation (this avoids dialing when egress failed) and still wait for actual publishing before treating the recording as valid. If you tell me the SDK and share the short code paths for: • how you start egress, and • how/when you create the SIP participant I’ll give a concrete code sample (node/python) showing: 1) waiting for participant track/publish events, 2) starting egress at the right moment (or using
track_composite_egress
), and 3) robust webhook/t imeout handling so you never dial unless recording is confirmed and recordings are synchronized. Sources: Best practices for managing webhook event streams | LiveKit | Universal Egress | Turns overview | LiveKit Documentation | server-sdk-ruby | livekit/livekit_composite | DeepWiki | Outbound Calling Script | LiveKit Documentation
g
I am using python sdk. I first: 1. make a dispatch requests 2. Then in the entrypoint I add egress 3. Then I do
Copy code
session_started = asyncio.create_task(
            session.start(
                agent=agent,
                record=True,
                room=ctx.room,
                room_input_options=RoomInputOptions(
                    # enable Krisp background voice and noise removal
                    noise_cancellation=noise_cancellation.BVCTelephony(),
                ),
            )
        )
4. Then I dial the end participant
t
Thanks — this is very helpful. You’re already close: dispatch → add egress in entrypoint → start the session → dial. The problem is that starting egress in the entrypoint (before the SIP participant has fully joined and started publishing audio) can produce recordings where the agent appears to start earlier than they actually do. Below are concrete changes and a Python example to get recordings properly synchronized. Main idea: only start or mark the recording as “active” after both sides are connected and actually publishing audio. Use the SDK start response plus webhooks/events to confirm readiness. Key changes to your flow • Do not treat the initial egress start (called in the entrypoint) as the point-of-truth for when recording should capture call audio. Instead: ◦ Start egress only after you confirm the SIP participant has answered and both agent and SIP participant have active audio tracks; or ◦ If you must call start egress earlier to guard dialing, wait for the
egress_started
webhook AND for both participants to be publishing audio before considering the recording valid. • Use
wait_until_answered=True
when creating the SIP participant so the create call only returns once the remote answered. • Prefer tracking actual publishing state (participant events or checking published tracks) rather than purely timing-based waits. • Consider
track_composite_egress
if you need tighter control of which tracks/timestamps are recorded. Concrete Python pattern (high-level) 1. Dispatch and ensure agent enters the room. Wait for agent’s
participant_joined
and confirm they are publishing audio (or at least ready). 2. Start egress OR prepare to start it, but do not dial yet. 3. Create the SIP participant with
wait_until_answered=True
so the call creation only returns once the remote answered. 4. After the SIP participant is connected, verify both participants have active audio tracks (either via participant state or via an event listener). 5. Only then consider the recording start confirmed. If you started egress earlier, wait for
egress_started
webhook before dialing; if you start egress now, call the start API and wait for its confirmation. 6. If any of the verification steps fail (egress fails to start, SIP call not answered, no track publishing), abort dialing or retry as appropriate. Example sketch (Python, async) — pattern to adapt to your code
Copy code
# Pseudocode / structural example — adapt to your existing clients and handlers

async def start_call_with_strict_egress_guard(lk_api, room_name, agent, phone_number, sip_trunk_id):
    # 1) Dispatch agent and wait for agent join + publishing
    await dispatch_agent_to_room(agent, room_name)
    await wait_for_participant_join(room_name, agent.identity, timeout=10)
    await wait_for_participant_publishing_audio(room_name, agent.identity, timeout=5)

    # 2) (Optional) start egress now to reserve egress and get an egress_id
    # If you start egress now, do NOT dial until you receive egress_started webhook.
    try:
        egress_resp = await lk_api.egress.start_room_composite_egress(
            StartRoomCompositeEgressRequest(
                room_name=room_name,
                output=...,  # your output settings
            )
        )
        egress_id = egress_resp.egress_id
    except Exception as e:
        # Egress failed to start — do not dial
        log("egress failed to start, aborting dial", e)
        return

    # 3) Create SIP participant but ensure the call is answered before returning
    sip_resp = await lk_api.sip.create_sip_participant(
        CreateSIPParticipantRequest(
            room_name=room_name,
            sip_trunk_id=sip_trunk_id,
            sip_call_to=phone_number,
            participant_identity=f"caller_{phone_number}",
            wait_until_answered=True,   # critical: block until remote answers
        )
    )

    # 4) Confirm the SIP participant joined AND is publishing audio
    caller_identity = sip_resp.participant_identity  # or the identity you passed
    await wait_for_participant_join(room_name, caller_identity, timeout=10)
    await wait_for_participant_publishing_audio(room_name, caller_identity, timeout=5)

    # 5) Confirm egress actually started (if you started it earlier) via webhook event
    # You should have a webhook handler that sets egress_confirmed[egress_id] = True
    if not await wait_for_egress_confirmed(egress_id, timeout=10):
        # Egress did not confirm — hang up / cleanup / do not treat recording as valid
        log("egress didn't confirm, tearing down call and cleaning up")
        await cleanup_room(room_name)
        return

    # At this point both participants are connected, publishing, and egress recording is confirmed.
    log("call connected and recording confirmed — proceed with call flow")
Important implementation details • wait_for_participant_join / wait_for_participant_publishing_audio: ◦ Implement these by subscribing to participant lifecycle events or polling room state via the admin/room APIs. ◦ Check whether the participant has any published audio tracks and whether those tracks have nonzero audio levels for a short period (a few frames) to ensure the publisher actually started sending media. • Webhook handling: ◦ Use the
egress_started
webhook to confirm the egress is actively recording. Treat the API start response as optimistic; rely on the webhook for proof-of-start. ◦ Use
participant_joined
and
participant_published_track
(or equivalent) events for both agent and caller. • Timing and timeouts: ◦ Use conservative short timeouts (5–15s) to avoid hanging your call flow. If timeouts happen, abort dialing or retry egress based on your policy. • Consider `track_composite_egress`: ◦ If timestamps still drift,
track_composite_egress
can let you pick exact tracks (agent + caller) and may produce cleaner sync between those tracks. ◦ Start track-based egress only once both tracks exist. • Small startup buffer: ◦ If you observe very small residual offset, consider delaying the visible start of the final recording by a few hundred milliseconds after both tracks are active (trim the beginning or add a small aligned silence) to align perceived start-of-speech. Next steps I can help with • I can provide a runnable Python example tailored to your existing session.start() usage and webhook handlers (showing exact event names and parsing). • Or, if you share the small snippets where you currently call
session.start(...)
and then dial, I will rewrite them to: ◦ start egress in a guarded way, ◦ wait for egress_started webhook, ◦ wait for both participants publishing audio, ◦ then proceed to dial or mark the recording as live. Which would you prefer — a full runnable Python example using your current
session.start()
pattern, or I should sketch the exact webhook handlers and state machine to add to your entrypoint?