snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
238
react/features/base/conference/networkChangeIceRestart.native.ts
Normal file
238
react/features/base/conference/networkChangeIceRestart.native.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user