Skip to content

fix: dangling audio elements when user ends the call#161

Open
adrivelasco wants to merge 1 commit into
VapiAI:mainfrom
adrivelasco:fix/audio-element-leak-on-user-end-call
Open

fix: dangling audio elements when user ends the call#161
adrivelasco wants to merge 1 commit into
VapiAI:mainfrom
adrivelasco:fix/audio-element-leak-on-user-end-call

Conversation

@adrivelasco
Copy link
Copy Markdown

@adrivelasco adrivelasco commented May 22, 2026

Summary

I've been experiencing issues in my app where after having many Vapi-powered conversations in a row, the bot goes to silence. I was able to consistently reproduce this on Safari only.

Root cause: The SDK creates an <audio data-participant-id="..."> element per remote participant via buildAudioPlayer and only removes it when Daily's participant-left event fires (destroyAudioPlayer).

When the user ends the call via vapi.stop()call.destroy(), Daily tears down the local client before the server delivers participant-left for the bot, so destroyAudioPlayer never runs for it. The element stays in the DOM with a dead MediaStream attached on srcObject, and a new one is appended on the next call. Safari degrades audibly once enough accumulate.

Reproduce steps

  1. Start a call with vapi.start(...).
  2. End the call from the client side (call vapi.stop()). Do not let the bot end it.
  3. Inspect the DOM: a stranded <audio data-participant-id="..."> element remains in <body> with a dead MediaStream on srcObject.
  4. Repeat the start/stop cycle several times. Each call appends a new dangling audio element.
  5. After a handful of cycles (like 3), assistant audio becomes choppy/quieter/cuts out, and teardown produces audible buffer-flush noise. Chrome tolerates the accumulation longer but eventually exhibits the same symptoms.

Fix

  • Add teardownAudioPlayer(player) that explicitly nulls srcObject (releasing the MediaStream reference so the playback graph is freed immediately instead of waiting on GC) and removes the element from the DOM.
  • Add destroyAllAudioPlayers() that sweeps every audio[data-participant-id] element through the same teardown.
  • Call the sweep in both cleanup() (covers the bot-ends-call path via left-meetingcleanup) and stop() (covers the user-ends-call path).
  • Existing destroyAudioPlayer(participantId) now routes through teardownAudioPlayer too, so cleanup behavior is identical across all three call sites.

The sweep is idempotent — if participant-left already removed an element, the selector finds nothing.

The SDK creates an <audio data-participant-id="..."> element per remote
participant via buildAudioPlayer and only removes it when Daily's
participant-left event fires (destroyAudioPlayer).

When the user ends the call via vapi.stop() -> call.destroy(), Daily
tears down the local client before the server delivers participant-left
for the bot, so destroyAudioPlayer never runs for it. The element stays
in the DOM with a dead MediaStream attached on srcObject, and a new one
is appended on the next call. Safari degrades audibly once enough
accumulate.

Sweeps every audio[data-participant-id] element on teardown and
explicitly releases the MediaStream reference (srcObject = null) so the
playback graph is freed immediately instead of waiting on GC. Runs in
both cleanup() and stop() so it covers the bot-ends-call path
(left-meeting -> cleanup) and the user-ends-call path (stop).
@adrivelasco adrivelasco changed the title fix: stranded audio elements when user ends the call fix: dangling audio elements when user ends the call May 22, 2026
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.

1 participant