Repository navigation
livekit-signaling: fix teardown gaps on requested close and response timeout - #1568
Merged
Merged
Conversation
โฆtimeout
A requested close (`close` or `restart`) aborted the stream, and the old signal
task then emitted `SignalEvent::Close("stream closed")`. A host that treats
Close as "start a resume" reconnected inside its own reconnect. The signal task
now emits Close only when the `TransportFailed` transition is accepted, which
the lifecycle already refuses after a requested close.
A join or reconnect response that failed or timed out dropped the fresh
`SignalStream` without closing it. `SignalStream` has no `Drop`, so its read
and write tasks kept the socket open. Both paths now close the stream.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Changeset โThis PR includes a changeset covering all affected packages:
|
Contributor
There was a problem hiding this comment.
๐ Devin Review: 1 flag
Not posted on this PR by your GitHub settings โ view it in Devin Review. (Configure)
Fixes issues with teardown gaps during requested close and response timeout in livekit-signaling.
`SimulateScenario::SignalReconnect` and `DisconnectSignalOnResume` closed the signal client and relied on the resulting `SignalEvent::Close` to start the resume. A requested close no longer emits that event, so the simulated loss reports itself through `on_session_disconnected`, with the same arguments the `Close` handler uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1egoman
approved these changes
Oct 29, 2026
lukasIO
added a commit
that referenced
this pull request
Nov 2, 2026
#1589) `SignalClient::close` cannot end a session for good. A closed client still accepts `restart`, because `restart` and the engine's simulated signal loss both close first and then resume. So a close that races a resume loses: the resume reopens the session, and nobody holds the new transport. The UniFFI wrapper from #1521 works around this with its own token, checked before `send` and around `restart`. 1egoman suggested on #1521 that the token belongs in `SignalClient`, as in the web SDK. Stacked on #1568, which merges first. ## Change `SignalOptions` has a new field, `cancellation_token`, owned by the caller. Cancelling it ends the session for good: - A connect in flight, including the region fallback, stops with the new `SignalError::Cancelled`. A connect is not retried on other regions after a cancel. - `restart` refuses a cancelled token before it dials. A resume in flight stops, and closes the transport it opened. - The signal task closes the transport without emitting `SignalEvent::Close`. - `send` drops the request instead of holding it for a flush that never runs. A send that the transport never completes gives up, because it holds the lock that the close needs. `close` does not change and still leaves the session resumable. The default token is never cancelled, so callers that do not set it see no change. Both the field and the variant are additions to `#[non_exhaustive]` types. The wrapper drops its checks around `restart` and its `CloseOnCancel` guard. A cancelled `connect_signal_session` call now cancels the token through a drop guard. Releasing a `SignalSession` only cancels the token, and the signal task does the close. The wrapper keeps its check in `send`, because `SignalClient::send` returns `()` and the wrapper reports `SignalRequestError::Closed`. The engine gives each `RtcSession` a fresh token, and cancels it in `close` after the `Leave` goes out. The fresh token replaces any token in `EngineOptions`, because a cancel from outside would end signalling without the engine noticing. The engine ends with `close` instead. ## Failure case A close lands while a resume is waiting on the server. Today the resume completes and installs a transport that nobody closes. With this change the resume stops with `Cancelled` and closes the transport it opened. If the cancel lands just after the resume installed its transport, `restart` still returns `Cancelled`, and the new signal task closes the transport. ## Verified - `cargo test -p livekit-signaling`: 51 passed. Three new tests: - `cancel_interrupts_an_attempt_waiting_on_the_server` cancels a connect and a resume that wait on a server that never answers. Both return `Cancelled`, no connection stays open, and the connect makes no region request. - `cancel_closes_a_connected_session_for_good` cancels a connected client. The signal task ends with the state `Closed` and emits no `Close`. A later `restart` returns `Cancelled`, a later `send` is not held, and the client is freed once dropped. - `cancel_abandons_a_write_the_transport_never_completes` stalls a send on a transport that never completes writes, then cancels. The send returns and the signal task closes the transport. Without the fix, the test fails after 5 s. - `cargo test -p livekit-ffi --no-default-features --features core-modules --lib signaling`: 9 passed. `dropping_mid_restart_stops_the_resume` now asserts that the resume never dials after the session is released. - `cargo test -p livekit --features "__lk-e2e-test,native-tls" --test reconnection_test --test engine_lifecycle_test` against `livekit-server --dev`: 9 passed, including `test_signal_reconnect_resumes` and `test_resume_failure_escalates_to_full_reconnect`. - `cargo make android-package` from `livekit-ffi/`: the Kotlin bindings compile. ๐ค Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merged
stephen-derosa
pushed a commit
that referenced
this pull request
Nov 4, 2026
> [!IMPORTANT] > Merging this pull request will create these releases # livekit-api 0.10.1 (2026-11-04) ## Fixes - livekit-signaling: end a session for good through a cancellation token - #1589 (@lukasIO) - livekit-signaling: fix teardown gaps on requested close and response timeout - #1568 (@lukasIO) # livekit-signaling 0.1.9 (2026-11-04) ## Fixes - Add `SignalOptions::cancellation_token`, which ends a signal session for good: a connect or resume in flight stops with `SignalError::Cancelled`, and the signal task closes the transport. - A requested `close` or `restart` no longer emits `SignalEvent::Close`. A join or reconnect response timeout now closes the WebSocket instead of leaving it open. # livekit 0.10.1 (2026-11-04) ## Fixes - Pre-encoded video publishing no longer requires a local encoder for its codec, so pre-encoded H265 works on hosts without an H265 encoder, a camera track requesting a codec no local encoder produces now publishes VP8 instead of a codec it cannot encode, and `PeerConnectionFactory::video_encoder_codecs` reports codec support using a non-exhaustive `libwebrtc::video_codec::VideoCodec` enum supporting `FromStr`, with `Auto` excluding the pre-encoded pass-through backend. - Preserve distinct frame metadata when queued video frames are captured within the same millisecond, including pre-encoded frames that share an RTP timestamp. - livekit-signaling: fix teardown gaps on requested close and response timeout - #1568 (@lukasIO) - `OfferOptions::offer_to_receive_audio` and `offer_to_receive_video` are now `Option<bool>`. `None` leaves libwebrtc's option unset, so receive-only transceivers stay receive-only. The default is still `Some(false)`. Single peer connection offers leave both unset and no longer rewrite `a=inactive` to `a=recvonly`. - Space reconnect attempts on the same schedule as livekit-client's `DefaultReconnectPolicy` (0.3 s, 1.2 s, 2.7 s, 4.8 s, then 7 s, plus up to 1 s of jitter) instead of full jitter, which could spend all ten attempts within a few seconds of outage. The jitter now comes from a randomly keyed hash rather than the clock's sub-second digits, which are all zero on macOS. No new reconnect attempt starts after `RECONNECT_MAX_DURATION` (60 s), so slow failing attempts cannot keep a room in `Reconnecting` for minutes. `RECONNECT_BACKOFF_MULTIPLIER` is deprecated: the schedule is quadratic in the attempt number. - Add `SignalOptions::cancellation_token`, which ends a signal session for good: a connect or resume in flight stops with `SignalError::Cancelled`, and the signal task closes the transport. - A requested `close` or `restart` no longer emits `SignalEvent::Close`. A join or reconnect response timeout now closes the WebSocket instead of leaving it open. - A received transcription with an invalid track sid is now dropped with a warning. Before, it made the client panic, which aborts the process in release builds. # livekit-ffi 0.13.4 (2026-11-04) ## Fixes - Pre-encoded video publishing no longer requires a local encoder for its codec, so pre-encoded H265 works on hosts without an H265 encoder, a camera track requesting a codec no local encoder produces now publishes VP8 instead of a codec it cannot encode, and `PeerConnectionFactory::video_encoder_codecs` reports codec support using a non-exhaustive `libwebrtc::video_codec::VideoCodec` enum supporting `FromStr`, with `Auto` excluding the pre-encoded pass-through backend. - Re-baseline the iOS and Android binary size budgets for the signaling client, which blocked publishing the 0.13.3 Swift and Android packages. - A failed FFI request no longer writes the full request to the error log or to the Node error message, so E2EE keys in `SetSharedKey` and `SetKey` requests stay out of logs. - Preserve distinct frame metadata when queued video frames are captured within the same millisecond, including pre-encoded frames that share an RTP timestamp. - Build the shipped staticlib and cdylib with LTO, shrinking the core-modules library by ~17% - livekit-signaling: fix teardown gaps on requested close and response timeout - #1568 (@lukasIO) - `OfferOptions::offer_to_receive_audio` and `offer_to_receive_video` are now `Option<bool>`. `None` leaves libwebrtc's option unset, so receive-only transceivers stay receive-only. The default is still `Some(false)`. Single peer connection offers leave both unset and no longer rewrite `a=inactive` to `a=recvonly`. - Space reconnect attempts on the same schedule as livekit-client's `DefaultReconnectPolicy` (0.3 s, 1.2 s, 2.7 s, 4.8 s, then 7 s, plus up to 1 s of jitter) instead of full jitter, which could spend all ten attempts within a few seconds of outage. The jitter now comes from a randomly keyed hash rather than the clock's sub-second digits, which are all zero on macOS. No new reconnect attempt starts after `RECONNECT_MAX_DURATION` (60 s), so slow failing attempts cannot keep a room in `Reconnecting` for minutes. `RECONNECT_BACKOFF_MULTIPLIER` is deprecated: the schedule is quadratic in the attempt number. - Add `SignalOptions::cancellation_token`, which ends a signal session for good: a connect or resume in flight stops with `SignalError::Cancelled`, and the signal task closes the transport. - A received transcription with an invalid track sid is now dropped with a warning. Before, it made the client panic, which aborts the process in release builds. # livekit-capture 0.1.9 (2026-11-04) ## Fixes - Pre-encoded video publishing no longer requires a local encoder for its codec, so pre-encoded H265 works on hosts without an H265 encoder, a camera track requesting a codec no local encoder produces now publishes VP8 instead of a codec it cannot encode, and `PeerConnectionFactory::video_encoder_codecs` reports codec support using a non-exhaustive `libwebrtc::video_codec::VideoCodec` enum supporting `FromStr`, with `Auto` excluding the pre-encoded pass-through backend. - Drop a transcription with an invalid track sid instead of panicking - #1600 (@lukasIO) - Preserve distinct frame metadata when queued video frames are captured within the same millisecond, including pre-encoded frames that share an RTP timestamp. - livekit-signaling: end a session for good through a cancellation token - #1589 (@lukasIO) - livekit-signaling: fix teardown gaps on requested close and response timeout - #1568 (@lukasIO) - `OfferOptions::offer_to_receive_audio` and `offer_to_receive_video` are now `Option<bool>`. `None` leaves libwebrtc's option unset, so receive-only transceivers stay receive-only. The default is still `Some(false)`. Single peer connection offers leave both unset and no longer rewrite `a=inactive` to `a=recvonly`. - Space reconnect attempts on the same schedule as livekit-client's `DefaultReconnectPolicy` (0.3 s, 1.2 s, 2.7 s, 4.8 s, then 7 s, plus up to 1 s of jitter) instead of full jitter, which could spend all ten attempts within a few seconds of outage. The jitter now comes from a randomly keyed hash rather than the clock's sub-second digits, which are all zero on macOS. No new reconnect attempt starts after `RECONNECT_MAX_DURATION` (60 s), so slow failing attempts cannot keep a room in `Reconnecting` for minutes. `RECONNECT_BACKOFF_MULTIPLIER` is deprecated: the schedule is quadratic in the attempt number. # webrtc-sys 0.3.53 (2026-11-04) ## Fixes - Pre-encoded video publishing no longer requires a local encoder for its codec, so pre-encoded H265 works on hosts without an H265 encoder, a camera track requesting a codec no local encoder produces now publishes VP8 instead of a codec it cannot encode, and `PeerConnectionFactory::video_encoder_codecs` reports codec support using a non-exhaustive `libwebrtc::video_codec::VideoCodec` enum supporting `FromStr`, with `Auto` excluding the pre-encoded pass-through backend. - Preserve distinct frame metadata when queued video frames are captured within the same millisecond, including pre-encoded frames that share an RTP timestamp. # libwebrtc 0.4.0 (2026-11-04) ## Breaking Changes - `OfferOptions::offer_to_receive_audio` and `offer_to_receive_video` are now `Option<bool>`. `None` leaves libwebrtc's option unset, so receive-only transceivers stay receive-only. The default is still `Some(false)`. Single peer connection offers leave both unset and no longer rewrite `a=inactive` to `a=recvonly`. ## Features - Pre-encoded video publishing no longer requires a local encoder for its codec, so pre-encoded H265 works on hosts without an H265 encoder, a camera track requesting a codec no local encoder produces now publishes VP8 instead of a codec it cannot encode, and `PeerConnectionFactory::video_encoder_codecs` reports codec support using a non-exhaustive `libwebrtc::video_codec::VideoCodec` enum supporting `FromStr`, with `Auto` excluding the pre-encoded pass-through backend. ## Fixes - Preserve distinct frame metadata when queued video frames are captured within the same millisecond, including pre-encoded frames that share an RTP timestamp. Co-authored-by: knope-bot[bot] <152252888+knope-bot[bot]@users.noreply.github.com>
JesseDietrichson
added a commit
to RootPlatform/livekit-rust-sdks
that referenced
this pull request
Nov 6, 2026
Upstream since livekit-ffi/v0.13.1: 35 commits, through livekit-ffi v0.13.4 (ab3711a). Among them: libwebrtc webrtc-89d790b -> webrtc-a545de4 without the roots behind the compromised eMudhra intermediates, and the first prebuilt built with the fixed external_audio_source.patch (livekit#1562); livekit-client's reconnect backoff, at least ~44 s with a 60 s cap (livekit#1457); signaling teardown without a spurious Close and without WebSockets left open after a failed join or resume, plus a per-session cancellation token (livekit#1568, livekit#1589); publisher senders detached before the PeerConnection closes (livekit#1570); a transcription with an invalid track sid dropped instead of aborting the process (livekit#1600); protocol 18 with negotiated data channels in single PC mode (livekit#1527); single PC offers without offer_to_receive and without the inactive->recvonly munge (livekit#1610); NVENC/Jetson delivered frames no longer reported as encode errors (livekit#1551); per-frame VAAPI parameter buffers destroyed (livekit#1524); pass-through codecs advertised and publish codec selection (livekit#1451); presentation timestamps kept by the encoder wrapper (livekit#1607); capture JoinCapture (livekit#1547) and use_legacy_data_streams removed (livekit#1575); rustls 0.23.46 and zip 9.0.5. Conflicts, all textual: - livekit/src/room/options.rs: the test module imports both sides' helpers. - peer_transport.rs: create_initial_offer's comment takes upstream's first sentence; its x-google-start-bitrate paragraph stays out, since the fork never took that munging. - peer_connection_factory.h: both sides' forward declarations. - video_encoder_factory.cpp: both sides' includes. Reviewed where both sides meet without a conflict: create_sender keeps the fork's stereo and external-audio hooks next to livekit#1451's producible-codec filter (the H.264 publishes are unchanged: OpenH264 is in every prebuilt); close() detaches senders through the fork's remove_track; the encoder factory keeps the MF backend and the per-codec de-dup after livekit#1451's pass-through formats; livekit#1607's wrapper fits the MF encoder, which delivers on the encoder queue and whose Release() is idempotent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
livekit-signalinghas two teardown gaps that block its use from the Swift SDK over UniFFI (#1521). First, a requested close emits a spuriousSignalEvent::Close("stream closed").SignalClient::restartcallsclose(), which aborts the stream, so the old signal task reports the end of the stream as a lost link. A host that readsCloseas "start a resume" then reconnects inside its own reconnect. Second, a join or reconnect response that fails or times out drops the freshSignalStreamwithout closing it.SignalStreamhas noDrop, so its read and write tasks keep the WebSocket open. Devin raised the second gap on #1521, and it was left out of scope there.Change
TransportFailedtransition, the signal task emitsClose. Otherwise it emits nothing. The lifecycle already refuses that transition after a requestedcloseorrestart, so a requested close stays silent. A remote close, a dropped link, and a ping timeout still emitClose, before the old stream closes.get_join_responseorget_reconnect_responsefails, a new helper,close_on_err, closes the stream.SignalInner::connectandSignalInner::restartboth use it.an_unanswered_transport_is_released_on_timeoutuses a mock connection that stays open and never answers, and checks that the join and reconnect timeouts both release it.close_is_reported_for_a_lost_transport_onlychecks thatcloseandrestartemit no event and that a dropped transport emitsClose.livekit-signalingtokio dev-dependency enablestest-util, so the 5 s timeout test runs on a paused clock.The
livekitengine already filters aClosethat arrives after it closes the session (rtc_session.rs,SignalEvent::Closehandler). It still getsCloseon a real link loss, and no longer gets the spurious one during its ownrestart.๐ค Generated with Claude Code