Skip to content

Fixed ScreenAudioCapturer crash, stale audio replay, and AudioRecord leak#982

Open
adrian-niculescu wants to merge 2 commits into
livekit:mainfrom
adrian-niculescu:fix/screen-audio-capturer-projection-loss
Open

Fixed ScreenAudioCapturer crash, stale audio replay, and AudioRecord leak#982
adrian-niculescu wants to merge 2 commits into
livekit:mainfrom
adrian-niculescu:fix/screen-audio-capturer-projection-loss

Conversation

@adrian-niculescu

@adrian-niculescu adrian-niculescu commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

When the MediaProjection backing a ScreenAudioCapturer stops (the user ends the share from the status bar chip, or another app takes over the projection), two things go wrong:

  • If it stops before the first mic buffer callback, initAudioRecord throws on WebRTC's audio record thread: AudioRecord.Builder.build() fails with UnsupportedOperationException, and the playback capture config builder can throw IllegalStateException. Only startRecording() was guarded, and the audio record thread has no handler around the buffer callback, so the process dies.
  • Once capture is running, AudioRecord.read() starts failing, but the return value was ignored and the buffer still holds the previous frame. That frame kept getting mixed into the mic track on every callback, so listeners heard the last captured 10 ms looping until the callback was detached.

initAudioRecord now returns false on any creation failure, matching its existing degrade paths, and releases the record if it never reaches the recording state. onBufferRequest only mixes fully read buffers; on a read error it logs, releases the AudioRecord, and the track degrades to mic-only audio. Read errors are not always ERROR_DEAD_OBJECT (Android 17 returns ERROR_BAD_VALUE after the record restore fails), so any negative result is treated as terminal.

The second commit fixes a related problem in the teardown path rather than in projection loss. releaseAudioResources() is called by the app from a thread of its choosing, while initAudioRecord runs on the audio record thread. A release landing between AudioRecord.Builder.build() and the assignment to the field saw a null audioRecord, did nothing, and left init to publish a recording AudioRecord that nothing owned. Publication and release now share a lock, and a released capturer stays released, so an init finishing after a release discards its record instead of stranding it.

Verified with the screenshare-audio example on an Android 17 Pixel 6a and and an emulator: stopping the share from the status bar chip while the capturer is attached logs a single AudioRecord.read failed: -2. Stopping screen share audio capture. and the session continues on mic audio with no crash. For the teardown race, driving 100 racing release/init pairs against a real MediaProjection stranded 9 recorders before the fix, and the remaining 91 creation attempts then failed with could not open input for device AUDIO_DEVICE_IN_REMOTE_SUBMIX, so the leak also breaks later capture attempts. After the fix all 100 discard cleanly and none fail. ./gradlew test, spotless, and detekt pass.

@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 80760fc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
client-sdk-android Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

initAudioRecord runs on WebRTC's audio record thread, while
releaseAudioResources is called by the app from a thread of its choosing.
A release landing between AudioRecord creation and its assignment to the
field saw a null audioRecord, did nothing, and left init to publish a
recording AudioRecord that nothing owned.

Publication and release now share a lock, and a released capturer stays
released, so an init that finishes after a release discards its
AudioRecord instead of stranding it. Leaked recorders hold the playback
capture input open: with 100 racing release/init pairs on an Android 17
emulator, 9 recorders were stranded and the remaining 91 creation
attempts failed with "could not open input for device
AUDIO_DEVICE_IN_REMOTE_SUBMIX". After the fix all 100 discard cleanly and
no creation fails.
@adrian-niculescu adrian-niculescu changed the title Fixed ScreenAudioCapturer audio thread crash and stale audio replay after MediaProjection stops Fixed ScreenAudioCapturer crash, stale audio replay, and AudioRecord leak Jul 26, 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