import React, { useRef, useEffect } from 'react'; import { useVoiceStore } from '../../stores/voiceStore'; import type { ScreenShareConfig } from '../../stores/voiceStore'; import { useSettingsStore } from '../../stores/settingsStore'; import { buildScreenShareOptions } from '../../utils/screenShare'; interface ScreenShareSettingsPopoverProps { open: boolean; onClose: () => void; } const ALL_RESOLUTIONS: { value: ScreenShareConfig['height']; label: string }[] = [ { value: 540, label: '540p' }, { value: 720, label: '720p' }, { value: 1080, label: '1080p' }, ]; const ALL_FRAME_RATES: { value: ScreenShareConfig['fps']; label: string }[] = [ { value: 30, label: '30' }, { value: 45, label: '45' }, { value: 60, label: '60' }, ]; const MODES: { value: ScreenShareConfig['mode']; label: string }[] = [ { value: 'gaming', label: 'Gaming' }, { value: 'text', label: 'Text' }, ]; function formatBitrate(bps: number): string { return `${(bps / 1_000_000).toFixed(bps % 1_000_000 === 0 ? 0 : 1)} Mbps`; } function formatDegradation(pref: RTCDegradationPreference): string { switch (pref) { case 'maintain-resolution': return 'hold resolution'; case 'maintain-framerate': return 'hold framerate'; case 'balanced': return 'balanced'; default: return pref; } } function formatKbps(kbps: number): string { return kbps >= 1000 ? `${(kbps / 1000).toFixed(kbps % 1000 === 0 ? 0 : 1)} Mbps` : `${kbps} kbps`; } export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSettingsPopoverProps) { const popoverRef = useRef(null); const config = useVoiceStore((s) => s.screenShareConfig); const setConfig = useVoiceStore((s) => s.setScreenShareConfig); const limits = useSettingsStore((s) => s.streamingLimits); const BITRATE_MIN = limits?.minBitrateKbps ?? 500; const BITRATE_MAX = limits?.maxBitrateKbps ?? 20000; const BITRATE_STEP = limits?.bitrateStepKbps ?? 500; const RESOLUTIONS = ALL_RESOLUTIONS.filter((r) => (limits?.allowedResolutions ?? [540, 720, 1080]).includes(r.value) ); const FRAME_RATES = ALL_FRAME_RATES.filter((f) => (limits?.allowedFramerates ?? [30, 45, 60]).includes(f.value) ); useEffect(() => { if (!open) return; const handleClick = (e: MouseEvent) => { if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { onClose(); } }; document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, [open, onClose]); // Auto-clamp persisted config if outside allowed bounds useEffect(() => { if (!limits) return; const patch: Partial = {}; if (!limits.allowedResolutions.includes(config.height)) { const closest = limits.allowedResolutions.reduce((a, b) => Math.abs(b - config.height) < Math.abs(a - config.height) ? b : a ) as ScreenShareConfig['height']; patch.height = closest; } if (!limits.allowedFramerates.includes(config.fps)) { const closest = limits.allowedFramerates.reduce((a, b) => Math.abs(b - config.fps) < Math.abs(a - config.fps) ? b : a ) as ScreenShareConfig['fps']; patch.fps = closest; } if (config.customBitrateKbps != null) { const clamped = Math.min(Math.max(config.customBitrateKbps, limits.minBitrateKbps), limits.maxBitrateKbps); if (clamped !== config.customBitrateKbps) patch.customBitrateKbps = clamped; } if (Object.keys(patch).length > 0) setConfig(patch); }, [limits, config, setConfig]); if (!open) return null; const result = buildScreenShareOptions(config); const pillBase = 'px-3 py-1.5 rounded-full text-[13px] font-medium transition-colors cursor-pointer select-none'; const pillSelected = 'bg-discord-blurple text-white'; const pillUnselected = 'bg-[#2b2d31] text-discord-text-secondary hover:bg-[#35373c]'; return (
Stream Settings
{/* Resolution */}
Resolution
{RESOLUTIONS.map((r) => ( ))}
{/* Frame Rate */}
Frame Rate
{FRAME_RATES.map((f) => ( ))}
{/* Content Mode */}
Content Mode
{MODES.map((m) => ( ))}
{/* Bitrate Override */}
Bitrate
{config.customBitrateKbps != null && ( )}
setConfig({ customBitrateKbps: Number(e.target.value) })} className="flex-1 h-1.5 accent-discord-blurple cursor-pointer appearance-none bg-[#4e5058] rounded-full [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-0 [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:cursor-pointer" /> {config.customBitrateKbps != null ? formatKbps(config.customBitrateKbps) : `Auto`}
{/* Footer — computed stats */}
{formatBitrate(result.publish.videoEncoding.maxBitrate)} · {formatDegradation(result.overdrive.degradationPreference)}
); }