diff --git a/server/src/plugins/notifications/index.ts b/server/src/plugins/notifications/index.ts index bd30a9b..98afae8 100644 --- a/server/src/plugins/notifications/index.ts +++ b/server/src/plugins/notifications/index.ts @@ -224,7 +224,6 @@ const notificationsPlugin: Plugin = { // Save config app.post('/api/notifications/config', requireAdmin, (req, res) => { - console.log(`${NB} Save request body:`, JSON.stringify(req.body)); const { channels } = req.body ?? {}; if (!Array.isArray(channels)) { res.status(400).json({ error: 'channels array erforderlich' }); return; } // Validate each channel config diff --git a/web/src/plugins/streaming/StreamingTab.tsx b/web/src/plugins/streaming/StreamingTab.tsx index 0898b2b..c6ab50a 100644 --- a/web/src/plugins/streaming/StreamingTab.tsx +++ b/web/src/plugins/streaming/StreamingTab.tsx @@ -80,6 +80,7 @@ export default function StreamingTab({ data }: { data: any }) { const remoteVideoRef = useRef(null); const peerConnectionsRef = useRef>(new Map()); const viewerPcRef = useRef(null); + const remoteStreamRef = useRef(null); const pendingCandidatesRef = useRef>(new Map()); const reconnectTimerRef = useRef | null>(null); const reconnectDelayRef = useRef(1000); @@ -161,12 +162,27 @@ export default function StreamingTab({ data }: { data: any }) { } }, []); + // ── Attach remote stream to video element (handles autoplay) ── + const attachRemoteStream = useCallback((videoEl: HTMLVideoElement, stream: MediaStream) => { + videoEl.srcObject = stream; + // Explicit play() to handle autoplay restrictions (e.g. fresh tab from Discord link) + const playPromise = videoEl.play(); + if (playPromise) { + playPromise.catch(() => { + // Autoplay blocked (no user interaction yet) → mute and retry + videoEl.muted = true; + videoEl.play().catch(() => {}); + }); + } + }, []); + // ── Viewer cleanup (only viewer PC, keeps broadcaster intact) ── const cleanupViewer = useCallback(() => { if (viewerPcRef.current) { viewerPcRef.current.close(); viewerPcRef.current = null; } + remoteStreamRef.current = null; if (remoteVideoRef.current) remoteVideoRef.current.srcObject = null; // Only clear viewer-related pending candidates (not broadcaster ones) }, []); @@ -275,7 +291,15 @@ export default function StreamingTab({ data }: { data: any }) { viewerPcRef.current = pc; pc.ontrack = (ev) => { - if (remoteVideoRef.current && ev.streams[0]) remoteVideoRef.current.srcObject = ev.streams[0]; + const stream = ev.streams[0]; + if (!stream) return; + // Store stream in ref so it survives even if video element isn't mounted yet + remoteStreamRef.current = stream; + const videoEl = remoteVideoRef.current; + if (videoEl) { + attachRemoteStream(videoEl, stream); + } + // else: useEffect below will attach once video element is ready setViewing(prev => prev ? { ...prev, phase: 'connected' } : prev); }; @@ -512,6 +536,14 @@ export default function StreamingTab({ data }: { data: any }) { }; }, []); + // ── Attach remote stream when video element becomes available ── + // Handles the race condition where ontrack fires before React renders the