import React, { useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { useVoiceStore } from '../../stores/voiceStore'; import type { ScreenShareConfig } from '../../stores/voiceStore'; import { useSettingsStore } from '../../stores/settingsStore'; import { buildScreenShareOptions } from '../../utils/screenShare'; import { useFloatingPosition } from '../../hooks/useFloatingPosition'; import { usePortalContainer } from '../../hooks/usePortalContainer'; import { Toggle } from '../ui/Toggle'; import { isElectron } from '../../platform/platform'; import { RESOLUTION_LABELS } from '@backspace/shared/src/constants'; interface ScreenShareSettingsPopoverProps { open: boolean; onClose: () => void; anchorRef: React.RefObject; } const MODES: { value: ScreenShareConfig['mode']; label: string }[] = [ { value: 'gaming', label: 'Gaming' }, { value: 'text', label: 'Text' }, ]; const CODEC_OPTIONS = [ { value: 'vp9' as const, label: 'Standard', sub: 'VP9' }, { value: 'hw' as const, label: 'NVIDIA / Apple', sub: 'H.264' }, ]; 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, anchorRef }: ScreenShareSettingsPopoverProps) { const popoverRef = useRef(null); const portalContainer = usePortalContainer(); const config = useVoiceStore((s) => s.screenShareConfig); const setConfig = useVoiceStore((s) => s.setScreenShareConfig); const hwOverdrive = useVoiceStore((s) => s.hwOverdrive); const setHwOverdrive = useVoiceStore((s) => s.setHwOverdrive); const limits = useSettingsStore((s) => s.streamingLimits); const { style } = useFloatingPosition(anchorRef, popoverRef, { placement: 'top', offset: 12, enabled: open, }); const BITRATE_MIN = limits?.minBitrateKbps ?? 500; const BITRATE_MAX = limits?.maxBitrateKbps ?? 20000; const BITRATE_STEP = limits?.bitrateStepKbps ?? 500; const RESOLUTIONS = (limits?.allowedResolutions ?? [540, 720, 1080]).map((r) => ({ value: r as ScreenShareConfig['height'], label: RESOLUTION_LABELS[r as keyof typeof RESOLUTION_LABELS] ?? `${r}p`, })); const FRAME_RATES = (limits?.allowedFramerates ?? [30, 45, 60]).map((f) => ({ value: f as ScreenShareConfig['fps'], label: `${f}`, })); 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 numericRes = limits.allowedResolutions.filter((r): r is number => r !== 'native'); if (typeof config.height === 'number' && numericRes.length > 0) { const h = config.height; patch.height = numericRes.reduce((a, b) => Math.abs(b - h) < Math.abs(a - h) ? b : a ); } else { // 'native' was disabled or no numeric options — fall back to highest numeric patch.height = numericRes.length > 0 ? Math.max(...numericRes) : 1080; } } if (!limits.allowedFramerates.includes(config.fps)) { const f = config.fps; const closest = limits.allowedFramerates.reduce((a, b) => Math.abs(b - f) < Math.abs(a - f) ? b : a ); 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-2.5 py-1.5 rounded-full text-[13px] font-medium transition-colors cursor-pointer select-none text-center'; const pillSelected = 'bg-accent-primary text-white'; const pillUnselected = 'bg-surface-elevated text-txt-secondary hover:bg-interactive-hover'; return createPortal(
Stream Settings
{/* Resolution */}
Resolution
{RESOLUTIONS.map((r) => ( ))}
{/* Frame Rate */}
Frame Rate
{FRAME_RATES.map((f) => ( ))}
{/* Content Mode */}
Content Mode
{MODES.map((m) => ( ))}
{/* Codec */}
Codec
{CODEC_OPTIONS.map((c) => { const isHw = c.value === 'hw'; const isSelected = isHw ? hwOverdrive : !hwOverdrive; return ( ); })}
{hwOverdrive && (
GPU hardware encoder · resets when stream ends
)}
{/* Bitrate */}
Bitrate
{limits?.allowCustomBitrate !== false && config.customBitrateKbps != null && ( )}
{limits?.allowCustomBitrate !== false ? (
setConfig({ customBitrateKbps: Number(e.target.value) })} className="flex-1 h-1.5 accent-accent-primary cursor-pointer appearance-none bg-interactive-muted 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`}
) : (
{formatKbps(Math.round(result.publish.videoEncoding.maxBitrate / 1000))}
Custom bitrate disabled by administrator
)}
{/* System Audio */}
System Audio
{isElectron() && config.shareAudio && (
Use Chrome for echo-free audio
)}
setConfig({ shareAudio: enabled })} />
{/* Footer — computed stats */}
{formatBitrate(result.publish.videoEncoding.maxBitrate)} · {formatDegradation(result.overdrive.degradationPreference)}
, portalContainer, ); }