Skip to content

Fixed ScreenCaptureService staying bound when screen share setup is cancelled#983

Open
adrian-niculescu wants to merge 1 commit into
livekit:mainfrom
adrian-niculescu:fix/screen-capture-connection-bind-leak
Open

Fixed ScreenCaptureService staying bound when screen share setup is cancelled#983
adrian-niculescu wants to merge 1 commit into
livekit:mainfrom
adrian-niculescu:fix/screen-capture-connection-bind-leak

Conversation

@adrian-niculescu

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

Copy link
Copy Markdown
Contributor

ScreenCaptureConnection records a binding only when onServiceConnected arrives, but the binding exists from the bindService call onwards. Cancel the coroutine in that window and the ServiceConnection stays registered, so BIND_AUTO_CREATE keeps ScreenCaptureService alive for the lifetime of the context.

stop() cannot be the whole answer here. LocalParticipant.setScreenShareEnabled awaits the bind inside setTrackEnabled, and the track it creates is not published or stored yet, so a cancellation there abandons the track without any finally to stop it. Nothing reaches stop() at all. A cancelled connect now releases the binding itself once no caller is left waiting on it, and stop() unbinds on that same wider condition. Callers stay tracked until connect() actually returns, so this covers a cancellation landing after onServiceConnected but before the resumed caller runs, which is otherwise the same leak with the binding already connected.

Two smaller problems live in the same window:

  • bindService registers a connection even when it returns false or throws, which the platform docs call out, so the existing throw IllegalStateException path leaked one as well.
  • stop() while a connect is pending never resumed the queued continuations, so startForegroundService suspended 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 failed bindService left 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 createScreencastTrack can also orphan the new track because setTrackEnabled only cleans it up when publishVideoTrack returns false; cancellation from startForegroundService or publishVideoTrack exits before that branch. Cancellation during the service bind happens before capture starts, so it strands the initialized SurfaceTextureHelper, VideoSource, capturer, and rtcTrack, but not a MediaProjection. Cancellation while publication is suspended happens after startCapture and can leave the active MediaProjection running too. This is outside the ScreenCaptureConnection fix. Fully addressing it requires cancellation-safe ownership transfer in LocalParticipant and correct SurfaceTextureHelper ownership in LocalScreencastVideoTrack, and is not attempted here, but happy to take it on if you want it in the same change.

@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 40e1881

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

@adrian-niculescu
adrian-niculescu force-pushed the fix/screen-capture-connection-bind-leak branch from 9a1ad60 to 7528bc6 Compare July 26, 2026 09:03
…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
adrian-niculescu force-pushed the fix/screen-capture-connection-bind-leak branch from 7528bc6 to 40e1881 Compare July 26, 2026 10:14
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