"use client"; import { useEffect, useRef } from "react"; import { compiledSubtitles, planTimeline, SECOND, type EditPlan, } from "@/lib/video-canvas/smart-edit/contract"; export function EditPreview({ plan, time, playing, onTime, onPlaying, onError, }: { plan: EditPlan; time: number; playing: boolean; onTime: (t: number) => void; onPlaying: (v: boolean) => void; onError: (s: string) => void; }) { const videos = useRef(new Map()), music = useRef(null), latest = useRef({ time, plan, onTime, onPlaying }); latest.current = { time, plan, onTime, onPlaying }; const timeline = planTimeline(plan), current = timeline.clips.filter((c) => time * SECOND >= c.start && time * SECOND < c.end), subs = compiledSubtitles(plan).filter((s) => time * SECOND >= s.start && time * SECOND < s.end); useEffect(() => { if (!playing) return; let frame = 0, last = 0; const tick = (now: number) => { const state = latest.current; if (last) { const next = Math.min( planTimeline(state.plan).duration / SECOND, state.time + (now - last) / 1000, ); state.onTime(next); if (next >= planTimeline(state.plan).duration / SECOND) { state.onPlaying(false); return; } } last = now; frame = requestAnimationFrame(tick); }; const hide = () => { if (document.hidden) latest.current.onPlaying(false); }; frame = requestAnimationFrame(tick); document.addEventListener("visibilitychange", hide); return () => { cancelAnimationFrame(frame); document.removeEventListener("visibilitychange", hide); }; }, [playing]); useEffect(() => { for (const clip of timeline.clips) { const video = videos.current.get(clip.id); if (!video) continue; const active = current.some((c) => c.id === clip.id); if (!active) { video.pause(); continue; } const target = clip.sourceIn / SECOND + (time - clip.start / SECOND) * clip.speed; if (Number.isFinite(target) && Math.abs(video.currentTime - target) > 0.1) video.currentTime = target; video.playbackRate = clip.speed; const index = timeline.clips.findIndex((c) => c.id === clip.id), next = timeline.clips[index + 1]; let gain = 1; if (clip.overlap && time * SECOND < clip.start + clip.overlap) gain = (time * SECOND - clip.start) / clip.overlap; if (next?.overlap && time * SECOND > next.start) gain = Math.min(gain, (clip.end - time * SECOND) / next.overlap); video.volume = Math.min(1, Math.max(0, clip.gain * gain)); if (playing) void video.play().catch(() => { onPlaying(false); onError("预览播放失败,请检查素材链接或点击播放重试"); }); else video.pause(); } const audio = music.current; if (audio && plan.music) { const target = time + plan.music.start / SECOND; if (Number.isFinite(audio.duration) && target >= audio.duration) { audio.pause(); return; } if (Math.abs(audio.currentTime - target) > 0.15) audio.currentTime = target; audio.volume = Math.min(1, plan.music.gain); if (playing) void audio.play().catch(() => { onPlaying(false); onError("背景音乐无法播放"); }); else audio.pause(); } }, [time, playing, plan]); return (
{timeline.clips.map((clip) => { const asset = plan.assets.find((a) => a.id === clip.assetId)!; const active = current.some((c) => c.id === clip.id), incoming = clip.overlap && time * SECOND < clip.start + clip.overlap; return (
); }