|
@@ -42,16 +42,28 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
right: { src: rightVideoSrc, label: '课件板书', tagColor: 'green' }
|
|
right: { src: rightVideoSrc, label: '课件板书', tagColor: 'green' }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ useEffect(()=> {
|
|
|
|
|
+
|
|
|
|
|
+ setVideos({
|
|
|
|
|
+ ...videos,
|
|
|
|
|
+ top: { src: topVideoSrc, label: '教室全景', tagColor: 'magenta' },
|
|
|
|
|
+ left: { src: leftVideoSrc, label: '教师特写', tagColor: 'blue' },
|
|
|
|
|
+ right: { src: rightVideoSrc, label: '课件板书', tagColor: 'green' }
|
|
|
|
|
+ })
|
|
|
|
|
+ }, [topVideoSrc, leftVideoSrc, rightVideoSrc])
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(()=> {
|
|
|
|
|
+ leftVideoRef.current?.load();
|
|
|
|
|
+ rightVideoRef.current?.load();
|
|
|
|
|
+ topVideoRef.current?.load();
|
|
|
|
|
+ }, [videos])
|
|
|
|
|
+
|
|
|
const handleSwitch = (position: 'left' | 'right') => {
|
|
const handleSwitch = (position: 'left' | 'right') => {
|
|
|
setVideos((prev: VideoState) => ({
|
|
setVideos((prev: VideoState) => ({
|
|
|
top: prev[position],
|
|
top: prev[position],
|
|
|
left: position === 'left' ? prev.top : prev.left,
|
|
left: position === 'left' ? prev.top : prev.left,
|
|
|
right: position === 'right' ? prev.top : prev.right
|
|
right: position === 'right' ? prev.top : prev.right
|
|
|
}));
|
|
}));
|
|
|
- leftVideoRef.current?.load();
|
|
|
|
|
- rightVideoRef.current?.load();
|
|
|
|
|
- topVideoRef.current?.load();
|
|
|
|
|
- audioRef.current?.load();
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
@@ -61,6 +73,26 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
|
const [isSeeking, setIsSeeking] = useState(false);
|
|
const [isSeeking, setIsSeeking] = useState(false);
|
|
|
|
|
|
|
|
|
|
+ // 是否加载中
|
|
|
|
|
+ const [waiting, setWating] = useState(true)
|
|
|
|
|
+ const isPlayingRef = useRef(false);
|
|
|
|
|
+ /** 所有视频是否都可以播放 */
|
|
|
|
|
+ const syncCanPlayRef = useRef({
|
|
|
|
|
+ top: false,
|
|
|
|
|
+ left: false,
|
|
|
|
|
+ right: false
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ /** 所有视频错误状态 */
|
|
|
|
|
+ const errorVideoRef = useRef({
|
|
|
|
|
+ top: false,
|
|
|
|
|
+ left: false,
|
|
|
|
|
+ right: false
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ /** 主要监听的视频是 TOP | LEFT | RIGHT */
|
|
|
|
|
+ const mainListenVideoRef = useRef<string>('')
|
|
|
|
|
+
|
|
|
const formatTime = (time: number) => {
|
|
const formatTime = (time: number) => {
|
|
|
const minutes = Math.floor(time / 60);
|
|
const minutes = Math.floor(time / 60);
|
|
|
const seconds = Math.floor(time % 60);
|
|
const seconds = Math.floor(time % 60);
|
|
@@ -91,6 +123,10 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
setIsPlaying(shouldPlay);
|
|
setIsPlaying(shouldPlay);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ useEffect(()=> {
|
|
|
|
|
+ isPlayingRef.current = isPlaying
|
|
|
|
|
+ }, [isPlaying])
|
|
|
|
|
+
|
|
|
const handleTimeUpdate = () => {
|
|
const handleTimeUpdate = () => {
|
|
|
if (!isSeeking && topVideoRef.current) {
|
|
if (!isSeeking && topVideoRef.current) {
|
|
|
setCurrentTime(topVideoRef.current.currentTime);
|
|
setCurrentTime(topVideoRef.current.currentTime);
|
|
@@ -98,6 +134,7 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
const newTime = parseFloat(e.target.value);
|
|
const newTime = parseFloat(e.target.value);
|
|
|
setCurrentTime(newTime);
|
|
setCurrentTime(newTime);
|
|
|
syncMedia(newTime);
|
|
syncMedia(newTime);
|
|
@@ -140,21 +177,199 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const topVideo = topVideoRef.current;
|
|
const topVideo = topVideoRef.current;
|
|
|
|
|
+ const leftVideo = leftVideoRef.current;
|
|
|
|
|
+ const rightVideo = rightVideoRef.current;
|
|
|
|
|
+ if(!topVideo || !leftVideo || !rightVideo) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const handleLoadedMetadata = () => {
|
|
const handleLoadedMetadata = () => {
|
|
|
if (topVideo) {
|
|
if (topVideo) {
|
|
|
setDuration(topVideo.duration);
|
|
setDuration(topVideo.duration);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- if (topVideo) {
|
|
|
|
|
- topVideo.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
|
|
|
- topVideo.addEventListener('timeupdate', handleTimeUpdate);
|
|
|
|
|
|
|
+ const syncPlay = () => {
|
|
|
|
|
+ if(true) {
|
|
|
|
|
+ // x v v
|
|
|
|
|
+ if(errorVideoRef.current.top && !errorVideoRef.current.left && !errorVideoRef.current.right) {
|
|
|
|
|
+ if(!syncCanPlayRef.current.left || !syncCanPlayRef.current.right) {
|
|
|
|
|
+ leftVideo.pause()
|
|
|
|
|
+ rightVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // leftVideo.play()
|
|
|
|
|
+ // rightVideo.play()
|
|
|
|
|
+ setWating(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ // v x v
|
|
|
|
|
+ } else if(!errorVideoRef.current.top && errorVideoRef.current.left && !errorVideoRef.current.right) {
|
|
|
|
|
+ if(!syncCanPlayRef.current.top || !syncCanPlayRef.current.right) {
|
|
|
|
|
+ topVideo.pause()
|
|
|
|
|
+ rightVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // topVideo.play()
|
|
|
|
|
+ // rightVideo.play()
|
|
|
|
|
+ setWating(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ // v v x
|
|
|
|
|
+ } else if(!errorVideoRef.current.top && !errorVideoRef.current.left && errorVideoRef.current.right) {
|
|
|
|
|
+ if(!syncCanPlayRef.current.top || !syncCanPlayRef.current.left) {
|
|
|
|
|
+ topVideo.pause()
|
|
|
|
|
+ leftVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // topVideo.play()
|
|
|
|
|
+ // leftVideo.play()
|
|
|
|
|
+ setWating(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ // v x x
|
|
|
|
|
+ } else if(!errorVideoRef.current.top && errorVideoRef.current.left && errorVideoRef.current.right) {
|
|
|
|
|
+ if(!syncCanPlayRef.current.top) {
|
|
|
|
|
+ topVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // topVideo.play()
|
|
|
|
|
+ setWating(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ // x v x
|
|
|
|
|
+ } else if(errorVideoRef.current.top && !errorVideoRef.current.left && errorVideoRef.current.right) {
|
|
|
|
|
+ if(!syncCanPlayRef.current.left) {
|
|
|
|
|
+ leftVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // leftVideo.play()
|
|
|
|
|
+ setWating(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ // x x v
|
|
|
|
|
+ } else if (errorVideoRef.current.top && errorVideoRef.current.left && !errorVideoRef.current.right) {
|
|
|
|
|
+ if(!syncCanPlayRef.current.right) {
|
|
|
|
|
+ rightVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // rightVideo.play()
|
|
|
|
|
+ setWating(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ // v v v
|
|
|
|
|
+ } else if(!errorVideoRef.current.top && !errorVideoRef.current.left && !errorVideoRef.current.right) {
|
|
|
|
|
+ if(!syncCanPlayRef.current.left || !syncCanPlayRef.current.right || !syncCanPlayRef.current.top) {
|
|
|
|
|
+ topVideo.pause()
|
|
|
|
|
+ leftVideo.pause()
|
|
|
|
|
+ rightVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // topVideo.play()
|
|
|
|
|
+ // leftVideo.play()
|
|
|
|
|
+ // rightVideo.play()
|
|
|
|
|
+ setWating(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ // x x x
|
|
|
|
|
+ } else {
|
|
|
|
|
+ topVideo.pause()
|
|
|
|
|
+ leftVideo.pause()
|
|
|
|
|
+ rightVideo.pause()
|
|
|
|
|
+ setIsPlaying(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ const onTopCanPlay = () => {
|
|
|
|
|
+ if(!mainListenVideoRef.current) {
|
|
|
|
|
+ mainListenVideoRef.current = "top"
|
|
|
|
|
+ }
|
|
|
|
|
+ syncCanPlayRef.current.top = true
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const onLeftCanPlay = () => {
|
|
|
|
|
+ if(!mainListenVideoRef.current) {
|
|
|
|
|
+ mainListenVideoRef.current = "left"
|
|
|
|
|
+ }
|
|
|
|
|
+ syncCanPlayRef.current.left = true
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+ const onRightCanPlay = () => {
|
|
|
|
|
+ if(!mainListenVideoRef.current) {
|
|
|
|
|
+ mainListenVideoRef.current = "right"
|
|
|
|
|
+ }
|
|
|
|
|
+ syncCanPlayRef.current.right = true
|
|
|
|
|
+ syncPlay()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const onTopWaiting = () => {
|
|
|
|
|
+ syncCanPlayRef.current.top = false
|
|
|
|
|
+ setWating(true)
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const onLeftWaiting = () => {
|
|
|
|
|
+ syncCanPlayRef.current.left = false
|
|
|
|
|
+ setWating(true)
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const onRightWaiting = () => {
|
|
|
|
|
+ syncCanPlayRef.current.right = false
|
|
|
|
|
+ setWating(true)
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const onTopError = () => {
|
|
|
|
|
+ errorVideoRef.current.top = true
|
|
|
|
|
+ syncCanPlayRef.current.top = false
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const onLeftError = () => {
|
|
|
|
|
+ errorVideoRef.current.left = true
|
|
|
|
|
+ syncCanPlayRef.current.left = false
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+ const onRightError = () => {
|
|
|
|
|
+ errorVideoRef.current.right = true
|
|
|
|
|
+ syncCanPlayRef.current.right = false
|
|
|
|
|
+ syncPlay()
|
|
|
|
|
+ }
|
|
|
|
|
+ // 不是所有的视频都能播放,可能有些视频没有上传成功,只能播放两个或一个视频,需要处理错误的视频
|
|
|
|
|
+
|
|
|
|
|
+ topVideo.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
|
|
|
+ topVideo.addEventListener('timeupdate', handleTimeUpdate);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ topVideo.addEventListener('canplay', onTopCanPlay);
|
|
|
|
|
+ topVideo.addEventListener('waiting', onTopWaiting);
|
|
|
|
|
+ topVideo.addEventListener('error', onTopError);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ leftVideo.addEventListener('canplay', onLeftCanPlay);
|
|
|
|
|
+ leftVideo.addEventListener('waiting', onLeftWaiting);
|
|
|
|
|
+ leftVideo.addEventListener('error', onLeftError);
|
|
|
|
|
+
|
|
|
|
|
+ rightVideo.addEventListener('canplay', onRightCanPlay);
|
|
|
|
|
+ rightVideo.addEventListener('waiting', onRightWaiting);
|
|
|
|
|
+ rightVideo.addEventListener('error', onRightError);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
return () => {
|
|
return () => {
|
|
|
if (topVideo) {
|
|
if (topVideo) {
|
|
|
|
|
+ topVideo.removeEventListener('error', onTopError);
|
|
|
topVideo.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
topVideo.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
|
topVideo.removeEventListener('timeupdate', handleTimeUpdate);
|
|
topVideo.removeEventListener('timeupdate', handleTimeUpdate);
|
|
|
|
|
+ topVideo.removeEventListener('canplay', onTopCanPlay);
|
|
|
|
|
+ topVideo.removeEventListener('waiting', onTopWaiting);
|
|
|
|
|
+ }
|
|
|
|
|
+ if(leftVideo) {
|
|
|
|
|
+ leftVideo.removeEventListener('canplay', onLeftCanPlay);
|
|
|
|
|
+ leftVideo.removeEventListener('waiting', onLeftWaiting);
|
|
|
|
|
+ leftVideo.removeEventListener('error', onLeftError);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(rightVideo) {
|
|
|
|
|
+ rightVideo.removeEventListener('canplay', onRightCanPlay);
|
|
|
|
|
+ rightVideo.removeEventListener('waiting', onRightWaiting);
|
|
|
|
|
+ rightVideo.removeEventListener('error', onRightError);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
}, []);
|
|
}, []);
|
|
@@ -187,28 +402,29 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className={`video-container ${isFullscreen ? 'fullscreen' : ''}`}>
|
|
<div className={`video-container ${isFullscreen ? 'fullscreen' : ''}`}>
|
|
|
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
|
|
|
|
|
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
|
|
|
<div className="video-wrapper">
|
|
<div className="video-wrapper">
|
|
|
<video
|
|
<video
|
|
|
ref={topVideoRef}
|
|
ref={topVideoRef}
|
|
|
- onEnded={togglePlay}
|
|
|
|
|
|
|
|
|
|
- style={{ objectFit: 'contain',backgroundColor: 'grey', width:'100%', height: 'calc((100vh - (88px + 64px + 52px))/3 * 2)' }}
|
|
|
|
|
- muted
|
|
|
|
|
|
|
+ onEnded={togglePlay}
|
|
|
|
|
+ style={{ objectFit: 'contain',backgroundColor: '#000', width:'100%', height: 'calc((100vh - (88px + 64px + 52px))/3 * 2)' }}
|
|
|
controls={false}
|
|
controls={false}
|
|
|
|
|
+ preload="auto"
|
|
|
disablePictureInPicture
|
|
disablePictureInPicture
|
|
|
>
|
|
>
|
|
|
<source src={videos.top.src} type="video/mp4" />
|
|
<source src={videos.top.src} type="video/mp4" />
|
|
|
</video>
|
|
</video>
|
|
|
<Tag className="video-tag top-tag" color={videos.top.tagColor}>{videos.top.label}</Tag>
|
|
<Tag className="video-tag top-tag" color={videos.top.tagColor}>{videos.top.label}</Tag>
|
|
|
</div>
|
|
</div>
|
|
|
- <div style={{ display: 'flex', justifyContent: 'center', gap: 10 }}>
|
|
|
|
|
- {['left', 'right'].map(pos => (
|
|
|
|
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'center', gap: 10, width: '100%' }}>
|
|
|
|
|
+ {['left', 'right'].map((pos, index) => (
|
|
|
<div key={pos} className="video-wrapper">
|
|
<div key={pos} className="video-wrapper">
|
|
|
<video
|
|
<video
|
|
|
|
|
+ preload="auto"
|
|
|
ref={pos === 'left' ? leftVideoRef : rightVideoRef}
|
|
ref={pos === 'left' ? leftVideoRef : rightVideoRef}
|
|
|
// width={isFullscreen ? '700' : '600'}
|
|
// width={isFullscreen ? '700' : '600'}
|
|
|
- style={{ objectFit: 'contain', backgroundColor: 'grey', width: '100%',height: 'calc((100vh - (88px + 64px + 52px))/3)' }}
|
|
|
|
|
|
|
+ style={{ objectFit: 'contain', backgroundColor: '#000', width: '100%',height: 'calc((100vh - (88px + 64px + 52px))/3)' }}
|
|
|
muted
|
|
muted
|
|
|
controls={false}
|
|
controls={false}
|
|
|
disablePictureInPicture
|
|
disablePictureInPicture
|
|
@@ -234,13 +450,21 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <audio ref={audioRef} src={audioSrc} />
|
|
|
|
|
|
|
+ {/* <audio ref={audioRef} src={audioSrc} /> */}
|
|
|
|
|
|
|
|
{/* 自定义控制器 */}
|
|
{/* 自定义控制器 */}
|
|
|
<div className="video-controls">
|
|
<div className="video-controls">
|
|
|
- <Button onClick={togglePlay}>
|
|
|
|
|
- {isPlaying ? '❚❚' : '▶'}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ waiting ? (
|
|
|
|
|
+ <div className="play-button-loader"></div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Button onClick={togglePlay}>
|
|
|
|
|
+ {
|
|
|
|
|
+ isPlaying ? '❚❚' : '▶'
|
|
|
|
|
+ }
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
<input
|
|
<input
|
|
|
type="range"
|
|
type="range"
|
|
|
min="0"
|
|
min="0"
|