I have a telephony agent and I want to record and ...
# ask-ai
h
I have a telephony agent and I want to record and export the conversation audio to an mp4 to S3. The file gets created and the agents audio is recorded but not the caller participants audio. Why would the callers audio get dropped? I'm using audio only room composite egress.
✅ 1
t
Thanks for reaching out, Roman. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m responding about your audio-only room composite egress where the agent audio is present but the caller audio is missing from the MP4 export. Short answer: room composite egress records the mixed audio that exists in the room. If the caller’s audio is dropped while the agent’s is recorded, the most likely cause is that the caller’s audio track is not being published or the caller isn’t actually connected to the LiveKit room that the egress joined. That behavior is not an inherent limitation of composite egress — it captures whatever audio would be heard by a participant in the room. Here are focused checks and actions you can take to find and fix the issue: 1) Verify the caller is joined to the same LiveKit room the egress uses • Confirm your SIP connector (or SIP->LiveKit flow) is successfully joining the SIP participant into the room before egress starts. • Check room participant lists at runtime to ensure the SIP/caller participant appears. 2) Confirm the caller’s audio track is published • Inspect server-side room state or logs to see if the caller has an active audio track (look for a published track/event for the SIP participant). • If you’re using a SIP gateway or a headless client to bridge the call, ensure it publishes the caller RTP into a LiveKit audio track rather than muting or dropping it. 3) Check egress timing: start egress after the caller’s track is present • If egress starts before the caller is joined/publishing, the initial recording may miss audio. Start egress after both participants and their audio tracks are present, or enable an auto-egress/dispatch rule that triggers once the call is connected. 4) Inspect codec/SSRC/transport issues in bridging • If you bridge SIP -> LiveKit, ensure the gateway correctly maps caller audio codecs and streams into LiveKit tracks. Misconfigured codec negotiation or dropped RTP could result in no audio published for the caller. 5) Verify egress configuration is audio-only composite (not single participant) • Confirm you’re using room composite egress (composite mixed audio) and not a recording mode that captures only a single participant or only the agent. 6) Look for logs and diagnostics to confirm what egress captured • Check egress logs (GStreamer/PulseAudio stages) and LiveKit room logs to confirm the mixed audio input the egress saw. If the egress shows only agent audio in its input, the upstream room mix is missing the caller. If you want, I can help narrow this down — please tell me: • How the caller is connected (SIP gateway / SIP connector name or flow) • Whether the SIP participant appears in the LiveKit room participant list • The sequence you use to start the call and start composite egress (do you start egress automatically, or manually after joining?) • Any relevant logs showing participant join/publish events or egress start events With those details I can suggest exact places to check or configuration changes to ensure both agent and caller audio are included in the MP4 sent to S3. Sources: Universal Egress | LiveKit | Universal Egress | LiveKit | examples/voice_agents | livekit/livekit_composite | DeepWiki | sip | livekit/livekit_composite | DeepWiki, with interactive elements
h
I’m using Telnyx and Livekit SIP cloud service. Show me what code changes I can make to help troubleshoot
t
Thanks — I’m responding about adding code-level checks and small changes you can make while using Telnyx + LiveKit SIP cloud so you can find why the caller audio isn’t being published to the room (and therefore not captured by composite egress). Before I give examples, please share the minimal snippets you’re currently using for: • The SIP gateway / Telnyx webhook or SIP connection code that bridges the PSTN caller into LiveKit. • How you create/join the LiveKit room for the SIP participant. • How/when you start the audio-only composite egress (API call or dispatch rule). If you can paste those, I’ll tailor fixes. Meanwhile, below are focused, practical code and logging changes you can add to pinpoint the problem. Pick the language/area that matches your setup. 1) Log participant join + published tracks (Node/JS example) • Add detailed logging when participants join the room and when they publish/unpublish tracks. This confirms whether the SIP caller is present and whether an audio track exists.
Copy code
// Assuming you have a LiveKit server-side SDK client and a way to listen to room events
room.on('participantConnected', (p) => {
  console.log(`[room] participant connected: ${p.identity} (${p.sid})`);
});

room.on('trackPublished', (publication, participant) => {
  console.log(`[room] track published by ${participant.identity}: kind=${publication.kind}, trackSid=${publication.trackSid}, muted=${publication.isMuted}`);
});

room.on('trackUnpublished', (publication, participant) => {
  console.log(`[room] track unpublished by ${participant.identity}: kind=${publication.kind}`);
});
What to look for: a
trackPublished
event for the SIP/caller participant with kind=audio. If you do not see it, the caller audio is not being published. 2) Confirm SIP gateway publishes audio from Telnyx (diagnostic steps) • In your Telnyx bridge/webhook code add logging for inbound RTP / media state and for any SIP -> LiveKit publish operations. • If you use a SIP gateway process (e.g., a media bridge), log when RTP packets are received from Telnyx and when you create a LiveKit Producer/Track from that RTP. Example pseudo-logging:
Copy code
[TELNYX] INVITE from +1xxx received, call-id=abc123
[TELNYX] RTP recv from 203.0.113.10: media packets count=1234
[BRIDGE] Creating LiveKit audio track for sipParticipantId=telnyx-abc123
[BRIDGE] LiveKit publish succeeded: trackSid=tr_...
What to look for: incoming RTP packets and a successful LiveKit publish. If RTP is not received, Telnyx settings (SIP ACL, RTP IPs/ports) may be blocking/incorrect. 3) Validate codecs and compatibility • Log negotiated codecs on the SIP side and check the LiveKit published track codec. If codec negotiation fails or the bridge drops media conversion, audio may not be published. Example checks to log (pseudo):
Copy code
[SIP] negotiated codecs: PCMA, PCMU, opus
[BRIDGE] converting PCMA -> opus: success
4) Start egress after the caller audio is present (timing) • If you’re programmatically starting egress, add a short delay or wait for the SIP participant’s audio trackPublished event before starting egress. Node/JS example:
Copy code
// wait for the caller to publish audio before starting egress
const maxWait = 10000; // ms
let audioPublished = false;

function waitForAudioPublished(participantSid) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => reject(new Error('audio not published in time')), maxWait);
    room.on('trackPublished', (publication, participant) => {
      if (participant.sid === participantSid && publication.kind === 'audio') {
        clearTimeout(timeout);
        audioPublished = true;
        resolve();
      }
    });
  });
}

// usage
await waitForAudioPublished(sipParticipantSid);
startCompositeEgress(...);
5) Capture egress-side input diagnostics • Enable verbose logging on the egress request and check any egress logs you get back. Confirm egress reports seeing mixed audio channels. • If using LiveKit egress API, include the room name and time you started the egress in your logs to correlate with participant publish events. 6) Quick test: create a simple WebRTC participant that plays the SIP RTP to the room • To rule out the gateway, create a test client that receives the Telnyx RTP stream locally and publishes a simple audio source (e.g., an injected audio file or loopback) to the room under a test identity. If that test audio appears in the recording, the issue is in the Telnyx->bridge step. 7) Add a participant-level mute/volume check • Log participant publication
muted
or
isMuted
flags. A muted publication will be silent to the room mix. 8) Inspect GStreamer / egress logs • If you can access egress logs, confirm what audio input GStreamer receives (it should show the room mixed audio). If GStreamer only shows the agent audio, that confirms the room mix lacks the caller. If you want, I can: • Draft concrete snippets for the exact SDK you use (Node, TypeScript, Python, or whatever you use for your SIP bridging). • Review any logs or code you paste here and point to the exact lines to change. Which language and pieces of code can you share (Telnyx webhook/bridge + LiveKit join/publish + egress start)? I’ll give precise edits and logging statements you can drop in. , with interactive elements
h
I need python examples
t
Thanks — I’m responding to your request for Python examples to troubleshoot why the caller’s audio (Telnyx/SIP participant) isn’t included in your audio-only room composite egress. Below are focused Python snippets and guidance you can drop into your SIP bridge / LiveKit server-side code. They help you: • log participant joins and published tracks, • confirm the SIP participant’s audio track is published, • wait for the caller audio before starting composite egress, • add basic SIP/RTP diagnostics to your bridging logic. If you share which LiveKit Python SDK or HTTP approach you use for egress/start calls, I can tailor the egress request snippet to your code. 1) Log participant joins and track publishes (server-side) • Use the LiveKit realtime events or poll room state. If you have a long-running room connection or can query room state, poll participants and publications. Example: polling room state via the LiveKit REST API (pseudo-HTTP). Replace
LIVEKIT_API_URL
,
API_KEY
,
API_SECRET
, and
room_name
with your values.
Copy code
import requests
import time
from requests.auth import HTTPBasicAuth

LIVEKIT_API_URL = "https://your-livekit-host/api"  # adjust
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
ROOM_NAME = "my_room"

def get_room_participants(room_name):
    url = f"{LIVEKIT_API_URL}/rooms/{room_name}"
    resp = requests.get(url, auth=HTTPBasicAuth(API_KEY, API_SECRET))
    resp.raise_for_status()
    return resp.json()

def log_room_state():
    room = get_room_participants(ROOM_NAME)
    participants = room.get("participants", [])
    print(f"[room] participants count={len(participants)}")
    for p in participants:
        identity = p.get("identity")
        sid = p.get("sid")
        print(f" - participant: identity={identity}, sid={sid}")
        for pub in p.get("publishedTracks", []):
            kind = pub.get("kind")
            track_sid = pub.get("sid")
            muted = pub.get("muted", False)
            print(f"   - track: kind={kind}, trackSid={track_sid}, muted={muted}")

if __name__ == "__main__":
    while True:
        try:
            log_room_state()
        except Exception as e:
            print("Error fetching room state:", e)
        time.sleep(5)
What to look for: the SIP/caller participant appears and has a published track with kind == "audio" and muted == False. If no audio track appears for the SIP participant, caller audio is not published into the room. 2) Server-side event handling via LiveKit webhook (recommended) • Configure LiveKit webhooks to notify your server on participantConnected / trackPublished events. Log those events and correlate them to your egress start. Example Flask webhook receiver:
Copy code
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/livekit/webhook", methods=["POST"])
def livekit_webhook():
    event = request.get_json()
    event_type = event.get("type")
    print(f"[webhook] event: {event_type}")
    if event_type == "participant_connected":
        p = event.get("participant", {})
        print(f" participantConnected: identity={p.get('identity')}, sid={p.get('sid')}")
    elif event_type == "track_published":
        pub = event.get("publication", {})
        p = event.get("participant", {})
        print(f" trackPublished: participant={p.get('identity')}, kind={pub.get('kind')}, trackSid={pub.get('sid')}, muted={pub.get('muted')}")
    return jsonify({"ok": True})
What to look for: a
track_published
event for the SIP/caller with
kind: "audio"
. 3) Wait for caller audio before starting composite egress • Start composite egress only after you detect the SIP participant’s audio trackPublished. This prevents recordings that start before the caller publishes audio. Example: use polling + egress start call.
Copy code
import time
import requests
from requests.auth import HTTPBasicAuth

# reuse LIVEKIT_API_URL, API_KEY, API_SECRET, ROOM_NAME from earlier

def caller_has_audio(room_name, caller_identity):
    room = get_room_participants(room_name)
    for p in room.get("participants", []):
        if p.get("identity") == caller_identity:
            for pub in p.get("publishedTracks", []):
                if pub.get("kind") == "audio" and not pub.get("muted", False):
                    return True
    return False

def start_composite_egress(room_name, output_s3_path):
    url = f"{LIVEKIT_API_URL}/egress/start"
    payload = {
        "room": room_name,
        "type": "room_composite",
        "audio_only": True,
        "output": {
            "s3": {
                "bucket": "your-bucket",
                "path": output_s3_path
            }
        }
    }
    resp = requests.post(url, json=payload, auth=HTTPBasicAuth(API_KEY, API_SECRET))
    resp.raise_for_status()
    return resp.json()

def ensure_egress_after_audio(room_name, caller_identity, output_s3_path, timeout=15):
    start = time.time()
    while time.time() - start < timeout:
        if caller_has_audio(room_name, caller_identity):
            print("[egress] caller audio present, starting composite egress")
            return start_composite_egress(room_name, output_s3_path)
        print("[egress] caller audio not yet present, waiting...")
        time.sleep(1)
    raise TimeoutError("Caller audio not published in time")

# usage
if __name__ == "__main__":
    resp = ensure_egress_after_audio(ROOM_NAME, "sip_caller_identity", "calls/rec1.mp4")
    print("egress started:", resp)
Note: adapt the egress start payload to the actual LiveKit egress API shape you use. 4) Telnyx / SIP bridging diagnostics (Python) • In your Telnyx webhook or bridge component, add logs for incoming SIP INVITE, answer, and RTP reception. If you use a SIP library, log RTP packet counts and payload types. If you use a media bridge that maps RTP -> LiveKit track, log the LiveKit publish success. Example pseudocode for Telnyx webhook handling:
Copy code
from flask import Flask, request

app = Flask(__name__)

@app.route("/telnyx/webhook", methods=["POST"])
def telnyx_webhook():
    ev = request.get_json()
    print("[telnyx] event type:", ev.get("event_type"))
    # Example: call.initiated / call.answered / media. event names depend on your Telnyx setup
    print("[telnyx] full payload:", ev)
    return "", 200
If you control a media bridge that receives RTP, add logging in the RTP handling path:
Copy code
# pseudo RTP handler
def on_rtp_packet(packet, src_addr):
    global rtp_count
    rtp_count += 1
    if rtp_count % 100 == 0:
        print(f"[rtp] received {rtp_count} packets from {src_addr}; last_payload_type={packet.payload_type}")
What to look for: consistent RTP packets arriving from Telnyx. If RTP is not arriving, check Telnyx call settings (SIP ACLs, RTP IPs/ports, NAT). 5) Quick test: publish an injected audio file as the SIP participant (Python) • To rule out the bridge, create a small test client that simulates the SIP participant by publishing an audio file into the room. If that audio appears in recordings, it narrows the problem to the Telnyx -> LiveKit bridge. If you want, I can: • Provide a concrete egress start payload format for LiveKit's egress API if you tell me whether you use LiveKit's HTTP egress API or a client SDK. • Draft a Telnyx-to-LiveKit bridge example showing how to receive Telnyx RTP and publish into LiveKit (this may be longer and depend on your media stack). Which of the following should I produce next? • A complete Python example that starts a minimal LiveKit client and publishes an audio file (to use as a test caller). • A LiveKit egress start example in Python that targets S3 with correct payload shape (please confirm you use the LiveKit HTTP egress API). • A Telnyx bridging snippet showing where to add RTP and publish logging (you can paste the gateway code and I’ll annotate it). Pick one and I’ll generate the tailored code. , with interactive elements
h
Which encoding type should I for the roomcomposite egress? MP4? Ogg? I'm just interested in audio
t
Thanks — I’m responding about which encoding/container to choose for your audio-only room composite egress. Short answer • Use MP4 (AAC) if you want maximum compatibility for playback, editing, or sharing the recording. • Use Ogg (Opus) if you prefer native WebRTC codec (Opus) quality and minimal transcoding for downstream processing (transcription, analytics). Why those two: • MP4: Broad device and tooling support. If you need to hand the file off to players, editors, or non-audio-specialized systems, MP4/AAC is the safest choice. • Ogg/Opus: Opus is the same codec WebRTC commonly uses and preserves voice quality and efficiency. Choose this when you control downstream tooling (transcription, ML pipelines) or want to avoid extra CPU/time for transcoding. Practical guidance for your telephony use case • If you only want a simple, compatible recording you can play in any media player or upload to S3 for business users, pick MP4. • If you are doing speech-to-text, analytics, or archiving where quality/size matters, pick Ogg/Opus to avoid unnecessary transcoding. • Both formats will include audio from all participants that are actually published into the LiveKit room. Choosing the format does not affect whether the caller’s audio is present — the missing audio is due to publishing/bridge issues, not the container. Quick checklist to pair with the format decision • Start egress only after you confirm the SIP/caller audio track is published (use the trackPublished event or poll room state). • If you pick MP4 and want AAC: confirm egress settings specify MP4 output (audio_only True / layout none). • If you pick Ogg: request Ogg/Opus output so no codec conversion is needed. • If you still see missing caller audio, follow the debugging steps we discussed earlier (verify SIP participant appears, confirm RTP is arriving from Telnyx, log trackPublished events, inspect egress/GStreamer input). If you want, I can: • Provide a concrete Python egress start payload for MP4 -> S3 or Ogg -> S3 (HTTP request shaped to your LiveKit egress API). • Show the small change to wait for a caller’s
trackPublished
event before calling egress (Python snippet). Which output would you like the example for — MP4 (AAC) or Ogg (Opus)? Sources: Universal Egress | LiveKit | Python SDKs | livekit/livekit_composite | DeepWiki, with interactive elements