177 lines
6.5 KiB
TypeScript
177 lines
6.5 KiB
TypeScript
import { useState, useRef, forwardRef, useEffect, KeyboardEvent, useCallback } from "react";
|
|
import Button from "../Button";
|
|
import { getIndex, getNearistIndex, parseSrt } from "../../subtitle";
|
|
import SubtitleDisplay from "./SubtitleDisplay";
|
|
|
|
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="flex flex-col w-9/12" onKeyDown={handleKeyDownEvent}>
|
|
<video className="max-h-80" ref={videoRef} onTimeUpdate={timeUpdate}></video>
|
|
<SubtitleDisplay subtitle={subtitle}></SubtitleDisplay>
|
|
<div className="buttons">
|
|
<ul className="">
|
|
<Button label={isPlaying ? 'PAUSE' : 'PLAY'} onClick={togglePlayPause}></Button>
|
|
<Button label="NEXT" onClick={next}></Button>
|
|
<Button label="PREVIOUS" onClick={previous}></Button>
|
|
<Button label="RESTART" onClick={restart}></Button>
|
|
<Button label={`AUTOPAUSE(${autoPause ? 'Y' : 'N'})`} onClick={handleAutoPauseToggle}></Button>
|
|
</ul>
|
|
</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; |