diff --git a/electron/main.js b/electron/main.js
index 632f71b..ef0829d 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -1,4 +1,4 @@
-const { app, BrowserWindow, session, shell, desktopCapturer, autoUpdater, dialog, ipcMain } = require('electron');
+const { app, BrowserWindow, session, shell, desktopCapturer, autoUpdater, dialog, ipcMain, Notification } = require('electron');
const path = require('path');
const { setupAdBlocker } = require('./ad-blocker');
@@ -92,10 +92,89 @@ function createWindow() {
// Setup ad blocker BEFORE loading URL
setupAdBlocker(session.defaultSession);
- // Enable screen capture (getDisplayMedia) in Electron
- session.defaultSession.setDisplayMediaRequestHandler((_request, callback) => {
- desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => {
- callback({ video: sources[0], audio: 'loopback' });
+ // Enable screen capture (getDisplayMedia) in Electron — always show picker
+ session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
+ const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: 320, height: 180 } });
+ if (sources.length === 0) {
+ callback({});
+ return;
+ }
+
+ // Build a picker window so the user always chooses
+ const picker = new BrowserWindow({
+ width: 680,
+ height: 520,
+ parent: mainWindow,
+ modal: true,
+ resizable: false,
+ minimizable: false,
+ maximizable: false,
+ title: 'Bildschirm / Fenster ausw\u00e4hlen',
+ backgroundColor: '#1a1b1e',
+ autoHideMenuBar: true,
+ webPreferences: { contextIsolation: true, nodeIntegration: false },
+ });
+
+ const sourceData = sources.map(s => ({
+ id: s.id,
+ name: s.name,
+ thumbnail: s.thumbnail.toDataURL(),
+ }));
+
+ const html = `
+
+Bildschirm oder Fenster ausw\u00e4hlen
+
+
+`;
+
+ picker.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
+
+ let resolved = false;
+ picker.webContents.on('page-title-updated', (_ev, title) => {
+ if (!title.startsWith('PICK:')) return;
+ resolved = true;
+ const selectedId = title.slice(5);
+ picker.close();
+ if (!selectedId) {
+ callback({});
+ return;
+ }
+ const chosen = sources.find(s => s.id === selectedId);
+ if (chosen) {
+ callback({ video: chosen, audio: 'loopback' });
+ } else {
+ callback({});
+ }
+ });
+
+ picker.on('closed', () => {
+ if (!resolved) callback({});
});
});
@@ -126,6 +205,14 @@ function createWindow() {
let isStreaming = false;
ipcMain.on('streaming-status', (_event, active) => { isStreaming = active; });
+ // Windows toast notifications from renderer
+ ipcMain.on('show-notification', (_event, title, body) => {
+ if (Notification.isSupported()) {
+ const notif = new Notification({ title, body, icon: path.join(__dirname, 'assets', 'icon.png') });
+ notif.show();
+ }
+ });
+
// Warn before closing if a stream is active
mainWindow.on('close', (event) => {
if (!isStreaming) return;
diff --git a/electron/preload.js b/electron/preload.js
index a53910d..ff2fc1b 100644
--- a/electron/preload.js
+++ b/electron/preload.js
@@ -11,4 +11,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
checkForUpdates: () => ipcRenderer.send('check-for-updates'),
installUpdate: () => ipcRenderer.send('install-update'),
setStreaming: (active) => ipcRenderer.send('streaming-status', active),
+ showNotification: (title, body) => ipcRenderer.send('show-notification', title, body),
});
diff --git a/server/src/plugins/streaming/index.ts b/server/src/plugins/streaming/index.ts
index 4d26b9c..20b1b20 100644
--- a/server/src/plugins/streaming/index.ts
+++ b/server/src/plugins/streaming/index.ts
@@ -103,10 +103,6 @@ function handleSignalingMessage(client: WsClient, msg: any): void {
return;
}
const password = String(msg.password || '').trim();
- if (!password) {
- sendTo(client, { type: 'error', code: 'PASSWORD_REQUIRED', message: 'Passwort ist Pflicht.' });
- return;
- }
const streamId = crypto.randomUUID();
const name = String(msg.name || 'Anon').slice(0, 32);
const title = String(msg.title || 'Screen Share').slice(0, 64);
@@ -130,7 +126,7 @@ function handleSignalingMessage(client: WsClient, msg: any): void {
// Notify all other clients
for (const c of wsClients.values()) {
if (c.id !== client.id) {
- sendTo(c, { type: 'stream_available', streamId, broadcasterName: name, title });
+ sendTo(c, { type: 'stream_available', streamId, broadcasterName: name, title, hasPassword: password.length > 0 });
}
}
console.log(`[Streaming] ${name} started "${title}" (${streamId.slice(0, 8)})`);
diff --git a/web/src/plugins/streaming/StreamingTab.tsx b/web/src/plugins/streaming/StreamingTab.tsx
index 56493ff..555dd21 100644
--- a/web/src/plugins/streaming/StreamingTab.tsx
+++ b/web/src/plugins/streaming/StreamingTab.tsx
@@ -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 }) {
setStreamPassword(e.target.value)}
disabled={isBroadcasting}