From 8b510c3b302f30468816540661eed167ac2221fc Mon Sep 17 00:00:00 2001 From: adel-signal Date: Thu, 19 Mar 2026 09:46:33 -0700 Subject: [PATCH] calling: add internal preferences for DRED, bitrate, VP9, sfu url --- ts/RemoteConfig.dom.ts | 3 + ts/components/Preferences.dom.stories.tsx | 12 ++ ts/components/Preferences.dom.tsx | 38 +++++ ts/components/PreferencesInternal.dom.tsx | 164 ++++++++++++++++++++++ ts/services/calling.preload.ts | 111 +++++++++++---- ts/state/smart/Preferences.preload.tsx | 31 ++++ ts/types/StorageKeys.std.ts | 14 ++ ts/windows/main/start.preload.ts | 6 +- 8 files changed, 350 insertions(+), 29 deletions(-) diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index eca62360ad..ba0b20288c 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -56,6 +56,9 @@ export type SemverKeyType = ArrayValues; const ScalarKeys = [ 'desktop.callQualitySurveyPPM', + 'desktop.calling.dredDuration.alpha', + 'desktop.calling.dredDuration.beta', + 'desktop.calling.dredDuration.prod', 'desktop.clientExpiration', 'desktop.internalUser', 'desktop.loggingErrorToasts', diff --git a/ts/components/Preferences.dom.stories.tsx b/ts/components/Preferences.dom.stories.tsx index f941df5faa..71349ffc7d 100644 --- a/ts/components/Preferences.dom.stories.tsx +++ b/ts/components/Preferences.dom.stories.tsx @@ -647,6 +647,18 @@ export default { }, cqsTestMode: false, setCqsTestMode: action('setCqsTestMode'), + dredDuration: 0, + setDredDuration: action('setDredDuration'), + directMaxBitrate: 1000000, + setDirectMaxBitrate: action('setDirectMaxBitrate'), + isDirectVp9Enabled: true, + setIsDirectVp9Enabled: action('setIsDirectVp9Enabled'), + groupMaxBitrate: 1000000, + setGroupMaxBitrate: action('setGroupMaxBitrate'), + isGroupVp9Enabled: false, + setIsGroupVp9Enabled: action('setIsDirectVp9Enabled'), + sfuUrl: 'https://sfu.voip.signal.org', + setSfuUrl: action('setSfuUrl'), } satisfies PropsType, } satisfies Meta; diff --git a/ts/components/Preferences.dom.tsx b/ts/components/Preferences.dom.tsx index 1362a2987f..d078f5483c 100644 --- a/ts/components/Preferences.dom.tsx +++ b/ts/components/Preferences.dom.tsx @@ -210,6 +210,14 @@ export type PropsDataType = { >; donationReceipts: ReadonlyArray; + + // calling internal preferences + dredDuration: number | undefined; + isDirectVp9Enabled: boolean | undefined; + directMaxBitrate: number | undefined; + isGroupVp9Enabled: boolean | undefined; + groupMaxBitrate: number | undefined; + sfuUrl: string | undefined; } & Omit; type PropsFunctionType = { @@ -345,6 +353,12 @@ type PropsFunctionType = { ) => Promise>>; cqsTestMode: boolean; setCqsTestMode: (value: boolean) => void; + setDredDuration: (value: number | undefined) => void; + setIsDirectVp9Enabled: (value: boolean | undefined) => void; + setDirectMaxBitrate: (value: number | undefined) => void; + setIsGroupVp9Enabled: (value: boolean | undefined) => void; + setGroupMaxBitrate: (value: number | undefined) => void; + setSfuUrl: (value: string | undefined) => void; // Localization i18n: LocalizerType; @@ -554,6 +568,18 @@ export function Preferences({ __dangerouslyRunAbitraryReadOnlySqlQuery, cqsTestMode, setCqsTestMode, + setDredDuration, + dredDuration, + setIsDirectVp9Enabled, + isDirectVp9Enabled, + setDirectMaxBitrate, + directMaxBitrate, + setIsGroupVp9Enabled, + isGroupVp9Enabled, + setGroupMaxBitrate, + groupMaxBitrate, + setSfuUrl, + sfuUrl, }: PropsType): React.JSX.Element { const storiesId = useId(); const themeSelectId = useId(); @@ -2333,6 +2359,18 @@ export function Preferences({ } cqsTestMode={cqsTestMode} setCqsTestMode={setCqsTestMode} + dredDuration={dredDuration} + setDredDuration={setDredDuration} + setIsDirectVp9Enabled={setIsDirectVp9Enabled} + isDirectVp9Enabled={isDirectVp9Enabled} + setDirectMaxBitrate={setDirectMaxBitrate} + directMaxBitrate={directMaxBitrate} + setIsGroupVp9Enabled={setIsGroupVp9Enabled} + isGroupVp9Enabled={isGroupVp9Enabled} + setGroupMaxBitrate={setGroupMaxBitrate} + groupMaxBitrate={groupMaxBitrate} + sfuUrl={sfuUrl} + setSfuUrl={setSfuUrl} /> } contentsRef={settingsPaneRef} diff --git a/ts/components/PreferencesInternal.dom.tsx b/ts/components/PreferencesInternal.dom.tsx index aad0868275..6d7f3c0b23 100644 --- a/ts/components/PreferencesInternal.dom.tsx +++ b/ts/components/PreferencesInternal.dom.tsx @@ -40,6 +40,19 @@ export function PreferencesInternal({ __dangerouslyRunAbitraryReadOnlySqlQuery, cqsTestMode, setCqsTestMode, + + dredDuration, + setDredDuration, + isDirectVp9Enabled, + setIsDirectVp9Enabled, + directMaxBitrate, + setDirectMaxBitrate, + isGroupVp9Enabled, + setIsGroupVp9Enabled, + groupMaxBitrate, + setGroupMaxBitrate, + sfuUrl, + setSfuUrl, }: { i18n: LocalizerType; validateBackup: () => Promise; @@ -65,6 +78,18 @@ export function PreferencesInternal({ ) => Promise>>; cqsTestMode: boolean; setCqsTestMode: (value: boolean) => void; + dredDuration: number | undefined; + setDredDuration: (value: number | undefined) => void; + isDirectVp9Enabled: boolean | undefined; + setIsDirectVp9Enabled: (value: boolean | undefined) => void; + directMaxBitrate: number | undefined; + setDirectMaxBitrate: (value: number | undefined) => void; + isGroupVp9Enabled: boolean | undefined; + setIsGroupVp9Enabled: (value: boolean | undefined) => void; + groupMaxBitrate: number | undefined; + setGroupMaxBitrate: (value: number | undefined) => void; + sfuUrl: string | undefined; + setSfuUrl: (value: string | undefined) => void; }): React.JSX.Element { const [messageCountBySchemaVersion, setMessageCountBySchemaVersion] = useState(); @@ -89,6 +114,57 @@ export function PreferencesInternal({ RowType > | null>(null); + const stripAndParseString = (input: string): number | undefined => { + const stripped = input.replace(/\D/g, ''); + return stripped.length !== 0 ? parseInt(stripped, 10) : undefined; + }; + + const handleDredDurationUpdate = useCallback( + (input: string) => { + const parsed = stripAndParseString(input); + if (parsed) { + setDredDuration(Math.min(100, parsed)); + } else { + setDredDuration(undefined); + } + }, + [setDredDuration] + ); + const handleDirectMaxBitrateUpdate = useCallback( + (input: string) => { + setDirectMaxBitrate(stripAndParseString(input)); + }, + [setDirectMaxBitrate] + ); + const handleGroupMaxBitrateUpdate = useCallback( + (input: string) => { + setGroupMaxBitrate(stripAndParseString(input)); + }, + [setGroupMaxBitrate] + ); + const handleSfuUrlUpdate = useCallback( + (input: string) => { + const url = input.trim(); + setSfuUrl(url.length !== 0 ? url : undefined); + }, + [setSfuUrl] + ); + const handleResetCallingOverrides = useCallback(() => { + setDredDuration(undefined); + setIsDirectVp9Enabled(undefined); + setDirectMaxBitrate(undefined); + setIsGroupVp9Enabled(undefined); + setGroupMaxBitrate(undefined); + setSfuUrl(undefined); + }, [ + setDredDuration, + setIsDirectVp9Enabled, + setDirectMaxBitrate, + setIsGroupVp9Enabled, + setGroupMaxBitrate, + setSfuUrl, + ]); + const validateBackup = useCallback(async () => { setIsValidationPending(true); setValidationResult(undefined); @@ -559,6 +635,94 @@ export function PreferencesInternal({ /> )} + + +
+ Clear custom calling preferences +
+
+ + Clear + +
+
+ +
+ DRED Duration (0 - 100) +
+
+ +
+
+
+ + +
Enable VP9
+
+ +
+
+ +
Max bitrate
+
+ +
+
+
+ + +
Enable VP9
+
+ +
+
+ +
Max bitrate
+
+ +
+
+ +
SFU URL
+
+ +
+
+
); } diff --git a/ts/services/calling.preload.ts b/ts/services/calling.preload.ts index 0abfdb7ae3..d97e5dc2b3 100644 --- a/ts/services/calling.preload.ts +++ b/ts/services/calling.preload.ts @@ -184,6 +184,9 @@ import { isCallFailure, shouldShowCallQualitySurvey, } from '../util/callQualitySurvey.dom.js'; +import * as RemoteConfig from '../RemoteConfig.dom.js'; +import { isAlpha, isBeta, isProduction } from '../util/version.std.js'; +import { parseIntOrThrow } from '../util/parseIntOrThrow.std.js'; const { i18n } = window.SignalContext; @@ -572,8 +575,11 @@ export class CallingClass { #localPreviewContainer: HTMLDivElement | undefined; #localPreview: HTMLVideoElement | undefined; #reduxInterface?: CallingReduxInterface; + #_sfuUrl?: string; - public _sfuUrl?: string; + public get sfuUrl(): string | undefined { + return itemStorage.get('sfuUrl') ?? this.#_sfuUrl; + } public _iceServerOverride?: GetIceServersResultType | string; @@ -606,7 +612,7 @@ export class CallingClass { throw new Error('CallingClass.initialize: Invalid uxActions.'); } - this._sfuUrl = sfuUrl; + this.#_sfuUrl = sfuUrl; RingRTC.setConfig({ field_trials: undefined, @@ -851,11 +857,11 @@ export class CallingClass { async createCallLink(): Promise { strictAssert( - this._sfuUrl, + this.sfuUrl, 'createCallLink() missing SFU URL; not creating call link' ); - const sfuUrl = this._sfuUrl; + const { sfuUrl } = this; const userId = Aci.parseFromServiceIdString( itemStorage.user.getCheckedAci() ); @@ -936,11 +942,11 @@ export class CallingClass { async deleteCallLink(callLink: CallLinkType): Promise { strictAssert( - this._sfuUrl, + this.sfuUrl, 'createCallLink() missing SFU URL; not deleting call link' ); - const sfuUrl = this._sfuUrl; + const { sfuUrl } = this; const logId = `deleteCallLink(${callLink.roomId})`; log.info(logId); @@ -973,10 +979,10 @@ export class CallingClass { name: string ): Promise { strictAssert( - this._sfuUrl, + this.sfuUrl, 'updateCallLinkName() missing SFU URL; not update call link name' ); - const sfuUrl = this._sfuUrl; + const { sfuUrl } = this; const logId = `updateCallLinkName(${callLink.roomId})`; log.info(`${logId}: Updating call link name`); @@ -1011,10 +1017,10 @@ export class CallingClass { restrictions: CallLinkRestrictions ): Promise { strictAssert( - this._sfuUrl, + this.sfuUrl, 'updateCallLinkRestrictions() missing SFU URL; not update call link restrictions' ); - const sfuUrl = this._sfuUrl; + const { sfuUrl } = this; const logId = `updateCallLinkRestrictions(${callLink.roomId})`; log.info(`${logId}: Updating call link restrictions`); @@ -1054,7 +1060,7 @@ export class CallingClass { async readCallLink( callLinkRootKey: CallLinkRootKey ): Promise { - if (!this._sfuUrl) { + if (!this.sfuUrl) { throw new Error('readCallLink() missing SFU URL; not handling call link'); } @@ -1066,7 +1072,7 @@ export class CallingClass { await getCallLinkAuthCredentialPresentation(callLinkRootKey); const result = await RingRTC.readCallLink( - this._sfuUrl, + this.sfuUrl, authCredentialPresentation.serialize(), callLinkRootKey ); @@ -1315,7 +1321,7 @@ export class CallingClass { return statefulPeekInfo; } - if (!this._sfuUrl) { + if (!this.sfuUrl) { throw new Error('Missing SFU URL; not peeking group call'); } @@ -1338,7 +1344,7 @@ export class CallingClass { const membershipProof = Bytes.fromString(proof); return RingRTC.peekGroupCall( - this._sfuUrl, + this.sfuUrl, membershipProof, this.#getGroupCallMembers(conversationId) ); @@ -1360,7 +1366,7 @@ export class CallingClass { ); } - if (!this._sfuUrl) { + if (!this.sfuUrl) { throw new Error('Missing SFU URL; not peeking call link call'); } @@ -1369,7 +1375,7 @@ export class CallingClass { await getCallLinkAuthCredentialPresentation(callLinkRootKey); const result = await RingRTC.peekCallLinkCall( - this._sfuUrl, + this.sfuUrl, authCredentialPresentation.serialize(), callLinkRootKey ); @@ -1412,7 +1418,7 @@ export class CallingClass { return existing; } - if (!this._sfuUrl) { + if (!this.sfuUrl) { throw new Error('Missing SFU URL; not connecting group call'); } @@ -1420,16 +1426,16 @@ export class CallingClass { log.info(logId); const groupIdBuffer = Bytes.fromBase64(groupId); - const dredDuration = 0; let isRequestingMembershipProof = false; + const config = this.#getRemoteAndOverrideConfigValues(); const outerGroupCall = RingRTC.getGroupCall( groupIdBuffer, - this._sfuUrl, + this.sfuUrl, new Uint8Array(), AUDIO_LEVEL_INTERVAL_MS, - dredDuration, + config.dredDuration, { ...this.#getGroupCallObserver(conversationId, CallMode.Group), async requestMembershipProof(groupCall) { @@ -1496,23 +1502,22 @@ export class CallingClass { const logId = `connectCallLinkCall(${roomId}`; log.info(logId); - if (!this._sfuUrl) { + if (!this.sfuUrl) { throw new Error( `${logId}: Missing SFU URL; not connecting group call link call` ); } - - const dredDuration = 0; + const config = this.#getRemoteAndOverrideConfigValues(); const outerGroupCall = RingRTC.getCallLinkCall( - this._sfuUrl, + this.sfuUrl, endorsementsPublicKey, authCredentialPresentation.serialize(), callLinkRootKey, adminPasskey, new Uint8Array(), AUDIO_LEVEL_INTERVAL_MS, - dredDuration, + config.dredDuration, this.#getGroupCallObserver(roomId, CallMode.Adhoc) ); @@ -3863,6 +3868,59 @@ export class CallingClass { return null; } + #getRemoteAndOverrideConfigValues(): { + dredDuration: number | undefined; + isDirectVp9Enabled: boolean | undefined; + directMaxBitrate: number | undefined; + isGroupVp9Enabled: boolean | undefined; + groupMaxBitrate: number | undefined; + } { + function dredDuration(version: string): number | undefined { + const override = itemStorage.get('dredDuration'); + if (override) { + return override; + } + + if (isProduction(version)) { + return tryParseInt( + RemoteConfig.getValue('desktop.calling.dredDuration.prod') + ); + } + + if (isBeta(version)) { + return tryParseInt( + RemoteConfig.getValue('desktop.calling.dredDuration.beta') + ); + } + + if (isAlpha(version)) { + return tryParseInt( + RemoteConfig.getValue('desktop.calling.dredDuration.alpha') + ); + } + + return undefined; + } + + function tryParseInt(v: string | undefined): number | undefined { + try { + return parseIntOrThrow(v, 'invalid'); + } catch (e) { + return undefined; + } + } + + const version = window.SignalContext.getVersion(); + + return { + dredDuration: dredDuration(version), + isDirectVp9Enabled: itemStorage.get('isDirectVp9Enabled'), + directMaxBitrate: itemStorage.get('directMaxBitrate'), + isGroupVp9Enabled: itemStorage.get('isGroupVp9Enabled'), + groupMaxBitrate: itemStorage.get('directMaxBitrate'), + }; + } + async #getIceServers(): Promise> { function iceServerConfigToList( iceServerConfig: GetIceServersResultType @@ -3964,6 +4022,7 @@ export class CallingClass { } const iceServers = await this.#getIceServers(); + const config = this.#getRemoteAndOverrideConfigValues(); // We do this again, since getIceServers is a call that can take some time if (call.endedReason) { @@ -3984,7 +4043,7 @@ export class CallingClass { hideIp: shouldRelayCalls || isContactUntrusted, dataMode: DataMode.Normal, audioLevelsIntervalMillis: AUDIO_LEVEL_INTERVAL_MS, - dredDuration: 0, + dredDuration: config.dredDuration, }; log.info('CallingClass.handleStartCall(): Proceeding'); diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index a7c761667e..f82afc8d2d 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -784,6 +784,25 @@ export function SmartPreferences(): React.JSX.Element | null { drop(itemStorage.put('cqsTestMode', value)); }, []); + const setDredDuration = useCallback((value: number | undefined) => { + drop(itemStorage.put('dredDuration', value)); + }, []); + const setIsDirectVp9Enabled = useCallback((value: boolean | undefined) => { + drop(itemStorage.put('isDirectVp9Enabled', value)); + }, []); + const setDirectMaxBitrate = useCallback((value: number | undefined) => { + drop(itemStorage.put('directMaxBitrate', value)); + }, []); + const setIsGroupVp9Enabled = useCallback((value: boolean | undefined) => { + drop(itemStorage.put('isGroupVp9Enabled', value)); + }, []); + const setGroupMaxBitrate = useCallback((value: number | undefined) => { + drop(itemStorage.put('groupMaxBitrate', value)); + }, []); + const setSfuUrl = useCallback((value: string | undefined) => { + drop(itemStorage.put('sfuUrl', value)); + }, []); + if (currentLocation.tab !== NavTab.Settings) { return null; } @@ -998,6 +1017,18 @@ export function SmartPreferences(): React.JSX.Element | null { } cqsTestMode={cqsTestMode} setCqsTestMode={setCqsTestMode} + dredDuration={items.dredDuration} + setDredDuration={setDredDuration} + setIsDirectVp9Enabled={setIsDirectVp9Enabled} + isDirectVp9Enabled={items.isDirectVp9Enabled} + setDirectMaxBitrate={setDirectMaxBitrate} + directMaxBitrate={items.directMaxBitrate} + setIsGroupVp9Enabled={setIsGroupVp9Enabled} + isGroupVp9Enabled={items.isGroupVp9Enabled} + setGroupMaxBitrate={setGroupMaxBitrate} + groupMaxBitrate={items.groupMaxBitrate} + sfuUrl={items.sfuUrl} + setSfuUrl={setSfuUrl} /> diff --git a/ts/types/StorageKeys.std.ts b/ts/types/StorageKeys.std.ts index 8eb0cf0cd0..013e2f05be 100644 --- a/ts/types/StorageKeys.std.ts +++ b/ts/types/StorageKeys.std.ts @@ -291,6 +291,14 @@ export type StorageAccessType = { defaultDimWallpaperInDarkMode: boolean; defaultAutoBubbleColor: boolean; + // Used for manually controlling calling settings + dredDuration: number | undefined; + isDirectVp9Enabled: boolean | undefined; + directMaxBitrate: number | undefined; + isGroupVp9Enabled: boolean | undefined; + groupMaxBitrate: number | undefined; + sfuUrl: string | undefined; + // Deprecated 'challenge:retry-message-ids': never; nextSignedKeyRotationTime: number; @@ -514,6 +522,12 @@ const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [ 'backupMediaDownloadIdle', 'callQualitySurveyCooldownDisabled', 'localDeleteWarningShown', + 'dredDuration', + 'directMaxBitrate', + 'isDirectVp9Enabled', + 'groupMaxBitrate', + 'isGroupVp9Enabled', + 'sfuUrl', ] as const satisfies ReadonlyArray; // Ensure every storage key is explicitly marked to be preserved or removed on unlink. diff --git a/ts/windows/main/start.preload.ts b/ts/windows/main/start.preload.ts index e0774e3bcc..39bcab2342 100644 --- a/ts/windows/main/start.preload.ts +++ b/ts/windows/main/start.preload.ts @@ -87,7 +87,7 @@ if ( return message?.attributes; }, getReduxState: () => window.reduxStore.getState(), - getSfuUrl: () => calling._sfuUrl, + getSfuUrl: () => calling.sfuUrl, getIceServerOverride: () => calling._iceServerOverride, getSocketStatus: () => getSocketStatus(), getStorageItem: (name: keyof StorageAccessType) => itemStorage.get(name), @@ -101,8 +101,8 @@ if ( } window.Flags[name] = value; }, - setSfuUrl: (url: string) => { - calling._sfuUrl = url; + setSfuUrl: async (url: string) => { + await itemStorage.put('sfuUrl', url); }, setIceServerOverride: ( override: GetIceServersResultType | string | undefined