Skip to content

fix: drain media before BYE on local hangup to prevent voicemail clipping - #811

Open
rkfshakti wants to merge 2 commits into
livekit:mainfrom
rkfshakti:fix/hangup-media-drain
Open

fix: drain media before BYE on local hangup to prevent voicemail clipping#811
rkfshakti wants to merge 2 commits into
livekit:mainfrom
rkfshakti:fix/hangup-media-drain

Conversation

@rkfshakti

Copy link
Copy Markdown

Fixes livekit/livekit#4737

Problem

When an agent ends a call right after wait_for_playout() returns — EndCall RPC, end_call tool, or removing the SIP participant — the last word of the utterance is clipped on the callee's end (voicemail scenario). The audio is already past the agent: it is buffered in the room mixer (5-frame ~100ms input buffer), the codec encoder, and in-flight RTP. wait_for_playout() only tracks the local queue, so it cannot see this tail.

The SIP bridge then sends BYE and closes media immediately, so whatever is still buffered never clears the wire. A trailing pause masks the bug because it gives the tail time to flush; an abrupt end-of-speech hangup clips the last word. The reporter's workaround (2s pre-hangup sleep) confirms the mechanism.

Fix

Add a configurable drain window hangup_drain_time (default 500ms): on a locally-initiated clean hangup, keep feeding media to the peer for the window before sending BYE. The window is bounded (default 500ms) and only applies to:

  • EndCall RPC (reason rpc)
  • ctx cancel / agent end-call (reason hangup)
  • participant removed from room (reason removed)
  • cancelled invite (inbound, reason cancelled)

Remote BYE (the peer hung up), errors, timeouts, media failures, and pre-connect failures are not delayed. Set hangup_drain_time: -1 to disable.

Test

TestOutboundHangupDrainsMediaBeforeBYE: on an established outbound call, an EndCall RPC must not emit BYE before the drain window has elapsed. Fails on the old code (BYE sent before the media drain window elapsed), passes with the fix. All media-port and outbound tests pass; the pre-existing auth-test failures on clean main are unrelated (they fail identically without this change).

…ping

When an agent ends a call (EndCall RPC, end_call tool, or participant
removed from the room) right after wait_for_playout() returns, the last
word of the utterance is clipped on the callee's end. The audio is
already past the agent — it is buffered in the room mixer (~100ms input
buffer), the encoder, and in-flight RTP — but the SIP bridge sends BYE
and closes media immediately, so the tail never clears the wire (issue
#4737).

Add a configurable hangup drain window (hangup_drain_time, default
500ms): on a locally-initiated clean hangup, keep feeding media for the
window before sending BYE. Remote BYE, errors, timeouts, and
pre-connect failures are not delayed.

Regression test: an EndCall RPC on an established outbound call must not
emit BYE before the drain window has elapsed. Fails on the old code
('BYE sent before the media drain window elapsed'), passes with the
fix.
devin-ai-integration[bot]

This comment was marked as resolved.

…nprivileged range

Devin review feedback on PR livekit#811:

1. inbound drainOnHangup: a remote CANCEL before the call is answered
   hit the 'cancelled' branch and slept the drain window with no media
   bridged. Gate the drain on the media actually being bridged
   (c.started broken) and drop 'cancelled' from the local-hangup set.
   Remote CANCEL is not a local hangup.

2. newTestMediaPort bound a random even port in 1-65535, which can land
   on a privileged port (<1024) and fail in CI with 'bind: permission
   denied' (seen on TestSetOfferReportsUnknownProvider). Constrain the
   test range to 10000-20000.
@rkfshakti

Copy link
Copy Markdown
Author

Addressed the Devin review finding (cancelled inbound call delayed): drainOnHangup now gates on the media actually being bridged (c.started broken) and "cancelled" is dropped from the local-hangup set — a remote CANCEL before 200 OK is not a local hangup and gets no drain. Also constrained the media-port test range to 10000-20000 so CI can't land on a privileged port (<1024, seen on TestSetOfferReportsUnknownProvider: 'bind: permission denied').

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.00000% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.22%. Comparing base (0460b40) to head (a871b74).
⚠️ Report is 354 commits behind head on main.

Files with missing lines Patch % Lines
pkg/sip/inbound.go 66.66% 4 Missing and 1 partial ⚠️
pkg/config/config.go 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #811      +/-   ##
==========================================
+ Coverage   65.25%   67.22%   +1.97%     
==========================================
  Files          51       42       -9     
  Lines        6588     8226    +1638     
==========================================
+ Hits         4299     5530    +1231     
- Misses       1915     2205     +290     
- Partials      374      491     +117     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Outbound SIP: last word of voicemail clipped on hangup

1 participant