Fixed ScreenAudioCapturer crash, stale audio replay, and AudioRecord leak#982
Open
adrian-niculescu wants to merge 2 commits into
Open
Conversation
…fter MediaProjection stops
adrian-niculescu
requested review from
MaxHeimbrock,
davidliu and
xianshijing-lk
as code owners
July 26, 2026 02:02
🦋 Changeset detectedLatest commit: 80760fc The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
initAudioRecordthrows on WebRTC's audio record thread:AudioRecord.Builder.build()fails withUnsupportedOperationException, and the playback capture config builder can throwIllegalStateException. OnlystartRecording()was guarded, and the audio record thread has no handler around the buffer callback, so the process dies.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.initAudioRecordnow returns false on any creation failure, matching its existing degrade paths, and releases the record if it never reaches the recording state.onBufferRequestonly 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 alwaysERROR_DEAD_OBJECT(Android 17 returnsERROR_BAD_VALUEafter 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, whileinitAudioRecordruns on the audio record thread. A release landing betweenAudioRecord.Builder.build()and the assignment to the field saw a nullaudioRecord, did nothing, and left init to publish a recordingAudioRecordthat 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 withcould 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.