dedsudiyu 1 week ago
parent
commit
a581be832d
2 changed files with 126 additions and 18 deletions
  1. 4 4
      .env.production
  2. 122 14
      src/views/learning/learningMaterials.vue

+ 4 - 4
.env.production

@@ -24,10 +24,10 @@ VUE_APP_VERSION_DIFFERENCE_FIELD = 'kuangYeDaXue_nanHu'
 # ####################外网接口配置####################
 
 # 贵州大学
-VUE_APP_BASE_API = 'gzu.zjznai.com/api'
+# VUE_APP_BASE_API = 'gzu.zjznai.com/api'
 
 # 1.8外网地址
-# VUE_APP_BASE_API = 'lab.zjznai.com/labTest'
+VUE_APP_BASE_API = 'lab.zjznai.com/labTest'
 
 # 1.88外网地址
 # VUE_APP_BASE_API = 'lab.zjznai.com/appTest'
@@ -41,10 +41,10 @@ VUE_APP_BASE_API = 'gzu.zjznai.com/api'
 # ####################内网接口配置####################
 
 # 贵州大学
-VUE_APP_BASE_LOCAL_API = '172.21.0.2/api'
+# VUE_APP_BASE_LOCAL_API = '172.21.0.2/api'
 
 # 1.8内网地址
-# VUE_APP_BASE_LOCAL_API = '192.168.1.8/api'
+VUE_APP_BASE_LOCAL_API = '192.168.1.8/api'
 
 # 1.88内网地址
 # VUE_APP_BASE_LOCAL_API = '192.168.1.88/labSystem'

+ 122 - 14
src/views/learning/learningMaterials.vue

@@ -9,15 +9,19 @@
     <!-- 课件展示区 -->
     <div class="courseware-box">
       <!-- 视频 -->
-      <video
-        v-if="officeType === 'video'"
-        class="video-player"
-        :src="lookUrl"
-        controls
-        playsinline
-        webkit-playsinline
-        x5-playsinline
-      ></video>
+      <div v-if="officeType === 'video'" class="video-wrap">
+        <video
+          ref="videoRef"
+          class="video-player"
+          controls
+          playsinline
+          webkit-playsinline
+          x5-playsinline
+          x5-video-player-type="h5"
+          x5-video-player-fullscreen="true"
+        ></video>
+        <div v-if="videoError" class="video-error-text">视频加载失败,请检查网络或联系管理员</div>
+      </div>
       <!-- 富文本/文章 -->
       <div v-else-if="officeType === 'text'" class="rich-text-box" v-html="lookUrl"></div>
       <!-- 文档(docx/excel/pdf) -->
@@ -91,6 +95,7 @@
   </div>
 </template>
 <script>
+  import Hls from 'hls.js'
   import {judgmentNetworkReturnAddress} from "@/utils/ruoyi";
   import VueOfficeDocx from '@vue-office/docx'
   import '@vue-office/docx/lib/index.css'
@@ -140,6 +145,11 @@
         // 定时器
         studyTimer: null,
         heartbeatValue: 30,
+        // 视频播放器
+        hls: null,
+        videoError: false,
+        _stallTimer: null,
+        _lastTime: 0,
       }
     },
     mounted() {
@@ -176,9 +186,13 @@
       // 课件渲染
       renderCourseware(data) {
         this.$set(this, 'officeNullType', false)
+        this.$set(this, 'videoError', false)
         if (data.materialType === 1) {
           this.$set(this, 'officeType', 'video')
           this.$set(this, 'lookUrl', localStorage.getItem('fileBrowseEnvironment')+'/'+data.attachmentPath)
+          this.$nextTick(() => {
+            this.initVideoPlayer(this.lookUrl)
+          })
         } else if (data.materialType === 3) {
           let ext = data.attachmentType
           if (ext === '.docx') {
@@ -202,7 +216,76 @@
       errorHandler() {
         this.$set(this, 'officeNullType', true)
       },
-      // 返回 - 小程序 navigateBack
+      // 初始化视频播放器(三段式兼容:iOS原生 → hls.js → X5兜底)
+      initVideoPlayer(url) {
+        const video = this.$refs.videoRef
+        if (!video) return
+        // 销毁旧实例
+        if (this.hls) {
+          this.hls.destroy()
+          this.hls = null
+        }
+        if (video.canPlayType('application/vnd.apple.mpegurl')) {
+          // iOS / Safari 原生支持 HLS
+          video.src = url
+        } else if (Hls.isSupported()) {
+          // Chrome / Firefox,用 hls.js 解析
+          this.hls = new Hls({
+            maxBufferLength: 60,
+            maxMaxBufferLength: 120,
+            maxBufferSize: 60 * 1000 * 1000,
+            lowLatencyMode: false,
+            enableWorker: true,
+          })
+          this.hls.loadSource(url)
+          this.hls.attachMedia(video)
+          this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
+            video.play().catch(() => {})
+            this._startStallDetector(video)
+          })
+          this.hls.on(Hls.Events.ERROR, (_, data) => {
+            if (data.fatal) {
+              switch (data.type) {
+                case Hls.ErrorTypes.NETWORK_ERROR:
+                  this.hls.startLoad()
+                  break
+                case Hls.ErrorTypes.MEDIA_ERROR:
+                  this.hls.recoverMediaError()
+                  break
+                default:
+                  this.hls.destroy()
+                  this.hls = null
+                  this.$set(this, 'videoError', true)
+                  break
+              }
+            }
+          })
+        } else {
+          // X5 / 老旧 WebView 降级:直接设 src 赌原生支持
+          video.src = url
+        }
+      },
+      // 卡死检测:进度条走但 currentTime 不动时,强制 seek 跳过卡点
+      _startStallDetector(video) {
+        this._stopStallDetector()
+        this._lastTime = video.currentTime
+        this._stallTimer = setInterval(() => {
+          if (video.paused || video.ended) return
+          const bufferedEnd = video.buffered.length ? video.buffered.end(video.buffered.length - 1) : 0
+          const hasBuffer = bufferedEnd - video.currentTime > 1
+          if (hasBuffer && video.currentTime === this._lastTime) {
+            // 有缓冲但 currentTime 没动,解码器卡死,跳过 0.1 秒
+            video.currentTime += 0.1
+          }
+          this._lastTime = video.currentTime
+        }, 1000)
+      },
+      _stopStallDetector() {
+        if (this._stallTimer) {
+          clearInterval(this._stallTimer)
+          this._stallTimer = null
+        }
+      },
       backPage() {
         if (window.wx && window.wx.miniProgram) {
           window.wx.miniProgram.navigateBack({ delta: 1 })
@@ -321,6 +404,11 @@
         this.studyTimer.destroy()
         this.studyTimer = null
       }
+      this._stopStallDetector()
+      if (this.hls) {
+        this.hls.destroy()
+        this.hls = null
+      }
     },
   }
 </script>
@@ -373,13 +461,33 @@
       overflow: hidden;
       background: #000;
 
-      .video-player {
-        width: 100%;
-        height: 100%;
-        object-fit: contain;
+      .video-wrap {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        position: relative;
         background: #000;
+        min-height: 0;
+        .video-player {
+          width: 100%;
+          flex: 1;
+          min-height: 0;
+          object-fit: contain;
+          background: #000;
+          display: block;
+        }
+        .video-error-text {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          color: #ccc;
+          font-size: 14px;
+          text-align: center;
+        }
       }
 
+
       .rich-text-box {
         flex: 1;
         overflow-y: auto;