Fixed ScreenCaptureService staying bound when screen share setup is cancelled#983
Open
adrian-niculescu wants to merge 1 commit into
Open
Conversation
adrian-niculescu
requested review from
MaxHeimbrock,
davidliu and
xianshijing-lk
as code owners
July 26, 2026 08:31
🦋 Changeset detectedLatest commit: 40e1881 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 |
adrian-niculescu
force-pushed
the
fix/screen-capture-connection-bind-leak
branch
from
July 26, 2026 09:03
9a1ad60 to
7528bc6
Compare
…ancelled ScreenCaptureConnection recorded a binding only once onServiceConnected arrived. A coroutine cancelled before connect() returned left the ServiceConnection registered, so BIND_AUTO_CREATE kept the service alive for the lifetime of the context. LocalParticipant.setScreenShareEnabled awaits the bind internally and abandons the track it just created when cancelled, so nothing reached stop() on that path. A cancelled connect now releases the binding itself once no caller is left waiting on it, and stop() unbinds on the same wider condition. Callers stay tracked until connect() actually returns, so a cancellation landing after the service connected but before the caller resumed releases the binding too. Both cover the documented case where bindService leaves a connection registered while reporting failure or throwing. Two paths could also leave connect() suspended forever. A stop() racing a connect could unbind and snapshot an empty waiter set just before the caller enqueued itself, so requesting the bind and registering the waiter now happen under one lock. A failed bindService left the state claiming a bind was in flight, which the next caller would wait on with nothing pending.
adrian-niculescu
force-pushed
the
fix/screen-capture-connection-bind-leak
branch
from
July 26, 2026 10:14
7528bc6 to
40e1881
Compare
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.
ScreenCaptureConnectionrecords a binding only whenonServiceConnectedarrives, but the binding exists from thebindServicecall onwards. Cancel the coroutine in that window and theServiceConnectionstays registered, soBIND_AUTO_CREATEkeepsScreenCaptureServicealive for the lifetime of the context.stop()cannot be the whole answer here.LocalParticipant.setScreenShareEnabledawaits the bind insidesetTrackEnabled, and the track it creates is not published or stored yet, so a cancellation there abandons the track without anyfinallyto stop it. Nothing reachesstop()at all. A cancelled connect now releases the binding itself once no caller is left waiting on it, andstop()unbinds on that same wider condition. Callers stay tracked untilconnect()actually returns, so this covers a cancellation landing afteronServiceConnectedbut before the resumed caller runs, which is otherwise the same leak with the binding already connected.Two smaller problems live in the same window:
bindServiceregisters a connection even when it returns false or throws, which the platform docs call out, so the existingthrow IllegalStateExceptionpath leaked one as well.stop()while a connect is pending never resumed the queued continuations, sostartForegroundServicesuspended forever.Two more ways a caller could suspend forever came out of tightening the state handling, and are fixed here too. A
stop()racing a connect could unbind and snapshot an empty waiter set just before the caller enqueued itself, so requesting the bind and registering the waiter now happen under one lock. A failedbindServiceleft the state claiming a bind was in flight, which the next caller would then wait on with nothing pending.Six tests cover the cancellation, rebind, post-connect cancellation, and pending-stop paths; five of them fail on main and pass here. The sixth pins the happy path, so releasing on cancellation cannot tear down a binding a connected caller still holds.
./gradlew test, spotless, and detekt pass.Worth flagging separately: cancellation after
createScreencastTrackcan also orphan the new track becausesetTrackEnabledonly cleans it up whenpublishVideoTrackreturns false; cancellation fromstartForegroundServiceorpublishVideoTrackexits before that branch. Cancellation during the service bind happens before capture starts, so it strands the initializedSurfaceTextureHelper,VideoSource, capturer, and rtcTrack, but not aMediaProjection. Cancellation while publication is suspended happens afterstartCaptureand can leave the activeMediaProjectionrunning too. This is outside theScreenCaptureConnectionfix. Fully addressing it requires cancellation-safe ownership transfer inLocalParticipantand correctSurfaceTextureHelperownership inLocalScreencastVideoTrack, and is not attempted here, but happy to take it on if you want it in the same change.