Streaming: Qualitäts-Presets (720p30 bis 4K60)

Dropdown im Topbar zum Wählen der Stream-Qualität vor dem Start:
- 720p30 (2.5 Mbit/s), 1080p30 (5), 1080p60 (8), 1440p60 (14), 4K60 (25)
- Steuert getDisplayMedia-Constraints + WebRTC maxBitrate/maxFramerate
- Default: 1080p60 (wie bisher)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-08 22:50:59 +01:00
parent 9c286ee877
commit 9edc93a0cd
2 changed files with 45 additions and 4 deletions

View file

@ -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<string | null>(null);
const [joinModal, setJoinModal] = useState<JoinModal | null>(null);
const [myStreamId, setMyStreamId] = useState<string | null>(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<ViewState | null>(null);
const qualityRef = useRef<typeof QUALITY_PRESETS[number]>(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}
/>
<select
className="stream-select-quality"
value={qualityIdx}
onChange={e => setQualityIdx(Number(e.target.value))}
disabled={isBroadcasting}
title="Stream-Qualität"
>
{QUALITY_PRESETS.map((p, i) => (
<option key={p.label} value={i}>{p.label}</option>
))}
</select>
{isBroadcasting ? (
<button className="stream-btn stream-btn-stop" onClick={stopBroadcast}>
{'\u23F9'} Stream beenden