From e22c7002379f63f2b76ddf1ea0f64ca9e94d77c4 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Thu, 27 Mar 2025 12:55:10 -0700 Subject: [PATCH] Calling: Add cache for relay server requests Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> --- ts/services/calling.ts | 93 ++++++++++++++++++++++++++++------------- ts/textsecure/WebAPI.ts | 1 + ts/types/Calling.ts | 12 ++++++ 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 1e37c39fed3..f8897296529 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -63,6 +63,8 @@ import { isMe } from '../util/whatTypeOfConversation'; import type { AvailableIODevicesType, CallEndedReason, + IceServerType, + IceServerCacheType, MediaDeviceSettings, PresentedSource, } from '../types/Calling'; @@ -180,6 +182,7 @@ const CLEAN_EXPIRED_GROUP_CALL_RINGS_INTERVAL = 10 * durations.MINUTE; const OUTGOING_SIGNALING_WAIT = 15 * durations.SECOND; const ICE_SERVER_IS_IP_LIKE = /(turn|turns|stun):[.\d]+/; + const MAX_CALL_DEBUG_STATS_TABS = 5; // We send group call update messages to tell other clients to peek, which triggers @@ -417,6 +420,9 @@ export class CallingClass { public _iceServerOverride?: GetIceServersResultType | string; + // A cache to limit requests for relay servers. + #iceServersCache: IceServerCacheType | undefined; + #lastMediaDeviceSettings?: MediaDeviceSettings; #deviceReselectionTimer?: NodeJS.Timeout; #callsLookup: { [key: string]: Call | GroupCall }; @@ -3396,17 +3402,10 @@ export class CallingClass { return null; } - async #handleStartCall(call: Call): Promise { - type IceServer = { - username?: string; - password?: string; - hostname?: string; - urls: Array; - }; - + async #getIceServers(): Promise> { function iceServerConfigToList( iceServerConfig: GetIceServersResultType - ): Array { + ): Array { if (!iceServerConfig.relays) { return []; } @@ -3427,6 +3426,61 @@ export class CallingClass { ]); } + const currentTime = Date.now(); + if ( + this.#iceServersCache && + currentTime < this.#iceServersCache.expirationTimestamp + ) { + // Use the cached value for iceServers. + return this.#iceServersCache.iceServers; + } + + let iceServers: Array = []; + // Set the default cache expiration time to now + 0. + let expirationTimestamp = currentTime; + + // The messaging context should have already been checked before entering + // this function, so this should be a noop. + const { messaging } = window.textsecure; + strictAssert(messaging, 'textsecure messaging not available'); + + const iceServerConfig = await messaging.server.getIceServers(); + + // Advance the next expiration time to the minimum provided ttl value, + // or if there were none, use 0 to disable the cache. + const minTtl = (iceServerConfig.relays ?? []).reduce( + (min, { ttl }) => Math.min(min, ttl ?? 0), + Infinity + ); + + expirationTimestamp += minTtl !== Infinity ? minTtl * durations.SECOND : 0; + + // Prioritize ice servers with IPs to avoid DNS entries and only include + // hostname with urlsWithIps. + iceServers = iceServerConfigToList(iceServerConfig); + + if (this._iceServerOverride) { + if (typeof this._iceServerOverride === 'string') { + if (ICE_SERVER_IS_IP_LIKE.test(this._iceServerOverride)) { + iceServers[0].urls = [this._iceServerOverride]; + iceServers = [iceServers[0]]; + } else { + iceServers[1].urls = [this._iceServerOverride]; + iceServers = [iceServers[1]]; + } + } else { + iceServers = iceServerConfigToList(this._iceServerOverride); + } + } + + if (iceServers.length > 0) { + // Update the cached value for iceServers. + this.#iceServersCache = { iceServers, expirationTimestamp }; + } + return iceServers; + } + + async #handleStartCall(call: Call): Promise { if (!window.textsecure.messaging) { log.error('CallingClass.handleStartCall: offline!'); return false; @@ -3453,8 +3507,7 @@ export class CallingClass { return false; } - const iceServerConfig = - await window.textsecure.messaging.server.getIceServers(); + const iceServers = await this.#getIceServers(); // We do this again, since getIceServers is a call that can take some time if (call.endedReason) { @@ -3470,24 +3523,6 @@ export class CallingClass { // If the peer is not a Signal Connection, force IP hiding. const isContactUntrusted = !isSignalConnection(conversation.attributes); - // Prioritize ice servers with IPs to avoid DNS only include - // hostname with urlsWithIps. - let iceServers = iceServerConfigToList(iceServerConfig); - - if (this._iceServerOverride) { - if (typeof this._iceServerOverride === 'string') { - if (ICE_SERVER_IS_IP_LIKE.test(this._iceServerOverride)) { - iceServers[0].urls = [this._iceServerOverride]; - iceServers = [iceServers[0]]; - } else { - iceServers[1].urls = [this._iceServerOverride]; - iceServers = [iceServers[1]]; - } - } else { - iceServers = iceServerConfigToList(this._iceServerOverride); - } - } - const callSettings = { iceServers, hideIp: shouldRelayCalls || isContactUntrusted, diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index efd26a38fbb..235d6eac082 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -923,6 +923,7 @@ export type IceServerGroupType = Readonly<{ urls?: ReadonlyArray; urlsWithIps?: ReadonlyArray; hostname?: string; + ttl?: number; }>; export type GetSenderCertificateResultType = Readonly<{ certificate: string }>; diff --git a/ts/types/Calling.ts b/ts/types/Calling.ts index bcfee32765b..3958957f05a 100644 --- a/ts/types/Calling.ts +++ b/ts/types/Calling.ts @@ -208,3 +208,15 @@ export enum ScreenShareStatus { Reconnecting = 'Reconnecting', Disconnected = 'Disconnected', } + +export type IceServerType = { + username?: string; + password?: string; + hostname?: string; + urls: Array; +}; + +export type IceServerCacheType = { + iceServers: Array; + expirationTimestamp: number; +};