// frontend/src/components/ui/FinishedVideoPreview.tsx 'use client' import { useMemo, useState, type SyntheticEvent } from 'react' import type { RecordJob } from '../../types' import HoverPopover from './HoverPopover' type Props = { job: RecordJob getFileName: (path: string) => string // 🔹 optional: bereits bekannte Dauer (Sekunden) durationSeconds?: number // 🔹 Callback nach oben, wenn wir die Dauer ermittelt haben onDuration?: (job: RecordJob, seconds: number) => void thumbTick?: number } export default function FinishedVideoPreview({ job, getFileName, durationSeconds, onDuration, thumbTick }: Props) { const file = getFileName(job.output || '') const [thumbOk, setThumbOk] = useState(true) const [metaLoaded, setMetaLoaded] = useState(false) // id für /api/record/preview: Dateiname ohne Extension const previewId = useMemo(() => { if (!file) return '' const dot = file.lastIndexOf('.') return dot > 0 ? file.slice(0, dot) : file }, [file]) const videoSrc = useMemo( () => (file ? `/api/record/video?file=${encodeURIComponent(file)}` : ''), [file] ) const hasDuration = typeof durationSeconds === 'number' && Number.isFinite(durationSeconds) && durationSeconds > 0 const tick = thumbTick ?? 0 // Zeitposition im Video: alle 3s ein Schritt, modulo Videolänge const thumbTimeSec = useMemo(() => { if (!durationSeconds || !Number.isFinite(durationSeconds) || durationSeconds <= 0) { // Keine Dauer bekannt → einfach bei 0s (erster Frame) bleiben return 0 } const step = 3 // Sekunden pro Schritt const steps = Math.max(0, Math.floor(tick)) // kleine Reserve, damit wir nicht exakt auf das letzte Frame springen const total = Math.max(durationSeconds - 0.1, step) return (steps * step) % total }, [durationSeconds, tick]) // Thumbnail (immer mit t=..., auch wenn t=0 → erster Frame) const thumbSrc = useMemo(() => { if (!previewId) return '' const params: string[] = [] // ⬅️ immer Zeitposition mitgeben, auch bei 0 params.push(`t=${encodeURIComponent(thumbTimeSec.toFixed(2))}`) // Versionierung für den Browser-Cache / Animation if (typeof thumbTick === 'number') { params.push(`v=${encodeURIComponent(String(thumbTick))}`) } const qs = params.length ? `&${params.join('&')}` : '' return `/api/record/preview?id=${encodeURIComponent(previewId)}${qs}` }, [previewId, thumbTimeSec, thumbTick]) const handleLoadedMetadata = (e: SyntheticEvent) => { setMetaLoaded(true) if (!onDuration) return const secs = e.currentTarget.duration if (Number.isFinite(secs) && secs > 0) { onDuration(job, secs) } } if (!videoSrc) { return (
) } return ( open && (
) } > {/* 🔹 Inline nur Thumbnail / Platzhalter */}
{thumbSrc && thumbOk ? ( {file} setThumbOk(false)} /> ) : (
)} {/* 🔍 Unsichtbares Video nur zum Metadaten-Laden (Dauer), wird genau EINMAL pro Datei geladen */} {onDuration && !hasDuration && !metaLoaded && (
) }