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:
@@ -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">
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user