Skip to content

fix(rtc): keep AudioMixer streams that miss the read timeout - #837

Closed
RaphaelFakhri wants to merge 2 commits into
livekit:mainfrom
RaphaelFakhri:fix/mixer-slow-stream-timeout
Closed

RaphaelFakhri wants to merge 2 commits into
livekit:mainfrom
RaphaelFakhri:fix/mixer-slow-stream-timeout

Conversation

@RaphaelFakhri

Copy link
Copy Markdown

Summary

AudioMixer no longer drops an async-generator stream that misses stream_timeout_ms once. The late audio is mixed when it arrives.

Problem

_get_contribution read each stream with asyncio.wait_for(stream.__anext__(), timeout). On timeout, wait_for cancels the read. For an async generator, that cancellation is thrown into the generator, which finishes. The next read raises StopAsyncIteration, so the mixer treats the stream as exhausted and removes it. A source with a slow first chunk (for example, a TTS stream with more than 100 ms of first-byte latency) produced no audio at all, and the only sign was a "timeout, ignoring" warning.

Changes

  • Keep the read that timed out and wait on it again in the next round, instead of cancelling it.
  • Cancel outstanding reads in remove_stream and aclose.
  • Add a test with a generator that waits 300 ms before its first frame.

Testing

pytest tests/rtc/test_mixer.py: the new test fails without the change (no frames with audio) and passes with it. The existing mixer test still passes.

@CLAassistant

CLAassistant commented Sep 29, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread livekit-rtc/livekit/rtc/audio_mixer.py Outdated
Comment on lines +120 to +122
for pending in self._pending.values():
pending.cancel()
self._pending.clear()

@devin-ai-integration devin-ai-integration Bot Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Mixer shutdown hangs on uncancellable streams

If an input iterator ignores cancellation, aclose waits forever for its read. OutputPlayer.aclose then never reaches output-device cleanup.

Learn more

The mixer accepts arbitrary async iterators as inputs. Cancellation requests do not force an iterator to finish: an iterator can catch CancelledError and continue waiting, or perform cleanup that never completes. The unconditional gather then prevents aclose from returning. OutputPlayer.aclose waits for mixer closure before closing its sounddevice stream.

Example: An iterator catches CancelledError while waiting for an upstream queue that never receives another frame. Calling mixer.aclose() cancels its read but waits forever; closing an OutputPlayer using that mixer also leaves the audio device open.

Recommended fix: Bound the wait for cancelled reads, and detach any non-cooperative reads with appropriate result/exception handling. Ensure AudioMixer.aclose can return even when an input iterator does not finish cancellation.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@RaphaelFakhri

Copy link
Copy Markdown
Author

Confirmed. aclose cancelled the pending stream reads but returned before the cancellation finished, so an async generator could still be running its finally block afterwards.

Fixed in 8a4db8b. aclose now awaits the cancelled reads with asyncio.gather(..., return_exceptions=True) before returning. Reads cancelled earlier by remove_stream are tracked and awaited too, without being cancelled a second time, since a second cancel would interrupt their cleanup. The new tests use a generator with a slow finally block and check that cleanup has finished when aclose returns, both for a pending read and for a stream removed while its read was pending.

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.

2 participants