Outbound SIP: last word of voicemail clipped on hangup
Describe the bug
When an outbound agent leaves a voicemail and hangs up right after wait_for_playout() returns, the last word is sometimes missing on the callee's end, even though our session logs and recordings show the complete audio. Intermittent, worse when the message ends abruptly with no trailing pause.
Server
- Version: 1.5.17 (couldn't find a fix for this in the changelog, happy to close if it's already landed in a different release)
- Environment: LiveKit Cloud, outbound via Twilio SIP trunk
- Voicemail detection is our own LLM-based tool, not LiveKit's AMD (shouldn't be relevant to root cause IMO)
To Reproduce
- Have the agent leave a voicemail (maybe session.say()) ending on a long word with no trailing silence (e.g. "...reach us at Constantinople.")
- Call
wait_for_playout(), then immediately remove the SIP participant - Listen to the delivered recording on the carrier/callee side, not our session log
Expected behavior Full voicemail delivered intact, including the last word.
Actual behavior The final word (or its tail) is missing on delivery. Our logs and recordings show complete audio since they're captured at the source, upstream of where the drop happens.
Proof Before/after MP3 attached (before = clipped, after = fixed via a 2s pre-hangup sleep workaround).
Additional context
What I think the root cause is: wait_for_playout() only tracks the local audio queue, not delivery through the SIP server and carrier. Removing the SIP participant sends a BYE and tears down immediately without flushing, so whatever's still in flight gets dropped. No trailing pause means the last word is still propagating when BYE fires; a natural pause gives it time to clear the wire first, which is why this is intermittent.
Workarounds:
- ~2s sleep before teardown. Works, but a blind guess at drain time.
- Append ~1.5-2s of trailing silence to the TTS output so
wait_for_playout()naturally covers the propagation tail (what I'm using now).
Suggestion: a graceful-drain/linger option before SIP teardown would fix this class of bug generally. Two shapes I'd love input on:
- A: a
linger_s/drain_timeoutparam on the hangup call - B: a
wait_for_egress_drain()method, called explicitly before hangup (mirrorswait_for_playout())
Happy to help implement once there's agreement on the shape.
Source: livekit/livekit