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