Repository navigation
Reconnect at once when the device network changes - #3
Merged
Merged
Conversation
Watch the device's networks (connectivity_plus behind a NetworkMonitor seam): while joined, a change is a retryable loss; while reconnecting, attempts wait when offline and restart with a fresh budget on a new network. ICE disconnected for 3 s is a retryable loss. Client pings every 5 s, opening the socket times out after 10 s, and a new session no longer waits for the old socket to finish closing. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
… (SPEC §9) On Android flutter_webrtc disposes the native handles of transceivers that have no mid whenever it looks up a negotiated one, so asking each transceiver for its direction failed as soon as a negotiation mixed both kinds: adding a track after an earlier negotiation, and so every reconnection that restores two tracks, failed to create the offer. On iOS transceivers without a mid share one id. The engine now records the receive transceivers it adds by their receiver track id and stops the excess from one snapshot, those without a mid first. The fake models the Android handle disposal, which turned seven existing tests red before the fix. Claude-Session: https://claude.ai/code/session_01WZooUEkqeaFSacezszAELH
A network report is acted on once the reports have been stable for 500 ms, so the brief "offline" platforms report while switching networks is not a loss of its own. Only losing a network (or reaching the same set through offline) is a change; gaining one keeps the connection's path. While reconnecting, an attempt that started after the change began is kept, a session that joins while offline does not complete the reconnection, the reconnection gives up after `offlineTimeout` (60 s) offline, and one reconnection makes at most 3 x maxAttempts attempts. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
With addTracksOnReconnect off, a network switch left the user publishing nothing until the app added its tracks again. It now defaults to true (deviation D26 from the web SDK); tests that cover the behaviour without restoration say so explicitly. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
FISHJAM_ID, SANDBOX_API_URL and PEER_TOKEN can come from a file passed with --dart-define-from-file; example/.env.example lists them and where to get them. With a Sandbox API URL the form starts in sandbox mode. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
A session that completed a reconnection was still treated as a reconnection attempt, so when the next reconnection began, the tracks it had restored were reported removed twice. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
Tracks were re-added one after another, each waiting for its own negotiation, so audio came back 0.6-1.8 s after video on devices. They are now started together and ride one negotiation cycle (SPEC §8.3). An attempt lost while restoring adds none of them; the next attempt restores them all. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
Such a report can only mean a new session is needed, so waiting 500 ms for it to settle only delayed the reconnection. Later reports settle against it as usual. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
Only joining while offline was checked. If the network went away while the tracks were being restored, Reconnected was emitted for a session that could not survive, and the network coming back started nothing because the client counted as joined. The reconnection now waits for the network and starts a new attempt then, as when the session joins offline. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
Without the connectivity plugin, as in an app's widget tests, the failed subscription went to FlutterError.onError instead of the stream, so it failed those tests although the client fell back to always online. The current networks are now read first; when that fails the watcher logs a warning and never subscribes. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
The old socket's close is not awaited, and after a network switch that socket has often failed already, so its close could fail as an unhandled error in the app's zone. The failure is now logged at debug level. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
A start whose first read of the network state failed stopped the watcher unconditionally, so it cancelled a newer start that had already subscribed. It now gives up silently once it is no longer the current run. Claude-Session: https://claude.ai/code/session_01DKNRwERcfKW2FtCEbRUjZA
The example now shows only media, mute toggles and leave, so a reconnection in it exercises tracks alone. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
Order the excess receive transceivers with two filters instead of a sort, build the network loss reason from the change name, and share the repeated reconnect sequence and offline config in the tests. Claude-Session: https://claude.ai/code/session_01PD5D7HqJhZwwEpL7KszKzZ
…t Joined An attempt that joined while offline, or that was still restoring its tracks when a new network appeared, was replaced without a loss of its own, so the peers its Joined announced were never reported gone: an app kept a peer that had left meanwhile. Every path that gives a session up now goes through one place that records them (D22). Claude-Session: https://claude.ai/code/session_01DKNRwERcfKW2FtCEbRUjZA
…ctionFailed A track is registered as soon as its addTrack starts, so a reconnection during that negotiation snapshotted it: the failed addTrack's id was reported removed although the app never got it, and the track was published again although the app was told it failed. The same happened to a track an abandoned attempt negotiated without its LocalTrackAdded reaching the app. The client now records the ids it announced and snapshots and removes only those. The status turns disconnected before the given-up session is torn down, so an app could call connect() from a ReconnectionFailed listener; the teardown then failed that new call with "disconnected (reconnectionFailed)". The pending connect() is now taken before the teardown, so only the call that started the session is settled by it. Claude-Session: https://claude.ai/code/session_01DKNRwERcfKW2FtCEbRUjZA
Waiting 500 ms for reports to settle never sped up a recovery on real devices: a network reported after offline was already acted on at once, no attempt starts while offline, and a Wi-Fi to Wi-Fi switch is either reported as offline then online or not reported at all. The wait only delayed noticing an offline and a network that went away next to another. Each report is now compared with the previous one as it arrives. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
Brings in audio output routing. The client takes both a network monitor and an audio output manager; the reconnection decision entries move to D24–D27 after main's D23. Claude-Session: https://claude.ai/code/session_01Q5vZKmaVuaPpgoRaUvLQT8
A network change and ICE disconnected for 3 s already end the session quickly; the client ping only has to catch a socket that dies on its own. A 5 s pong timeout turns a briefly congested link into a full reconnection. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
Since every network report is acted on at once, a change begins when it is handled, so no attempt in progress can have started after it: the check that kept such an attempt never held. A new network now always supersedes an attempt in progress, as it already did in practice. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
close() on a WebSocketTransport whose connect had not completed left the open running, so the socket could still open after the session using it was gone. The transport now opens through an HttpClient of its own and closing it aborts the open, which then fails like any socket that cannot be opened (SPEC §2.1). The connect timeout uses the same client, so the separate connectionTimeout and the TimeoutException special case go. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
start() read its run number only after awaiting stop(), so two starts in a row both saw the same number, both subscribed, and stop() ended only one of them. The run number is now taken before any await, the baseline is set once the subscription exists, and stop() is synchronous, so it takes effect at once. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
Since receive transceivers are tracked instead of asked for their direction, getDirection() and getCurrentDirection() had no caller left; they go from the seam, the adapter and the fake. Stopping excess receive transceivers is now two passes over one snapshot, those without a mid first, which is what it did before, written plainly. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
…ection A connect() made while the session given up before it was still torn down (from a ReconnectionFailed listener, or without awaiting leave()) opened at once: the old Disconnected came after the new Joined and the old teardown left the audio communication mode off during the new call. connect() now opens its socket once that teardown has finished (SPEC §3.2.6). Reconnection, simplified without changing what the app sees: - LocalTrackRemoved is emitted in one place, right after the next Joined, for every local track the app was told about, instead of by the manager and the client with flags to avoid reporting a track twice. - The manager loses duplicated resets and a dead condition, its offline handling in onSessionJoined is one flow, and giving up has one helper. - The network is watched only with reconnection enabled (SPEC §6.1). The fixture replays use the default reconnection config, which publishes tracks again since that default changed. The client tests share rejoin() and negotiate() helpers for the repeated join and negotiation steps. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
A receive transceiver whose receiver has no track has no id to record it by, so it is not counted and the next offerData adds another one. That was silent; it is now logged as a warning. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
A network change reports the loss and then acts on the reconnection it started, which worked only because the async loss handler ran its decision before its first await. The decision is now a plain synchronous method; only ending the session, when reconnection is refused, runs on. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
The network monitor reported connectivity_plus's `mobile` as `cellular`. Nothing depends on the name, so every kind now keeps the plugin's name. Claude-Session: https://claude.ai/code/session_01Xn8b3WbMz12ct7ugWKC7hC
Renames only; no behavior change. - NetworkMonitor -> NetworkSource (own file), NetworkWatcher -> NetworkChangeDetector, ConnectivityNetworkMonitor -> ConnectivityNetworkSource - ReconnectManager: _fail -> _markFailed, _giveUp -> _failAndEndSession, _attempt -> _attemptsOnThisNetwork, _timer -> _nextAttemptTimer - FishjamClient: _onRetryableLoss -> _reconnectOrEnd, _abandon -> _markSessionLost, _ending -> _previousSessionEnded - ReceiveTransceivers and the transport: clearer field and local names - Shorter, literal comments; drop the ones that restate the code Claude-Session: https://claude.ai/code/session_016r1PaAEmRovribj1Mq23Bg
- NetworkSource: current() -> getNetworkTypes(), changes -> networkTypesChanged (and the fake's matching fields) - NetworkChangeDetector: _previousNetworks -> _previousNetworkStates, set before subscribing so it is never read unset - The track restore comment no longer claims failures are reported - Drop a comment in ReceiveTransceivers that restated the code Claude-Session: https://claude.ai/code/session_016r1PaAEmRovribj1Mq23Bg
Magmusacy
marked this pull request as ready for review
October 25, 2026 21:02
graszka22
approved these changes
Oct 26, 2026
graszka22
left a comment
Contributor
There was a problem hiding this comment.
This behaves differently than RN (addTracksOnReconnect default, detecting network changes) but i think flutter sdk's behaviour now makes more sense.
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.
Summary
Reconnect as soon as the device's network changes, instead of waiting for the old connection to time out.
SDK
connectivity_plus. Losing or switching a network while joined starts a reconnection at once.ReconnectConfig.offlineTimeout, 60 s by default) instead of being used up.disconnectedfor 3 s counts as lost.addTracksOnReconnectnow defaults totrue.connect()may be called from aReconnectionFailedlistener; it opens once the previous session has ended.Example app
example/.env.exampleto.env.localand runflutter run --dart-define-from-file=.env.local.