2025-08-29 09:39:16 +08:00

690 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X } from 'lucide-react';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
import { mockScriptData } from '@/components/script-renderer/mock';
import { Skeleton } from '@/components/ui/skeleton';
import { TaskObject } from '@/api/DTO/movieEdit';
import { Button, Tooltip } from 'antd';
import { Video } from 'lucide-react';
interface MediaViewerProps {
taskObject: TaskObject;
scriptData: any;
currentSketchIndex: number;
isVideoPlaying: boolean;
onEditModalOpen: (tab: string) => void;
onToggleVideoPlay: () => void;
setIsPauseWorkFlow: (isPause: boolean) => void;
setAnyAttribute: any;
isPauseWorkFlow: boolean;
applyScript: any;
mode: string;
onOpenChat?: () => void;
setVideoPreview?: (url: string, id: string) => void;
}
export const MediaViewer = React.memo(function MediaViewer({
taskObject,
scriptData,
currentSketchIndex,
isVideoPlaying,
onEditModalOpen,
onToggleVideoPlay,
setIsPauseWorkFlow,
setAnyAttribute,
isPauseWorkFlow,
applyScript,
mode,
onOpenChat,
setVideoPreview
}: MediaViewerProps) {
const mainVideoRef = useRef<HTMLVideoElement>(null);
const finalVideoRef = useRef<HTMLVideoElement>(null);
// 音量控制状态
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(0.8);
// 最终视频控制状态
const [isFinalVideoPlaying, setIsFinalVideoPlaying] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
const [finalVideoReady, setFinalVideoReady] = useState(false);
const [userHasInteracted, setUserHasInteracted] = useState(false);
// 音量控制函数
const toggleMute = () => {
setUserHasInteracted(true);
setIsMuted(!isMuted);
if (mainVideoRef.current) {
mainVideoRef.current.muted = !isMuted;
}
if (finalVideoRef.current) {
finalVideoRef.current.muted = !isMuted;
}
};
const handleVolumeChange = (newVolume: number) => {
setUserHasInteracted(true);
setVolume(newVolume);
if (mainVideoRef.current) {
mainVideoRef.current.volume = newVolume;
}
if (finalVideoRef.current) {
finalVideoRef.current.volume = newVolume;
}
};
// 应用音量设置到视频元素
const applyVolumeSettings = (videoElement: HTMLVideoElement) => {
if (videoElement) {
videoElement.volume = volume;
videoElement.muted = isMuted;
}
};
useEffect(() => {
if (finalVideoRef.current && finalVideoReady) {
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('最终视频自动播放被阻止:', error);
// 如果自动播放被阻止,将状态设置为暂停
setIsFinalVideoPlaying(false);
});
} else {
finalVideoRef.current.pause();
}
}
}, [isFinalVideoPlaying, finalVideoReady]);
// 最终视频播放控制
const toggleFinalVideoPlay = () => {
setUserHasInteracted(true);
setIsFinalVideoPlaying(!isFinalVideoPlaying);
};
// 处理最终视频加载完成
const handleFinalVideoLoaded = () => {
if (finalVideoRef.current) {
setFinalVideoReady(true);
applyVolumeSettings(finalVideoRef.current);
// 如果当前状态是应该播放的,尝试播放
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('最终视频自动播放被阻止:', error);
setIsFinalVideoPlaying(false);
});
}
}
};
// 处理视频点击 - 首次交互时尝试播放
const handleVideoClick = () => {
if (!userHasInteracted && finalVideoRef.current && finalVideoReady) {
setUserHasInteracted(true);
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
});
}
}
};
// 使用 useMemo 缓存最终视频元素,避免重复创建和请求
const memoizedFinalVideoElement = useMemo(() => {
console.log('final', taskObject.final);
if (!taskObject.final?.url) return null;
return (
<video
ref={finalVideoRef}
className="w-full h-full object-cover rounded-lg"
src={taskObject.final.url}
autoPlay={isFinalVideoPlaying}
loop
playsInline
preload="metadata"
poster={`${taskObject.final.url}?vframe/jpg/offset/1`}
onLoadedData={handleFinalVideoLoaded}
onPlay={() => setIsFinalVideoPlaying(true)}
onPause={() => setIsFinalVideoPlaying(false)}
onClick={handleVideoClick}
/>
);
}, [taskObject.final?.url, isFinalVideoPlaying, handleFinalVideoLoaded, handleVideoClick]);
// 包装编辑按钮点击事件
const handleEditClick = (tab: string, from?: string) => {
if (from === 'final') {
// 暂停视频播放
finalVideoRef.current?.pause();
}
// TODO 点击没有任何事件效果,页面没变化
setUserHasInteracted(true);
onEditModalOpen(tab);
};
// 全屏控制
const toggleFullscreen = () => {
setUserHasInteracted(true);
if (!document.fullscreenElement) {
// 进入全屏
if (finalVideoRef.current) {
finalVideoRef.current.requestFullscreen?.() ||
(finalVideoRef.current as any).webkitRequestFullscreen?.() ||
(finalVideoRef.current as any).msRequestFullscreen?.();
setIsFullscreen(true);
}
} else {
// 退出全屏
document.exitFullscreen?.() ||
(document as any).webkitExitFullscreen?.() ||
(document as any).msExitFullscreen?.();
setIsFullscreen(false);
}
};
// 视频播放控制
useEffect(() => {
if (mainVideoRef.current) {
applyVolumeSettings(mainVideoRef.current);
if (isVideoPlaying) {
mainVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
});
} else {
mainVideoRef.current.pause();
}
}
}, [isVideoPlaying]);
// 当切换视频时重置视频播放
useEffect(() => {
if (mainVideoRef.current) {
applyVolumeSettings(mainVideoRef.current);
mainVideoRef.current.currentTime = 0;
if (isVideoPlaying) {
mainVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
});
}
}
}, [currentSketchIndex]);
// 音量设置变化时应用到所有视频
useEffect(() => {
if (mainVideoRef.current) {
applyVolumeSettings(mainVideoRef.current);
}
if (finalVideoRef.current) {
applyVolumeSettings(finalVideoRef.current);
}
}, [volume, isMuted]);
// 监听全屏状态变化
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
document.removeEventListener('msfullscreenchange', handleFullscreenChange);
};
}, []);
// 组件卸载时清理视频状态
useEffect(() => {
return () => {
// 清理最终视频状态
setFinalVideoReady(false);
if (finalVideoRef.current) {
finalVideoRef.current.pause();
}
};
}, []);
// 渲染音量控制组件
const renderVolumeControls = () => (
<div className="flex items-center gap-2">
{/* 静音按钮 */}
<GlassIconButton
icon={isMuted ? VolumeX : Volume2}
onClick={toggleMute}
size="sm"
/>
{/* 音量滑块 - 一直显示 */}
<div className="flex items-center gap-2">
<div className="relative">
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:shadow-lg
[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:cursor-pointer
[&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:shadow-lg"
style={{
background: `linear-gradient(to right, white 0%, white ${volume * 100}%, rgba(255,255,255,0.2) ${volume * 100}%, rgba(255,255,255,0.2) 100%)`
}}
/>
</div>
<span className="text-xs text-white/70 w-8 text-center">
{Math.round(volume * 100)}%
</span>
</div>
</div>
);
// 渲染最终成片
const renderFinalVideo = () => {
// 使用真实的final数据如果没有则使用默认值
return (
<div
className="relative w-full h-full rounded-lg overflow-hidden"
key={`render-video-${taskObject.final.url}`}
>
<div className="relative w-full h-full group">
{/* 背景模糊的视频 */}
<motion.div
className="absolute inset-0 overflow-hidden"
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
animate={{ filter: "blur(20px)", scale: 1.1, opacity: 0.5 }}
transition={{ duration: 0.8, ease: "easeInOut" }}
>
<video
className="w-full h-full rounded-lg object-cover object-center"
src={taskObject.final.url}
loop
playsInline
muted
/>
</motion.div>
{/* 最终成片视频 */}
<motion.div
initial={{ clipPath: "inset(0 50% 0 50%)", filter: "blur(10px)" }}
animate={{ clipPath: "inset(0 0% 0 0%)", filter: "blur(0px)" }}
transition={{
clipPath: { duration: 1.2, ease: [0.43, 0.13, 0.23, 0.96] },
filter: { duration: 0.6, delay: 0.3 }
}}
className="relative z-10 w-full h-full"
>
{memoizedFinalVideoElement}
</motion.div>
{/* 操作按钮组 */}
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 z-10 gap-2 hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
onClick={() => handleEditClick('3', 'final')}
/>
</motion.div>
</AnimatePresence> */}
{/* 视频信息浮层 */}
<motion.div
className="absolute bottom-0 left-0 right-0 z-10 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1, duration: 0.6 }}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<motion.div
className="w-2 h-2 rounded-full bg-emerald-500"
animate={{
scale: [1, 1.2, 1],
opacity: [1, 0.6, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<span className="text-sm font-medium text-white/90">{taskObject.final.note === 'final' ? 'Final product' : 'Trailer Video'}</span>
</div>
</div>
</motion.div>
{/* 底部控制区域 */}
<motion.div
className="absolute bottom-16 left-4 z-10 flex items-center gap-3"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 1, duration: 0.6 }}
>
{/* 播放/暂停按钮 */}
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="relative"
>
<GlassIconButton
icon={isFinalVideoPlaying ? Pause : Play}
onClick={toggleFinalVideoPlay}
size="sm"
/>
</motion.div>
{/* 音量控制 */}
{renderVolumeControls()}
{/* 全屏按钮 */}
<GlassIconButton
icon={isFullscreen ? Minimize : Maximize}
onClick={toggleFullscreen}
size="sm"
/>
</motion.div>
{/* 完成标记 */}
<motion.div
className="absolute top-4 right-4 px-3 py-1.5 rounded-full bg-emerald-500/20 backdrop-blur-sm
border border-emerald-500/30 text-emerald-400 text-sm font-medium"
initial={{ opacity: 0, scale: 0.8, x: 20 }}
animate={{ opacity: 1, scale: 1, x: 0 }}
transition={{ delay: 1.2, duration: 0.6 }}
>
Task completed
</motion.div>
</div>
</div>
);
};
// 渲染视频内容
const renderVideoContent = () => {
const urls = taskObject.videos.data[currentSketchIndex].urls ? taskObject.videos.data[currentSketchIndex].urls.join(',') : '';
return (
<div
className="relative w-full h-full rounded-lg group"
key={`render-video-${urls}`}
>
{/* 背景模糊的图片 */}
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
<div className="absolute inset-0 overflow-hidden z-20">
{/* 生成中 */}
{taskObject.videos.data[currentSketchIndex].video_status === 0 && (
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
<div className="text-blue-500 text-2xl font-bold flex items-center gap-2">
<Loader2 className="w-10 h-10 animate-spin" />
<span>Generating...</span>
</div>
</div>
)}
{/* 生成失败 */}
{taskObject.videos.data[currentSketchIndex].video_status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
<div className="text-red-500 text-2xl font-bold flex items-center gap-2">
<X className="w-10 h-10" />
<span>Failed</span>
</div>
</div>
)}
</div>
)}
{/* 视频 多个 取第一个 */}
{ taskObject.videos.data[currentSketchIndex].urls && (
<>
<motion.div
initial={{ clipPath: "inset(0 100% 0 0)" }}
animate={{ clipPath: "inset(0 0% 0 0)" }}
transition={{ duration: 0.8, ease: [0.43, 0.13, 0.23, 0.96] }}
className="relative z-10 w-full h-full"
>
<video
ref={mainVideoRef}
key={taskObject.videos.data[currentSketchIndex].urls[0]}
className="w-full h-full rounded-lg object-cover object-center relative z-10"
src={taskObject.videos.data[currentSketchIndex].urls[0]}
autoPlay={isVideoPlaying}
loop={true}
playsInline
onLoadedData={() => applyVolumeSettings(mainVideoRef.current!)}
onEnded={() => {
if (isVideoPlaying) {
// 自动切换到下一个视频的逻辑在父组件处理
}
}}
/>
</motion.div>
{/* 添加到chat去编辑 按钮 */}
<Tooltip title="Add to chat to edit">
<Button
className="absolute top-4 left-4 z-[21] bg-white/10 backdrop-blur-sm border border-white/20 text-white"
onClick={() => {
const currentVideo = taskObject.videos.data[currentSketchIndex];
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0 && setVideoPreview) {
setVideoPreview(currentVideo.urls[0], currentVideo.video_id);
if (onOpenChat) onOpenChat();
}
}}
>
<Video className="w-4 h-4" />
<span className="text-xs">Chat to edit</span>
</Button>
</Tooltip>
</>
)}
{/* 操作按钮组 */}
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 gap-2 z-[21] hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
onClick={() => handleEditClick('3')}
/>
</motion.div>
</AnimatePresence> */}
{/* 底部控制区域 */}
{ taskObject.videos.data[currentSketchIndex].video_status === 1 && (
<AnimatePresence>
<motion.div
className="absolute bottom-4 left-4 z-[21] flex items-center gap-3"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
{/* 播放按钮 */}
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="relative"
>
<GlassIconButton
icon={isVideoPlaying ? Pause : Play}
onClick={onToggleVideoPlay}
size="sm"
/>
</motion.div>
{/* 音量控制 */}
{renderVolumeControls()}
</motion.div>
</AnimatePresence>
)}
</div>
);
};
// 渲染分镜草图
const renderSketchContent = (currentSketch: any) => {
if (!currentSketch) return null;
return (
<div
className="relative w-full h-full rounded-lg group"
key={`render-sketch-${currentSketch.url}`}
>
{/* 状态 */}
{currentSketch.status === 0 && (
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
<div className="text-blue-500 text-2xl font-bold flex items-center gap-2">
<Loader2 className="w-10 h-10 animate-spin" />
<span>Generating...</span>
</div>
</div>
)}
{currentSketch.status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
<div className="text-red-500 text-2xl font-bold flex items-center gap-2">
<X className="w-10 h-10" />
<span>Failed</span>
</div>
</div>
)}
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
{currentSketch.status === 1 && (
<AnimatePresence mode="wait">
<motion.img
key={currentSketch.url}
src={currentSketch.url}
className="w-full h-full rounded-lg object-cover"
// 用 circle clip-path 实现“扩散”
initial={{ clipPath: "circle(0% at 50% 50%)" }}
animate={{ clipPath: "circle(150% at 50% 50%)" }}
exit={{ opacity: 0 }}
transition={{ duration: 1, ease: "easeInOut" }}
/>
</AnimatePresence>
)}
<div className="absolute top-0 left-0 right-0 p-2">
{/* 角色类型 */}
{currentSketch.type === 'role' && (
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
<span className="text-xs text-purple-400">Role: {currentSketch.name}</span>
</div>
)}
</div>
{/* 操作按钮组 */}
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 gap-2 hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
onClick={() => handleEditClick('1')}
/>
</motion.div>
</AnimatePresence> */}
{/* 底部播放按钮 */}
<AnimatePresence>
<motion.div
className="absolute bottom-4 left-4"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="relative"
>
</motion.div>
</motion.div>
</AnimatePresence>
</div>
);
};
// 渲染剧本
const renderScriptContent = () => {
return (
<div className="relative w-full h-full bg-white/10 rounded-lg overflow-hidden p-2">
{
scriptData ? (
<ScriptRenderer
data={scriptData}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
mode={mode}
/>
) : (
<div className="flex gap-2 w-full h-full">
<div className="w-[70%] h-full rounded-lg gap-2 flex flex-col">
<Skeleton className="w-full h-[33%] rounded-lg" />
<Skeleton className="w-full h-[33%] rounded-lg" />
<Skeleton className="w-full h-[33%] rounded-lg" />
</div>
<div className="w-[30%] h-full rounded-lg">
<Skeleton className="w-full h-full rounded-lg" />
</div>
</div>
)
}
</div>
);
};
// 根据当前步骤渲染对应内容
if (taskObject.currentStage === 'final_video') {
return renderFinalVideo();
}
if (taskObject.currentStage === 'video') {
return renderVideoContent();
}
if (taskObject.currentStage === 'script') {
return renderScriptContent();
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
return renderSketchContent([...taskObject.roles.data, ...taskObject.scenes.data][currentSketchIndex]);
}
return null;
});