|
@@ -0,0 +1,188 @@
|
|
|
|
|
+
|
|
|
|
|
+import React, { useRef, useState } from 'react';
|
|
|
|
|
+import './style/index.less';
|
|
|
|
|
+import { Button } from 'antd';
|
|
|
|
|
+
|
|
|
|
|
+interface SyncVideoPlayerProps {
|
|
|
|
|
+ fullClassRoomSrc: string;
|
|
|
|
|
+ closeUpTeacherSrc: string;
|
|
|
|
|
+ blackWritingSrc: string;
|
|
|
|
|
+ audioSrc: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
|
|
|
|
|
+ fullClassRoomSrc,
|
|
|
|
|
+ closeUpTeacherSrc,
|
|
|
|
|
+ blackWritingSrc,
|
|
|
|
|
+ audioSrc
|
|
|
|
|
+}) => {
|
|
|
|
|
+ const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
|
+ const audioRef = useRef<HTMLAudioElement>(null);
|
|
|
|
|
+ const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
|
+ const [currentTime, setCurrentTime] = useState(0);
|
|
|
|
|
+ const [duration, setDuration] = useState(0);
|
|
|
|
|
+ const [volume, setVolume] = useState(1);
|
|
|
|
|
+ const [isFullscreen, setIsFullscreen] = useState(false);
|
|
|
|
|
+ const [isSeeking, setIsSeeking] = useState(false);
|
|
|
|
|
+ const [currentVideo, setCurrentVideo] = useState('fullClassRoom');
|
|
|
|
|
+
|
|
|
|
|
+ const videos = {
|
|
|
|
|
+ fullClassRoom: fullClassRoomSrc,
|
|
|
|
|
+ closeUpTeacher: closeUpTeacherSrc,
|
|
|
|
|
+ blackWriting: blackWritingSrc
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const formatTime = (time: number) => {
|
|
|
|
|
+ const minutes = Math.floor(time / 60);
|
|
|
|
|
+ const seconds = Math.floor(time % 60);
|
|
|
|
|
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const syncMedia = (time: number) => {
|
|
|
|
|
+ if (videoRef.current) videoRef.current.currentTime = time;
|
|
|
|
|
+ if (audioRef.current) audioRef.current.currentTime = time;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const togglePlay = () => {
|
|
|
|
|
+ const shouldPlay = !isPlaying;
|
|
|
|
|
+ if (shouldPlay) {
|
|
|
|
|
+ videoRef.current?.play();
|
|
|
|
|
+ audioRef.current?.play();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ videoRef.current?.pause();
|
|
|
|
|
+ audioRef.current?.pause();
|
|
|
|
|
+ }
|
|
|
|
|
+ setIsPlaying(shouldPlay);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleTimeUpdate = () => {
|
|
|
|
|
+ if (!isSeeking && videoRef.current) {
|
|
|
|
|
+ setCurrentTime(videoRef.current.currentTime);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
+ const newTime = parseFloat(e.target.value);
|
|
|
|
|
+ setCurrentTime(newTime);
|
|
|
|
|
+ syncMedia(newTime);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSeekMouseDown = () => {
|
|
|
|
|
+ setIsSeeking(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSeekMouseUp = () => {
|
|
|
|
|
+ setIsSeeking(false);
|
|
|
|
|
+ if (isPlaying) {
|
|
|
|
|
+ videoRef.current?.play();
|
|
|
|
|
+ audioRef.current?.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
+ const newVolume = parseFloat(e.target.value);
|
|
|
|
|
+ setVolume(newVolume);
|
|
|
|
|
+ if (audioRef.current) {
|
|
|
|
|
+ audioRef.current.volume = newVolume;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const toggleFullscreen = () => {
|
|
|
|
|
+ if (!isFullscreen) {
|
|
|
|
|
+ const container = document.querySelector('.video-container');
|
|
|
|
|
+ if (container?.requestFullscreen) {
|
|
|
|
|
+ container.requestFullscreen();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (document.exitFullscreen) {
|
|
|
|
|
+ document.exitFullscreen();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ setIsFullscreen(!isFullscreen);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const changeVideo = (videoType: string) => {
|
|
|
|
|
+ setCurrentVideo(videoType);
|
|
|
|
|
+ if (isPlaying) {
|
|
|
|
|
+ videoRef.current?.load();
|
|
|
|
|
+ audioRef.current?.load();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
|
|
|
+ <div className="video-switcher">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={() => changeVideo('fullClassRoom')}
|
|
|
|
|
+ type={currentVideo === 'fullClassRoom' ? 'primary' : 'default'}
|
|
|
|
|
+ >
|
|
|
|
|
+ 教室全景
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={() => changeVideo('closeUpTeacher')}
|
|
|
|
|
+ type={currentVideo === 'closeUpTeacher' ? 'primary' : 'default'}
|
|
|
|
|
+ >
|
|
|
|
|
+ 教师特写
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={() => changeVideo('blackWriting')}
|
|
|
|
|
+ type={currentVideo === 'blackWriting' ? 'primary' : 'default'}
|
|
|
|
|
+ >
|
|
|
|
|
+ 课件板书
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className={`video-container ${isFullscreen ? 'fullscreen' : ''}`}>
|
|
|
|
|
+ <div className="video-wrapper" >
|
|
|
|
|
+ <video
|
|
|
|
|
+ ref={videoRef}
|
|
|
|
|
+ src={videos[currentVideo]}
|
|
|
|
|
+ onEnded={togglePlay}
|
|
|
|
|
+ onTimeUpdate={handleTimeUpdate}
|
|
|
|
|
+ onLoadedMetadata={() => setDuration(videoRef.current?.duration || 0)}
|
|
|
|
|
+ width="80%"
|
|
|
|
|
+ height={isFullscreen ? '950' : '580'}
|
|
|
|
|
+ style={{ objectFit: 'contain', backgroundColor: 'grey' }}
|
|
|
|
|
+ muted
|
|
|
|
|
+ controls={false}
|
|
|
|
|
+ disablePictureInPicture
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <audio ref={audioRef} src={audioSrc} />
|
|
|
|
|
+
|
|
|
|
|
+ <div className="video-controls">
|
|
|
|
|
+ <Button onClick={togglePlay}>
|
|
|
|
|
+ {isPlaying ? '❚❚' : '▶'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="range"
|
|
|
|
|
+ min="0"
|
|
|
|
|
+ max="100"
|
|
|
|
|
+ value={duration ? (currentTime / duration) * 100 : 0}
|
|
|
|
|
+ onChange={handleSeek}
|
|
|
|
|
+ onMouseDown={handleSeekMouseDown}
|
|
|
|
|
+ onMouseUp={handleSeekMouseUp}
|
|
|
|
|
+ className="progress"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className="time-display">
|
|
|
|
|
+ {formatTime(currentTime)} / {formatTime(duration)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="range"
|
|
|
|
|
+ min="0"
|
|
|
|
|
+ max="1"
|
|
|
|
|
+ step="0.01"
|
|
|
|
|
+ value={volume}
|
|
|
|
|
+ onChange={handleVolumeChange}
|
|
|
|
|
+ className="volume"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button onClick={toggleFullscreen}>
|
|
|
|
|
+ {isFullscreen ? '⤢' : '⤡'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export default SyncVideoPlayer;
|