Files
fluxer/fluxer_app/src/features/voice/utils/ScreenShareOptions.ts
T

220 lines
7.3 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import type {ScreenshareResolution, StreamingMode} from '@app/features/voice/state/VoiceSettings';
import type {ScreenShareCaptureOptions, TrackPublishOptions, VideoEncoding} from 'livekit-client';
const DIMENSIONS: Record<
ScreenshareResolution,
{
width: number;
height: number;
}
> = {
low_240p: {width: 426, height: 240},
low_480p: {width: 854, height: 480},
medium: {width: 1280, height: 720},
high: {width: 1920, height: 1080},
ultra: {width: 2560, height: 1440},
source: {width: 3840, height: 2160},
};
export const SCREEN_SHARE_FORCED_VIDEO_BITRATE_BPS = 7000000;
export const SCREEN_SHARE_DEGRADATION_PREFERENCE: NonNullable<TrackPublishOptions['degradationPreference']> =
'maintain-resolution';
export const SUPPORTED_SCREEN_SHARE_FRAME_RATES = [15, 30, 60, 90, 120] as const;
export type SupportedScreenShareFrameRate = (typeof SUPPORTED_SCREEN_SHARE_FRAME_RATES)[number];
export function resolveScreenShareFrameRate(frameRate: number): SupportedScreenShareFrameRate {
if (frameRate >= 120) return 120;
if (frameRate >= 90) return 90;
if (frameRate >= 60) return 60;
if (frameRate >= 30) return 30;
return 15;
}
export function getScreenShareDimensions(resolution: ScreenshareResolution): {
width: number;
height: number;
} {
return DIMENSIONS[resolution];
}
export function getScreenShareEncoding(
frameRate: number,
maxBitrateBps = SCREEN_SHARE_FORCED_VIDEO_BITRATE_BPS,
): VideoEncoding {
return {
maxBitrate: Math.min(SCREEN_SHARE_FORCED_VIDEO_BITRATE_BPS, maxBitrateBps),
maxFramerate: frameRate,
priority: 'high',
};
}
export const STREAMING_MODE_PRESETS: Record<
Exclude<StreamingMode, 'custom'>,
{
resolution: ScreenshareResolution;
frameRate: SupportedScreenShareFrameRate;
}
> = {
gaming: {resolution: 'ultra', frameRate: 60},
screenshare: {resolution: 'source', frameRate: 15},
};
const FREE_STREAMING_MODE_PRESETS: Record<
Exclude<StreamingMode, 'custom'>,
{
resolution: ScreenshareResolution;
frameRate: SupportedScreenShareFrameRate;
}
> = {
gaming: {resolution: 'medium', frameRate: 30},
screenshare: {resolution: 'medium', frameRate: 15},
};
export interface BuiltScreenShareOptions {
captureOptions: ScreenShareCaptureOptions;
publishOptions: TrackPublishOptions;
}
export interface ScreenShareBuildConfig {
resolution: ScreenshareResolution;
frameRate: number;
includeAudio: boolean;
contentHint?: ScreenShareCaptureOptions['contentHint'];
sourceDimensions?: {
width: number;
height: number;
};
preferredDisplaySurface?: 'window' | 'monitor';
useBrowserAudioPicker?: boolean;
}
type ScreenShareVideoOptions = NonNullable<Exclude<ScreenShareCaptureOptions['video'], true>> & {
cursor?: 'always' | 'motion' | 'never';
};
function resolveScreenShareCursorCapture(
preferredDisplaySurface?: ScreenShareBuildConfig['preferredDisplaySurface'],
): 'always' | 'never' {
return preferredDisplaySurface === 'window' ? 'never' : 'always';
}
export function resolveEffectiveScreenShareDimensions(
resolution: ScreenshareResolution,
sourceDimensions?: {
width: number;
height: number;
},
): {
width: number;
height: number;
} {
const preset = getScreenShareDimensions(resolution);
if (resolution !== 'source' || !sourceDimensions) return preset;
if (sourceDimensions.width <= 0 || sourceDimensions.height <= 0) return preset;
return {
width: Math.min(preset.width, sourceDimensions.width),
height: Math.min(preset.height, sourceDimensions.height),
};
}
export function buildScreenShareOptions(config: ScreenShareBuildConfig): BuiltScreenShareOptions;
export function buildScreenShareOptions(resolution: ScreenshareResolution, frameRate: number): BuiltScreenShareOptions;
export function buildScreenShareOptions(
configOrResolution: ScreenShareBuildConfig | ScreenshareResolution,
maybeFrameRate?: number,
): BuiltScreenShareOptions {
const config: ScreenShareBuildConfig =
typeof configOrResolution === 'object'
? configOrResolution
: {resolution: configOrResolution, frameRate: maybeFrameRate ?? 30, includeAudio: true};
const {width, height} = resolveEffectiveScreenShareDimensions(config.resolution, config.sourceDimensions);
const resolvedFrameRate = resolveScreenShareFrameRate(config.frameRate);
const video: ScreenShareVideoOptions = {
cursor: resolveScreenShareCursorCapture(config.preferredDisplaySurface),
...(config.preferredDisplaySurface ? {displaySurface: config.preferredDisplaySurface} : {}),
};
return {
captureOptions: {
audio: config.includeAudio,
...(config.contentHint ? {contentHint: config.contentHint} : {}),
...(config.includeAudio ? {restrictOwnAudio: true} : {}),
selfBrowserSurface: 'include',
monitorTypeSurfaces: config.preferredDisplaySurface === 'window' ? 'exclude' : 'include',
systemAudio: config.includeAudio && config.useBrowserAudioPicker ? 'include' : 'exclude',
windowAudio: config.includeAudio ? (config.useBrowserAudioPicker ? 'system' : 'window') : 'exclude',
resolution: {width, height, frameRate: resolvedFrameRate},
video,
},
publishOptions: {
degradationPreference: SCREEN_SHARE_DEGRADATION_PREFERENCE,
screenShareEncoding: getScreenShareEncoding(resolvedFrameRate),
},
};
}
const FREE_TIER_FALLBACK_RESOLUTION: ScreenshareResolution = 'medium';
const FREE_TIER_RESOLUTIONS: ReadonlyArray<ScreenshareResolution> = ['low_240p', 'low_480p', 'medium'];
const FREE_TIER_MAX_FRAME_RATE: SupportedScreenShareFrameRate = 30;
function isFreeTierResolution(resolution: ScreenshareResolution): boolean {
return FREE_TIER_RESOLUTIONS.includes(resolution);
}
function clampToFreeTier(
resolution: ScreenshareResolution,
frameRate: SupportedScreenShareFrameRate,
): {
resolution: ScreenshareResolution;
frameRate: SupportedScreenShareFrameRate;
} {
const cappedResolution: ScreenshareResolution = isFreeTierResolution(resolution)
? resolution
: FREE_TIER_FALLBACK_RESOLUTION;
const cappedFrameRate: SupportedScreenShareFrameRate =
frameRate > FREE_TIER_MAX_FRAME_RATE ? FREE_TIER_MAX_FRAME_RATE : frameRate;
return {resolution: cappedResolution, frameRate: cappedFrameRate};
}
export function resolveStreamingModeSettings(
mode: StreamingMode,
customResolution: ScreenshareResolution,
customFrameRate: number,
hasHigherQuality: boolean = true,
): {
resolution: ScreenshareResolution;
frameRate: SupportedScreenShareFrameRate;
} {
const resolved =
mode === 'custom'
? {resolution: customResolution, frameRate: resolveScreenShareFrameRate(customFrameRate)}
: (hasHigherQuality ? STREAMING_MODE_PRESETS : FREE_STREAMING_MODE_PRESETS)[mode];
if (hasHigherQuality) {
return resolved;
}
return clampToFreeTier(resolved.resolution, resolved.frameRate);
}
export type ScreenShareContext = 'display' | 'device';
export function normaliseStreamingModeForContext(mode: StreamingMode, context: ScreenShareContext): StreamingMode {
if (context === 'device' && mode === 'screenshare') {
return 'gaming';
}
return mode;
}
export function normaliseResolutionForContext(
resolution: ScreenshareResolution,
context: ScreenShareContext,
hasHigherQuality: boolean,
): ScreenshareResolution {
if (!hasHigherQuality && !isFreeTierResolution(resolution)) {
return FREE_TIER_FALLBACK_RESOLUTION;
}
if (context === 'device' && resolution === 'source') {
return hasHigherQuality ? 'ultra' : FREE_TIER_FALLBACK_RESOLUTION;
}
return resolution;
}