diff --git a/web/src/plugins/streaming/StreamingTab.tsx b/web/src/plugins/streaming/StreamingTab.tsx index c6ab50a..595dc75 100644 --- a/web/src/plugins/streaming/StreamingTab.tsx +++ b/web/src/plugins/streaming/StreamingTab.tsx @@ -43,6 +43,16 @@ function formatElapsed(startedAt: string): string { return `${m}:${String(s).padStart(2, '0')}`; } +// ── Quality Presets ── + +const QUALITY_PRESETS = [ + { label: '720p30', width: 1280, height: 720, fps: 30, bitrate: 2_500_000 }, + { label: '1080p30', width: 1920, height: 1080, fps: 30, bitrate: 5_000_000 }, + { label: '1080p60', width: 1920, height: 1080, fps: 60, bitrate: 8_000_000 }, + { label: '1440p60', width: 2560, height: 1440, fps: 60, bitrate: 14_000_000 }, + { label: '4K60', width: 3840, height: 2160, fps: 60, bitrate: 25_000_000 }, +] as const; + // ── Component ── export default function StreamingTab({ data }: { data: any }) { @@ -51,6 +61,7 @@ export default function StreamingTab({ data }: { data: any }) { const [userName, setUserName] = useState(() => localStorage.getItem('streaming_name') || ''); const [streamTitle, setStreamTitle] = useState('Screen Share'); const [streamPassword, setStreamPassword] = useState(''); + const [qualityIdx, setQualityIdx] = useState(2); // Default: 1080p60 const [error, setError] = useState(null); const [joinModal, setJoinModal] = useState(null); const [myStreamId, setMyStreamId] = useState(null); @@ -88,8 +99,10 @@ export default function StreamingTab({ data }: { data: any }) { // Refs that mirror state (avoid stale closures in WS handler) const isBroadcastingRef = useRef(false); const viewingRef = useRef(null); + const qualityRef = useRef(QUALITY_PRESETS[2]); useEffect(() => { isBroadcastingRef.current = isBroadcasting; }, [isBroadcasting]); useEffect(() => { viewingRef.current = viewing; }, [viewing]); + useEffect(() => { qualityRef.current = QUALITY_PRESETS[qualityIdx]; }, [qualityIdx]); // Notify Electron about streaming status for close-warning useEffect(() => { @@ -257,13 +270,13 @@ export default function StreamingTab({ data }: { data: any }) { if (ev.candidate) wsSend({ type: 'ice_candidate', targetId: viewerId, candidate: ev.candidate.toJSON() }); }; - // 60 fps + high bitrate + // Apply quality preset to WebRTC encoding const videoSender = pc.getSenders().find(s => s.track?.kind === 'video'); if (videoSender) { const params = videoSender.getParameters(); if (!params.encodings || params.encodings.length === 0) params.encodings = [{}]; - params.encodings[0].maxFramerate = 60; - params.encodings[0].maxBitrate = 8_000_000; + params.encodings[0].maxFramerate = qualityRef.current.fps; + params.encodings[0].maxBitrate = qualityRef.current.bitrate; videoSender.setParameters(params).catch(() => {}); } @@ -402,8 +415,9 @@ export default function StreamingTab({ data }: { data: any }) { setStarting(true); try { + const q = qualityRef.current; const stream = await navigator.mediaDevices.getDisplayMedia({ - video: { frameRate: { ideal: 60 }, width: { ideal: 1920 }, height: { ideal: 1080 } }, + video: { frameRate: { ideal: q.fps }, width: { ideal: q.width }, height: { ideal: q.height } }, audio: true, }); localStreamRef.current = stream; @@ -757,6 +771,17 @@ export default function StreamingTab({ data }: { data: any }) { onChange={e => setStreamPassword(e.target.value)} disabled={isBroadcasting} /> + {isBroadcasting ? (