Explorar o código

feat: 单路播放完善

NeeDaye hai 6 meses
pai
achega
8f1e9f96c1

+ 188 - 0
src/pages/Play/components/SinglePlayer.tsx

@@ -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;

+ 5 - 0
src/pages/Play/components/style/index.less

@@ -64,3 +64,8 @@
 .top-tag {
   font-size: 16px;
 }
+.video-switcher {
+  display: flex;
+  justify-content: center;
+  gap: 20px;
+}

+ 19 - 9
src/pages/Play/index.tsx

@@ -6,6 +6,7 @@ import './style/index.less';
 import { Button, Card, Flex } from 'antd';
 import { ArrowLeftOutlined } from '@ant-design/icons';
 import './style/index.less'
+import SingleVideoPlayer from '@/pages/Play/components/SinglePlayer';
 import SyncVideoPlayer from '@/pages/Play/components/SyncPlayer';
 import { useLocation } from '@@/exports';
 import { getCourseVideos } from '@/services/play/PlayController';
@@ -27,9 +28,9 @@ const TableList: React.FC<unknown> = () => {
           teacherCourseId: searchParams.get('courseId') as string,
         })
         if (data.code === 200) {
-          setFullClassRoom(data.data.find(item => item.fileType === 1)?.fileUrl as string);
-          setCloseUpTeacher(data.data?.find(item => item.fileType === 2)?.fileUrl as string);
-          setBlackWriting(data.data.find(item => item.fileType === 3)?.fileUrl as string);
+          setCloseUpTeacher(data.data?.find(item => item.fileType === 1)?.fileUrl as string);
+          setFullClassRoom(data.data.find(item => item.fileType === 3)?.fileUrl as string);
+          setBlackWriting(data.data.find(item => item.fileType === 2)?.fileUrl as string);
           setAudioClass(data.data.find(item => item.fileType === 4)?.fileUrl as string);
         }
       }
@@ -81,12 +82,21 @@ const TableList: React.FC<unknown> = () => {
               </Button>
             </Flex>
             <div style={{ flex: 1 }}>
-              <SyncVideoPlayer
-                topVideoSrc={fullClassRoom}
-                leftVideoSrc={closeUpTeacher}
-                rightVideoSrc={blackWriting}
-                audioSrc={audioClass}
-              />
+              {selected === 'single' ? (
+                <SingleVideoPlayer
+                  fullClassRoomSrc={fullClassRoom}
+                  closeUpTeacherSrc={closeUpTeacher}
+                  blackWritingSrc={blackWriting}
+                  audioSrc={audioClass}
+                />
+                ) : (
+                <SyncVideoPlayer
+                  topVideoSrc={fullClassRoom}
+                  leftVideoSrc={closeUpTeacher}
+                  rightVideoSrc={blackWriting}
+                  audioSrc={audioClass}
+                />
+              )}
             </div>
           </div>
         ) : (