calling: add internal preferences for DRED, bitrate, VP9, sfu url

This commit is contained in:
adel-signal
2026-03-19 09:46:33 -07:00
committed by GitHub
parent b624e48cf1
commit 8b510c3b30
8 changed files with 350 additions and 29 deletions

View File

@@ -56,6 +56,9 @@ export type SemverKeyType = ArrayValues<typeof SemverKeys>;
const ScalarKeys = [
'desktop.callQualitySurveyPPM',
'desktop.calling.dredDuration.alpha',
'desktop.calling.dredDuration.beta',
'desktop.calling.dredDuration.prod',
'desktop.clientExpiration',
'desktop.internalUser',
'desktop.loggingErrorToasts',

View File

@@ -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<PropsType>;

View File

@@ -210,6 +210,14 @@ export type PropsDataType = {
>;
donationReceipts: ReadonlyArray<DonationReceipt>;
// calling internal preferences
dredDuration: number | undefined;
isDirectVp9Enabled: boolean | undefined;
directMaxBitrate: number | undefined;
isGroupVp9Enabled: boolean | undefined;
groupMaxBitrate: number | undefined;
sfuUrl: string | undefined;
} & Omit<MediaDeviceSettings, 'availableCameras'>;
type PropsFunctionType = {
@@ -345,6 +353,12 @@ type PropsFunctionType = {
) => Promise<ReadonlyArray<RowType<object>>>;
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}

View File

@@ -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<BackupValidationResultType>;
@@ -65,6 +78,18 @@ export function PreferencesInternal({
) => Promise<ReadonlyArray<RowType<object>>>;
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<MessageCountBySchemaVersionType>();
@@ -89,6 +114,57 @@ export function PreferencesInternal({
RowType<object>
> | 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({
/>
)}
</SettingsRow>
<SettingsRow title="Calling General">
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">
Clear custom calling preferences
</div>
<div className="Preferences__one-third-flow Preferences__one-third-flow--justify-end">
<AxoButton.Root
variant="destructive"
size="lg"
onClick={handleResetCallingOverrides}
>
Clear
</AxoButton.Root>
</div>
</FlowingSettingsControl>
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">
DRED Duration (0 - 100)
</div>
<div className="Preferences__one-third-flow Preferences__one-third-flow--justify-end">
<AutoSizeTextArea
i18n={i18n}
value={dredDuration?.toString(10)}
onChange={handleDredDurationUpdate}
placeholder="0 - 100"
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
/>
</div>
</FlowingSettingsControl>
</SettingsRow>
<SettingsRow title="Direct Calls">
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">Enable VP9</div>
<div className="Preferences__one-third-flow Preferences__one-third-flow--justify-end">
<AxoSwitch.Root
checked={isDirectVp9Enabled ?? true}
onCheckedChange={setIsDirectVp9Enabled}
/>
</div>
</FlowingSettingsControl>
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">Max bitrate</div>
<div className="Preferences__one-third-flow Preferences__one-third-flow--justify-end">
<AutoSizeTextArea
i18n={i18n}
value={directMaxBitrate?.toString(10)}
onChange={handleDirectMaxBitrateUpdate}
placeholder="Default"
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
/>
</div>
</FlowingSettingsControl>
</SettingsRow>
<SettingsRow title="Group/Adhoc Calls">
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">Enable VP9</div>
<div className="Preferences__one-third-flow Preferences__one-third-flow--justify-end">
<AxoSwitch.Root
checked={isGroupVp9Enabled ?? false}
onCheckedChange={setIsGroupVp9Enabled}
/>
</div>
</FlowingSettingsControl>
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">Max bitrate</div>
<div className="Preferences__one-third-flow Preferences__one-third-flow--justify-end">
<AutoSizeTextArea
i18n={i18n}
value={groupMaxBitrate?.toString(10)}
onChange={handleGroupMaxBitrateUpdate}
placeholder="Default"
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
/>
</div>
</FlowingSettingsControl>
<FlowingSettingsControl>
<div className="Preferences__one-third-flow">SFU URL</div>
<div className="Preferences__two-thirds-flow Preferences__two-thirds-flow--justify-end">
<AutoSizeTextArea
i18n={i18n}
value={sfuUrl}
onChange={handleSfuUrlUpdate}
placeholder="https://sfu.voip.signal.org"
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
/>
</div>
</FlowingSettingsControl>
</SettingsRow>
</div>
);
}

View File

@@ -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<CallLinkType> {
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<void> {
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<CallLinkStateType> {
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<CallLinkStateType> {
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<CallLinkStateType | null> {
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<Array<IceServerType>> {
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');

View File

@@ -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}
/>
</AxoProvider>
</StrictMode>

View File

@@ -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<keyof StorageAccessType>;
// Ensure every storage key is explicitly marked to be preserved or removed on unlink.

View File

@@ -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