feat(app): pick screen share bitrate up to the limit (#3411)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-12 02:17:53 +02:00
1 parent e0640d68ff
commit c17091768c
52 files changed
+879 -242

No files matched your search

@@ -102,6 +102,25 @@ const MIRROR_CAMERA_DESCRIPTOR = msg({
message: 'Mirror camera',
comment: 'Switch label in the video tab for flipping the local camera preview horizontally.',
});
const SCREEN_SHARE_BITRATE_DESCRIPTOR = msg({
message: 'Bitrate',
comment: 'Short label in the video tab for the screen share video bitrate select. Keep it concise.',
});
const SCREEN_SHARE_BITRATE_DESCRIPTION_DESCRIPTOR = msg({
message:
'With Automatic, the bitrate follows your resolution and frame rate. Higher values keep fast motion sharper and use more upload bandwidth.',
comment:
'Description under the screen share bitrate select in the video tab. Automatic is the label of the default option.',
});
const SCREEN_SHARE_BITRATE_AUTOMATIC_DESCRIPTOR = msg({
message: 'Automatic',
comment: 'Default option of the screen share bitrate select in the video tab.',
});
const SCREEN_SHARE_BITRATE_MBPS_DESCRIPTOR = msg({
message: '{bitrate} Mbps',
comment:
'Option label of the screen share bitrate select in the video tab. bitrate is a formatted number of megabits per second. Mbps is a technical unit token.',
});
const SCREEN_SHARE_QUALITY_DESCRIPTOR = msg({
message: 'Screen share quality',
comment: 'Short label in the video tab. Keep it concise.',
@@ -176,6 +195,7 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
const screenShareState = resolveUserVideoTabScreenShareState({
quality: screenShareQuality,
hintSetting: screenShareContentHint,
bitrate: voiceSettings.getScreenShareBitrateBudget(),
selfHosted: !showPremium,
});
const screenshareResolutionOptions: ReadonlyArray<ComboboxOption<OfferedScreenShareResolution>> =
@@ -189,6 +209,15 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
value,
label: i18n._(FPS_DESCRIPTOR, {frameRate: value}),
}));
const bitrateOptions: ReadonlyArray<ComboboxOption<number | null>> = [
{value: null, label: i18n._(SCREEN_SHARE_BITRATE_AUTOMATIC_DESCRIPTOR)},
...screenShareState.bitrateOptions.map((value) => ({
value,
label: i18n._(SCREEN_SHARE_BITRATE_MBPS_DESCRIPTOR, {
bitrate: i18n.number(value / 1_000_000, {maximumFractionDigits: 1}),
}),
})),
];
const handleCameraPreview = async () => {
const granted = await requestPermission();
if (granted) {
@@ -387,6 +416,19 @@ export const VideoTab: React.FC<VideoTabProps> = observer(
})}
</div>
)}
{hasHigherQuality && (
<CompactComboboxRow<number | null>
label={i18n._(SCREEN_SHARE_BITRATE_DESCRIPTOR)}
description={i18n._(SCREEN_SHARE_BITRATE_DESCRIPTION_DESCRIPTOR)}
options={bitrateOptions}
value={screenShareState.bitrate}
onChange={(value) => VoiceSettingsCommands.update({screenShareBitrate: value})}
isSearchable={false}
controlWidth="small"
dataFlx="user.video-tab.select.screen-share-bitrate-change"
data-flx="user.user-video-tab.video-tab.compact-combobox-row.screen-share-bitrate-change"
/>
)}
</div>
<div className={styles.actionRow} data-flx="user.video-tab.advanced-video-settings-section">
<span className={styles.actionLabel} data-flx="user.video-tab.advanced-video-settings-label">
@@ -12,6 +12,7 @@ import type {ScreenShareContentHint} from '@app/features/voice/utils/CodecCapabi
import {
resolveScreenShareFrameRate,
resolveScreenShareTarget,
type ScreenShareBitrateBudget,
type ScreenShareContext,
type ScreenShareQualityInput,
SUPPORTED_SCREEN_SHARE_FRAME_RATES,
@@ -35,10 +36,15 @@ const FREE_SCREEN_SHARE_RESOLUTIONS: ReadonlyArray<OfferedScreenShareResolution>
const PREMIUM_SCREEN_SHARE_RESOLUTIONS: ReadonlyArray<OfferedScreenShareResolution> = ['high', 'ultra', 'source'];
const FREE_SCREEN_SHARE_FRAME_RATES: ReadonlyArray<SupportedScreenShareFrameRate> = [15, 30];
const PREMIUM_SCREEN_SHARE_FRAME_RATES: ReadonlyArray<SupportedScreenShareFrameRate> = [15, 30, 60];
const SCREEN_SHARE_BITRATE_STEPS_BPS: ReadonlyArray<number> = [
2_000_000, 4_000_000, 6_000_000, 8_000_000, 10_000_000, 15_000_000, 20_000_000, 25_000_000, 30_000_000, 40_000_000,
50_000_000,
];
export interface UserVideoTabScreenShareInput {
quality: ScreenShareQualityInput;
hintSetting: ScreenShareContentHint;
bitrate: ScreenShareBitrateBudget;
selfHosted: boolean;
}
@@ -52,6 +58,8 @@ export interface UserVideoTabScreenShareState {
frameRate: SupportedScreenShareFrameRate;
resolutionOptions: ReadonlyArray<UserVideoTabResolutionOption>;
frameRateOptions: ReadonlyArray<SupportedScreenShareFrameRate>;
bitrate: number | null;
bitrateOptions: ReadonlyArray<number>;
preset: Exclude<StreamingMode, 'custom'> | null;
presetOverriddenByContext: boolean;
saved: {resolution: OfferedScreenShareResolution; frameRate: SupportedScreenShareFrameRate} | null;
@@ -82,8 +90,20 @@ function buildFrameRateOptions(
return SUPPORTED_SCREEN_SHARE_FRAME_RATES.filter((value) => value === effective || offered.includes(value));
}
function buildBitrateOptions(budget: ScreenShareBitrateBudget): ReadonlyArray<number> {
const offered = new Set(SCREEN_SHARE_BITRATE_STEPS_BPS.filter((value) => value < budget.ceilingBps));
offered.add(budget.ceilingBps);
if (budget.chosenBps !== null) offered.add(budget.chosenBps);
return [...offered].sort((a, b) => a - b);
}
export function resolveUserVideoTabScreenShareState(input: UserVideoTabScreenShareInput): UserVideoTabScreenShareState {
const target = resolveScreenShareTarget({...input.quality, sourceDimensions: null, hintSetting: input.hintSetting});
const target = resolveScreenShareTarget({
...input.quality,
sourceDimensions: null,
hintSetting: input.hintSetting,
bitrate: input.bitrate,
});
const resolution = offeredScreenShareResolution(target.resolution);
return {
resolution,
@@ -95,6 +115,8 @@ export function resolveUserVideoTabScreenShareState(input: UserVideoTabScreenSha
resolution,
),
frameRateOptions: buildFrameRateOptions(input.quality.entitled, target.frameRate),
bitrate: input.bitrate.chosenBps,
bitrateOptions: buildBitrateOptions(input.bitrate),
preset: input.quality.mode === 'custom' ? null : input.quality.mode,
presetOverriddenByContext: selectStreamSettingsPresetOverriddenByContext(input.quality.mode, input.quality.context),
saved: target.tierLimited