feat: add manual codec selection (VP9/H.264) to stream settings

VP9 is default — better quality per bit with H.264 SIMULCAST backup
for Safari. H.264 option available for zero-CPU hardware encoding.
Codec choice is independent of gaming/text mode, which still controls
contentHint and degradationPreference.
This commit is contained in:
Jannis Braun
2026-03-23 22:44:31 +01:00
parent e7a18bd28c
commit 894ce68194
3 changed files with 38 additions and 11 deletions
@@ -20,6 +20,11 @@ const MODES: { value: ScreenShareConfig['mode']; label: string }[] = [
{ value: 'text', label: 'Text' }, { value: 'text', label: 'Text' },
]; ];
const CODECS: { value: ScreenShareConfig['codec']; label: string; desc: string }[] = [
{ value: 'vp9', label: 'VP9', desc: 'Less bandwidth, more CPU' },
{ value: 'h264', label: 'H.264', desc: 'Less CPU, more bandwidth' },
];
function formatBitrate(bps: number): string { function formatBitrate(bps: number): string {
return `${(bps / 1_000_000).toFixed(bps % 1_000_000 === 0 ? 0 : 1)} Mbps`; return `${(bps / 1_000_000).toFixed(bps % 1_000_000 === 0 ? 0 : 1)} Mbps`;
} }
@@ -178,6 +183,27 @@ export function ScreenShareSettingsPopover({ open, onClose, anchorRef }: ScreenS
</div> </div>
</div> </div>
{/* Codec */}
<div>
<div className="text-[11px] text-txt-tertiary font-semibold uppercase tracking-wider mb-1.5">
Codec
</div>
<div className="flex gap-1.5">
{CODECS.map((c) => (
<button
key={c.value}
onClick={() => setConfig({ codec: c.value })}
className={`${pillBase} flex-1 flex flex-col items-center gap-0.5 !py-1.5 ${config.codec === c.value ? pillSelected : pillUnselected}`}
>
<span>{c.label}</span>
<span className={`text-[9px] font-normal ${config.codec === c.value ? 'text-white/70' : 'text-txt-tertiary'}`}>
{c.desc}
</span>
</button>
))}
</div>
</div>
{/* Bitrate Override */} {/* Bitrate Override */}
<div> <div>
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
+2 -1
View File
@@ -10,6 +10,7 @@ export interface ScreenShareConfig {
height: number | 'native'; height: number | 'native';
fps: number; fps: number;
mode: 'gaming' | 'text'; mode: 'gaming' | 'text';
codec: 'vp9' | 'h264';
customBitrateKbps: number | null; customBitrateKbps: number | null;
shareAudio: boolean; shareAudio: boolean;
} }
@@ -145,7 +146,7 @@ export const useVoiceStore = create<VoiceState>()(
inputDeviceId: 'default', inputDeviceId: 'default',
outputDeviceId: 'default', outputDeviceId: 'default',
focusedParticipantId: null, focusedParticipantId: null,
screenShareConfig: { height: 720, fps: 60, mode: 'gaming', customBitrateKbps: null, shareAudio: !isElectron() }, screenShareConfig: { height: 720, fps: 60, mode: 'gaming', codec: 'vp9', customBitrateKbps: null, shareAudio: !isElectron() },
participantVolumes: new Map(), participantVolumes: new Map(),
setParticipantVolume: (userId, volume) => { setParticipantVolume: (userId, volume) => {
set((state) => { set((state) => {
+10 -10
View File
@@ -131,33 +131,33 @@ export function buildScreenShareOptions(config: ScreenShareConfig): ScreenShareB
const bps = clampedKbps * 1000; const bps = clampedKbps * 1000;
const minBps = Math.round(bps * 0.25); const minBps = Math.round(bps * 0.25);
// Gaming mode: H.264 primary (hardware NVENC/QSV encoding, zero CPU impact on games) // VP9: better quality per bit, software-encoded (CPU). Best for text/static content.
// Text mode: VP9 primary (better compression for sharp text) with H.264 backup for Safari // H.264: hardware-encoded via NVENC/QSV/VCE (zero CPU). Universal browser support.
const isGaming = mode === 'gaming'; // When VP9 is primary, H.264 SIMULCAST backup handles Safari/incompatible viewers.
// When H.264 is primary, no backup needed — universally supported.
const useVp9 = config.codec !== 'h264';
return { return {
capture: { width: captureWidth, height: captureHeight, frameRate: fps }, capture: { width: captureWidth, height: captureHeight, frameRate: fps },
publish: { publish: {
videoCodec: isGaming ? 'h264' : 'vp9', videoCodec: useVp9 ? 'vp9' : 'h264',
videoEncoding: { maxBitrate: bps, maxFramerate: fps }, videoEncoding: { maxBitrate: bps, maxFramerate: fps },
simulcast: false, simulcast: false,
// H.264 is universally supported — no backup needed for gaming mode. ...(useVp9 ? {
// VP9 needs H.264 backup for Safari/incompatible viewers.
...(isGaming ? {} : {
backupCodec: { backupCodec: {
codec: 'h264' as const, codec: 'h264' as const,
encoding: { maxBitrate: bps, maxFramerate: fps }, encoding: { maxBitrate: bps, maxFramerate: fps },
}, },
backupCodecPolicy: BackupCodecPolicy.SIMULCAST, backupCodecPolicy: BackupCodecPolicy.SIMULCAST,
}), } : {}),
}, },
overdrive: { overdrive: {
maxBitrate: bps, maxBitrate: bps,
maxFramerate: fps, maxFramerate: fps,
minBitrate: minBps, minBitrate: minBps,
degradationPreference: isGaming ? 'balanced' : 'maintain-resolution', degradationPreference: mode === 'text' ? 'maintain-resolution' : 'balanced',
}, },
contentHint: isGaming ? 'motion' : 'detail', contentHint: mode === 'text' ? 'detail' : 'motion',
}; };
} }