mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
fix(voice): never auto-select AV1 or HEVC for screen sharing (#2111)
This commit is contained in:
1 parent
059bcc6c53
commit
3b1eb56713
13 files changed
+555
-387
No files matched your search
@@ -61,6 +61,8 @@ type VoiceSettingsPatch = Partial<{
|
||||
disablePictureInPicturePopoutScreenShare: boolean;
|
||||
preferredVideoCodec: CodecPreference;
|
||||
preferredScreenShareCodec: CodecPreference;
|
||||
screenShareAv1OptIn: boolean;
|
||||
screenShareHevcOptIn: boolean;
|
||||
emulatedDecodeVideoCodecCap: CodecPreference;
|
||||
screenShareContentHint: ScreenShareContentHint;
|
||||
screenShareEncoderMode: ScreenShareEncoderMode;
|
||||
@@ -108,6 +110,8 @@ const CAMERA_BACKGROUND_REFRESH_KEYS: Array<keyof VoiceSettingsPatch> = [
|
||||
const CAMERA_CAPTURE_REFRESH_KEYS: Array<keyof VoiceSettingsPatch> = ['cameraResolution'];
|
||||
const SCREEN_SHARE_CODEC_REFRESH_KEYS: Array<keyof VoiceSettingsPatch> = [
|
||||
'preferredScreenShareCodec',
|
||||
'screenShareAv1OptIn',
|
||||
'screenShareHevcOptIn',
|
||||
'screenShareEncoderMode',
|
||||
'screenShareSoftwareQuality',
|
||||
'screenShareScalabilityMode',
|
||||
|
||||
@@ -179,8 +179,14 @@ export function getScreenShareCodecPreferenceOrder(
|
||||
const encoderMode = resolveEffectiveScreenShareEncoderMode(VoiceSettings.getScreenShareEncoderMode());
|
||||
const automaticOrder =
|
||||
encoderMode === 'software' ? SOFTWARE_CODEC_PREFERENCE : getHardwareFirstScreenShareCodecPreferenceOrder();
|
||||
if (preference !== 'auto') return [preference, ...automaticOrder.filter((codec) => codec !== preference)];
|
||||
return automaticOrder;
|
||||
const order =
|
||||
preference !== 'auto' ? [preference, ...automaticOrder.filter((codec) => codec !== preference)] : automaticOrder;
|
||||
const filtered = order.filter(
|
||||
(codec) =>
|
||||
(codec !== 'av1' || VoiceSettings.getScreenShareAv1OptIn()) &&
|
||||
(codec !== 'h265' || VoiceSettings.getScreenShareHevcOptIn()),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getHardwareFirstScreenShareCodecPreferenceOrder(): ReadonlyArray<VideoCodec> {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
let av1OptIn = false;
|
||||
let hevcOptIn = false;
|
||||
|
||||
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
|
||||
default: {
|
||||
getScreenShareEncoderMode: () => 'software',
|
||||
getPreferredScreenShareCodec: () => 'auto',
|
||||
getScreenShareAv1OptIn: () => av1OptIn,
|
||||
getScreenShareHevcOptIn: () => hevcOptIn,
|
||||
},
|
||||
}));
|
||||
|
||||
const {getScreenShareCodecPreferenceOrder} = await import('./ScreenShareCodecNegotiation');
|
||||
|
||||
describe('screen-share codec preference order opt-in filter', () => {
|
||||
beforeEach(() => {
|
||||
av1OptIn = false;
|
||||
hevcOptIn = false;
|
||||
});
|
||||
|
||||
it('excludes AV1 and HEVC from the automatic preference order by default', () => {
|
||||
const order = getScreenShareCodecPreferenceOrder('auto');
|
||||
expect(order).not.toContain('av1');
|
||||
expect(order).not.toContain('h265');
|
||||
expect(order).toContain('vp9');
|
||||
});
|
||||
|
||||
it('includes AV1 only once its opt-in is on', () => {
|
||||
expect(getScreenShareCodecPreferenceOrder('auto')).not.toContain('av1');
|
||||
av1OptIn = true;
|
||||
expect(getScreenShareCodecPreferenceOrder('auto')).toContain('av1');
|
||||
});
|
||||
|
||||
it('includes HEVC only once its opt-in is on', () => {
|
||||
expect(getScreenShareCodecPreferenceOrder('auto')).not.toContain('h265');
|
||||
hevcOptIn = true;
|
||||
expect(getScreenShareCodecPreferenceOrder('auto')).toContain('h265');
|
||||
});
|
||||
});
|
||||
@@ -101,6 +101,8 @@ type VoiceSettingsUpdate = Partial<{
|
||||
disablePictureInPicturePopoutScreenShare: boolean;
|
||||
preferredVideoCodec: CodecPreference;
|
||||
preferredScreenShareCodec: CodecPreference;
|
||||
screenShareAv1OptIn: boolean;
|
||||
screenShareHevcOptIn: boolean;
|
||||
emulatedDecodeVideoCodecCap: CodecPreference;
|
||||
screenShareContentHint: ScreenShareContentHint;
|
||||
screenShareEncoderMode: ScreenShareEncoderMode;
|
||||
@@ -259,6 +261,28 @@ function applyOutputVolumeRecalibrationMigrationV1(parsed: Record<string, unknow
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyScreenShareAv1OptOutMigrationV1(parsed: Record<string, unknown>): boolean {
|
||||
if (parsed.screenShareAv1OptOutMigratedV1 === true) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.preferredScreenShareCodec === 'av1') {
|
||||
parsed.preferredScreenShareCodec = 'auto';
|
||||
}
|
||||
parsed.screenShareAv1OptOutMigratedV1 = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyScreenShareHevcOptOutMigrationV1(parsed: Record<string, unknown>): boolean {
|
||||
if (parsed.screenShareHevcOptOutMigratedV1 === true) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.preferredScreenShareCodec === 'h265') {
|
||||
parsed.preferredScreenShareCodec = 'auto';
|
||||
}
|
||||
parsed.screenShareHevcOptOutMigratedV1 = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyNoiseSuppressionStandardDefaultMigrationV1(parsed: Record<string, unknown>): boolean {
|
||||
if (parsed.noiseSuppressionStandardDefaultMigratedV1 === true) {
|
||||
return false;
|
||||
@@ -332,6 +356,10 @@ class VoiceSettings {
|
||||
disablePictureInPicturePopoutScreenShare = false;
|
||||
preferredVideoCodec: CodecPreference = 'auto';
|
||||
preferredScreenShareCodec: CodecPreference = 'auto';
|
||||
screenShareAv1OptIn = false;
|
||||
screenShareHevcOptIn = false;
|
||||
screenShareAv1OptOutMigratedV1 = false;
|
||||
screenShareHevcOptOutMigratedV1 = false;
|
||||
emulatedDecodeVideoCodecCap: CodecPreference = 'auto';
|
||||
screenShareContentHintPrefV2: ScreenShareContentHint = DEFAULT_SCREEN_SHARE_CONTENT_HINT;
|
||||
screenShareEncoderModePrefV2: ScreenShareEncoderMode = DEFAULT_SCREEN_SHARE_ENCODER_MODE;
|
||||
@@ -403,6 +431,8 @@ class VoiceSettings {
|
||||
getPreferredVideoCodec: false,
|
||||
getEmulatedDecodeVideoCodecCap: false,
|
||||
getPreferredScreenShareCodec: false,
|
||||
getScreenShareAv1OptIn: false,
|
||||
getScreenShareHevcOptIn: false,
|
||||
getScreenShareContentHint: false,
|
||||
getScreenShareContentHintOverride: false,
|
||||
getScreenShareEncoderMode: false,
|
||||
@@ -454,10 +484,14 @@ class VoiceSettings {
|
||||
changed = applyStreamingModeDefaultMigrationV1(parsed) || changed;
|
||||
changed = applyOutputVolumeRecalibrationMigrationV1(parsed) || changed;
|
||||
changed = applyNoiseSuppressionStandardDefaultMigrationV1(parsed) || changed;
|
||||
changed = applyScreenShareAv1OptOutMigrationV1(parsed) || changed;
|
||||
changed = applyScreenShareHevcOptOutMigrationV1(parsed) || changed;
|
||||
if (changed) {
|
||||
AppStorage.setItem('VoiceSettings', JSON.stringify(parsed));
|
||||
}
|
||||
this.outputVolumeRecalibratedV1 = parsed.outputVolumeRecalibratedV1 === true;
|
||||
this.screenShareAv1OptOutMigratedV1 = parsed.screenShareAv1OptOutMigratedV1 === true;
|
||||
this.screenShareHevcOptOutMigratedV1 = parsed.screenShareHevcOptOutMigratedV1 === true;
|
||||
} catch (error) {
|
||||
logger.warn('Failed to migrate persisted voice settings:', error);
|
||||
}
|
||||
@@ -508,6 +542,10 @@ class VoiceSettings {
|
||||
'disablePictureInPicturePopoutScreenShare',
|
||||
'preferredVideoCodec',
|
||||
'preferredScreenShareCodec',
|
||||
'screenShareAv1OptIn',
|
||||
'screenShareHevcOptIn',
|
||||
'screenShareAv1OptOutMigratedV1',
|
||||
'screenShareHevcOptOutMigratedV1',
|
||||
'emulatedDecodeVideoCodecCap',
|
||||
'screenShareContentHintPrefV2',
|
||||
'screenShareEncoderModePrefV2',
|
||||
@@ -871,9 +909,19 @@ class VoiceSettings {
|
||||
}
|
||||
|
||||
getPreferredScreenShareCodec(): CodecPreference {
|
||||
if (this.preferredScreenShareCodec === 'av1' && !this.screenShareAv1OptIn) return 'auto';
|
||||
if (this.preferredScreenShareCodec === 'h265' && !this.screenShareHevcOptIn) return 'auto';
|
||||
return this.preferredScreenShareCodec;
|
||||
}
|
||||
|
||||
getScreenShareAv1OptIn(): boolean {
|
||||
return this.screenShareAv1OptIn;
|
||||
}
|
||||
|
||||
getScreenShareHevcOptIn(): boolean {
|
||||
return this.screenShareHevcOptIn;
|
||||
}
|
||||
|
||||
getScreenShareContentHint(): ScreenShareContentHint {
|
||||
return this.screenShareContentHint;
|
||||
}
|
||||
@@ -1056,6 +1104,8 @@ class VoiceSettings {
|
||||
if (validated.preferredVideoCodec !== undefined) this.preferredVideoCodec = validated.preferredVideoCodec;
|
||||
if (validated.preferredScreenShareCodec !== undefined)
|
||||
this.preferredScreenShareCodec = validated.preferredScreenShareCodec;
|
||||
if (validated.screenShareAv1OptIn !== undefined) this.screenShareAv1OptIn = validated.screenShareAv1OptIn;
|
||||
if (validated.screenShareHevcOptIn !== undefined) this.screenShareHevcOptIn = validated.screenShareHevcOptIn;
|
||||
if (validated.emulatedDecodeVideoCodecCap !== undefined)
|
||||
this.emulatedDecodeVideoCodecCap = validated.emulatedDecodeVideoCodecCap;
|
||||
if (validated.screenShareContentHint !== undefined) this.screenShareContentHint = validated.screenShareContentHint;
|
||||
@@ -1137,6 +1187,8 @@ class VoiceSettings {
|
||||
const screenShareBackupCodecMode = validateScreenShareBackupCodecMode(
|
||||
data.screenShareBackupCodecMode ?? this.screenShareBackupCodecMode,
|
||||
);
|
||||
const screenShareAv1OptIn = data.screenShareAv1OptIn ?? this.screenShareAv1OptIn;
|
||||
const screenShareHevcOptIn = data.screenShareHevcOptIn ?? this.screenShareHevcOptIn;
|
||||
const validCameraResolutions: Array<CameraResolution> = ['low', 'medium', 'high'];
|
||||
if (!validCameraResolutions.includes(cameraResolution)) {
|
||||
cameraResolution = 'medium';
|
||||
@@ -1216,7 +1268,13 @@ class VoiceSettings {
|
||||
disablePictureInPicturePopoutScreenShare:
|
||||
data.disablePictureInPicturePopoutScreenShare ?? this.disablePictureInPicturePopoutScreenShare,
|
||||
preferredVideoCodec: data.preferredVideoCodec ?? this.preferredVideoCodec,
|
||||
preferredScreenShareCodec: data.preferredScreenShareCodec ?? this.preferredScreenShareCodec,
|
||||
preferredScreenShareCodec: validateCodecPreference(
|
||||
data.preferredScreenShareCodec ?? this.preferredScreenShareCodec,
|
||||
screenShareAv1OptIn,
|
||||
screenShareHevcOptIn,
|
||||
),
|
||||
screenShareAv1OptIn,
|
||||
screenShareHevcOptIn,
|
||||
emulatedDecodeVideoCodecCap: data.emulatedDecodeVideoCodecCap ?? this.emulatedDecodeVideoCodecCap,
|
||||
screenShareContentHint: validateScreenShareContentHint(
|
||||
data.screenShareContentHint ?? this.screenShareContentHint,
|
||||
@@ -1278,6 +1336,12 @@ function validateLastScreenShareSource(value: unknown): LastScreenShareSource |
|
||||
};
|
||||
}
|
||||
|
||||
function validateCodecPreference(value: unknown, allowAv1: boolean, allowHevc: boolean): CodecPreference {
|
||||
if (value === 'av1') return allowAv1 ? 'av1' : 'auto';
|
||||
if (value === 'h265') return allowHevc ? 'h265' : 'auto';
|
||||
return value === 'vp8' || value === 'vp9' || value === 'h264' || value === 'auto' ? value : 'auto';
|
||||
}
|
||||
|
||||
function validateScreenShareEncoderMode(mode: unknown): ScreenShareEncoderMode {
|
||||
return mode === 'hardware' || mode === 'software' || mode === 'auto' ? mode : DEFAULT_SCREEN_SHARE_ENCODER_MODE;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// @vitest-environment happy-dom
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {runInAction} from 'mobx';
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/voice/utils/VoiceBackgroundAvailability', () => ({
|
||||
areVoiceBackgroundsAvailable: () => true,
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/app/utils/LimitResolverAdapter', () => ({
|
||||
LimitResolver: {resolve: () => 0},
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/app/utils/LimitUtils', () => ({
|
||||
isLimitToggleEnabled: () => false,
|
||||
}));
|
||||
|
||||
const AppStorage = (await import('@app/features/platform/state/PersistentStorage')).default;
|
||||
|
||||
AppStorage.setItem('VoiceSettings', JSON.stringify({preferredScreenShareCodec: 'av1'}));
|
||||
|
||||
const storageWrites: Array<string> = [];
|
||||
AppStorage.subscribe(
|
||||
(event) => {
|
||||
if (event.newValue != null) storageWrites.push(event.newValue);
|
||||
},
|
||||
{key: 'VoiceSettings'},
|
||||
);
|
||||
|
||||
const {default: VoiceSettings} = await import('./VoiceSettings');
|
||||
|
||||
describe('AV1/HEVC screen-share opt-in', () => {
|
||||
it('rewrites a stored AV1 screen-share preference back to automatic on first launch', () => {
|
||||
const migrated = JSON.parse(storageWrites[0]);
|
||||
expect(migrated.preferredScreenShareCodec).toBe('auto');
|
||||
expect(migrated.screenShareAv1OptOutMigratedV1).toBe(true);
|
||||
expect(migrated.screenShareHevcOptOutMigratedV1).toBe(true);
|
||||
expect(VoiceSettings.screenShareAv1OptOutMigratedV1).toBe(true);
|
||||
expect(VoiceSettings.screenShareHevcOptOutMigratedV1).toBe(true);
|
||||
expect(VoiceSettings.screenShareAv1OptIn).toBe(false);
|
||||
expect(VoiceSettings.screenShareHevcOptIn).toBe(false);
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('auto');
|
||||
});
|
||||
|
||||
it('hands out automatic while a hydrated AV1 or HEVC preference is not opted in', () => {
|
||||
VoiceSettings.updateSettings({screenShareAv1OptIn: false, screenShareHevcOptIn: false});
|
||||
runInAction(() => {
|
||||
VoiceSettings.preferredScreenShareCodec = 'av1';
|
||||
});
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('auto');
|
||||
runInAction(() => {
|
||||
VoiceSettings.preferredScreenShareCodec = 'h265';
|
||||
});
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('auto');
|
||||
});
|
||||
|
||||
it('refuses to store an AV1 or HEVC preference while the opt-in is off', () => {
|
||||
VoiceSettings.updateSettings({
|
||||
screenShareAv1OptIn: false,
|
||||
screenShareHevcOptIn: false,
|
||||
preferredScreenShareCodec: 'auto',
|
||||
});
|
||||
VoiceSettings.updateSettings({preferredScreenShareCodec: 'av1'});
|
||||
expect(VoiceSettings.preferredScreenShareCodec).toBe('auto');
|
||||
VoiceSettings.updateSettings({preferredScreenShareCodec: 'h265'});
|
||||
expect(VoiceSettings.preferredScreenShareCodec).toBe('auto');
|
||||
});
|
||||
|
||||
it('accepts an AV1 preference together with its opt-in in one patch', () => {
|
||||
VoiceSettings.updateSettings({screenShareAv1OptIn: false, preferredScreenShareCodec: 'auto'});
|
||||
VoiceSettings.updateSettings({screenShareAv1OptIn: true, preferredScreenShareCodec: 'av1'});
|
||||
expect(VoiceSettings.preferredScreenShareCodec).toBe('av1');
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('av1');
|
||||
});
|
||||
|
||||
it('accepts an HEVC preference together with its opt-in in one patch', () => {
|
||||
VoiceSettings.updateSettings({screenShareHevcOptIn: false, preferredScreenShareCodec: 'auto'});
|
||||
VoiceSettings.updateSettings({screenShareHevcOptIn: true, preferredScreenShareCodec: 'h265'});
|
||||
expect(VoiceSettings.preferredScreenShareCodec).toBe('h265');
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('h265');
|
||||
});
|
||||
|
||||
it('drops the AV1 preference again when the opt-in is turned back off', () => {
|
||||
VoiceSettings.updateSettings({screenShareAv1OptIn: true, preferredScreenShareCodec: 'av1'});
|
||||
VoiceSettings.updateSettings({screenShareAv1OptIn: false});
|
||||
expect(VoiceSettings.preferredScreenShareCodec).toBe('auto');
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('auto');
|
||||
});
|
||||
|
||||
it('drops the HEVC preference again when the opt-in is turned back off', () => {
|
||||
VoiceSettings.updateSettings({screenShareHevcOptIn: true, preferredScreenShareCodec: 'h265'});
|
||||
VoiceSettings.updateSettings({screenShareHevcOptIn: false});
|
||||
expect(VoiceSettings.preferredScreenShareCodec).toBe('auto');
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('auto');
|
||||
});
|
||||
|
||||
it('leaves an always-available codec preference alone', () => {
|
||||
VoiceSettings.updateSettings({preferredScreenShareCodec: 'vp9'});
|
||||
expect(VoiceSettings.preferredScreenShareCodec).toBe('vp9');
|
||||
expect(VoiceSettings.getPreferredScreenShareCodec()).toBe('vp9');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {HardwareEncodeReport} from '@app/features/voice/utils/GpuEncoderCapabilities';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
let av1OptIn = false;
|
||||
let hevcOptIn = false;
|
||||
let desktop = true;
|
||||
let gpuReport: HardwareEncodeReport | null = null;
|
||||
|
||||
vi.mock('@app/features/voice/state/VoiceSettings', () => ({
|
||||
default: {
|
||||
getScreenShareAv1OptIn: () => av1OptIn,
|
||||
getScreenShareHevcOptIn: () => hevcOptIn,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/devtools/utils/DesktopTroubleshootingUtils', () => ({
|
||||
getCachedDesktopTroubleshootingSettings: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/ui/utils/NativeUtils', () => ({
|
||||
guessPlatform: () => 'windows',
|
||||
isChromiumBrowser: () => true,
|
||||
isDesktop: () => desktop,
|
||||
isFirefoxBrowser: () => false,
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/voice/utils/GpuEncoderCapabilities', () => ({
|
||||
getGpuEncoderReportSync: () => gpuReport,
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/voice/utils/NativeHardwareEncoderCapabilities', () => ({
|
||||
getNativeHardwareEncoderCapabilitiesSync: () => null,
|
||||
hasNativeHardwareEncoder: () => false,
|
||||
resetNativeHardwareEncoderCapabilities: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/voice/utils/OpenH264Status', () => ({
|
||||
getOpenH264StatusSync: () => null,
|
||||
resetOpenH264Status: () => undefined,
|
||||
}));
|
||||
|
||||
Object.defineProperty(globalThis, 'RTCRtpSender', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: {
|
||||
getCapabilities: () => ({
|
||||
codecs: [
|
||||
{mimeType: 'video/VP8'},
|
||||
{mimeType: 'video/VP9'},
|
||||
{mimeType: 'video/H264'},
|
||||
{mimeType: 'video/H265'},
|
||||
{mimeType: 'video/AV1'},
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const {getCodecCapabilityReport, resetCachedCodecCapabilities, selectAutomaticScreenShareCodec} = await import(
|
||||
'./CodecCapabilityDetector'
|
||||
);
|
||||
|
||||
const ALL_SOFTWARE: HardwareEncodeReport = {
|
||||
av1: 'software',
|
||||
h265: 'software',
|
||||
h264: 'software',
|
||||
vp9: 'software',
|
||||
vp8: 'software',
|
||||
};
|
||||
|
||||
describe('screen-share AV1/HEVC opt-in gate', () => {
|
||||
beforeEach(() => {
|
||||
av1OptIn = false;
|
||||
hevcOptIn = false;
|
||||
desktop = true;
|
||||
gpuReport = null;
|
||||
resetCachedCodecCapabilities();
|
||||
});
|
||||
|
||||
it('reports AV1 as unsupported until the user opts in, even when the encoder is there', () => {
|
||||
expect(getCodecCapabilityReport().av1).toMatchObject({supported: false, reason: 'opt-in-required'});
|
||||
av1OptIn = true;
|
||||
resetCachedCodecCapabilities();
|
||||
expect(getCodecCapabilityReport().av1).toMatchObject({supported: true, reason: 'supported'});
|
||||
});
|
||||
|
||||
it('reports HEVC as unsupported until the user opts in, even when the encoder is there', () => {
|
||||
expect(getCodecCapabilityReport().h265).toMatchObject({supported: false, reason: 'opt-in-required'});
|
||||
hevcOptIn = true;
|
||||
resetCachedCodecCapabilities();
|
||||
expect(getCodecCapabilityReport().h265).toMatchObject({supported: true, reason: 'supported'});
|
||||
});
|
||||
|
||||
it('leaves the always-available codecs alone while AV1 and HEVC are gated', () => {
|
||||
const report = getCodecCapabilityReport();
|
||||
expect(report.vp9.supported).toBe(true);
|
||||
expect(report.h264.supported).toBe(true);
|
||||
expect(report.vp8.supported).toBe(true);
|
||||
expect(report.av1.supported).toBe(false);
|
||||
expect(report.h265.supported).toBe(false);
|
||||
});
|
||||
|
||||
it('never selects AV1 for the forced-software path while the opt-in is off', () => {
|
||||
expect(selectAutomaticScreenShareCodec('software')).toEqual({codec: 'vp9', reason: 'software-vp9'});
|
||||
av1OptIn = true;
|
||||
resetCachedCodecCapabilities();
|
||||
expect(selectAutomaticScreenShareCodec('software')).toEqual({codec: 'av1', reason: 'software-av1'});
|
||||
});
|
||||
|
||||
it('never selects AV1 for the desktop hardware path while the opt-in is off', () => {
|
||||
gpuReport = {...ALL_SOFTWARE, av1: 'hardware'};
|
||||
expect(selectAutomaticScreenShareCodec('auto').codec).not.toBe('av1');
|
||||
av1OptIn = true;
|
||||
resetCachedCodecCapabilities();
|
||||
expect(selectAutomaticScreenShareCodec('auto')).toEqual({codec: 'av1', reason: 'hardware-av1'});
|
||||
});
|
||||
|
||||
it('never selects HEVC for the desktop hardware path while the opt-in is off', () => {
|
||||
gpuReport = {...ALL_SOFTWARE, h265: 'hardware'};
|
||||
expect(selectAutomaticScreenShareCodec('auto').codec).not.toBe('h265');
|
||||
hevcOptIn = true;
|
||||
resetCachedCodecCapabilities();
|
||||
expect(selectAutomaticScreenShareCodec('auto')).toEqual({codec: 'h265', reason: 'hardware-h265'});
|
||||
});
|
||||
|
||||
it('never selects AV1 or HEVC for the software fallback path while the opt-ins are off', () => {
|
||||
desktop = false;
|
||||
gpuReport = ALL_SOFTWARE;
|
||||
const selection = selectAutomaticScreenShareCodec('auto').codec;
|
||||
expect(selection).not.toBe('av1');
|
||||
expect(selection).not.toBe('h265');
|
||||
});
|
||||
|
||||
it('rebuilds the memoised report when either opt-in is flipped at runtime', () => {
|
||||
expect(getCodecCapabilityReport().av1.supported).toBe(false);
|
||||
expect(getCodecCapabilityReport().h265.supported).toBe(false);
|
||||
av1OptIn = true;
|
||||
expect(getCodecCapabilityReport().av1.supported).toBe(true);
|
||||
hevcOptIn = true;
|
||||
expect(getCodecCapabilityReport().h265.supported).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
isFirefoxBrowser,
|
||||
type NativePlatform,
|
||||
} from '@app/features/ui/utils/NativeUtils';
|
||||
import VoiceSettings from '@app/features/voice/state/VoiceSettings';
|
||||
import {getGpuEncoderReportSync, type HardwareEncodeAnswer} from '@app/features/voice/utils/GpuEncoderCapabilities';
|
||||
import {
|
||||
getNativeHardwareEncoderCapabilitiesSync,
|
||||
@@ -35,6 +36,7 @@ export type CodecSupportReason =
|
||||
| 'unsupported-system'
|
||||
| 'unavailable-on-platform'
|
||||
| 'capabilities-unavailable'
|
||||
| 'opt-in-required'
|
||||
| 'runtime-failed';
|
||||
|
||||
export interface CodecSupportInfo {
|
||||
@@ -84,6 +86,8 @@ let cachedReport: CodecCapabilityReport | null = null;
|
||||
let cachedReportGpuKey: object | null | undefined;
|
||||
let cachedReportNativeHardwareEncoderKey: object | null | undefined;
|
||||
let cachedReportHardwareAccelerationDisabled: boolean | undefined;
|
||||
let cachedReportAv1OptIn: boolean | undefined;
|
||||
let cachedReportHevcOptIn: boolean | undefined;
|
||||
const runtimeEncodeFailureCodecs = new Set<VideoCodec>();
|
||||
|
||||
interface RawProbeResult {
|
||||
@@ -160,6 +164,22 @@ function getScreenShareCodecPolicyUnsupported(
|
||||
codec: keyof CodecCapabilities,
|
||||
context: CodecPolicyContext,
|
||||
): Omit<CodecSupportInfo, 'hardwareAccelerated'> | null {
|
||||
if (codec === 'av1' && !VoiceSettings.getScreenShareAv1OptIn()) {
|
||||
return {
|
||||
supported: false,
|
||||
reason: 'opt-in-required',
|
||||
detail:
|
||||
'AV1 screen sharing is off by default because it may cause compatibility issues for viewers. We’re working on improving this. Turn on AV1 screen sharing in Advanced settings to use it.',
|
||||
};
|
||||
}
|
||||
if (codec === 'h265' && !VoiceSettings.getScreenShareHevcOptIn()) {
|
||||
return {
|
||||
supported: false,
|
||||
reason: 'opt-in-required',
|
||||
detail:
|
||||
'H.265 (HEVC) screen sharing is off by default because it may cause compatibility issues for viewers. We’re working on improving this. Turn on H.265 screen sharing in Advanced settings to use it.',
|
||||
};
|
||||
}
|
||||
if (context.firefox) {
|
||||
switch (codec) {
|
||||
case 'av1':
|
||||
@@ -318,17 +338,23 @@ export function getCodecCapabilityReport(): CodecCapabilityReport {
|
||||
const currentGpu = getGpuEncoderReportSync();
|
||||
const currentNativeHardwareEncoder = getNativeHardwareEncoderCapabilitiesSync();
|
||||
const hardwareAccelerationDisabled = isDesktopHardwareAccelerationDisabled();
|
||||
const av1OptIn = VoiceSettings.getScreenShareAv1OptIn();
|
||||
const hevcOptIn = VoiceSettings.getScreenShareHevcOptIn();
|
||||
if (
|
||||
cachedReport &&
|
||||
cachedReportGpuKey === currentGpu &&
|
||||
cachedReportNativeHardwareEncoderKey === currentNativeHardwareEncoder &&
|
||||
cachedReportHardwareAccelerationDisabled === hardwareAccelerationDisabled
|
||||
cachedReportHardwareAccelerationDisabled === hardwareAccelerationDisabled &&
|
||||
cachedReportAv1OptIn === av1OptIn &&
|
||||
cachedReportHevcOptIn === hevcOptIn
|
||||
)
|
||||
return cachedReport;
|
||||
cachedReport = buildReport();
|
||||
cachedReportGpuKey = currentGpu;
|
||||
cachedReportNativeHardwareEncoderKey = currentNativeHardwareEncoder;
|
||||
cachedReportHardwareAccelerationDisabled = hardwareAccelerationDisabled;
|
||||
cachedReportAv1OptIn = av1OptIn;
|
||||
cachedReportHevcOptIn = hevcOptIn;
|
||||
return cachedReport;
|
||||
}
|
||||
|
||||
@@ -506,6 +532,8 @@ export function resetCachedCodecCapabilities(): void {
|
||||
cachedReportGpuKey = undefined;
|
||||
cachedReportNativeHardwareEncoderKey = undefined;
|
||||
cachedReportHardwareAccelerationDisabled = undefined;
|
||||
cachedReportAv1OptIn = undefined;
|
||||
cachedReportHevcOptIn = undefined;
|
||||
runtimeEncodeFailureCodecs.clear();
|
||||
resetNativeHardwareEncoderCapabilities();
|
||||
resetOpenH264Status();
|
||||
|
||||
Reference in new issue
Block a user