// frontend/src/components/ui/LiveVideo.tsx 'use client' import { useEffect, useRef, useState } from 'react' import { applyInlineVideoPolicy, DEFAULT_INLINE_MUTED } from './videoPolicy' export default function LiveVideo({ src, muted = DEFAULT_INLINE_MUTED, volume = 1, className, roomStatus, onVolumeChange, onLoadingStart, onReady, }: { src: string muted?: boolean volume?: number className?: string roomStatus?: string onVolumeChange?: (volume: number, muted: boolean) => void onLoadingStart?: () => void onReady?: () => void }) { const ref = useRef(null) const [broken, setBroken] = useState(false) const [brokenReason, setBrokenReason] = useState<'private' | 'hidden' | 'away' | 'offline' | 'unknown' | null>(null) const onLoadingStartRef = useRef(onLoadingStart) const onReadyRef = useRef(onReady) useEffect(() => { onLoadingStartRef.current = onLoadingStart }, [onLoadingStart]) useEffect(() => { onReadyRef.current = onReady }, [onReady]) const normalizeBrokenReason = ( status?: string ): 'private' | 'hidden' | 'away' | 'offline' | 'unknown' => { const s = String(status ?? '').trim().toLowerCase() if (s === 'private') return 'private' if (s === 'hidden') return 'hidden' if (s === 'away') return 'away' if (s === 'offline') return 'offline' return 'unknown' } const brokenMessage = ( reason: 'private' | 'hidden' | 'away' | 'offline' | 'unknown' | null ): string => { switch (reason) { case 'hidden': return 'Cam is hidden' case 'away': return 'Model is away' case 'private': return 'Private show in progress.' case 'offline': return 'Model is offline' case 'unknown': return 'Live video unavailable' default: return 'Live video unavailable' } } useEffect(() => { let watchdogTimer: number | null = null let readySent = false let stalledRetries = 0 let attemptStartedAt = Date.now() let lastProgressAt = Date.now() const video = ref.current if (!video) return setBroken(false) setBrokenReason(null) applyInlineVideoPolicy(video, { muted }) video.volume = Math.max(0, Math.min(1, volume)) video.muted = muted || volume <= 0 const hardReset = () => { try { video.pause() video.removeAttribute('src') video.load() } catch {} } const markBroken = () => { const reason = normalizeBrokenReason(roomStatus) setBroken(true) setBrokenReason(reason) } const beginLoad = () => { readySent = false attemptStartedAt = Date.now() lastProgressAt = Date.now() onLoadingStartRef.current?.() hardReset() video.src = src video.load() const playPromise = video.play() if (playPromise && typeof playPromise.catch === 'function') { playPromise.catch(() => { // Nicht sofort abbrechen – Watchdog entscheidet nach Timeout. }) } } hardReset() if (!src) { setBroken(false) setBrokenReason(null) return } const markReady = () => { lastProgressAt = Date.now() if (readySent) return readySent = true onReadyRef.current?.() } const onTime = () => { lastProgressAt = Date.now() markReady() } const onLoadedData = () => { lastProgressAt = Date.now() markReady() } const onCanPlay = () => { lastProgressAt = Date.now() markReady() } const onPlaying = () => { lastProgressAt = Date.now() markReady() } const onError = () => { markBroken() } video.addEventListener('timeupdate', onTime) video.addEventListener('loadeddata', onLoadedData) video.addEventListener('canplay', onCanPlay) video.addEventListener('playing', onPlaying) video.addEventListener('error', onError) beginLoad() watchdogTimer = window.setInterval(() => { const now = Date.now() // Stream wurde nie ready -> nicht ewig Spinner zeigen if (!readySent && now - attemptStartedAt > 12_000) { if (stalledRetries < 1) { stalledRetries += 1 beginLoad() return } markBroken() return } // Stream war ready, hängt aber später if (readySent && !video.paused && now - lastProgressAt > 12_000) { if (stalledRetries < 1) { stalledRetries += 1 beginLoad() return } markBroken() } }, 1_000) return () => { if (watchdogTimer) window.clearInterval(watchdogTimer) watchdogTimer = null video.removeEventListener('timeupdate', onTime) video.removeEventListener('loadeddata', onLoadedData) video.removeEventListener('canplay', onCanPlay) video.removeEventListener('playing', onPlaying) video.removeEventListener('error', onError) hardReset() } }, [src, roomStatus]) useEffect(() => { const video = ref.current if (!video) return const safeVolume = Math.max(0, Math.min(1, volume)) video.volume = safeVolume video.muted = muted || safeVolume <= 0 }, [volume, muted]) useEffect(() => { const video = ref.current if (!video || !onVolumeChange) return const emit = () => { onVolumeChange(video.volume, video.muted) } video.addEventListener('volumechange', emit) return () => video.removeEventListener('volumechange', emit) }, [onVolumeChange]) if (broken) { return (
{brokenMessage(brokenReason)}
) } return (