Skip to content

Fixed setTrackEnabled orphaning track resources when cancelled before publish completes - #986

Open
adrian-niculescu wants to merge 1 commit into
livekit:mainfrom
adrian-niculescu:fix/screen-share-enable-cancellation-cleanup
Open

Fixed setTrackEnabled orphaning track resources when cancelled before publish completes#986
adrian-niculescu wants to merge 1 commit into
livekit:mainfrom
adrian-niculescu:fix/screen-share-enable-cancellation-cleanup

Conversation

@adrian-niculescu

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

Copy link
Copy Markdown
Contributor

Fixes #985.

setTrackEnabled had no cleanup around its suspend points: a cancellation landing in startForegroundService or publishVideoTrack dropped the freshly created screencast track without stopping or disposing it, and once the bind had succeeded, ScreenCaptureService stayed bound for the lifetime of the context (the case #983 could not reach from inside ScreenCaptureConnection). Each enable path now ends in a finally that tears down a track it could not publish: capture stops, the track is stopped and disposed, the service binding is released, and ScreenCaptureParams.onStop is invoked. The camera and microphone paths get the same treatment; their failure cleanup previously ran only when publish returned false.

publishTrackImpl negotiates the sender transceiver concurrently with the add track request, so a cancelled or failed request could leave a negotiated transceiver holding its sender and SDP m-section with no publication to unpublish. The sender is now detached whenever no publication is created, which also covers the publish-timeout path. The rollback is deliberately narrow: a publish usually fails because the signal connection died, and that same failure starts the reconnect that renegotiates the publisher, so the rollback runs only while its publisher is connected and idle. Neither connectionState nor the reconnect job has moved by the time a dying session fails its pending publishes, so idleness is tracked with a session identity replaced at every boundary; cleanup resuming after a soft reconnect no longer matches and leaves the publisher alone. The check runs on the RTC thread immediately ahead of the mutation it guards. Stopping the transceiver to release its m-section is reserved for video, matching unpublishTrack.

Two smaller fixes ride along: LocalScreencastVideoTrack never registered its SurfaceTextureHelper with the track's CloseableManager the way LocalVideoTrack.createTrack does, so the capture thread survived dispose() even on normal teardown; and a projection stop already dispatched on its callback thread can reach the track after cleanup has disposed it, where ScreenCapturerAndroid.stopCapture throws, so LocalScreencastVideoTrack.stop() tolerates losing that race instead of crashing the callback thread.

Two behavioral notes: onStop on the publish-failure path now runs after teardown rather than before, so a throwing callback cannot skip the cleanup, and it is delivered at most once per enable attempt (the platform ending the projection mid-publish already delivers it; the failure path delivered it again).

Eleven tests cover cancellation at each suspend point, the rollback and each of its limits, single onStop delivery, the late projection stop, helper disposal, and the surviving happy path; six fail on main. ./gradlew test, spotless, and :livekit-android-sdk:detektRelease pass (repository-wide detekt is red on pre-existing findings).

@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f5218d0

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

@adrian-niculescu
adrian-niculescu force-pushed the fix/screen-share-enable-cancellation-cleanup branch 3 times, most recently from 62dabc8 to 413face Compare July 27, 2026 18:33
@adrian-niculescu
adrian-niculescu marked this pull request as ready for review July 27, 2026 18:52

@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 potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@adrian-niculescu
adrian-niculescu force-pushed the fix/screen-share-enable-cancellation-cleanup branch 3 times, most recently from ba87fa2 to 25fd158 Compare July 27, 2026 21:19
@adrian-niculescu
adrian-niculescu marked this pull request as draft July 27, 2026 21:30
@adrian-niculescu
adrian-niculescu force-pushed the fix/screen-share-enable-cancellation-cleanup branch from 25fd158 to 549ca9a Compare July 27, 2026 21:34
… publish completes

A cancellation landing in startForegroundService or publishVideoTrack
unwound out of setTrackEnabled without any cleanup, abandoning the newly
created screencast track and, when the bind had already succeeded,
leaving ScreenCaptureService bound for the lifetime of the context. Each
enable path now tears the unpublished track down in a finally block, so
cancellation and publish failures release capture, track resources, and
the service binding, and invoke the onStop callback exactly once, even
when the platform ended the projection while publishing was in flight.

A projection stop already dispatched on its callback thread can reach
the track after that cleanup disposed it, where ScreenCapturerAndroid
throws from stopCapture. LocalScreencastVideoTrack.stop() now tolerates
losing that race instead of crashing the callback thread.

publishTrackImpl negotiates the sender transceiver concurrently with the
add track request, so a failed or cancelled request could leave a
negotiated transceiver holding its sender and SDP m-section with no
publication to unpublish. The sender is now detached whenever no
publication is created. The rollback targets exactly the sender this
attempt created on the transport that created it, so it cannot detach a
concurrent publish of the same track, and the handle is recorded
non-cancellably so a cancellation cannot discard the RTC-thread result.
It runs only while that publisher is connected with no reconnect in
flight, since a publish usually fails because the signal connection died
and that same failure starts the reconnect which renegotiates the
publisher. Neither connectionState nor the reconnect job has moved at
the point a dying session fails its pending publishes, and a resume
leave fails them without starting a reconnect at all, so a session
identity is replaced at every boundary and each sender handle keeps the
one that owned its transceiver. Matching that identity is what makes the
decision independent of when a failed publish resumes: a soft reconnect
keeps the publisher, so cleanup arriving after the next session is
serving would otherwise mutate it. The check runs on the RTC thread
immediately ahead of the mutation it guards. Stopping the transceiver
and releasing its m-section is reserved for video, matching
unpublishTrack.

LocalScreencastVideoTrack also never released its SurfaceTextureHelper,
so its capture thread outlived dispose() even on the normal teardown
path. The helper is now registered with the track's CloseableManager,
matching LocalVideoTrack.

Fixes livekit#985
@adrian-niculescu
adrian-niculescu force-pushed the fix/screen-share-enable-cancellation-cleanup branch from 549ca9a to f5218d0 Compare July 27, 2026 21:47
@adrian-niculescu
adrian-niculescu marked this pull request as ready for review July 27, 2026 22:03
@adrian-niculescu

Copy link
Copy Markdown
Contributor Author

Exercised this on a few devices and refined it over a couple of passes since opening. It's ready for review now.

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.

Cancellation during screen share enable orphans track resources after service bind

1 participant