forked from 77media/video-flow
pc端播放组件优化
This commit is contained in:
parent
e6a3916f66
commit
cbc4a2193d
@ -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
|
||||
[&::-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 [&::-moz-range-thumb]:shadow-lg"
|
||||
[&::-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>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user