snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 15:56:32 +05:00
commit 1d2bfcac0c
3159 changed files with 464524 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
import { IStore } from '../../app/types';
import { JitsiIceRestartReason } from '../lib-jitsi-meet';
import { INetInfoState } from '../net-info/reducer';
import { getCurrentConference } from './functions';
import logger from './logger';
/**
* How long the network state has to stay put before a change is acted upon. A real interface handover produces a
* burst of events from the OS (wifi may drop to 'none' and back before settling on cellular), and reacting to each
* one would request several restarts for a single physical event.
*/
export const NETWORK_CHANGE_DEBOUNCE_MS = 3000;
/**
* The minimum time between two ICE restarts triggered by a network change. Jicofo rate-limits the requests server
* side as well; this keeps a flapping radio from burning through that budget.
*/
export const NETWORK_CHANGE_COOLDOWN_MS = 30000;
/**
* A tag used to make the log lines of this module greppable together with the ones the bridge and Jicofo emit.
*/
const LOG_PREFIX = 'ICE restart (network change):';
/**
* The identity of the network the current JVB ICE session was established on, as returned by
* {@link getNetworkIdentity}. A restart is requested when the device's current network no longer matches it.
*/
let sessionNetworkIdentity: string | undefined;
/**
* Set when a SET_NETWORK_INFO update reports the device offline while a session is active. Consumed (and
* cleared) the next time the device is back online. A reconnect is treated as a change even if it lands back
* on the exact same address (e.g. the same DHCP lease) - the interface genuinely went down and came back up,
* so any ICE candidates gathered on it are stale regardless of whether the address string still matches.
*/
let hadOfflineGap = false;
/**
* The pending debounce timer, if any.
*/
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
/**
* When the last restart was requested, used to enforce {@link NETWORK_CHANGE_COOLDOWN_MS}.
*/
let lastRequestTime = 0;
/**
* Derives a stable identifier for the network the device is currently attached to. Two values comparing unequal
* mean the local address the media session was set up on is no longer usable, so the session's local ICE
* candidates are stale.
*
* The network type alone is not enough: moving between two wifi networks (leaving home for the office) keeps the
* type at 'wifi' while changing the local address, so the address is included when the OS reports one. It is
* reported for wifi and ethernet but not for cellular, so two different cellular attachments are
* indistinguishable here and will not be detected as a change.
*
* @param {INetInfoState} netInfo - The 'base/net-info' state.
* @returns {string|undefined} - The identity, or undefined if the network type is not known yet.
*/
export function getNetworkIdentity(netInfo?: INetInfoState): string | undefined {
const networkType = netInfo?.networkType;
if (!networkType) {
return undefined;
}
const ipAddress = netInfo?.details?.ipAddress;
return ipAddress ? `${networkType}/${ipAddress}` : networkType;
}
/**
* Records the network the JVB ICE session has just been established on, so that later changes can be detected
* relative to it. Called when a conference is joined and after a restart has been requested.
*
* @param {IStore} store - The redux store.
* @returns {void}
*/
export function noteSessionNetwork(store: IStore): void {
sessionNetworkIdentity = getNetworkIdentity(store.getState()['features/base/net-info']);
logger.debug(`${LOG_PREFIX} session network is ${sessionNetworkIdentity}`);
}
/**
* Drops any pending work and forgets the session's network. Called when the conference goes away.
*
* @returns {void}
*/
export function resetNetworkChangeState(): void {
if (debounceTimer) {
clearTimeout(debounceTimer);
debounceTimer = undefined;
}
sessionNetworkIdentity = undefined;
hadOfflineGap = false;
}
/**
* Requests the restart, if all of the conditions still hold. Everything is re-read from the store here rather than
* captured when the timer was armed, so that a network which changed and changed back within the debounce window
* does not produce a pointless restart.
*
* @param {IStore} store - The redux store.
* @private
* @returns {void}
*/
function _restartIce(store: IStore): void {
const state = store.getState();
const netInfo = state['features/base/net-info'];
const identity = getNetworkIdentity(netInfo);
const recoveringFromOfflineGap = hadOfflineGap;
if (!identity || (!recoveringFromOfflineGap && identity === sessionNetworkIdentity)) {
logger.debug(`${LOG_PREFIX} network settled back to ${sessionNetworkIdentity}, nothing to do`);
return;
}
// Going offline is not something a restart can recover from - there is no interface to move to. The next
// change, when the device attaches to something, is what we want to act on.
if (!netInfo?.isOnline) {
logger.info(`${LOG_PREFIX} offline (${identity}), not requesting a restart`);
hadOfflineGap = true;
return;
}
hadOfflineGap = false;
const conference = getCurrentConference(state);
if (!conference) {
return;
}
if (!conference.isIceRestartSupported()) {
return;
}
// While P2P is active the JVB session is on hold and not carrying media, so there is nothing to preserve.
if (conference.isP2PActive()) {
logger.info(`${LOG_PREFIX} P2P is active, not requesting a restart`);
return;
}
const sinceLastRequest = Date.now() - lastRequestTime;
if (sinceLastRequest < NETWORK_CHANGE_COOLDOWN_MS) {
logger.warn(`${LOG_PREFIX} suppressed, last request was ${sinceLastRequest}ms ago`);
return;
}
logger.info(`${LOG_PREFIX} requesting a restart, ${sessionNetworkIdentity} -> ${identity}`);
// Set before the request so that a second change arriving while this one is in flight does not request
// another restart.
lastRequestTime = Date.now();
conference.restartJvbIce(JitsiIceRestartReason.NETWORK_CHANGE)
.then(() => {
// The request was accepted; the bridge's new transport arrives asynchronously. Treat the new network
// as the session's own from here so a repeat of the same change is not acted upon again.
noteSessionNetwork(store);
})
.catch((error: Error) => {
// Not fatal and deliberately not escalated: the pre-existing recovery flow (a full session restart
// once ICE actually fails) is untouched, and until then media keeps flowing on the old path.
logger.warn(`${LOG_PREFIX} request failed: ${error?.message ?? error}`);
});
}
/**
* Reacts to a change of the device's network. An in-place ICE restart is make-before-break - the bridge keeps
* carrying media on the established path while the new one is brought up - so it is safe to request whenever the
* local address the session was set up on may have gone stale, rather than only once ICE has failed.
*
* The work is debounced; see {@link NETWORK_CHANGE_DEBOUNCE_MS}.
*
* @param {IStore} store - The redux store.
* @returns {void}
*/
export function maybeRestartIceOnNetworkChange(store: IStore): void {
const state = store.getState();
// No session to move.
if (!getCurrentConference(state)) {
return;
}
const netInfo = state['features/base/net-info'];
const identity = getNetworkIdentity(netInfo);
if (!identity) {
return;
}
// The OS had not reported a network yet when the conference was joined, so this is the first chance to learn
// which network the session was established on.
if (sessionNetworkIdentity === undefined) {
sessionNetworkIdentity = identity;
return;
}
if (!netInfo?.isOnline) {
hadOfflineGap = true;
return;
}
// Still on the network the session was established on, and no intervening offline gap to recover from.
if (identity === sessionNetworkIdentity && !hadOfflineGap) {
return;
}
const { enableIceRestartOnNetworkChange = true } = state['features/base/config'];
if (!enableIceRestartOnNetworkChange) {
return;
}
logger.debug(`${LOG_PREFIX} ${sessionNetworkIdentity} -> ${identity}, settling for `
+ `${NETWORK_CHANGE_DEBOUNCE_MS}ms`);
debounceTimer && clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
debounceTimer = undefined;
_restartIce(store);
}, NETWORK_CHANGE_DEBOUNCE_MS);
}