From cbc4a2193d76e4edac66cfa1d5ba6a0655410046 Mon Sep 17 00:00:00 2001 From: qikongjian Date: Mon, 29 Sep 2025 16:01:38 +0800 Subject: [PATCH] =?UTF-8?q?pc=E7=AB=AF=E6=92=AD=E6=94=BE=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/work-flow/media-viewer.tsx | 195 +++++++++++--------- 1 file changed, 112 insertions(+), 83 deletions(-) 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 = () => ( -
- {/* 静音按钮 */} - + // 渲染底部通栏控制条(与图2一致) + const renderBottomControls = ( + isFinal: boolean, + onPlayToggle: () => void, + playing: boolean + ) => ( +
+
+ {/* 播放/暂停 */} + - {/* 音量滑块 - 一直显示 */} -
-
+ {/* 静音,仅图标 */} + + + {/* 进度条 */} +
handleVolumeChange(parseFloat(e.target.value))} - className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer + value={progressPercent} + onChange={(e) => seekTo(parseFloat(e.target.value))} + className="w-full 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" + [&::-webkit-slider-thumb]:cursor-pointer + [&::-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" 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%)` + background: `linear-gradient(to right, white 0%, white ${progressPercent}%, rgba(255,255,255,0.2) ${progressPercent}%, rgba(255,255,255,0.2) 100%)` }} />
- - {Math.round(volume * 100)}% - + + {/* 剩余时间 */} +
+ {formatRemaining(duration, currentTime)} +
+ + {/* 画中画 */} + + + {/* 全屏 */} +
); @@ -432,35 +507,9 @@ export const MediaViewer = React.memo(function MediaViewer({ )}
- {/* 底部控制区域 */} - - {/* 播放/暂停按钮 */} - - - - - {/* 音量控制 */} - {renderVolumeControls()} - - {/* 全屏按钮 */} - + {/* 底部通栏控制区域 */} + + {renderBottomControls(true, toggleFinalVideoPlay, isFinalVideoPlaying)}
@@ -625,31 +674,11 @@ export const MediaViewer = React.memo(function MediaViewer({ */} - {/* 底部控制区域 */} + {/* 底部通栏控制区域(仅生成成功时显示) */} { taskObject.videos.data[currentSketchIndex].video_status === 1 && ( - - {/* 播放按钮 */} - - - - - {/* 音量控制 */} - {renderVolumeControls()} + + {renderBottomControls(false, onToggleVideoPlay, isVideoPlaying)} )}