diff --git a/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx b/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx index 2a62d52cc..24d7f2e28 100644 --- a/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx +++ b/fluxer_app/src/features/voice/components/StreamSettingsMenuContent.tsx @@ -260,7 +260,6 @@ export async function pushActiveStreamSettings( resolution, frameRate, includeAudio, - streamingMode: normalisedMode, contentHint, preferredDisplaySurface, }); diff --git a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareCodecMigration.ts b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareCodecMigration.ts index fdefb4b06..f65ee3398 100644 --- a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareCodecMigration.ts +++ b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareCodecMigration.ts @@ -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 { 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}); diff --git a/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts b/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts index b6350d679..d81a3bc12 100644 --- a/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts +++ b/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts @@ -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) { diff --git a/fluxer_app/src/features/voice/state/VoiceSettings.ts b/fluxer_app/src/features/voice/state/VoiceSettings.ts index fce9fb668..473a60460 100644 --- a/fluxer_app/src/features/voice/state/VoiceSettings.ts +++ b/fluxer_app/src/features/voice/state/VoiceSettings.ts @@ -57,7 +57,7 @@ const PREMIUM_SCREENSHARE_RESOLUTIONS: ReadonlySet = 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): return true; } +function applyScreenShareContentHintDefaultMigrationV1(parsed: Record): 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): 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', diff --git a/fluxer_app/src/features/voice/state/VoiceSettingsCodecOptIn.test.ts b/fluxer_app/src/features/voice/state/VoiceSettingsCodecOptIn.test.ts index 67f8a3d0d..5e4fb2071 100644 --- a/fluxer_app/src/features/voice/state/VoiceSettingsCodecOptIn.test.ts +++ b/fluxer_app/src/features/voice/state/VoiceSettingsCodecOptIn.test.ts @@ -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 = []; 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(); + }); +}); diff --git a/fluxer_app/src/features/voice/utils/ScreenShareOptions.test.ts b/fluxer_app/src/features/voice/utils/ScreenShareOptions.test.ts index 1762959b2..0bc938571 100644 --- a/fluxer_app/src/features/voice/utils/ScreenShareOptions.test.ts +++ b/fluxer_app/src/features/voice/utils/ScreenShareOptions.test.ts @@ -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({ diff --git a/fluxer_app/src/features/voice/utils/ScreenShareOptions.ts b/fluxer_app/src/features/voice/utils/ScreenShareOptions.ts index 44ba02110..84fd48f35 100644 --- a/fluxer_app/src/features/voice/utils/ScreenShareOptions.ts +++ b/fluxer_app/src/features/voice/utils/ScreenShareOptions.ts @@ -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 = - 'maintain-framerate'; -export const SCREEN_SHARE_DEFAULT_DEGRADATION_PREFERENCE: NonNullable = - 'maintain-framerate'; export const SCREEN_SHARE_DEGRADATION_PREFERENCE: NonNullable = - SCREEN_SHARE_DEFAULT_DEGRADATION_PREFERENCE; -export const SCREEN_SHARE_CONTENT_HINT: NonNullable = '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), }, }; diff --git a/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts b/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts index 18b713ef2..1aac9eb73 100644 --- a/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts +++ b/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts @@ -312,7 +312,6 @@ function getConfiguredScreenShareOptions( resolution, frameRate, includeAudio, - streamingMode: normalisedStreamingMode, contentHint, sourceDimensions, preferredDisplaySurface,