ZW\zwf97 пре 6 месеци
родитељ
комит
e645d15155

+ 10 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -2,5 +2,15 @@
   <profile version="1.0">
     <option name="myName" value="Project Default" />
     <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="myValues">
+        <value>
+          <list size="1">
+            <item index="0" class="java.lang.String" itemvalue="tsx" />
+          </list>
+        </value>
+      </option>
+      <option name="myCustomValuesEnabled" value="true" />
+    </inspection_tool>
   </profile>
 </component>

+ 155 - 71
src/pages/Play/components/Player.tsx

@@ -1,117 +1,201 @@
 
 import React, { useRef, useState, useEffect } from 'react';
+import './style/index.less';
 import { Button } from 'antd';
 
-interface VideoSyncPlayerProps {
-  videoUrls: string[];
+interface VideoPlayerProps {
+  topVideoSrc: string;
+  leftVideoSrc: string;
+  rightVideoSrc: string;
+  audioSrc: string;
 }
 
-const VideoSyncPlayer: React.FC<VideoSyncPlayerProps> = ({ videoUrls }) => {
-  const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
-  const [progress, setProgress] = useState(0);
+const VideoPlayer: React.FC<VideoPlayerProps> = ({
+   topVideoSrc,
+   leftVideoSrc,
+   rightVideoSrc,
+   audioSrc
+ }) => {
+  const topVideoRef = useRef<HTMLVideoElement>(null);
+  const leftVideoRef = useRef<HTMLVideoElement>(null);
+  const rightVideoRef = 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 syncVideos = () => {
-    const mainVideo = videoRefs.current[0];
-    if (!mainVideo) return;
-
-    const currentTime = mainVideo.currentTime;
-    setProgress((currentTime / duration) * 100);
-
-    videoRefs.current.forEach(video => {
-      if (video && video !== mainVideo) {
-        video.currentTime = currentTime;
-        if (isPlaying) {
-          video.play()
-        } else {
-          video.pause()
-        }
-      }
+  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) => {
+    [topVideoRef, leftVideoRef, rightVideoRef].forEach(ref => {
+      if (ref.current) ref.current.currentTime = time;
     });
+    if (audioRef.current) audioRef.current.currentTime = time;
   };
 
-  // 处理播放/暂停
   const togglePlay = () => {
-    setIsPlaying(!isPlaying);
+    const shouldPlay = !isPlaying;
+    [topVideoRef, leftVideoRef, rightVideoRef].forEach(ref => {
+      if (shouldPlay) {
+        ref.current?.play()
+      } else {
+        ref.current?.pause()
+      }
+    });
+    if (shouldPlay) {
+      audioRef.current?.play()
+    } else {
+      audioRef.current?.pause()
+    }
+    setIsPlaying(shouldPlay);
+  };
+
+  const handleTimeUpdate = () => {
+    if (!isSeeking && topVideoRef.current) {
+      setCurrentTime(topVideoRef.current.currentTime);
+    }
   };
 
-  // 处理进度条拖动
   const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
-    const seekTime = (Number(e.target.value) / 100) * duration;
-    videoRefs.current.forEach(video => {
-      if (video) video.currentTime = seekTime;
-    });
+    const newTime = parseFloat(e.target.value);
+    setCurrentTime(newTime);
+    syncMedia(newTime);
   };
 
-  // 初始化视频设置
-  useEffect(() => {
-    const mainVideo = videoRefs.current[0];
-    if (!mainVideo) return;
+  const handleSeekMouseDown = () => {
+    setIsSeeking(true);
+  };
 
+  const handleSeekMouseUp = () => {
+    setIsSeeking(false);
+    if (isPlaying) {
+      [topVideoRef, leftVideoRef, rightVideoRef].forEach(ref => ref.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);
+  };
+
+  useEffect(() => {
+    const topVideo = topVideoRef.current;
     const handleLoadedMetadata = () => {
-      setDuration(mainVideo.duration);
-      if (videoRefs.current.every(v => v!.readyState > 2)) {
-        setIsPlaying(true);
+      if (topVideo) {
+        setDuration(topVideo.duration);
       }
     };
 
-    mainVideo.addEventListener('loadedmetadata', handleLoadedMetadata);
+    if (topVideo) {
+      topVideo.addEventListener('loadedmetadata', handleLoadedMetadata);
+      topVideo.addEventListener('timeupdate', handleTimeUpdate);
+    }
+
     return () => {
-      mainVideo.removeEventListener('loadedmetadata', handleLoadedMetadata);
+      if (topVideo) {
+        topVideo.removeEventListener('loadedmetadata', handleLoadedMetadata);
+        topVideo.removeEventListener('timeupdate', handleTimeUpdate);
+      }
     };
   }, []);
 
   return (
-    <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
-      {/* 主视频区域 */}
-      <div>
-        <video
-          ref={el => videoRefs.current[0] = el}
-          width="1150"
-          height="475"
-          style={{ objectFit: 'cover' }}
-          muted
-          onTimeUpdate={syncVideos}
-        >
-          <source src={videoUrls[0]} type="video/mp4" />
-        </video>
-      </div>
-
-      {/* 副视频区域 */}
-      <div style={{ display: 'flex', justifyContent: 'center', gap: 25 }}>
-        {[1, 2].map(index => (
+    <div className={`video-container ${isFullscreen ? 'fullscreen' : ''}`}>
+      <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
+        <div>
+          <video
+            ref={topVideoRef}
+            width="1150"
+            height="475"
+            style={{ objectFit: 'cover' }}
+            muted
+          >
+            <source src={topVideoSrc} type="video/mp4" />
+          </video>
+        </div>
+        <div style={{ display: 'flex', justifyContent: 'center', gap: 25 }}>
           <video
-            key={index}
-            ref={el => videoRefs.current[index] = el}
-            width="565"
-            height="375"
+            ref={leftVideoRef}
+            width={isFullscreen ? '640' : '565'}
+            height={isFullscreen ? '480' : '375'}
             style={{ objectFit: 'cover' }}
             muted
-            onTimeUpdate={syncVideos}
           >
-            <source src={videoUrls[index]} type="video/mp4" />
+            <source src={leftVideoSrc} type="video/mp4" />
           </video>
-        ))}
+          <video
+            ref={rightVideoRef}
+            width={isFullscreen ? '640' : '565'}
+            height={isFullscreen ? '480' : '375'}
+            style={{ objectFit: 'cover' }}
+            muted
+          >
+            <source src={rightVideoSrc} type="video/mp4" />
+          </video>
+        </div>
       </div>
 
-      {/* 控制区域 */}
-      <div style={{ padding: 20, display: 'flex', flexDirection: 'column' }}>
-        <Button onClick={togglePlay} style={{ marginBottom: 10 }}>
-          {isPlaying ? '暂停' : '播放'}
+      <audio ref={audioRef} src={audioSrc} />
+
+      {/* 自定义控制器 */}
+      <div className="video-controls">
+        <Button onClick={togglePlay}>
+          {isPlaying ? '❚❚' : '▶'}
         </Button>
         <input
           type="range"
           min="0"
-          max="100"
-          value={progress}
+          max={duration || 100}
+          value={currentTime}
           onChange={handleSeek}
-          style={{ width: '100%' }}
+          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>
   );
 };
 
-export default VideoSyncPlayer;
+export default VideoPlayer;

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

@@ -0,0 +1,52 @@
+ideo-container {
+  position: relative;
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.video-controls {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+}
+
+.video-controls button {
+  background: none;
+  border: none;
+  color: white;
+  font-size: 18px;
+  cursor: pointer;
+  margin: 0 5px;
+}
+
+.progress {
+  flex-grow: 1;
+  margin: 0 10px;
+}
+
+.volume {
+  width: 80px;
+  margin: 0 10px;
+}
+
+.fullscreen {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background: black;
+  z-index: 9999;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.fullscreen .video-controls {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}

+ 26 - 18
src/pages/Play/index.tsx

@@ -7,6 +7,7 @@ import { Card } from 'antd';
 import { ArrowLeftOutlined } from '@ant-design/icons';
 import dayjs from 'dayjs';
 import './style/index.less'
+import VideoPlayer from '@/pages/Play/components/Player';
 // import ReactPlayer from 'react-player'
 
 const TableList: React.FC<unknown> = () => {
@@ -17,10 +18,11 @@ const TableList: React.FC<unknown> = () => {
           <div>
             <a
               onClick={() => {
-                history.go(-1)
+                history.go(-1);
               }}
             >
-              <ArrowLeftOutlined />课程播放
+              <ArrowLeftOutlined />
+              课程播放
             </a>
           </div>
           <div className={'header-right-box'}>
@@ -36,25 +38,31 @@ const TableList: React.FC<unknown> = () => {
       <Card style={{ marginInline: 20, marginTop: 10 }}>
         <div style={{ display: 'flex' }}>
           <div>左侧切换播放模式</div>
-          <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
-            <div>
-              <video width="1150" height="475" style={{ objectFit: 'cover' }} autoPlay>
-                <source src="http://player.alicdn.com/video/aliyunmedia.mp4" type="video/ogg" />
-              </video>
-            </div>
-            <div style={{ display: 'flex', justifyContent:'center', gap: 25 }}>
-              <video width="565" height="375" style={{ objectFit: 'cover' }} autoPlay>
-                <source src="http://player.alicdn.com/video/aliyunmedia.mp4" type="video/ogg" />
-              </video>
-              <video width="565" height="375" style={{ objectFit: 'cover' }} autoPlay>
-                <source src="http://player.alicdn.com/video/aliyunmedia.mp4" type="video/ogg" />
-              </video>
-            </div>
-          </div>
+          {/*<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>*/}
+          {/*  <div>*/}
+          {/*    <video width="1150" height="475" style={{ objectFit: 'cover' }} autoPlay>*/}
+          {/*      <source src="http://player.alicdn.com/video/aliyunmedia.mp4" type="video/ogg" />*/}
+          {/*    </video>*/}
+          {/*  </div>*/}
+          {/*  <div style={{ display: 'flex', justifyContent:'center', gap: 25 }}>*/}
+          {/*    <video width="565" height="375" style={{ objectFit: 'cover' }} autoPlay>*/}
+          {/*      <source src="http://player.alicdn.com/video/aliyunmedia.mp4" type="video/ogg" />*/}
+          {/*    </video>*/}
+          {/*    <video width="565" height="375" style={{ objectFit: 'cover' }} autoPlay>*/}
+          {/*      <source src="http://player.alicdn.com/video/aliyunmedia.mp4" type="video/ogg" />*/}
+          {/*    </video>*/}
+          {/*  </div>*/}
+          {/*</div>*/}
+          <VideoPlayer
+            topVideoSrc={'http://player.alicdn.com/video/aliyunmedia.mp4'}
+            leftVideoSrc={'http://player.alicdn.com/video/aliyunmedia.mp4'}
+            rightVideoSrc={'http://player.alicdn.com/video/aliyunmedia.mp4'}
+            audioSrc={''}
+          />
         </div>
       </Card>
     </PageContainer>
-  )
+  );
 };
 
 export default TableList;