Skip to content

fix(voice): don't hang or buffer audio when the recorder encoder fails - #7741

Merged
longcw merged 2 commits into
livekit:mainfrom
ANDROIDHASSAN:fix/recorder-encode-thread-failure
Oct 12, 2026
Merged

longcw merged 2 commits into
livekit:mainfrom
ANDROIDHASSAN:fix/recorder-encode-thread-failure

Conversation

@ANDROIDHASSAN

@ANDROIDHASSAN ANDROIDHASSAN commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem: RecorderIO._encode_thread creates the output directory, opens the Ogg container and adds the Opus stream before its try. If any of those raise (an unwritable or misconfigured session directory, a codec that can't be added), the thread dies without reaching the finally that resolves _close_fut. RecorderIO.aclose() then waits on _close_fut forever, and so do AgentSession.aclose() and the job shutdown in job.py that awaits it.

A second problem compounds the first: once the encode thread has exited, nothing drains self._q, but on_frame and on_played keep queueing every input and played frame for the rest of the session, so the audio piles up in memory until the process ends.

Fix:

  • Setup now runs inside the try, so a failure is logged by the existing handler and the finally still resolves _close_fut. The container is entered with with av.open(...) before the stream is added, so a failed add_stream still closes the file.
  • All queue puts go through a new _put, which drops items once _close_fut is done, which is when the encode thread has exited. RecorderAudioOutput.capture_frame checks the same condition, so the agent side stops buffering segments too.
  • start() creates a fresh queue, so audio left behind by a run whose encoder failed can't reach the next run's timeline.

Most of the source diff is re-indentation. git diff -w shows about 15 changed lines.

Tests

Three new cases in tests/test_recorder_io.py. Both point the recorder at a path whose parent is a regular file, so mkdir raises:

  • test_a_failed_encoder_setup_does_not_hang_close: aclose() must return within 5 s. On main it times out, and pytest reports the encode thread's unhandled FileExistsError.
  • test_a_failed_encoder_stops_buffering_audio: after the encode thread exits, ten more input frames must not grow the queue, and an agent frame must not open a segment buffer. On main this test errors, because the encode thread dies unhandled and aclose() never returns.
  • test_a_restart_does_not_record_audio_left_over_from_a_failed_run: a frame left in a failed run's queue must not appear in the next run's file.
$ uv run pytest tests/test_recorder_io.py tests/test_recording.py tests/test_telemetry_recording_disabled.py -q
101 passed
$ uv run pytest --unit --audio_eot -q
4966 passed, 5 skipped

ruff format, ruff check and make type-check are clean.

Scope: only recorder_io.py. recording still reports True after a failure, so job.py still calls aclose(), which now returns. This PR does not change that behaviour. #7740 touches the codec setup lines in the same function. Whichever PR merges second needs a small rebase, and I'll do it.

🤖 Generated with Claude Code

The encode thread created the output directory, opened the container and
added the stream before its try block. When any of those raised (an
unwritable session directory, a missing codec), the thread died without
reaching the finally that resolves _close_fut, so RecorderIO.aclose()
waited forever and with it AgentSession.aclose() and job shutdown.

Setup now runs inside the try, and the container is entered before the
stream is added so a failed add_stream still closes the file.

Once the encode thread has exited nothing drains the queue, yet every
input and played frame kept being queued for the rest of the session.
All puts now go through _put, which drops items after _close_fut is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ANDROIDHASSAN
ANDROIDHASSAN requested a review from a team as a code owner October 11, 2026 13:16
devin-ai-integration[bot]

This comment was marked as resolved.

…oder failure

- RecorderAudioOutput kept accumulating each segment's frames after the
  encode thread had exited. It now checks the same _encoding condition
  that guards the queue.
- start() creates a fresh queue, so audio left in the queue by a run
  whose encoder failed cannot be encoded on the next run's timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@longcw longcw 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.

lgtm!

@longcw
longcw merged commit b58e83f into livekit:main Oct 12, 2026
19 checks passed
ANDROIDHASSAN added a commit to ANDROIDHASSAN/agents that referenced this pull request Oct 12, 2026
…trate

Resolve conflicts with livekit#7741: apply bit_rate and vbr inside the encode thread's try block, and keep both sets of tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ANDROIDHASSAN
ANDROIDHASSAN deleted the fix/recorder-encode-thread-failure branch October 12, 2026 10:51
ANDROIDHASSAN added a commit to ANDROIDHASSAN/agents that referenced this pull request Oct 13, 2026
…trate

Resolve conflicts with livekit#7741: apply bit_rate and vbr inside the encode thread's try block, and keep both sets of tests.
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