pc端播放组件优化

This commit is contained in:
qikongjian 2025-09-29 16:01:38 +08:00
parent e6a3916f66
commit cbc4a2193d

View File

@ -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 = () => (
<div className="flex items-center gap-2">
{/* 静音按钮 */}
<GlassIconButton
icon={isMuted ? VolumeX : Volume2}
onClick={toggleMute}
size="sm"
/>
// 渲染底部通栏控制条与图2一致
const renderBottomControls = (
isFinal: boolean,
onPlayToggle: () => void,
playing: boolean
) => (
<div
className="absolute left-0 right-0 bottom-2 z-[21] px-6"
data-alt={isFinal ? 'final-controls' : 'video-controls'}
>
<div className="flex items-center gap-3">
{/* 播放/暂停 */}
<GlassIconButton icon={playing ? Pause : Play} onClick={onPlayToggle} size="sm" />
{/* 音量滑块 - 一直显示 */}
<div className="flex items-center gap-2">
<div className="relative">
{/* 静音,仅图标 */}
<GlassIconButton icon={isMuted ? VolumeX : Volume2} onClick={toggleMute} size="sm" />
{/* 进度条 */}
<div className="flex-1 flex items-center">
<input
type="range"
min="0"
max="1"
max="100"
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
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%)`
}}
/>
</div>
<span className="text-xs text-white/70 w-8 text-center">
{Math.round(volume * 100)}%
</span>
{/* 剩余时间 */}
<div className="text-white/80 text-sm w-14 text-right select-none" data-alt="time-remaining">
{formatRemaining(duration, currentTime)}
</div>
{/* 画中画 */}
<GlassIconButton icon={PictureInPicture2} onClick={requestPip} size="sm" />
{/* 全屏 */}
<GlassIconButton icon={isFullscreen ? Minimize : Maximize} onClick={toggleFullscreen} size="sm" />
</div>
</div>
);
@ -432,35 +507,9 @@ export const MediaViewer = React.memo(function MediaViewer({
)}
</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 initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }}>
{renderBottomControls(true, toggleFinalVideoPlay, isFinalVideoPlaying)}
</motion.div>
</div>
</div>
@ -625,31 +674,11 @@ export const MediaViewer = React.memo(function MediaViewer({
</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 initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }}>
{renderBottomControls(false, onToggleVideoPlay, isVideoPlaying)}
</motion.div>
</AnimatePresence>
)}