Pārlūkot izejas kodu

fix:放通统一登录

longyifan 5 mēneši atpakaļ
vecāks
revīzija
6aae675634

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
src/assets/img/error_assets.svg


BIN
src/assets/logo.png


+ 23 - 9
src/layouts/SideMenu.tsx

@@ -10,7 +10,8 @@ import { history } from '@@/core/history';
 import { useLocation, useModel } from '@@/exports';
 import { getTeachers } from '@/services/side/SideController';
 import { getTeacherCourses } from '@/services/record/RecordController';
-import CourseImage from '@/assets/img/course_image.png';
+
+import ErrorImg from '@/assets/img/error_assets.svg';
 
 
 interface SideMenuProps {
@@ -215,7 +216,8 @@ function SideMenu  ({
                       style={{
                         backgroundColor: '#F8FAFC',
                         cursor: 'pointer',
-                        marginBottom: '10px'
+                        marginBottom: '10px',
+                        borderRadius: '10px 10px 0 0' 
                       }}
                       onClick={() => {
                         history.push(`/play?teacherId=${item.teacherId}&courseId=${item.id}`);
@@ -234,13 +236,25 @@ function SideMenu  ({
                       }}
                       className={item.id === activeCourseKey ? 'course-active' : ''}
                     >
-                      <Image
-                        width={'100%'}
-                        style={{ aspectRatio: '16/9', borderRadius: '10px 10px 0 0' }}
-                        preview={false}
-                        src={CourseImage}
-                        // src={item.fullScreenOssUrl + '?x-oss-process=video/snapshot,t_0,f_jpg,w_800,h_450'}
-                      />
+                      {
+                        item.fullScreenOssUrl ? 
+                        <video
+                          width={'100%'}
+                          style={{ aspectRatio: '16/9', borderRadius: '10px 10px 0 0' }}
+                          controls={false}
+                          preload="metadata"
+                          onError={()=> {
+                            console.log('error load');
+                          }}
+                          disablePictureInPicture
+                          src={item.fullScreenOssUrl}
+                        /> : <Image
+                          width={'100%'}
+                          style={{ aspectRatio: '16/9', borderRadius: '10x 10px 0 0' }}
+                          preview={false}
+                          src={ErrorImg}
+                        /> 
+                      }
                       <div className="course-info">
                         <div style={{ fontWeight: 'bold', fontSize: '18px' }}>{item.courseTitle}</div>
                         <div className="info-flex">

+ 2 - 2
src/layouts/index.tsx

@@ -78,9 +78,9 @@ const App: React.FC = () => {
           <Popover placement="bottom" content={<UserPopover />}>
             <div className="header-user">
               <Avatar style={{ backgroundColor: '#fde3cf', color: '#f56a00' }}>
-                {userInfo.expertName.charAt(0)}
+                {userInfo.expertName?.charAt(0)}
               </Avatar>
-              <span className="header-user-name"> {userInfo.expertName}</span>
+              <span className="header-user-name"> {userInfo.expertName || ''}</span>
             </div>
           </Popover>
         </Header>

+ 54 - 16
src/pages/Login/index.tsx

@@ -1,24 +1,62 @@
-import { useNavigate } from '@umijs/max';
-import React from "react";
-import {Button, Flex} from "antd";
+import { useLocation, useNavigate } from '@umijs/max';
+import React, { useEffect, useState } from "react";
+import {Button, Flex, Spin} from "antd";
+import Logo from '@/assets/logo.png'
+import './styles/index.less'
 
 
 const LoginPage: React.FC = () => {
+  const redirect_url = window.location.host+"/login/callback";
+  
   const navigate = useNavigate();
+  const location = useLocation();
+  useEffect(()=> {
+    const queryParams = new URLSearchParams(location.search);
+    const sessionId = queryParams.get('sessionId');
+   
+    
+    if(sessionId) {
+      navigate('/login/callback?sessionId='+sessionId)
+    } else {
+      // 设置3秒后自动跳转(实际项目中替换为真实的认证逻辑)
+      const timer = setTimeout(() => {
+        window.location.href = "https://szkj.zjer.cn/hlwxx/webAuthorize?redirect_url="+redirect_url;
+      }, 3000);
+      return () => clearTimeout(timer);
+      
+    }
+  })
+  const [showLoader, setShowLoader] = useState<boolean>(true);
+  
   return (
-    <Flex style={{ height: '100vh' }} justify="center" align="center" gap="middle" vertical>
-      <h1>统一登录(没有测试账号,待提供)</h1>
-      <Button
-        style={{ width: "200px" }}
-        color="primary"
-        variant="solid"
-        onClick={() => {
-          navigate('/login/callback?sessionId=xxxxxxxx')
-        }}
-      >
-        登录
-      </Button>
-    </Flex>
+    <div className="unified-login-container">
+      {showLoader && (
+        <div className="brand-loader">
+          <div className="loader-logo">
+            <img src={Logo} alt="课堂实录-专家评审" />
+          </div>
+          <Spin 
+            size="large" 
+          />
+          <div className="security-badge">
+            <span>正在安全跳转到统一登录平台...</span>
+          </div>
+        </div>
+      )}
+    </div>
+    // <Flex style={{ height: '100vh' }} justify="center" align="center" gap="middle" vertical>
+    //   <h1>统一登录(没有测试账号,待提供)</h1>
+    //   <Button
+    //     style={{ width: "200px" }}
+    //     color="primary"
+    //     variant="solid"
+    //     onClick={() => {
+    //       navigate('/login/callback?sessionId=xxxxxxxx')
+    //     }}
+    //   >
+    //     登录
+    //   </Button>
+    // </Flex>
   );
 }
 

+ 46 - 0
src/pages/Login/styles/index.less

@@ -0,0 +1,46 @@
+/* BrandLoader.css */
+.unified-login-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: #f8f9fa;
+  z-index: 9999;
+}
+
+.brand-loader {
+  text-align: center;
+  padding: 40px;
+  border-radius: 8px;
+}
+
+.loader-logo {
+  margin-bottom: 24px;
+}
+
+.loader-logo img {
+  height: 48px;
+}
+
+.custom-tip {
+  color: #666;
+  font-size: 16px;
+  margin-top: 16px;
+  display: inline-block;
+}
+
+.security-badge {
+  margin-top: 24px;
+  font-size: 14px;
+  color: #888;
+}
+
+.security-badge span {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}

+ 9 - 1
src/pages/LoginCallback/index.tsx

@@ -1,7 +1,7 @@
 import { useNavigate } from "@umijs/max";
 import React, { useEffect } from 'react';
 import * as LoginController from '@/services/login/LoginController';
-import { Flex, Spin } from 'antd';
+import { Flex, Spin, message } from 'antd';
 import {useLocation} from "@@/exports";
 
 
@@ -18,6 +18,13 @@ const LoginPage: React.FC = () => {
         localStorage.setItem('token', data.data.accessToken as string)
         localStorage.setItem('userInfo', JSON.stringify(data.data.userInfo))
         navigate('/');
+      } else {
+        message.error('专家用户不存在');
+        const redirect_url = window.location.host+"/login/callback";
+        const timer = setTimeout(() => {
+          clearTimeout(timer);
+          window.location.href = "https://szkj.zjer.cn/hlwxx/webAuthorize?redirect_url="+redirect_url;
+        }, 3000);
       }
     }
   };
@@ -29,6 +36,7 @@ const LoginPage: React.FC = () => {
   return (
     <Flex style={{ height: '100vh' }} justify="center" align="center" gap="middle" vertical>
       <Spin size="large" />
+      <div>正在登录中...</div>
     </Flex>
   );
 }

+ 5 - 8
src/pages/Play/components/SinglePlayer.tsx

@@ -172,18 +172,15 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
             onEnded={handleEnded}
             onTimeUpdate={handleTimeUpdate}
             onLoadedMetadata={() => setDuration(videoRef.current?.duration || 0)}
-            width="80%"
-            height={isFullscreen ? '950' : '580'}
-            style={{ objectFit: 'contain', backgroundColor: 'grey' }}
-            muted
-            controls={false}
+            style={{ objectFit: 'contain', backgroundColor: '#000',width: '100%', height: 'calc((100vh - (88px + 64px + 58px)))'  }}
+            controls={true}
             disablePictureInPicture
           />
         </div>
 
-        <audio ref={audioRef} src={audioSrc} />
+        {/* <audio ref={audioRef} src={audioSrc} /> */}
 
-        <div className="video-controls">
+        {/* <div className="video-controls">
           <Button onClick={togglePlay}>
             {isPlaying ? '❚❚' : '▶'}
           </Button>
@@ -213,7 +210,7 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
           <Button onClick={toggleFullscreen}>
             {isFullscreen ? '⤢' : '⤡'}
           </Button>
-        </div>
+        </div> */}
       </div>
     </div>
   );

+ 242 - 18
src/pages/Play/components/SyncPlayer.tsx

@@ -42,16 +42,28 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
     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') => {
     setVideos((prev: VideoState) => ({
       top: prev[position],
       left: position === 'left' ? prev.top : prev.left,
       right: position === 'right' ? prev.top : prev.right
     }));
-    leftVideoRef.current?.load();
-    rightVideoRef.current?.load();
-    topVideoRef.current?.load();
-    audioRef.current?.load();
   };
 
   const [isPlaying, setIsPlaying] = useState(false);
@@ -61,6 +73,26 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
   const [isFullscreen, setIsFullscreen] = 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 minutes = Math.floor(time / 60);
     const seconds = Math.floor(time % 60);
@@ -91,6 +123,10 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
     setIsPlaying(shouldPlay);
   };
 
+  useEffect(()=> {
+    isPlayingRef.current = isPlaying
+  }, [isPlaying])
+
   const handleTimeUpdate = () => {
     if (!isSeeking && topVideoRef.current) {
       setCurrentTime(topVideoRef.current.currentTime);
@@ -98,6 +134,7 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
   };
 
   const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setIsPlaying(false)
     const newTime = parseFloat(e.target.value);
     setCurrentTime(newTime);
     syncMedia(newTime);
@@ -140,21 +177,199 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
 
   useEffect(() => {
     const topVideo = topVideoRef.current;
+    const leftVideo = leftVideoRef.current;
+    const rightVideo = rightVideoRef.current;
+    if(!topVideo || !leftVideo || !rightVideo) {
+      return;
+    }
     const handleLoadedMetadata = () => {
       if (topVideo) {
         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 () => {
       if (topVideo) {
+        topVideo.removeEventListener('error', onTopError);
         topVideo.removeEventListener('loadedmetadata', handleLoadedMetadata);
         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 (
     <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">
           <video
             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}
+            preload="auto"
             disablePictureInPicture
           >
             <source src={videos.top.src} type="video/mp4" />
           </video>
           <Tag className="video-tag top-tag" color={videos.top.tagColor}>{videos.top.label}</Tag>
         </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">
               <video
+                preload="auto"
                 ref={pos === 'left' ? leftVideoRef : rightVideoRef}
                 // 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
                 controls={false}
                 disablePictureInPicture
@@ -234,13 +450,21 @@ const SyncVideoPlayer: React.FC<SyncVideoPlayerProps> = ({
         </div>
       </div>
 
-      <audio ref={audioRef} src={audioSrc} />
+      {/* <audio ref={audioRef} src={audioSrc} /> */}
 
       {/* 自定义控制器 */}
       <div className="video-controls">
-        <Button onClick={togglePlay}>
-          {isPlaying ? '❚❚' : '▶'}
-        </Button>
+        {
+          waiting ? (
+            <div className="play-button-loader"></div>
+          ) : (
+            <Button onClick={togglePlay}>
+              {
+                isPlaying ? '❚❚' : '▶' 
+              }
+            </Button>
+          )
+        }
         <input
           type="range"
           min="0"

+ 28 - 1
src/pages/Play/components/style/index.less

@@ -1,5 +1,5 @@
 .video-controls {
-  width: 80%;
+  width: 100%;
   margin: 0 auto;
   display: flex;
   align-items: center;
@@ -49,6 +49,7 @@
 
 .video-wrapper {
   position: relative;
+  width: 100%;
 }
 
 .video-tag {
@@ -68,4 +69,30 @@
   display: flex;
   justify-content: center;
   gap: 20px;
+}
+
+
+
+.play-button-loader {
+  width: 30px;
+  height: 30px;
+  position: relative;
+  margin-right: 13px;
+}
+
+.play-button-loader::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  border: 3px solid rgba(255, 255, 255, 0.3);
+  border-radius: 50%;
+  border-top-color: #fff;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
 }

+ 3 - 2
src/pages/Play/index.tsx

@@ -21,6 +21,7 @@ const TableList: React.FC<unknown> = () => {
 
   useEffect(() => {
     if(location.search) {
+      
       const searchParams = new URLSearchParams(location.search);
       const onData = async () => {
         const data: API.PlayRes = await getCourseVideos({
@@ -28,10 +29,10 @@ const TableList: React.FC<unknown> = () => {
           teacherCourseId: searchParams.get('courseId') as string,
         })
         if (data.code === 200) {
+          
           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);
         }
       }
       onData()
@@ -61,7 +62,7 @@ const TableList: React.FC<unknown> = () => {
                 单路播放
               </Button>
             </Flex>
-            <div style={{ flex: 1 }}>
+            <div style={{ flex: 1, marginLeft: 12}}>
               {selected === 'single' ? (
                 <SingleVideoPlayer
                   fullClassRoomSrc={fullClassRoom}

+ 22 - 7
src/pages/Record/index.tsx

@@ -9,6 +9,7 @@ import { history } from "@umijs/max";
 import { useLocation, useModel, useRouteProps } from '@@/exports';
 import { getExpertActives, getSelectSubject, getTeacherCourses } from '@/services/record/RecordController';
 import { ActionType } from '@ant-design/pro-table/lib';
+import ErrorImg from '@/assets/img/error_assets.svg';
 
 const TableList: React.FC<unknown> = () => {
   const routeProps = useRouteProps();
@@ -83,7 +84,7 @@ const TableList: React.FC<unknown> = () => {
         }}
         renderItem={(item) => (
           <div
-            style={{ backgroundColor: '#F8FAFC', cursor: 'pointer' }}
+            style={{ backgroundColor: '#F8FAFC', cursor: 'pointer',borderRadius: '15px 15px 0 0' }}
             onClick={() => {
               history.push('/play?teacherId=' + teacherId + '&courseId=' + item.id);
               setCourseInfo({
@@ -98,12 +99,26 @@ const TableList: React.FC<unknown> = () => {
               localStorage.setItem('courseDate', `${item.courseDate.split(' ')[0]}(${item.lessonStartTime} ~ ${item.lessonEndTime})`);
             }}
           >
-            <Image
-              width={'100%'}
-              style={{ aspectRatio: '16/9', borderRadius: '15px 15px 0 0' }}
-              preview={false}
-              src={item.fullScreenOssUrl + '?x-oss-process=video/snapshot,t_0,f_jpg,w_800,h_450'}
-            />
+            {
+              item.fullScreenOssUrl ? 
+              <video
+                width={'100%'}
+                style={{ aspectRatio: '16/9', borderRadius: '15px 15px 0 0' }}
+                controls={false}
+                preload="metadata"
+                onError={()=> {
+                  console.log('error load');
+                }}
+                disablePictureInPicture
+                src={item.fullScreenOssUrl}
+              /> : <Image
+                width={'100%'}
+                style={{ aspectRatio: '16/9', borderRadius: '15px 15px 0 0' }}
+                preview={false}
+                src={ErrorImg}
+              /> 
+            }
+            
             <div className="course-info">
               <div className="info-flex">
                 <div style={{ fontWeight: 'bold', fontSize: '18px' }}>{item.courseTitle}</div>