mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(voice): keep screen share resolution under constraint (#2413)
This commit is contained in:
1 parent
9bd0019759
commit
8158d44732
8 files changed
+72
-37
No files matched your search
@@ -260,7 +260,6 @@ export async function pushActiveStreamSettings(
|
||||
resolution,
|
||||
frameRate,
|
||||
includeAudio,
|
||||
streamingMode: normalisedMode,
|
||||
contentHint,
|
||||
preferredDisplaySurface,
|
||||
});
|
||||
|
||||
+10
-2
@@ -41,6 +41,11 @@ interface MigrationContext {
|
||||
readonly previousTrackName: string | undefined;
|
||||
}
|
||||
|
||||
type ScreenShareVideoConstraints = MediaTrackConstraints & {
|
||||
colorSpace?: string;
|
||||
cursor?: 'always' | 'motion' | 'never';
|
||||
};
|
||||
|
||||
interface MigrationState {
|
||||
candidateMediaStreamTrack: MediaStreamTrack | null;
|
||||
candidatePublication: LocalTrackPublication | null;
|
||||
@@ -85,14 +90,17 @@ export class VoiceEngineV2AppScreenShareCodecMigration {
|
||||
): Promise<boolean> {
|
||||
assert.ok(screenShareTrack);
|
||||
assert.ok(resolution);
|
||||
const nextConstraints: MediaTrackConstraints = {};
|
||||
const mediaStreamTrack = screenShareTrack.mediaStreamTrack;
|
||||
const currentConstraints = mediaStreamTrack.getConstraints() as ScreenShareVideoConstraints;
|
||||
const nextConstraints: ScreenShareVideoConstraints = {...currentConstraints};
|
||||
if (resolution.width > 0) nextConstraints.width = {ideal: resolution.width};
|
||||
if (resolution.height > 0) nextConstraints.height = {ideal: resolution.height};
|
||||
if (resolution.frameRate !== undefined) {
|
||||
nextConstraints.frameRate = {ideal: resolution.frameRate, max: resolution.frameRate};
|
||||
}
|
||||
if (JSON.stringify(currentConstraints) === JSON.stringify(nextConstraints)) return true;
|
||||
try {
|
||||
await screenShareTrack.mediaStreamTrack.applyConstraints(nextConstraints);
|
||||
await mediaStreamTrack.applyConstraints(nextConstraints);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.warn('Failed to update active screen share constraints', {error, resolution});
|
||||
|
||||
+21
-5
@@ -10,6 +10,7 @@ import {ScreenShareAudioCaptureError} from '@app/features/voice/utils/ScreenShar
|
||||
import type {ScreenShareCaptureOptions} from 'livekit-client';
|
||||
|
||||
type DisplayMediaVideoConstraints = MediaTrackConstraints & {
|
||||
colorSpace?: string;
|
||||
cursor?: 'always' | 'motion' | 'never';
|
||||
displaySurface?: 'browser' | 'monitor' | 'window';
|
||||
};
|
||||
@@ -92,6 +93,21 @@ export function getDisplayMediaOptions(options?: ScreenShareCaptureOptions): Dis
|
||||
} as DisplayMediaStreamOptions;
|
||||
}
|
||||
|
||||
function buildCapturedDisplayMediaConstraints(
|
||||
displayMediaOptions: DisplayMediaStreamOptions,
|
||||
cursor: 'always' | 'motion' | 'never',
|
||||
): MediaTrackConstraints {
|
||||
const requestedVideo =
|
||||
typeof displayMediaOptions.video === 'object' && displayMediaOptions.video
|
||||
? (displayMediaOptions.video as DisplayMediaVideoConstraints)
|
||||
: undefined;
|
||||
const constraints: DisplayMediaVideoConstraints = {colorSpace: 'rec709', cursor};
|
||||
if (requestedVideo?.width !== undefined) constraints.width = requestedVideo.width;
|
||||
if (requestedVideo?.height !== undefined) constraints.height = requestedVideo.height;
|
||||
if (requestedVideo?.frameRate !== undefined) constraints.frameRate = requestedVideo.frameRate;
|
||||
return constraints;
|
||||
}
|
||||
|
||||
export async function createDisplayScreenShareTracks(
|
||||
options?: ScreenShareCaptureOptions,
|
||||
captureContext?: DisplayScreenShareCaptureContext,
|
||||
@@ -99,7 +115,8 @@ export async function createDisplayScreenShareTracks(
|
||||
if (!navigator.mediaDevices.getDisplayMedia) {
|
||||
throw new Error('getDisplayMedia not supported');
|
||||
}
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia(getDisplayMediaOptions(options));
|
||||
const displayMediaOptions = getDisplayMediaOptions(options);
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
|
||||
try {
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
if (!videoTrack) {
|
||||
@@ -108,11 +125,10 @@ export async function createDisplayScreenShareTracks(
|
||||
if (options?.contentHint) {
|
||||
videoTrack.contentHint = options.contentHint;
|
||||
}
|
||||
await videoTrack.applyConstraints({colorSpace: 'rec709'} as MediaTrackConstraints).catch(() => undefined);
|
||||
const cursor = resolveCapturedDisplayMediaCursorCapture(videoTrack, options);
|
||||
if ((videoTrack.getSettings() as DisplayMediaTrackSettings).cursor !== cursor) {
|
||||
await videoTrack.applyConstraints({cursor} as MediaTrackConstraints).catch(() => undefined);
|
||||
}
|
||||
await videoTrack
|
||||
.applyConstraints(buildCapturedDisplayMediaConstraints(displayMediaOptions, cursor))
|
||||
.catch(() => undefined);
|
||||
const capturedAudioTrack = stream.getAudioTracks()[0];
|
||||
const audioTrack = capturedAudioTrack?.readyState === 'live' ? capturedAudioTrack : undefined;
|
||||
if (captureContext?.requireAudio && !audioTrack) {
|
||||
|
||||
@@ -57,7 +57,7 @@ const PREMIUM_SCREENSHARE_RESOLUTIONS: ReadonlySet<ScreenshareResolution> = new
|
||||
export const CAMERA_EFFECT_STRENGTH_MIN = 0;
|
||||
export const CAMERA_EFFECT_STRENGTH_MAX = 100;
|
||||
export const CAMERA_EFFECT_STRENGTH_DEFAULT = 50;
|
||||
export const DEFAULT_SCREEN_SHARE_CONTENT_HINT: ScreenShareContentHint = 'auto';
|
||||
export const DEFAULT_SCREEN_SHARE_CONTENT_HINT: ScreenShareContentHint = 'text';
|
||||
export const DEFAULT_SCREEN_SHARE_ENCODER_MODE: ScreenShareEncoderMode = 'auto';
|
||||
export const DEFAULT_SCREEN_SHARE_SOFTWARE_QUALITY: ScreenShareSoftwareQuality = 'balanced';
|
||||
export const DEFAULT_SCREEN_SHARE_SCALABILITY_MODE: ScreenShareScalabilityModePreference = 'auto';
|
||||
@@ -249,6 +249,17 @@ function applyStreamingModeDefaultMigrationV1(parsed: Record<string, unknown>):
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyScreenShareContentHintDefaultMigrationV1(parsed: Record<string, unknown>): boolean {
|
||||
if (parsed.screenShareContentHintDefaultMigratedV1 === true) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.screenShareContentHintPrefV2 === undefined || parsed.screenShareContentHintPrefV2 === 'auto') {
|
||||
parsed.screenShareContentHintPrefV2 = DEFAULT_SCREEN_SHARE_CONTENT_HINT;
|
||||
}
|
||||
parsed.screenShareContentHintDefaultMigratedV1 = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyOutputVolumeRecalibrationMigrationV1(parsed: Record<string, unknown>): boolean {
|
||||
if (parsed.outputVolumeRecalibratedV1 === true) {
|
||||
return false;
|
||||
@@ -361,6 +372,7 @@ class VoiceSettings {
|
||||
screenShareHevcOptOutMigratedV1 = false;
|
||||
emulatedDecodeVideoCodecCap: CodecPreference = 'auto';
|
||||
screenShareContentHintPrefV2: ScreenShareContentHint = DEFAULT_SCREEN_SHARE_CONTENT_HINT;
|
||||
screenShareContentHintDefaultMigratedV1 = false;
|
||||
screenShareEncoderModePrefV2: ScreenShareEncoderMode = DEFAULT_SCREEN_SHARE_ENCODER_MODE;
|
||||
screenShareSoftwareQualityPrefV2: ScreenShareSoftwareQuality = DEFAULT_SCREEN_SHARE_SOFTWARE_QUALITY;
|
||||
screenShareScalabilityModePrefV2: ScreenShareScalabilityModePreference = DEFAULT_SCREEN_SHARE_SCALABILITY_MODE;
|
||||
@@ -479,6 +491,7 @@ class VoiceSettings {
|
||||
changed = applyScreenShareAudioConsentMigrationV1(parsed) || changed;
|
||||
changed = applyScreenShareAudioDefaultOnMigrationV1(parsed) || changed;
|
||||
changed = applyStreamingModeDefaultMigrationV1(parsed) || changed;
|
||||
changed = applyScreenShareContentHintDefaultMigrationV1(parsed) || changed;
|
||||
changed = applyOutputVolumeRecalibrationMigrationV1(parsed) || changed;
|
||||
changed = applyNoiseSuppressionStandardDefaultMigrationV1(parsed) || changed;
|
||||
changed = applyScreenShareAv1OptOutMigrationV1(parsed) || changed;
|
||||
@@ -545,6 +558,7 @@ class VoiceSettings {
|
||||
'screenShareHevcOptOutMigratedV1',
|
||||
'emulatedDecodeVideoCodecCap',
|
||||
'screenShareContentHintPrefV2',
|
||||
'screenShareContentHintDefaultMigratedV1',
|
||||
'screenShareEncoderModePrefV2',
|
||||
'screenShareSoftwareQualityPrefV2',
|
||||
'screenShareScalabilityModePrefV2',
|
||||
|
||||
@@ -18,7 +18,10 @@ vi.mock('@app/features/app/utils/LimitUtils', () => ({
|
||||
|
||||
const AppStorage = (await import('@app/features/platform/state/PersistentStorage')).default;
|
||||
|
||||
AppStorage.setItem('VoiceSettings', JSON.stringify({preferredScreenShareCodec: 'av1'}));
|
||||
AppStorage.setItem(
|
||||
'VoiceSettings',
|
||||
JSON.stringify({preferredScreenShareCodec: 'av1', screenShareContentHintPrefV2: 'auto'}),
|
||||
);
|
||||
|
||||
const storageWrites: Array<string> = [];
|
||||
AppStorage.subscribe(
|
||||
@@ -132,3 +135,19 @@ describe('AV1/HEVC screen-share opt-in', () => {
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('vp9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('screen share content hint default', () => {
|
||||
it('moves a stored automatic content hint to text on first launch', () => {
|
||||
const migrated = JSON.parse(storageWrites[0]);
|
||||
expect(migrated.screenShareContentHintPrefV2).toBe('text');
|
||||
expect(migrated.screenShareContentHintDefaultMigratedV1).toBe(true);
|
||||
expect(VoiceSettings.getScreenShareContentHint()).toBe('text');
|
||||
expect(VoiceSettings.getScreenShareContentHintOverride()).toBe('text');
|
||||
});
|
||||
|
||||
it('keeps an automatic content hint chosen after the migration', () => {
|
||||
VoiceSettings.updateSettings({screenShareContentHint: 'auto'});
|
||||
expect(VoiceSettings.getScreenShareContentHint()).toBe('auto');
|
||||
expect(VoiceSettings.getScreenShareContentHintOverride()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -85,31 +85,21 @@ describe('buildScreenShareOptions', () => {
|
||||
windowAudio: 'exclude',
|
||||
});
|
||||
});
|
||||
it('prefers framerate for detail-oriented shares', () => {
|
||||
it('prefers resolution for detail-oriented shares', () => {
|
||||
const {publishOptions} = buildScreenShareOptions({
|
||||
resolution: 'medium',
|
||||
frameRate: 30,
|
||||
includeAudio: true,
|
||||
});
|
||||
expect(publishOptions.degradationPreference).toBe('maintain-framerate');
|
||||
expect(publishOptions.degradationPreference).toBe('maintain-resolution');
|
||||
});
|
||||
it('prefers framerate for non-gaming high-framerate shares', () => {
|
||||
it('keeps resolution for gaming-style high-framerate shares', () => {
|
||||
const {publishOptions} = buildScreenShareOptions({
|
||||
resolution: 'ultra',
|
||||
frameRate: 60,
|
||||
includeAudio: true,
|
||||
streamingMode: 'screenshare',
|
||||
});
|
||||
expect(publishOptions.degradationPreference).toBe('maintain-framerate');
|
||||
});
|
||||
it('prefers framerate degradation for gaming streams', () => {
|
||||
const {publishOptions} = buildScreenShareOptions({
|
||||
resolution: 'ultra',
|
||||
frameRate: 60,
|
||||
includeAudio: true,
|
||||
streamingMode: 'gaming',
|
||||
});
|
||||
expect(publishOptions.degradationPreference).toBe('maintain-framerate');
|
||||
expect(publishOptions.degradationPreference).toBe('maintain-resolution');
|
||||
});
|
||||
it('passes the selected content hint through capture options', () => {
|
||||
const {captureOptions} = buildScreenShareOptions({
|
||||
|
||||
@@ -18,13 +18,8 @@ const DIMENSIONS: Record<
|
||||
source: {width: 3840, height: 2160},
|
||||
};
|
||||
export const SCREEN_SHARE_FORCED_VIDEO_BITRATE_BPS = 7000000;
|
||||
export const SCREEN_SHARE_GAMING_DEGRADATION_PREFERENCE: NonNullable<TrackPublishOptions['degradationPreference']> =
|
||||
'maintain-framerate';
|
||||
export const SCREEN_SHARE_DEFAULT_DEGRADATION_PREFERENCE: NonNullable<TrackPublishOptions['degradationPreference']> =
|
||||
'maintain-framerate';
|
||||
export const SCREEN_SHARE_DEGRADATION_PREFERENCE: NonNullable<TrackPublishOptions['degradationPreference']> =
|
||||
SCREEN_SHARE_DEFAULT_DEGRADATION_PREFERENCE;
|
||||
export const SCREEN_SHARE_CONTENT_HINT: NonNullable<ScreenShareCaptureOptions['contentHint']> = 'motion';
|
||||
'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];
|
||||
@@ -85,7 +80,6 @@ export interface ScreenShareBuildConfig {
|
||||
resolution: ScreenshareResolution;
|
||||
frameRate: number;
|
||||
includeAudio: boolean;
|
||||
streamingMode?: StreamingMode;
|
||||
contentHint?: ScreenShareCaptureOptions['contentHint'];
|
||||
sourceDimensions?: {
|
||||
width: number;
|
||||
@@ -140,10 +134,6 @@ export function buildScreenShareOptions(
|
||||
cursor: resolveScreenShareCursorCapture(config.preferredDisplaySurface),
|
||||
...(config.preferredDisplaySurface ? {displaySurface: config.preferredDisplaySurface} : {}),
|
||||
};
|
||||
const degradationPreference =
|
||||
config.streamingMode === 'gaming'
|
||||
? SCREEN_SHARE_GAMING_DEGRADATION_PREFERENCE
|
||||
: SCREEN_SHARE_DEFAULT_DEGRADATION_PREFERENCE;
|
||||
return {
|
||||
captureOptions: {
|
||||
audio: config.includeAudio,
|
||||
@@ -157,7 +147,7 @@ export function buildScreenShareOptions(
|
||||
video,
|
||||
},
|
||||
publishOptions: {
|
||||
degradationPreference,
|
||||
degradationPreference: SCREEN_SHARE_DEGRADATION_PREFERENCE,
|
||||
screenShareEncoding: getScreenShareEncoding(resolvedFrameRate),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -312,7 +312,6 @@ function getConfiguredScreenShareOptions(
|
||||
resolution,
|
||||
frameRate,
|
||||
includeAudio,
|
||||
streamingMode: normalisedStreamingMode,
|
||||
contentHint,
|
||||
sourceDimensions,
|
||||
preferredDisplaySurface,
|
||||
|
||||
Reference in new issue
Block a user