优化视频播放器UI
This commit is contained in:
175
src/app/srt-player/VideoPlayer/VideoPanel.tsx
Normal file
175
src/app/srt-player/VideoPlayer/VideoPanel.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { useState, useRef, forwardRef, useEffect, KeyboardEvent, useCallback } from "react";
|
||||
import SubtitleDisplay from "./SubtitleDisplay";
|
||||
import Button from "@/components/Button";
|
||||
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
||||
|
||||
type VideoPanelProps = {
|
||||
videoUrl: string | null;
|
||||
srtUrl: string | null;
|
||||
};
|
||||
|
||||
const VideoPanel = forwardRef<HTMLVideoElement, VideoPanelProps>((
|
||||
{ videoUrl, srtUrl }, videoRef
|
||||
) => {
|
||||
videoRef = videoRef as React.RefObject<HTMLVideoElement>;
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [srtLength, setSrtLength] = useState<number>(0);
|
||||
const [progress, setProgress] = useState<number>(-1);
|
||||
const [autoPause, setAutoPause] = useState<boolean>(true);
|
||||
const [spanText, setSpanText] = useState<string>('');
|
||||
const [subtitle, setSubtitle] = useState<string>('');
|
||||
const parsedSrtRef = useRef<{ start: number; end: number; text: string; }[] | null>(null);
|
||||
const rafldRef = useRef<number>(0);
|
||||
const ready = useRef({
|
||||
'vid': false,
|
||||
'sub': false,
|
||||
'all': function () { return this.vid && this.sub }
|
||||
});
|
||||
|
||||
const togglePlayPause = useCallback(() => {
|
||||
if (!videoUrl) return;
|
||||
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
if (video.paused || video.currentTime === 0) {
|
||||
video.play();
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
setIsPlaying(!video.paused);
|
||||
}, [videoRef, videoUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const cb = () => {
|
||||
if (ready.current.all()) {
|
||||
if (!parsedSrtRef.current) {
|
||||
;
|
||||
} else if (isPlaying) {
|
||||
// 这里负责显示当前时间的字幕与自动暂停
|
||||
const srt = parsedSrtRef.current;
|
||||
const ct = videoRef.current?.currentTime as number;
|
||||
const index = getIndex(srt, ct);
|
||||
if (index !== null) {
|
||||
setSubtitle(srt[index].text)
|
||||
if (autoPause && ct >= (srt[index].end - 0.05) && ct < srt[index].end) {
|
||||
videoRef.current!.currentTime = srt[index].start;
|
||||
togglePlayPause();
|
||||
}
|
||||
} else {
|
||||
setSubtitle('');
|
||||
}
|
||||
} else {
|
||||
;
|
||||
}
|
||||
}
|
||||
rafldRef.current = requestAnimationFrame(cb);
|
||||
}
|
||||
rafldRef.current = requestAnimationFrame(cb);
|
||||
return () => {
|
||||
cancelAnimationFrame(rafldRef.current);
|
||||
}
|
||||
}, [autoPause, isPlaying, togglePlayPause, videoRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoUrl && videoRef.current) {
|
||||
videoRef.current.src = videoUrl;
|
||||
videoRef.current.load();
|
||||
setIsPlaying(false);
|
||||
ready.current['vid'] = true;
|
||||
}
|
||||
}, [videoRef, videoUrl]);
|
||||
useEffect(() => {
|
||||
if (srtUrl) {
|
||||
fetch(srtUrl)
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
parsedSrtRef.current = parseSrt(data);
|
||||
setSrtLength(parsedSrtRef.current.length);
|
||||
ready.current['sub'] = true;
|
||||
});
|
||||
}
|
||||
}, [srtUrl]);
|
||||
|
||||
const timeUpdate = () => {
|
||||
if (!parsedSrtRef.current || !videoRef.current) return;
|
||||
const index = getIndex(parsedSrtRef.current, videoRef.current.currentTime);
|
||||
if (!index) return;
|
||||
setSpanText(`${index + 1}/${parsedSrtRef.current.length}`)
|
||||
}
|
||||
|
||||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (videoRef.current && parsedSrtRef.current) {
|
||||
const newProgress = parseInt(e.target.value);
|
||||
videoRef.current.currentTime = parsedSrtRef.current[newProgress]?.start || 0;
|
||||
setProgress(newProgress);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoPauseToggle = () => {
|
||||
setAutoPause(!autoPause);
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (!parsedSrtRef.current || !videoRef.current) return;
|
||||
const i = getNearistIndex(parsedSrtRef.current, videoRef.current.currentTime);
|
||||
if (i != null && i + 1 < parsedSrtRef.current.length) {
|
||||
videoRef.current.currentTime = parsedSrtRef.current[i + 1].start;
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}
|
||||
|
||||
const previous = () => {
|
||||
if (!parsedSrtRef.current || !videoRef.current) return;
|
||||
const i = getNearistIndex(parsedSrtRef.current, videoRef.current.currentTime);
|
||||
if (i != null && i - 1 >= 0) {
|
||||
videoRef.current.currentTime = parsedSrtRef.current[i - 1].start;
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}
|
||||
|
||||
const restart = () => {
|
||||
if (!parsedSrtRef.current || !videoRef.current) return;
|
||||
const i = getNearistIndex(parsedSrtRef.current, videoRef.current.currentTime);
|
||||
if (i != null && i >= 0) {
|
||||
videoRef.current.currentTime = parsedSrtRef.current[i].start;
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDownEvent = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'n') {
|
||||
next();
|
||||
} else if (e.key === 'p') {
|
||||
previous();
|
||||
} else if (e.key === ' ') {
|
||||
togglePlayPause();
|
||||
} else if (e.key === 'r') {
|
||||
restart();
|
||||
} else if (e.key === 'a') {
|
||||
; handleAutoPauseToggle();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col" onKeyDown={handleKeyDownEvent}>
|
||||
<video className="bg-gray-200" ref={videoRef} onTimeUpdate={timeUpdate}></video>
|
||||
<SubtitleDisplay subtitle={subtitle}></SubtitleDisplay>
|
||||
<div className="buttons flex mt-2 gap-2 flex-wrap">
|
||||
<Button label={isPlaying ? '暂停' : '播放'} onClick={togglePlayPause}></Button>
|
||||
<Button label="上句" onClick={previous}></Button>
|
||||
<Button label="下句" onClick={next}></Button>
|
||||
<Button label="句首" onClick={restart}></Button>
|
||||
<Button label={`自动播放(${autoPause ? '是' : '否'})`} onClick={handleAutoPauseToggle}></Button>
|
||||
</div>
|
||||
<input className="seekbar" type="range" min={0} max={srtLength} onChange={handleSeek} step={1} value={progress}></input>
|
||||
<span>{spanText}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
VideoPanel.displayName = 'VideoPanel';
|
||||
|
||||
export default VideoPanel;
|
||||
Reference in New Issue
Block a user