Networks drop: a user walks out of Wi-Fi range, a train goes into a tunnel, a laptop switches from Wi-Fi to a phone hotspot. When this happens, the SDK reconnects on its own. This page explains what happens, what you can control, and how to show it to users.
All code on this page runs in your app. You don't need any backend code for reconnection.
What happens when the network drops
- The SDK notices the connection is gone and fires
reconnecting. - It retries, waiting a little longer between each try (up to a set limit), so it doesn't flood a bad network.
- Each retry first asks the server to resume the session it had. If the drop was short (under about two minutes), the media server still has it, and media never stopped: the call carries on with no renegotiation at all. If it can't be resumed, the SDK builds a fresh media connection and republishes your tracks.
- If a retry works, it fires
reconnected. Your camera, mic, and screen share are already back. You don't need to redo anything. - If every retry fails, it stops and reports
CONNECTION_FAILED. Now your app should offer a "Rejoin" button.
The SDK also recovers when only the media connection fails and the
signaling connection is fine (a network that drops UDP mid-call, say): it
fires reconnecting, rejoins with a new media connection, and fires
reconnected once media flows again. And when the server is restarted
for a deploy, clients come back within two seconds, resuming their
sessions.
This works the same on every platform.
Turn auto-reconnect on or off
It's on by default, and most apps should leave it on. Turn it off only if you want to handle every drop yourself, for example to send the user back to a lobby screen instead.
createRTCClient({ token, endpoint, autoReconnect: true }); // defaultnew Livqeno({ token, endpoint, autoReconnect: true }); // defaultReact Native can also watch the network with
@react-native-community/netinfo (optional). When the user switches from
Wi-Fi to cellular, the SDK recovers straight away instead of waiting for a
timeout: a reconnect that was waiting runs at once, and a live call
restarts ICE onto the new network. You don't need to do anything for this.
onNetworkReconnect is for the case the SDK can't fix by itself: the
network came back but the room is already disconnected or failed.
new Livqeno({
onNetworkReconnect: () => {
// connectivity returned, but the room gave up — offer a rejoin
},
});Livqeno(token: token, endpoint: endpoint, iceServers: iceServers, autoReconnect: true); // defaultFlutter has no built-in network watcher. If you use one (such as
connectivity_plus), call room.handleNetworkChange() when the network
changes, for the same quick recovery React Native gets.
On web, the same hook is room.handleNetworkChange(). Browsers rarely tell
a page when the network changes, so the web SDK never calls it on its own.
| Setting | What happens when the network drops |
|---|---|
autoReconnect: true (default) | You get reconnecting, then reconnected. If all retries fail: disconnected plus an error with code CONNECTION_FAILED. |
autoReconnect: false | Goes straight to disconnected. No retries. |
Show a "Reconnecting…" banner
Users are much more patient when they can see what's happening. A banner turns "the call froze" into "it's reconnecting".
room.on('reconnecting', () => showBanner('Reconnecting…'));
room.on('reconnected', () => hideBanner());
room.on('error', (error) => {
if (error.code === 'CONNECTION_FAILED') {
showError('Lost the connection. Try rejoining.');
}
});room.on('reconnecting', () => showBanner('Reconnecting…'));
room.on('reconnected', () => hideBanner());These are the same Room events as on web.
room.connectionStateChanges.listen((state) {
if (state == LivqenoConnectionState.reconnecting) showBanner('Reconnecting…');
if (state == LivqenoConnectionState.connected) hideBanner();
});Flutter uses streams instead of events. See Flutter SDK.
When the SDK does not reconnect
Some disconnects are on purpose, and reconnecting would be wrong. The
disconnected event carries a reason, so your UI can say the right thing:
| Reason | Why | What to show |
|---|---|---|
CLIENT_INITIATED | Your app called leave(). | Nothing. |
REMOVED | Your backend removed this participant. | "You were removed from the call." Don't rejoin automatically. |
ROOM_CLOSED | The room was closed. | "The call has ended." |
TOKEN_EXPIRED | The token expired and there was no way to get a new one. | Get a fresh token and join again. See Keep long calls alive. |
CONNECTION_FAILED | The network or server failed and every retry did too. | A "Rejoin" button. |
room.on('disconnected', (reason) => {
if (reason === 'REMOVED') showNotice('You were removed from the call.');
else if (reason === 'ROOM_CLOSED') showNotice('The call has ended.');
else if (reason !== 'CLIENT_INITIATED') showRejoinButton();
});React Native uses the same Room event.
room.connectionStateChanges.listen((state) {
if (state != LivqenoConnectionState.disconnected) return;
switch (room.disconnectReason) {
case LivqenoDisconnectReason.removed:
showNotice('You were removed from the call.');
case LivqenoDisconnectReason.roomClosed:
showNotice('The call has ended.');
default:
break;
}
});Keep long calls alive
An RTC token is short-lived. Pass refreshToken and the SDK asks your
backend for a new one before the old one expires, then hands it to the
live session. There is no reconnect and no glitch. It is also used if a
reconnect would otherwise present an expired token.
createRTCClient({
token,
endpoint,
refreshToken: async () => {
const res = await fetch('/api/rtc-token', { method: 'POST' });
return (await res.json()).token; // same room and identity as before
},
});
room.on('warning', (warning) => {
// 'TOKEN_REFRESH_FAILED': refreshToken threw, or the server refused the
// new token. The call carries on with the old token for now.
// 'TOKEN_EXPIRING': the token expires soon and there is no refreshToken.
console.warn(warning.code, warning.message);
});new Livqeno({ token, endpoint, refreshToken: () => fetchNewRtcToken() });Livqeno(token: token, endpoint: endpoint, refreshToken: fetchNewRtcToken);
room.warnings.listen((warning) => debugPrint('$warning'));The new token must be minted for the same room and identity. The
server refuses one that isn't, and you get a TOKEN_REFRESH_FAILED
warning.
What users see during a reconnect
- Other people's video and audio stay connected through a short drop.
You usually don't need to re-attach anything. If the session couldn't
be resumed, each remote track fires
trackUnsubscribedand thentrackSubscribedagain with a new track, so attach the new one. - Your own camera, mic, and screen share come back automatically. You
don't call
enableCamera()again, and nothing is sent twice. - If the state goes
disconnected→failed, the room can't recover. Join again from scratch.
App lifecycle (React Native)
When the app goes to the background:
- The call stays connected. A quick app switch shouldn't make someone leave the meeting.
- Video stops (the OS pauses the camera) and starts again when the user comes back.
Want to leave the call instead? That's up to you:
new Livqeno({
onAppStateChange: (state) => {
if (state === 'background') void livqeno.leave(); // your call, not automatic
},
});To keep audio playing in the background, see Background Audio.
Show call quality
A connection can stay up but still be bad: choppy video, robotic audio. A quality badge (like a signal-strength icon) tells the user it's their network, not your app.
The media server scores every participant's connection once a second, from what it measures itself: packet loss, jitter, round-trip time, and for someone receiving video, how much bandwidth they have against what their videos need. It pushes the score when it changes, so there's nothing to poll:
room.on('connectionQualityChanged', (participant, quality) => {
// 'excellent' | 'good' | 'poor' | 'lost'
if (participant === room.localParticipant) updateQualityBadge(quality);
else updateTileBadge(participant.identity, quality);
});
room.localParticipant.connectionQuality; // the current score, 'unknown' until the first oneThis works the same on Web and React Native. React has
useConnectionQuality(participant); Flutter has
room.connectionQualityChanges and participant.connectionQuality.
Against a server older than this feature the score stays 'unknown'
(never a guess). room.getConnectionStats() has the raw numbers either way
(see Diagnostics).
The same numbers also reach Livqeno's servers. See them with
livqeno connections inspect, or in the Event Catalogue.
Common errors
| Error | Cause | Fix |
|---|---|---|
CONNECTION_FAILED | Every reconnect attempt failed. | Join again with client.join() / livqeno.join(). Get a fresh token if the old one expired. |
TOKEN_EXPIRED | The server ended the session because its token expired. | Pass refreshToken so the SDK can renew it. |
| Reconnect never happens | autoReconnect: false is set. | Remove it, or handle disconnected yourself and call join() again. |
Tips for production
- Don't write your own retry loop around
join(). It will fight the SDK's own reconnects. Wait forfailed, then join again. - A "Reconnecting…" banner that never goes away usually means your app hit an error during the reconnect. Look for an uncaught error nearby.
Next steps
- Diagnostics:
getConnectionStats()in full, to explain why quality is poor. - Background Audio: mobile background behavior.
- Troubleshooting: connection failures that don't throw an error.