Fix capturer state desync when startCapture fails after the base state transition - #1095
Open
tarsyang wants to merge 1 commit into
Open
Fix capturer state desync when startCapture fails after the base state transition#1095tarsyang wants to merge 1 commit into
tarsyang wants to merge 1 commit into
Conversation
tarsyang
requested review from
hiroshihorie,
pblazej and
xianshijing-lk
as code owners
August 20, 2026 17:44
VideoCapturer.startCapture() increments the start/stop counter before
subclasses perform the actual device-level start work. When that work
throws (e.g. CameraCapturer: no capture device, unresolvable format, or
an AVFoundation start error), the counter was left incremented: the
capturer permanently reported .started while nothing was capturing, and
every subsequent startCapture() call returned false early ("already
started") instead of retrying. On iOS this turns one transient failure
during a background-return resume into a camera that stays dead for the
rest of the session while the track still reports unmuted.
Balances the counter on failure with try? await stopCapture(), which
also runs the subclass stop body and tears down any partially
configured device state. Applied to every start path that can fail
after the base call: CameraCapturer, ARCameraCapturer,
MacOSScreenCapturer, InAppScreenCapturer (throwing paths), and
BroadcastScreenCapturer (receiver creation failure path that returns
false).
tarsyang
force-pushed
the
fix/capturer-start-state-rollback
branch
from
August 20, 2026 17:49
59cdc5c to
9054973
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.
Problem
VideoCapturer.startCapture()increments the start/stop counter andnotifies
.startedbefore subclasses perform the actual device-levelstart work. When that work throws, the counter is left incremented:
.startedwhile nothing iscapturing.
startCapture()call returnsfalseearly("already started") instead of retrying, and
Track.start()does notinspect the return value, so the track keeps reporting
started/unmuted with a dead capturer.
On iOS this is reachable on every foreground return: the background
suspend/resume path goes through
resume() → unmute → Track.start() → startCapture()andRoom.appWillEnterForegroundonly logs the resumeerror, so one transient
CameraCapturerfailure (no capture device,unresolvable format, or an AVFoundation start error during the app
transition) leaves the camera dead for the rest of the session while
track.isMuted == false, with no error surfaced to the app. The onlyrecovery is unpublishing and republishing the track.
Fix
Balance the counter on failure with
try? await stopCapture()beforerethrowing (or before reporting failure): the capturer returns to
.stopped, and running the subclass stop body tears down any partiallyconfigured device state. Applied to every start path that can fail
after the base call:
CameraCapturer,ARCameraCapturer,MacOSScreenCapturer,InAppScreenCapturer: the start body moves into a privateperformStartCapture()and the override wraps it in do/catch.BroadcastScreenCapturer: the receiver-creation failure pathreturned
falsewhile leaving the counter incremented; it nowbalances the counter first (whether that path should throw instead of
returning
falseis left unchanged). Balancing also resets thepre-filled dimensions completer, so publishing with a misconfigured
broadcast bundle now fails at the publish-time dimensions wait
instead of publishing a track that never produces frames.
No public API changes.
Note
MacOSScreenCapturer.stopCapture()has the mirrored issue on the stopside: throwing after the counter already decremented leaves a running
SCStreamon a capturer that reports.stopped. Left out to keep thisPR focused on the start path; happy to follow up.
The structural alternative (base class transitions state only after the
subclass start work succeeds, template-method style) would also cover
third-party capturer subclasses, but it changes the public subclassing
contract, so this PR keeps the non-breaking per-subclass shape.
Testing
VideoCapturerStartFailureTests: a capturer failing after thebase transition returns to
.stopped, and a second attempt reachesthe subclass again instead of silently returning
false..started; the second call returnsfalse).