diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx index 7a143ed..b6c30e1 100644 --- a/components/pages/work-flow/media-viewer.tsx +++ b/components/pages/work-flow/media-viewer.tsx @@ -2,7 +2,7 @@ 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, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, CircleAlert, PenTool } from 'lucide-react'; +import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, PictureInPicture2, PenTool } 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'; @@ -69,6 +69,8 @@ export const MediaViewer = React.memo(function MediaViewer({ // 音量控制状态 const [isMuted, setIsMuted] = useState(false); const [volume, setVolume] = useState(0.8); + const [duration, setDuration] = useState(0); + const [currentTime, setCurrentTime] = useState(0); // 最终视频控制状态 const [isFinalVideoPlaying, setIsFinalVideoPlaying] = useState(true); @@ -175,6 +177,9 @@ export const MediaViewer = React.memo(function MediaViewer({ if (finalVideoRef.current) { setFinalVideoReady(true); applyVolumeSettings(finalVideoRef.current); + try { + setDuration(Number.isFinite(finalVideoRef.current.duration) ? finalVideoRef.current.duration : 0); + } catch {} // 如果当前状态是应该播放的,尝试播放 if (isFinalVideoPlaying) { @@ -235,16 +240,15 @@ export const MediaViewer = React.memo(function MediaViewer({ // 全屏控制 const toggleFullscreen = () => { setUserHasInteracted(true); + const target = activeVideoRef().current; if (!document.fullscreenElement) { - // 进入全屏 - if (finalVideoRef.current) { - finalVideoRef.current.requestFullscreen?.() || - (finalVideoRef.current as any).webkitRequestFullscreen?.() || - (finalVideoRef.current as any).msRequestFullscreen?.(); + if (target) { + target.requestFullscreen?.() || + (target as any).webkitRequestFullscreen?.() || + (target as any).msRequestFullscreen?.(); setIsFullscreen(true); } } else { - // 退出全屏 document.exitFullscreen?.() || (document as any).webkitExitFullscreen?.() || (document as any).msExitFullscreen?.(); @@ -263,6 +267,9 @@ export const MediaViewer = React.memo(function MediaViewer({ } else { mainVideoRef.current.pause(); } + try { + setDuration(Number.isFinite(mainVideoRef.current.duration) ? mainVideoRef.current.duration : 0); + } catch {} } }, [isVideoPlaying]); @@ -289,6 +296,61 @@ export const MediaViewer = React.memo(function MediaViewer({ } }, [volume, isMuted]); + // 绑定时间更新(同时监听两个 video) + useEffect(() => { + const mv = mainVideoRef.current; + const fv = finalVideoRef.current; + const onTimeUpdate = (e: Event) => { + const el = e.currentTarget as HTMLVideoElement; + setCurrentTime(el.currentTime || 0); + if (Number.isFinite(el.duration)) setDuration(el.duration); + }; + mv?.addEventListener('timeupdate', onTimeUpdate); + fv?.addEventListener('timeupdate', onTimeUpdate); + mv?.addEventListener('loadedmetadata', onTimeUpdate); + fv?.addEventListener('loadedmetadata', onTimeUpdate); + return () => { + mv?.removeEventListener('timeupdate', onTimeUpdate); + fv?.removeEventListener('timeupdate', onTimeUpdate); + mv?.removeEventListener('loadedmetadata', onTimeUpdate); + fv?.removeEventListener('loadedmetadata', onTimeUpdate); + }; + }, []); + + const activeVideoRef = () => { + // 根据当前阶段选择活跃的 video 引用 + const effectiveStage = (selectedView === 'final' && taskObject.final?.url) + ? 'final_video' + : (!['script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage); + return effectiveStage === 'final_video' ? finalVideoRef : mainVideoRef; + }; + + const progressPercent = duration > 0 ? Math.min(100, Math.max(0, (currentTime / duration) * 100)) : 0; + + const formatRemaining = (dur: number, cur: number) => { + const remain = Math.max(0, Math.round(dur - cur)); + const m = Math.floor(remain / 60); + const s = remain % 60; + return `-${m}:${s.toString().padStart(2, '0')}`; + }; + + const seekTo = (pct: number) => { + const ref = activeVideoRef().current; + if (!ref || !Number.isFinite(ref.duration)) return; + const t = (pct / 100) * ref.duration; + ref.currentTime = t; + setCurrentTime(t); + }; + + const requestPip = async () => { + try { + const ref = activeVideoRef().current as any; + if (ref && typeof ref.requestPictureInPicture === 'function') { + await ref.requestPictureInPicture(); + } + } catch {} + }; + // 监听全屏状态变化 useEffect(() => { const handleFullscreenChange = () => { @@ -317,41 +379,54 @@ export const MediaViewer = React.memo(function MediaViewer({ }; }, []); - // 渲染音量控制组件 - const renderVolumeControls = () => ( -