ai-chatbot/components/canvas/smart-edit/TransitionPreview.tsx

76 lines
2.2 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { type EditPlan, planTimeline, SECOND } from "@/lib/video-canvas/smart-edit/contract";
import { EditPreview } from "./EditPreview";
/** Preview the selected edit boundary with the same timing/compositing used by the editor. */
export function TransitionPreview({
plan,
index,
cover = false,
}: {
plan: EditPlan;
index: number;
cover?: boolean;
}) {
const preview = useMemo(
() => ({
...plan,
subtitles: false,
music: undefined,
audioClips: [],
clips: plan.clips
.slice(index - 1, index + 1)
.map((c) => ({ ...c, gain: 0, audioDetached: true })),
}),
[plan, index],
);
const timeline = useMemo(() => planTimeline(preview), [preview]);
const boundary = timeline.clips[1];
const start = Math.max(0, boundary.transitionStart / SECOND - 0.25);
const end = Math.min(
timeline.duration / SECOND,
(boundary.transitionStart + boundary.overlap) / SECOND + 0.25,
);
const [time, setTime] = useState(start),
[error, setError] = useState(""),
[visible, setVisible] = useState(true);
useEffect(() => {
const change = () => setVisible(!document.hidden);
document.addEventListener("visibilitychange", change);
return () => document.removeEventListener("visibilitychange", change);
}, []);
useEffect(() => {
let request = 0,
epoch = 0;
setError("");
setTime(start);
const tick = (now: number) => {
if (document.hidden) epoch = 0;
else {
if (!epoch) epoch = now;
setTime(start + (((now - epoch) / 1000) % (end - start)));
}
request = requestAnimationFrame(tick);
};
request = requestAnimationFrame(tick);
return () => cancelAnimationFrame(request);
}, [start, end, preview]);
return (
<div className="se-transition-preview" aria-label="当前片段转场动态预览">
<EditPreview
plan={preview}
time={time}
playing={visible}
externalClock
onTime={setTime}
onPlaying={() => {}}
onError={setError}
fit
cover={cover}
/>
{error && <div className="se-transition-preview-error">{error}</div>}
</div>
);
}