Skip to content

Fix broadcast screen share hanging without frames - #1142

Merged
pblazej merged 5 commits into
mainfrom
blaze/broadcast-fixes
Oct 6, 2026
Merged

pblazej merged 5 commits into
mainfrom
blaze/broadcast-fixes

Conversation

@pblazej

@pblazej pblazej commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

When the automatic screen share publish failed, the broadcast extension kept retrying against a socket with no listener, so the broadcast came up without frames.

  • Publish: _publish checked permissions before the publisher, and a disconnected room resets the local participant's permissions, so it failed as Unknown(Participant does not have permission to publish); it now checks the publisher first, stops the broadcast on failure, and every LiveKitErrorType has a description.
  • IPC: the listener treats .waiting as transient, reconnects every 100 ms instead of busy-looping (UInt64(0.1) truncated the delay to zero), and copies the shared NWParameters so concurrent listeners stop binding each other's paths, which also de-flakes IPCChannelTests.
  • Capturer: stopping before the extension connects now cancels the waiting listener instead of leaking the capturer, and a second active broadcast capturer (e.g. two connected rooms) logs a warning, since only one of them can receive the broadcast.

Network.framework is not the cause: the SO_NECP_LISTENUUID warnings also appear in successful runs.

Fixes: #1132

devin-ai-integration[bot]

This comment was marked as resolved.

- Check for a room and publisher before permissions in _publish; a
  disconnected room resets the local participant to default permissions,
  so publishing while disconnected reported a permission error
- Stop the broadcast when the automatic screen share publish fails,
  instead of leaving the extension retrying against a stale socket
- Skip the automatic publish in a disconnected room, so a stale Room
  cannot stop another room's working broadcast
- Treat NWListener .waiting as transient rather than fatal
- Fix the IPC reconnect delay, which UInt64(0.1) truncated to zero
- Copy the shared NWParameters before setting requiredLocalEndpoint, so
  concurrent listeners stop binding each other's socket paths
- Describe every LiveKitErrorType case; insufficientPermissions and
  deviceAccessDenied printed as "Unknown"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pblazej
pblazej force-pushed the blaze/broadcast-fixes branch from 0a1fcf1 to bbf2f5a Compare September 30, 2026 06:42
Two broadcast screen share capturers in one process (e.g. two connected
rooms) listen on the same socket path, and only one of them ever gets the
extension's connection; the other published a track with no frames and
logged nothing. Warn when a second capturer starts.

The capturer's receiver task waited for that connection forever and held
the capturer strongly: stopCapture only closed a receiver that was never
set, so every capture stopped before the extension connected leaked the
capturer and its NWListener. Keep the task, cancel it on stop, and close
the receiver from its cancellation handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

- A receiver task cancelled by stopCapture no longer calls stopCapture
  itself when it unwinds; after a quick restart that call stopped the new
  capture and cancelled its receiver
- Count a capturer as active only while it has a receiver task, so a
  start without a socket path no longer leaves the count inflated and
  triggers a false multiple-share warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

The receiver task could fail and call stopCapture before createReceiver
stored it, so stop found no task to uncount and createReceiver then
counted a capturer that had already stopped. Create, store and count the
task under the receiver-task lock, which stopCapture also takes before
uncounting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@xianshijing-lk xianshijing-lk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some questions

@@ -49,17 +52,26 @@ class BroadcastScreenCapturer: BufferCapturer, @unchecked Sendable {
}

private func createReceiver() -> Bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can createReceiver() be called more than once ?

any chance that it will overwrite existing receiverTask without cancelling it or decrementing activeCount ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log("Broadcast receiver connected", .debug)
self.receiver = receiver
let isAnotherActive = receiverTask.mutate { receiverTask in
receiverTask = Task { [weak self] in await self?.receive(from: socketPath) }.cancellable()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should activeCount track the task lifetime ? rather than task creation ?

If it is for task lifetime, I wonder if we should do

receiverTask = Task { [weak self] in
    Self.activeCount.mutate { $0 += 1 }

    defer {
        Self.activeCount.mutate { $0 -= 1 }
    }

    await self?.receive(from: socketPath)
}.cancellable()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pblazej
pblazej merged commit f5ef3e6 into main Oct 6, 2026
33 checks passed
@pblazej
pblazej deleted the blaze/broadcast-fixes branch October 6, 2026 11:41
hiroshihorie added a commit that referenced this pull request Oct 6, 2026
…the picker (#1145)

Stacked on #1142.

On iOS 27 with default options, a broadcast started from Control Center
never reaches the broadcast extension path. `broadcastStateChanged`
calls `setScreenShare(enabled: true)`, which takes the ScreenCaptureKit
branch first because `useScreenCaptureKit` defaults to `true`. What
happens next depends on the app state when the broadcast starts:

- App active (Control Center closed during the countdown): the in-app
SCK picker shows up, and Cancel or the 60s timeout fails the publish.
- App inactive (Control Center still open): no picker shows, and the
system cancels it about 2s later.

On main the failure is only logged, so the broadcast keeps running and
nothing gets published. With #1142 the failure calls `requestStop()`, so
the broadcast ends.

This change makes the publish triggered by a broadcast start use
Broadcast Capture when the room is set up for the extension. Explicit
`setScreenShare` calls keep using ScreenCaptureKit, so a stale
`isBroadcasting` (for example after the extension is killed without
posting stopped) can't turn them into a track that never receives
frames.

Verified on an iPhone 17 Pro Max on iOS 27.0.1 with the example app and
its extension, in both Control Center cases: the
`BroadcastScreenCapturer` track is published, the receiver connects,
frames are sent, and there is no picker and no `requestStop`. Unit tests
cover the routing helper, since the SCK branch is compiled out on the
simulator.
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.

3 participants