Streaming: Bildschirmauswahl-Picker, Passwort optional, Windows-Toast-Notifications

- Electron: setDisplayMediaRequestHandler zeigt jetzt immer einen modalen Picker
  mit Thumbnails statt automatisch die letzte Quelle zu verwenden
- Passwort bei Streams ist jetzt optional (Server + Frontend)
- Streams ohne Passwort: direkter Beitritt ohne Modal
- hasPassword wird korrekt im stream_available Event übertragen
- Windows Toast-Notifications via Electron Notification API für
  "Stream gestartet" und "Neuer Stream" Events
- Browser-Variante nutzt weiterhin Web Notification API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-08 00:16:42 +01:00
parent fa964318f2
commit e146a28416
4 changed files with 123 additions and 21 deletions

View file

@ -162,6 +162,9 @@ export default function StreamingTab({ data }: { data: any }) {
setIsBroadcasting(true);
isBroadcastingRef.current = true;
setStarting(false);
if ((window as any).electronAPI?.showNotification) {
(window as any).electronAPI.showNotification('Stream gestartet', 'Dein Stream ist jetzt live!');
}
break;
case 'stream_available':
@ -173,15 +176,15 @@ export default function StreamingTab({ data }: { data: any }) {
title: msg.title,
startedAt: new Date().toISOString(),
viewerCount: 0,
hasPassword: true,
hasPassword: !!msg.hasPassword,
}];
});
// Toast notification for new stream
if (Notification.permission === 'granted') {
new Notification('Neuer Stream', {
body: `${msg.broadcasterName} streamt: ${msg.title}`,
icon: '/assets/icon.png',
});
const notifBody = `${msg.broadcasterName} streamt: ${msg.title}`;
if ((window as any).electronAPI?.showNotification) {
(window as any).electronAPI.showNotification('Neuer Stream', notifBody);
} else if (Notification.permission === 'granted') {
new Notification('Neuer Stream', { body: notifBody, icon: '/assets/icon.png' });
}
break;
@ -343,7 +346,6 @@ export default function StreamingTab({ data }: { data: any }) {
// ── Start broadcasting ──
const startBroadcast = useCallback(async () => {
if (!userName.trim()) { setError('Bitte gib einen Namen ein.'); return; }
if (!streamPassword.trim()) { setError('Passwort ist Pflicht.'); return; }
if (!navigator.mediaDevices?.getDisplayMedia) {
setError('Dein Browser unterstützt keine Bildschirmfreigabe.');
return;
@ -365,7 +367,7 @@ export default function StreamingTab({ data }: { data: any }) {
connectWs();
const waitForWs = () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsSend({ type: 'start_broadcast', name: userName.trim(), title: streamTitle.trim() || 'Screen Share', password: streamPassword.trim() });
wsSend({ type: 'start_broadcast', name: userName.trim(), title: streamTitle.trim() || 'Screen Share', password: streamPassword.trim() || undefined });
} else { setTimeout(waitForWs, 100); }
};
waitForWs();
@ -393,9 +395,25 @@ export default function StreamingTab({ data }: { data: any }) {
}, [wsSend]);
// ── Join as viewer ──
const joinStreamDirectly = useCallback((streamId: string) => {
setError(null);
setViewing({ streamId, phase: 'connecting' });
connectWs();
const waitForWs = () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsSend({ type: 'join_viewer', name: userName.trim() || 'Viewer', streamId });
} else { setTimeout(waitForWs, 100); }
};
waitForWs();
}, [userName, connectWs, wsSend]);
const openJoinModal = useCallback((s: StreamInfo) => {
setJoinModal({ streamId: s.id, streamTitle: s.title, broadcasterName: s.broadcasterName, password: '', error: null });
}, []);
if (s.hasPassword) {
setJoinModal({ streamId: s.id, streamTitle: s.title, broadcasterName: s.broadcasterName, password: '', error: null });
} else {
joinStreamDirectly(s.id);
}
}, [joinStreamDirectly]);
const submitJoinModal = useCallback(() => {
if (!joinModal) return;
@ -587,7 +605,7 @@ export default function StreamingTab({ data }: { data: any }) {
<input
className="stream-input stream-input-password"
type="password"
placeholder="Passwort"
placeholder="Passwort (optional)"
value={streamPassword}
onChange={e => setStreamPassword(e.target.value)}
disabled={isBroadcasting}