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

@ -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 = `<!DOCTYPE html>
<html><head><meta charset="utf-8"><style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#1a1b1e;color:#e0e0e0;font-family:system-ui,sans-serif;padding:16px;overflow-y:auto}
h2{font-size:16px;margin-bottom:12px;color:#ccc}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
.item{background:#2a2a3e;border:2px solid transparent;border-radius:10px;cursor:pointer;overflow:hidden;transition:border-color .15s,transform .15s}
.item:hover{border-color:#7c5cff;transform:scale(1.03)}
.item img{width:100%;height:120px;object-fit:cover;display:block;background:#111}
.item .label{padding:8px 10px;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cancel-row{text-align:center;margin-top:14px}
.cancel-btn{background:#3a3a4e;color:#e0e0e0;border:none;padding:8px 24px;border-radius:6px;cursor:pointer;font-size:14px}
.cancel-btn:hover{background:#4a4a5e}
</style></head><body>
<h2>Bildschirm oder Fenster ausw\u00e4hlen</h2>
<div class="grid" id="grid"></div>
<div class="cancel-row"><button class="cancel-btn" onclick="choose(null)">Abbrechen</button></div>
<script>
const sources = ${JSON.stringify(sourceData)};
const grid = document.getElementById('grid');
sources.forEach(s => {
const div = document.createElement('div');
div.className = 'item';
div.innerHTML = '<img src="' + s.thumbnail + '"><div class="label">' + s.name.replace(/</g,'&lt;') + '</div>';
div.onclick = () => choose(s.id);
grid.appendChild(div);
});
function choose(id) {
document.title = 'PICK:' + (id || '');
}
</script></body></html>`;
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;