Repository navigation
[bug] Track published via SDP renegotiation never forwarded to subscribers — server OnTrack doesn't fire, publish times out at 30s (self-hosted) #1104
Description
Versions
- livekit_client (Dart): 2.7.0
- flutter_webrtc: 1.4.0 (libwebrtc m144)
- livekit-server (self-hosted): 1.11.0 (also reproduced on 1.10.0)
- Flutter: 3.41.4
- iOS: 18.2 and 26.0 (both repro)
- Devices: iPhone 13, iPhone 15 Pro, iPad mini 6
- Topology: multi-PC (default, not single-PC mode)
What happens
-
Host connects to Room, publishes camera + mic (initial offer). Works.
-
Host taps "Share screen" → our native bridge presents
RPSystemBroadcastPickerView→ user taps Start Broadcast. -
iOS Broadcast Upload Extension starts pushing
CMSampleBuffers via the Unix socket in the App Group container. -
SDK's
BroadcastManagerfiresisBroadcasting=true. Our Dart code does NOT callsetScreenShareEnabled(true)— we rely on SDK auto-publish (after removing our manual call, a race condition was resolved in a prior diagnostic loop).Room is configured with:
RoomOptions(
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
),
)- Client logs
LocalTrackPublishedEventfor screenShareVideo — SDK side believes publish succeeded. - Server side: receives SDP renegotiation (offer/answer pair completes cleanly, NO unhandled RTP, NO ICE errors, mid=3 video VP8 with simulcast layers q;h).
- Exactly 30s later: server fires publish time out from the supervisor (SetMaxSubscribedQuality + supervisor state machine). Track removed server-side. Viewer NEVER receives TrackSubscribed for screenShareVideo.
Confirmed NOT the cause
- Network: UDP 50000-60000 open server-side; camera/mic over the same path work flawlessly.
- ICE: candidates exchanged, transport stays connected.
- SDP renegotiation: completes successfully on both sides.
- Codec: VP8 (also tried H.264 via Room options) — same outcome.
- Simulcast: reproduces with 1 layer too (manually reduced via defaultVideoPublishOptions).
- Race condition with our bridge: a prior loop removed our manual setScreenShareEnabled call; SDK auto-publishes via its own BroadcastManager listener. Bug persists.
- Identity / token permissions: subscriber token has wildcard subscribe permission; server logs show no permission denial.
- Server version: reproduces identically on 1.10.0 and 1.11.0.
Suspected pattern
This matches webrtc-rs PR #750 ("Handle receiver reuse during renegotiation") in behavior — active receivers (camera) appear to cause the new transceiver for screen share to be marked NOT HANDLED. Is there an equivalent fix in pion-go currently shipping with livekit-server? Or in the SDK's add-track flow?
Questions
- Is this a known issue with publishing a 2nd video track via renegotiation while another video track is active in the same Room?
- Is singlePeerConnection mode planned for client-sdk-flutter? The Swift SDK exposes it (2.13.0+); we suspect single-PC mode would bypass this entirely.
- Is there a recommended workaround (e.g. pre-allocating transceivers at connect time, publish-order tricks, dynacast off, dummy track pre-publish)?
- Is there a way to disable the supervisor's hardcoded 30s publish timeout server-side while we diagnose?
Forensic data available on request
- Full SDP offer + answer of the failing renegotiation
- Timestamped client logs (both host and viewer side)
- Server logs at debug level showing the 30s supervisor timeout
- Confirmation of zero unhandled RTP packets during the broadcast window
Happy to share via secure channel or attach to this issue.
Impact
This blocks shipping screen share in our production live streaming application. We are not in a position to migrate to LiveKit Cloud or rewrite the host to Swift SDK. Looking for a workaround or fix timeline guidance.
Thanks!
Activity
ianagallardo7 commentedon Jun 4, 2026
Update — Server-side root cause confirmed (with forensic logs)
We dug deeper with a self-hosted server-side investigation and have a more precise diagnosis. Sharing it here since it may help others and the maintainers.
What we tried (and ruled out)
We attempted a dual-connection workaround: the host opens a second, dedicated LiveKit Room (separate participant identity,
<id>_screen) that publishes only the screen share track — no camera, no mic on that connection. The theory was that the bug required an already-active camera track competing on the same PeerConnection.Result: the bug still reproduces. Even when the screen share is the only track on a fresh connection, with no camera/mic competing, the publish still times out at exactly 30s. This rules out "camera already active" as the trigger.
Root cause (server logs)
On our self-hosted server we traced it to the publisher's
OnTrackcallback (pion peer connection) never firing for a track added via SDP renegotiation post-connect. Consequences:publish time outat exactly 30s (pkg/rtc/supervisor/publication_monitor.go,publishWaitDuration = 30 * time.Second, hardcoded).AddPublishedTrack(the actual forwarding path to subscribers) is never reached — so even if the supervisor timeout were disabled, the viewer would still never receive the track.Server-side log excerpt (identities/room anonymized):
T+0.000 participant _screen joined (UseSinglePeerConnection: false)
T+0.5 participant active — UDP/ICE OK, SDK Flutter 2.7.0, iOS
T+0.5 CanPublish: true, CanSubscribe: false
T+~0.7 client: LocalTrackPublished source=screenShareVideo (client believes publish succeeded)
T+30.0 ERROR supervisor: "publish time out" (trackID ...) ← exactly 30s
track removed server-side; subscriber never receives it
Key detail: the client SDK reports
LocalTrackPublishedand stays "sharing" with no error for the full duration, while the server silently times out and never forwards. The host has no signal that publishing actually failed.Environment
publication_monitor.go— is byte-identical across these; unchanged for ~18 months).UseSinglePeerConnection: false(legacy connect path). The Flutter SDK 2.7.0 does not sendWrappedJoinRequest, so single-PC mode is never engaged.Questions for maintainers
OnTracknot firing for renegotiation-added tracks a known issue at the pion/transceiver level?WrappedJoinRequest/ single-PC mode planned for the Flutter SDK? It appears to be the path that would avoid this entirely.Happy to share full (un-anonymized) server logs privately if useful. This reproduces 100% on our setup.
[-][bug] Screen share publish via renegotiation times out at exactly 30s when camera is already active (iOS Broadcast Extension, self-hosted server)[/-][+][bug] Track published via SDP renegotiation never forwarded to subscribers — server OnTrack doesn't fire, publish times out at 30s (self-hosted)[/+]ianagallardo7 commentedon Jun 4, 2026
Update — controlled isolation tests confirm the bug is in the
server's handling of the iOS Broadcast Extension feed (not client-side)
Following the initial report, I ran 4 controlled tests over the same
self-hosted server (v1.11.0, pion/webrtc v4.2.11) to isolate which
variable triggers the supervisor
publish time out. In every test theserver never logs
mediaTrack publishedfor the screen-share-sidetrack, suggesting
OnTracknever fires for that specific feed pipeline.The
supervisor error on publication: "publish time out"always landsat exactly +30s after the publisher is
active.Test matrix
setCameraEnabled(true)post-connectmediaTrack publishedfiresq,h,flayers within ~3s. Subscribers getTrackSubscribedinstantly.setScreenShareEnabled(true)→TrackSource.screenShareVideo, simulcastq;h, scalabilityL1T3), published on the host's primarysupervisor error: publish time outat +30s.mediaTrack publishednever appears.<hostId>_screen, token withcanPublish: true, canSubscribe: false)trackID, same supervisor timeout, same absence ofmediaTrack published.LocalVideoTrack(TrackSource.camera, ...)(nosetScreenShareEnabled, no screen-share specific encoding), published on the secondary participant<hostId>_screen, publish-only token)mediaTrack publishedlog., nomediaTrack published` log.What this rules out
After the matrix above, the following are confirmed to be NOT the
trigger:
TrackSource.SCREEN_SHARE_VIDEOfield (test 4 usedCAMERAand failed)q;h/ scalabilitymode
L1T3(test 4 used camera defaultsq;h;f+ no SVC mode andfailed)
post-connect renegotiation and work — test 1)
_screen(test 5 publishedon the primary participant identity and failed identically)
canSubscribe: falseremoved in test 5 — same failure)Roominstance (tests 4 and 5 differ only inwhich
Room.localParticipant.publishVideoTrack(track)is called on)The remaining variable
The only common factor across the 4 failing tests is the source of
the
MediaStream: all four obtained the stream via the iOS BroadcastUpload Extension pipeline (
useiOSBroadcastExtension: true,FlutterBroadcastScreenCapturer, sample buffers crossing the App GroupUnix socket). The real-camera baseline obtains its stream via
getUserMedia(FlutterRPCameraCapturer).This strongly points to the bug living at the boundary where the server
(pion) tries to associate incoming RTP packets from the broadcast feed
with the declared transceiver. The signal-level publish flow completes
(server allocates a
TrackInfoand atrackID), but the media-trackregistration that would trigger
mediaTrack publishedand theforwarding pipeline never happens.
Server-side log signature
For every failing test the server log shows this exact pattern:
INFO service/roommanager.go:415 starting RTC session participant=
INFO rtc/room.go:1262 participant active participant= connectionType=udp
(ICE/UDP/DTLS all fine, no transport errors)
ERROR supervisor/participant_supervisor.go:156 supervisor error on publication
trackID=TR_xxxxxxxxxxxx error="publish time out"
⤷ +30s after "participant active"
There is no
rtc/participant.go:2274 mediaTrack publishedlog forthat
trackID, while the same log line appears reliably (1-3 secondsafter
participant active) for the camera/microphone tracks in thesame room.
There are also no
unhandled RTP ssrcerrors for the failing tracks,which suggests the RTP packets aren't even arriving at the per-track
receiver path — or they arrive but don't match the transceiver mid /
rid binding expected for that publication.
Client-side reproduction (for completeness)
ianagallardo7 commentedon Jun 4, 2026
Update — Twilio and OpenTok succeed with the same Broadcast Extension feed; the difference appears to be publish-at-initial-offer vs post-connect renegotiation
Looking into how other WebRTC SDKs handle the iOS Broadcast Extension, both Twilio Video and OpenTok/Vonage ship working screen-share-from-broadcast-extension examples (public repos). Comparing their approach to the Flutter SDK's surfaced what may be the relevant structural difference.
What they do differently
ConnectOptions.videoTracks; OpenTok: atomicsession publish)VideoSource/VideoCaptureconsumingCMSampleBufferin-processThe common factor in the working implementations: the screen track is part of the initial SDP negotiation, not added later via renegotiation. This lines up with what we observed — the publisher's
OnTracknever fires for the track we add via post-connect renegotiation, while camera/mic (also added post-connect in our flow) work.Why this matters for the Flutter SDK
FastConnectOptions.screenexists, but internally it still callspublishVideoTrackafterEngineConnectedEvent→engine.addTrack→negotiate()→ post-connect renegotiation. So it doesn't replicate the "track in initial offer" pattern that Twilio/OpenTok rely on.Questions
client-sdk-flutterto include a video track in the initial SDP offer (i.e. before the first negotiation completes), similar to Twilio'sConnectOptions.videoTracks?singlePeerConnectionmode (available in the Swift SDK 2.13+) change the publish path such that this renegotiation issue is avoided? Is there a roadmap for exposing it in the Flutter SDK?Happy to provide more detail. Thanks again.