Outbound SIP call via Plivo Zentrunk. Agent audio is confirmed good (published track, 298 KB upstream, SIP participant received 141 KB downstream, 0% packet loss) but the callee hears complete silence for the entire call. Inbound audio works — the agent transcribes the caller correctly. SIP response 200, codec PCMU/8000, TLS signaling, India region.
PCAP shows as unavailable on the call page. Could you check whether RTP was sent toward Plivo for this call, and whether the Opus→PCMU transcode succeeded?
Plivo’s CDR reports codec G722 while the LiveKit call page shows PCMU/8000.
@Gurbir_Singh That codec discrepancy you spotted is the most likely cause, and worth chasing before the RTP question.
LiveKit SIP offers PCMU, PCMA and G722 by default. So if Plivo’s CDR says G722 while the LiveKit call page says PCMU, the two ends disagree about what is in the stream. The docs are explicit about what that produces: “the call might connect, but audio exchange fails between endpoints or requires an external service to transcode the audio.” That matches your symptoms exactly, including healthy byte counts and 0% loss, since packets flow fine when only the decode is wrong.
Quick test, pin the offer to PCMU on the outbound trunk or on CreateSIPParticipant:
The rest of what you asked, whether RTP actually left toward Plivo and why the PCAP shows unavailable, needs server-side visibility. That part is one for LiveKit staff with your call ID.
Thank you @abidullahcs.uk . That’s a very useful pointer, and the onlyListedCodecs option looks like exactly the right thing to try. I’ll pin the outbound trunk to PCMU and report back with the result.
One detail that may narrow it further: I pulled the SIP PCAP from Plivo’s side, and the Plivo↔carrier leg negotiates cleanly - Plivo offers RTP/AVP 8 0 101, the carrier answers RTP / AVP 0 8 101 (PCMU first), both sendrecv, valid port, 0% loss reported on the LiveKit side. So the disagreement isn’t on that leg. It has to be on the LiveKit↔Plivo leg, which is where Plivo’s CDR reports G722 while the LiveKit call page shows PCMU/8000 for the same call.
That also matches livekit/sip#95, where LiveKit’s SDP answered with m=audio … RTP/AVP 0 while still sending a=rtpmap:9 G722/8000 - an m-line and rtpmap that disagree. Packets flow, the decode is wrong, and you get exactly what we see: healthy byte counts and total silence.
Two things I’d still value your view on:
If pinning to PCMU fixes it, is the underlying issue worth reporting - a default codec set that can produce a mismatch against a provider that itself negotiates correctly?
On the ordering side, the PCAP shows 183 Session Progress with P-Early-Media: sendonly at 16:11:59.42 and 200 OK at 16:12:01.11, while our worker considered the call answered at 16:11:56. Is waitUntilAnswered on CreateSIPParticipant the intended gate for “don’t speak yet”, or is watching sip.callStatus reach active the more reliable signal? We’re fixing that side regardless.
What would make it stick is the piece you don’t have yet. Your PCAP covers Plivo↔carrier and clears that leg nicely. The disagreement is on LiveKit↔Plivo, so it’s the SDP for that leg you want, and Plivo support can usually pull it. With that you’ve got a real report. Without it, it’s a CDR field disagreeing with a dashboard.
On the gating, they’re not alternatives, the SDK leans on both. From the AMD docstring in voice/amd/detector.py:
For SIP participants, the no-speech timer and audio/transcript processing are deferred until sip.callStatus == “active” so pre-answer audio (ringback, carrier early media, dialtone) does not poison the classifier or burn the no-speech budget.
And its recommended pattern still passes wait_until_answered=True. So one gates when the API call returns, the other is what the framework itself trusts before touching media. Your 183 with P-Early-Media: sendonly is precisely the case that docstring is describing, so I’d use both.
One small thing from your timeline: your worker called it answered at 16:11:56, ahead of the 183 at 16:11:59.42 and the 200 OK at 16:12:01.11. Whatever it’s keying on there isn’t either of them.
Fair correction on sip#95 - I’d checked the issue and not its close state. Good to know it’s long fixed.
I’ve asked Plivo for the LiveKit-facing leg’s SDP. Agreed that without it there’s no report worth filing, just two dashboards disagreeing.
The gating answer is the useful part - thank you. Deferring media handling until sip.callStatus == “active” while still passing waitUntilAnswered is exactly the shape I needed, and your point that our 16:11:56 lines up with neither the 183 nor the 200 OK is right. It’s keying on something else entirely, most likely the SIP participant joining the room, which happens well before the carrier answers. That’s the bug on our side and it’s being fixed now.
I’ll report back with the PCMU pin result and the LiveKit-side SDP once Plivo sends it.
Do you have a PCAP for the call? You can get one from LiveKit side if you disable TLS and try it. You will see the PCAP on your dashboard for that call.
that explains the blank PCAP field: our trunk uses TLS signaling, so there’s nothing capturable. I’ll try a plain-signaling call to get one, but not yet - I’ve just pinned the offer to PCMU only via CreateSIPParticipant’s media config, and I want that tested in isolation before changing a second variable.
@abidullahcs.uk - will do. If the PCMU pin doesn’t fix it, the TLS-off PCAP is exactly what gets us the LiveKit-facing SDP that Plivo support hasn’t sent yet, and I’ll post what it shows.
Two notes for anyone who finds this thread later:
SIPMediaConfig in livekit-server-sdk@2.17.0 is a protobuf message class, not a plain object - passing an object literal type-errors rather than silently doing nothing, which is the failure mode you’d want here.
And on the timing: we already passed waitUntilAnswered: true. What was missing was the media-side half - deferring anything that produces audio until the sip.callStatus attribute reaches active. Those really are two separate gates, as you said.
@Gurbir_Singh Makes sense to keep those separate, the TLS-off call will still be there afterwards.
Worth having your SIPMediaConfig note in the thread. The docs show that block as JSON, which is what I pasted, so anyone on the typed SDK is going to hit exactly what you hit. Type-erroring beats a config that quietly does nothing.