浏览代码

feat: 同步播放主屏切换

NeeDaye 6 月之前
父节点
当前提交
21efee9d29

+ 4 - 0
src/layouts/SideMenu.tsx

@@ -207,6 +207,10 @@ function SideMenu  ({
                       onClick={() => {
                       onClick={() => {
                         history.push(`/play?teacherId=${item.teacherId}&courseId=${item.id}`);
                         history.push(`/play?teacherId=${item.teacherId}&courseId=${item.id}`);
                         setActiveCourseKey(item.id);
                         setActiveCourseKey(item.id);
+                        localStorage.setItem('periodName', item.periodName);
+                        localStorage.setItem('courseTitle', item.courseTitle);
+                        localStorage.setItem('subjectName', item.subjectName);
+                        localStorage.setItem('courseDate', item.courseDate);
                       }}
                       }}
                       className={item.id === activeCourseKey ? 'course-active' : ''}
                       className={item.id === activeCourseKey ? 'course-active' : ''}
                     >
                     >

+ 106 - 65
src/pages/Play/components/Player.tsx

@@ -1,18 +1,28 @@
 
 
 import React, { useRef, useState, useEffect } from 'react';
 import React, { useRef, useState, useEffect } from 'react';
 import './style/index.less';
 import './style/index.less';
-import { Button } from 'antd';
+import { Button, Tag } from 'antd';
 
 
-interface VideoPlayerProps {
-  syncPlay: boolean;
+interface VideoSource {
+  src: string;
+  label: string;
+  tagColor: string;
+}
+
+interface VideoState {
+  top: VideoSource;
+  left: VideoSource;
+  right: VideoSource;
+}
+
+interface SyncVideoPlayerProps {
   topVideoSrc: string;
   topVideoSrc: string;
   leftVideoSrc: string;
   leftVideoSrc: string;
   rightVideoSrc: string;
   rightVideoSrc: string;
   audioSrc: string;
   audioSrc: string;
 }
 }
 
 
-const VideoPlayer: React.FC<VideoPlayerProps> = ({
-   syncPlay,
+const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
    topVideoSrc,
    topVideoSrc,
    leftVideoSrc,
    leftVideoSrc,
    rightVideoSrc,
    rightVideoSrc,
@@ -21,6 +31,31 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
   const topVideoRef = useRef<HTMLVideoElement>(null);
   const topVideoRef = useRef<HTMLVideoElement>(null);
   const leftVideoRef = useRef<HTMLVideoElement>(null);
   const leftVideoRef = useRef<HTMLVideoElement>(null);
   const rightVideoRef = useRef<HTMLVideoElement>(null);
   const rightVideoRef = useRef<HTMLVideoElement>(null);
+  const [videos, setVideos] = useState<{
+    top: VideoSource;
+    left: VideoSource;
+    right: VideoSource;
+  }>({
+    top: { src: topVideoSrc, label: '教室全景', tagColor: 'magenta' },
+    left: { src: leftVideoSrc, label: '教师特写', tagColor: 'blue' },
+    right: { src: rightVideoSrc, label: '课件板书', tagColor: 'green' }
+  });
+
+  const handleSwitch = (position: 'left' | 'right') => {
+    setVideos((prev: VideoState) => ({
+      top: prev[position],
+      left: position === 'left' ? prev.top : prev.left,
+      right: position === 'right' ? prev.top : prev.right
+    }));
+    if (position === 'left') {
+      leftVideoRef.current?.load();
+    } else {
+      rightVideoRef.current?.load();
+    }
+    topVideoRef.current?.load();
+  };
+
+
   const audioRef = useRef<HTMLAudioElement>(null);
   const audioRef = useRef<HTMLAudioElement>(null);
   const [isPlaying, setIsPlaying] = useState(false);
   const [isPlaying, setIsPlaying] = useState(false);
   const [currentTime, setCurrentTime] = useState(0);
   const [currentTime, setCurrentTime] = useState(0);
@@ -129,80 +164,86 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
 
 
   return (
   return (
     <div className={`video-container ${isFullscreen ? 'fullscreen' : ''}`}>
     <div className={`video-container ${isFullscreen ? 'fullscreen' : ''}`}>
-      <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
-        <div>
+      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+        <div className="video-wrapper">
           <video
           <video
             ref={topVideoRef}
             ref={topVideoRef}
-            width='1150'
+            onEnded={togglePlay}
+            width={isFullscreen ? '1310' : '1150'}
             height={isFullscreen ? '550' : '380'}
             height={isFullscreen ? '550' : '380'}
-            style={{ objectFit: 'contain',backgroundColor: 'black' }}
-            muted={syncPlay}
-            controls={!syncPlay}
+            style={{ objectFit: 'contain',backgroundColor: 'grey' }}
+            muted
+            controls={false}
           >
           >
-            <source src={topVideoSrc} 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>
         </div>
         </div>
         <div style={{ display: 'flex', justifyContent: 'center', gap: 10 }}>
         <div style={{ display: 'flex', justifyContent: 'center', gap: 10 }}>
-          <video
-            ref={leftVideoRef}
-            width={isFullscreen ? '650' : '570'}
-            height={isFullscreen ? '480' : '240'}
-            style={{ objectFit: 'contain',backgroundColor: 'black' }}
-            muted={syncPlay}
-            controls={!syncPlay}
-          >
-            <source src={leftVideoSrc} type="video/mp4" />
-          </video>
-          <video
-            ref={rightVideoRef}
-            width={isFullscreen ? '650' : '570'}
-            height={isFullscreen ? '480' : '240'}
-            style={{ objectFit: 'contain',backgroundColor: 'black' }}
-            muted={syncPlay}
-            controls={!syncPlay}
-          >
-            <source src={rightVideoSrc} type="video/mp4" />
-          </video>
+          {['left', 'right'].map(pos => (
+            <div key={pos} className="video-wrapper">
+              <video
+                ref={pos === 'left' ? leftVideoRef : rightVideoRef}
+                width={isFullscreen ? '650' : '570'}
+                height={isFullscreen ? '420' : '240'}
+                style={{ objectFit: 'contain', backgroundColor: 'grey' }}
+                muted
+                controls={false}
+              >
+                <source src={videos[pos].src} type="video/mp4" />
+              </video>
+              <div className="video-tag">
+                <Tag color={videos[pos].tagColor}>
+                  {videos[pos].label}
+                </Tag>
+                <Tag
+                  color="orange"
+                  style={{ cursor: 'pointer' }}
+                  onClick={() => handleSwitch(pos)}
+                >
+                  主屏显示
+                </Tag>
+              </div>
+            </div>
+          ))}
         </div>
         </div>
       </div>
       </div>
 
 
       <audio ref={audioRef} src={audioSrc} />
       <audio ref={audioRef} src={audioSrc} />
 
 
       {/* 自定义控制器 */}
       {/* 自定义控制器 */}
-      {syncPlay && (
-        <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 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 VideoPlayer;
+export default SyncVideoPlayer;

+ 18 - 6
src/pages/Play/components/style/index.less

@@ -1,9 +1,3 @@
-ideo-container {
-  position: relative;
-  max-width: 1200px;
-  margin: 0 auto;
-}
-
 .video-controls {
 .video-controls {
   width: 80%;
   width: 80%;
   margin: 0 auto;
   margin: 0 auto;
@@ -52,3 +46,21 @@ ideo-container {
   left: 0;
   left: 0;
   right: 0;
   right: 0;
 }
 }
+
+.video-wrapper {
+  position: relative;
+}
+
+.video-tag {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  z-index: 1;
+  font-size: 14px;
+  padding: 2px 8px;
+  border-radius: 4px;
+}
+
+.top-tag {
+  font-size: 16px;
+}

+ 38 - 38
src/pages/Play/index.tsx

@@ -5,9 +5,8 @@ import React, { useEffect, useState } from 'react';
 import './style/index.less';
 import './style/index.less';
 import { Button, Card, Flex } from 'antd';
 import { Button, Card, Flex } from 'antd';
 import { ArrowLeftOutlined } from '@ant-design/icons';
 import { ArrowLeftOutlined } from '@ant-design/icons';
-import dayjs from 'dayjs';
 import './style/index.less'
 import './style/index.less'
-import VideoPlayer from '@/pages/Play/components/Player';
+import SyncVideoPlayer from '@/pages/Play/components/SyncPlayer';
 import { useLocation } from '@@/exports';
 import { useLocation } from '@@/exports';
 import { getCourseVideos } from '@/services/play/PlayController';
 import { getCourseVideos } from '@/services/play/PlayController';
 
 
@@ -53,47 +52,48 @@ const TableList: React.FC<unknown> = () => {
             </a>
             </a>
           </div>
           </div>
           <div className={'header-right-box'}>
           <div className={'header-right-box'}>
-            <div>授课学科: 数学</div>
-            <div>授课章节: 第一课第一节</div>
-            <div>所属学校: 杭州市滨江区市一中</div>
-            <div>所属年级: 初一</div>
-            <div>授课老师: 张三</div>
-            <div>授课时间: {dayjs().format('YYYY-MM-DD hh:mm:ss')}</div>
+            <div>授课学科: {localStorage.getItem('subjectName')}</div>
+            <div>课程主题: {localStorage.getItem('courseTitle')}</div>
+            <div>所属学校: {localStorage.getItem('schoolName')}</div>
+            <div>所属年级: {localStorage.getItem('periodName')}</div>
+            <div>授课老师: {localStorage.getItem('teacherName')}</div>
+            <div>授课时间: {localStorage.getItem('courseDate')}</div>
           </div>
           </div>
         </div>
         </div>
       </Card>
       </Card>
       <Card className={'video-box'} style={{ marginInline: 20, marginTop: 10 }}>
       <Card className={'video-box'} style={{ marginInline: 20, marginTop: 10 }}>
-        <div style={{ display: 'flex' }}>
-          <Flex vertical gap={10}>
-            <Button
-              color={selected === 'sync' ? "primary" : "default"}
-              variant={selected === 'sync' ? "solid" : "filled"}
-              onClick={() => setSelected('sync')}
-            >
-              同步播放
-            </Button>
-            <Button
-              color={selected === 'single' ? "primary" : "default"}
-              variant={selected === 'single' ? "solid" : "filled"}
-              onClick={() => setSelected('single')}
-            >
-              单路播放
-            </Button>
-          </Flex>
-          <div style={{ flex: 1 }}>
-            {
-              fullClassRoom && (
-                <VideoPlayer
-                  syncPlay={selected === 'sync'}
-                  topVideoSrc={fullClassRoom}
-                  leftVideoSrc={closeUpTeacher}
-                  rightVideoSrc={blackWriting}
-                  audioSrc={audioClass}
-                />
-              )
-            }
+        { fullClassRoom ? (
+          <div style={{ display: 'flex' }}>
+            <Flex vertical gap={10}>
+              <Button
+                color={selected === 'sync' ? "primary" : "default"}
+                variant={selected === 'sync' ? "solid" : "filled"}
+                onClick={() => setSelected('sync')}
+              >
+                同步播放
+              </Button>
+              <Button
+                color={selected === 'single' ? "primary" : "default"}
+                variant={selected === 'single' ? "solid" : "filled"}
+                onClick={() => setSelected('single')}
+              >
+                单路播放
+              </Button>
+            </Flex>
+            <div style={{ flex: 1 }}>
+              <SyncVideoPlayer
+                topVideoSrc={fullClassRoom}
+                leftVideoSrc={closeUpTeacher}
+                rightVideoSrc={blackWriting}
+                audioSrc={audioClass}
+              />
+            </div>
           </div>
           </div>
-        </div>
+        ) : (
+          <Flex justify="center" align="center" style={{ height: 'calc(100vh - 280px)' }}>
+            <h1>暂无视频资源</h1>
+          </Flex>
+        )}
       </Card>
       </Card>
     </PageContainer>
     </PageContainer>
   );
   );

+ 4 - 0
src/pages/Record/index.tsx

@@ -91,6 +91,10 @@ const TableList: React.FC<unknown> = () => {
             style={{ backgroundColor: '#F8FAFC', cursor: 'pointer' }}
             style={{ backgroundColor: '#F8FAFC', cursor: 'pointer' }}
             onClick={() => {
             onClick={() => {
               history.push('/play?teacherId=' + teacherId + '&courseId=' + item.id);
               history.push('/play?teacherId=' + teacherId + '&courseId=' + item.id);
+              localStorage.setItem('periodName', item.periodName);
+              localStorage.setItem('courseTitle', item.courseTitle);
+              localStorage.setItem('subjectName', item.subjectName);
+              localStorage.setItem('courseDate', item.courseDate);
             }}
             }}
           >
           >
             <Image
             <Image