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:
parent
9c286ee877
commit
9edc93a0cd
2 changed files with 45 additions and 4 deletions
|
|
@ -43,6 +43,16 @@ function formatElapsed(startedAt: string): string {
|
||||||
return `${m}:${String(s).padStart(2, '0')}`;
|
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 ──
|
// ── Component ──
|
||||||
|
|
||||||
export default function StreamingTab({ data }: { data: any }) {
|
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 [userName, setUserName] = useState(() => localStorage.getItem('streaming_name') || '');
|
||||||
const [streamTitle, setStreamTitle] = useState('Screen Share');
|
const [streamTitle, setStreamTitle] = useState('Screen Share');
|
||||||
const [streamPassword, setStreamPassword] = useState('');
|
const [streamPassword, setStreamPassword] = useState('');
|
||||||
|
const [qualityIdx, setQualityIdx] = useState(2); // Default: 1080p60
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [joinModal, setJoinModal] = useState<JoinModal | null>(null);
|
const [joinModal, setJoinModal] = useState<JoinModal | null>(null);
|
||||||
const [myStreamId, setMyStreamId] = useState<string | 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)
|
// Refs that mirror state (avoid stale closures in WS handler)
|
||||||
const isBroadcastingRef = useRef(false);
|
const isBroadcastingRef = useRef(false);
|
||||||
const viewingRef = useRef<ViewState | null>(null);
|
const viewingRef = useRef<ViewState | null>(null);
|
||||||
|
const qualityRef = useRef<typeof QUALITY_PRESETS[number]>(QUALITY_PRESETS[2]);
|
||||||
useEffect(() => { isBroadcastingRef.current = isBroadcasting; }, [isBroadcasting]);
|
useEffect(() => { isBroadcastingRef.current = isBroadcasting; }, [isBroadcasting]);
|
||||||
useEffect(() => { viewingRef.current = viewing; }, [viewing]);
|
useEffect(() => { viewingRef.current = viewing; }, [viewing]);
|
||||||
|
useEffect(() => { qualityRef.current = QUALITY_PRESETS[qualityIdx]; }, [qualityIdx]);
|
||||||
|
|
||||||
// Notify Electron about streaming status for close-warning
|
// Notify Electron about streaming status for close-warning
|
||||||
useEffect(() => {
|
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() });
|
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');
|
const videoSender = pc.getSenders().find(s => s.track?.kind === 'video');
|
||||||
if (videoSender) {
|
if (videoSender) {
|
||||||
const params = videoSender.getParameters();
|
const params = videoSender.getParameters();
|
||||||
if (!params.encodings || params.encodings.length === 0) params.encodings = [{}];
|
if (!params.encodings || params.encodings.length === 0) params.encodings = [{}];
|
||||||
params.encodings[0].maxFramerate = 60;
|
params.encodings[0].maxFramerate = qualityRef.current.fps;
|
||||||
params.encodings[0].maxBitrate = 8_000_000;
|
params.encodings[0].maxBitrate = qualityRef.current.bitrate;
|
||||||
videoSender.setParameters(params).catch(() => {});
|
videoSender.setParameters(params).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,8 +415,9 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
setStarting(true);
|
setStarting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const q = qualityRef.current;
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
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,
|
audio: true,
|
||||||
});
|
});
|
||||||
localStreamRef.current = stream;
|
localStreamRef.current = stream;
|
||||||
|
|
@ -757,6 +771,17 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
onChange={e => setStreamPassword(e.target.value)}
|
onChange={e => setStreamPassword(e.target.value)}
|
||||||
disabled={isBroadcasting}
|
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 ? (
|
{isBroadcasting ? (
|
||||||
<button className="stream-btn stream-btn-stop" onClick={stopBroadcast}>
|
<button className="stream-btn stream-btn-stop" onClick={stopBroadcast}>
|
||||||
{'\u23F9'} Stream beenden
|
{'\u23F9'} Stream beenden
|
||||||
|
|
|
||||||
|
|
@ -408,6 +408,22 @@
|
||||||
width: 140px;
|
width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stream-select-quality {
|
||||||
|
width: 120px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color var(--transition);
|
||||||
|
}
|
||||||
|
.stream-select-quality:focus { border-color: var(--accent); }
|
||||||
|
.stream-select-quality:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.stream-select-quality option { background: var(--bg-secondary); color: var(--text-normal); }
|
||||||
|
|
||||||
/* ── Lock icon on tile ── */
|
/* ── Lock icon on tile ── */
|
||||||
.stream-tile-lock {
|
.stream-tile-lock {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue