refactor(voice): clamp premium video quality at read time (#2380)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-02 17:31:39 +02:00
1 parent 12c6fb7b7f
commit 501adff13d
4 files changed
+15 -50

No files matched your search

@@ -45,7 +45,6 @@ import Users from '@app/features/user/state/Users';
import WebAuthnCredentials, {type WebAuthnCredential} from '@app/features/user/state/WebAuthnCredentials'; import WebAuthnCredentials, {type WebAuthnCredential} from '@app/features/user/state/WebAuthnCredentials';
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade'; import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
import RtcRegions from '@app/features/voice/state/RtcRegions'; import RtcRegions from '@app/features/voice/state/RtcRegions';
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
import type {RtcRegionResponse, Channel as WireChannel} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; import type {RtcRegionResponse, Channel as WireChannel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import type {UserPrivate, User as WireUser} from '@fluxer/schema/src/domains/user/UserResponseSchemas'; import type {UserPrivate, User as WireUser} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {runInAction} from 'mobx'; import {runInAction} from 'mobx';
@@ -145,7 +144,6 @@ function handleReadyInternal(data: ReadyPayload, context: GatewayHandlerContext)
void accountStorage.updateAccountUserData(user.id, userData); void accountStorage.updateAccountUserData(user.id, userData);
void AccountManager.updateAccountUserData(user.id, userData); void AccountManager.updateAccountUserData(user.id, userData);
} }
VoiceSettings.handleGatewayReady(data.user);
Authentication.handleGatewayReady({user: data.user}); Authentication.handleGatewayReady({user: data.user});
void PremiumCommands.refreshPremiumState().catch((error) => { void PremiumCommands.refreshPremiumState().catch((error) => {
logger.warn('Failed to refresh premium state after READY', error); logger.warn('Failed to refresh premium state after READY', error);
@@ -6,7 +6,6 @@ import Messages from '@app/features/messaging/state/MessagingMessages';
import Permission from '@app/features/permissions/state/Permission'; import Permission from '@app/features/permissions/state/Permission';
import QuickSwitcher from '@app/features/search/state/QuickSwitcher'; import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
import Users from '@app/features/user/state/Users'; import Users from '@app/features/user/state/Users';
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
import type {User} from '@fluxer/schema/src/domains/user/UserResponseSchemas'; import type {User} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
interface UserUpdatePayload { interface UserUpdatePayload {
@@ -20,7 +19,6 @@ interface UserUpdatePayload {
export function handleUserUpdate(data: UserUpdatePayload, _context: GatewayHandlerContext): void { export function handleUserUpdate(data: UserUpdatePayload, _context: GatewayHandlerContext): void {
Users.handleUserUpdate(data as User); Users.handleUserUpdate(data as User);
VoiceSettings.handleUserUpdate(data);
Messages.handleUserUpdate({user: {id: data.id}}); Messages.handleUserUpdate({user: {id: data.id}});
Permission.handleUserUpdate(data.id); Permission.handleUserUpdate(data.id);
QuickSwitcher.recomputeIfOpen(); QuickSwitcher.recomputeIfOpen();
@@ -631,8 +631,8 @@ const CameraPreviewModalContent = observer((props: CameraPreviewModalProps) => {
VoiceSettings.videoDeviceId, VoiceSettings.videoDeviceId,
VoiceSettings.backgroundImageId, VoiceSettings.backgroundImageId,
VoiceSettings.mirrorCamera, VoiceSettings.mirrorCamera,
VoiceSettings.cameraResolution, VoiceSettings.getCameraResolution(),
VoiceSettings.videoFrameRate, VoiceSettings.getVideoFrameRate(),
voiceBackgroundsAvailable, voiceBackgroundsAvailable,
]); ]);
const voiceSettings = VoiceSettings; const voiceSettings = VoiceSettings;
@@ -19,7 +19,6 @@ import {
type VoiceProcessingMode, type VoiceProcessingMode,
} from '@app/features/voice/utils/VoiceProcessingProfile'; } from '@app/features/voice/utils/VoiceProcessingProfile';
import {clampVoiceVolumePercent} from '@app/features/voice/utils/VoiceVolumeUtils'; import {clampVoiceVolumePercent} from '@app/features/voice/utils/VoiceVolumeUtils';
import type {UserPrivate} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {makeAutoObservable} from 'mobx'; import {makeAutoObservable} from 'mobx';
export type VoiceBackgroundMediaKind = 'static' | 'animated' | 'video'; export type VoiceBackgroundMediaKind = 'static' | 'animated' | 'video';
@@ -53,6 +52,8 @@ const VIDEO_FRAME_RATE_MAX = 120;
const VIDEO_FRAME_RATE_DEFAULT = 30; const VIDEO_FRAME_RATE_DEFAULT = 30;
const DEFAULT_BROWSER_NOISE_SUPPRESSION = true; const DEFAULT_BROWSER_NOISE_SUPPRESSION = true;
const DEFAULT_DEEP_FILTER_NOISE_SUPPRESSION = false; const DEFAULT_DEEP_FILTER_NOISE_SUPPRESSION = false;
const FREE_VIDEO_FRAME_RATE_MAX = 30;
const PREMIUM_SCREENSHARE_RESOLUTIONS: ReadonlySet<ScreenshareResolution> = new Set(['high', 'ultra', 'source']);
export const CAMERA_EFFECT_STRENGTH_MIN = 0; export const CAMERA_EFFECT_STRENGTH_MIN = 0;
export const CAMERA_EFFECT_STRENGTH_MAX = 100; export const CAMERA_EFFECT_STRENGTH_MAX = 100;
export const CAMERA_EFFECT_STRENGTH_DEFAULT = 50; export const CAMERA_EFFECT_STRENGTH_DEFAULT = 50;
@@ -388,10 +389,11 @@ class VoiceSettings {
private listeners = new Set<() => void>(); private listeners = new Set<() => void>();
constructor() { constructor() {
makeAutoObservable<this, 'listeners' | 'notifyListeners'>( makeAutoObservable<this, 'hasHigherVideoQuality' | 'listeners' | 'notifyListeners'>(
this, this,
{ {
listeners: false, listeners: false,
hasHigherVideoQuality: false,
getInputDeviceId: false, getInputDeviceId: false,
getOutputDeviceId: false, getOutputDeviceId: false,
getVideoDeviceId: false, getVideoDeviceId: false,
@@ -572,7 +574,6 @@ class VoiceSettings {
'lastScreenShareSource', 'lastScreenShareSource',
'prioritizeSpeakingParticipants', 'prioritizeSpeakingParticipants',
]); ]);
this.updateSettings({});
} }
get showVoiceConnectionId(): boolean { get showVoiceConnectionId(): boolean {
@@ -663,47 +664,6 @@ class VoiceSettings {
this.adaptiveScreenShareQualityPrefV2 = value; this.adaptiveScreenShareQualityPrefV2 = value;
} }
handleGatewayReady(user: UserPrivate): void {
if (this.isUserPremium(user.premium_type)) {
return;
}
if (!this.hasHigherVideoQuality()) {
this.sanitizePremiumSettings();
}
}
handleUserUpdate(user: Partial<UserPrivate>): void {
if (user.premium_type === undefined) {
return;
}
if (this.isUserPremium(user.premium_type)) {
return;
}
if (!this.hasHigherVideoQuality()) {
this.sanitizePremiumSettings();
}
}
private isUserPremium(premiumType: number | null | undefined): boolean {
return premiumType != null && premiumType > 0;
}
private sanitizePremiumSettings(): void {
if (
this.screenshareResolution === 'high' ||
this.screenshareResolution === 'ultra' ||
this.screenshareResolution === 'source'
) {
this.screenshareResolution = 'medium';
}
if (this.cameraResolution === 'high') {
this.cameraResolution = 'medium';
}
if (this.videoFrameRate > 30) {
this.videoFrameRate = 30;
}
}
private hasHigherVideoQuality(): boolean { private hasHigherVideoQuality(): boolean {
const featureFlag = isLimitToggleEnabled( const featureFlag = isLimitToggleEnabled(
{ {
@@ -797,6 +757,9 @@ class VoiceSettings {
} }
getCameraResolution(): CameraResolution { getCameraResolution(): CameraResolution {
if (this.cameraResolution === 'high' && !this.hasHigherVideoQuality()) {
return 'medium';
}
return this.cameraResolution; return this.cameraResolution;
} }
@@ -805,10 +768,16 @@ class VoiceSettings {
} }
getScreenshareResolution(): ScreenshareResolution { getScreenshareResolution(): ScreenshareResolution {
if (PREMIUM_SCREENSHARE_RESOLUTIONS.has(this.screenshareResolution) && !this.hasHigherVideoQuality()) {
return 'medium';
}
return this.screenshareResolution; return this.screenshareResolution;
} }
getVideoFrameRate(): number { getVideoFrameRate(): number {
if (this.videoFrameRate > FREE_VIDEO_FRAME_RATE_MAX && !this.hasHigherVideoQuality()) {
return FREE_VIDEO_FRAME_RATE_MAX;
}
return this.videoFrameRate; return this.videoFrameRate;
} }