|
|
@@ -1,9 +1,8 @@
|
|
|
<template>
|
|
|
<div class="video-component">
|
|
|
- <!-- 注意:移除了 :src="url",因为 src 将由 JS 动态绑定 -->
|
|
|
<video
|
|
|
ref="videoRef"
|
|
|
- autoplay controls muted
|
|
|
+ controls
|
|
|
preload="metadata"
|
|
|
class="video-player"
|
|
|
@timeupdate="onTimeUpdate"
|
|
|
@@ -11,6 +10,7 @@
|
|
|
@pause="saveProgress"
|
|
|
@ended="onEnded"
|
|
|
></video>
|
|
|
+ <div v-if="videoError" class="video-error-text">视频加载失败,请检查网络或联系管理员</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -34,7 +34,10 @@
|
|
|
data() {
|
|
|
return {
|
|
|
_saveTimer: null,
|
|
|
- hls: null, // 新增:存储 hls 实例
|
|
|
+ _stallTimer: null,
|
|
|
+ _lastTime: 0,
|
|
|
+ hls: null,
|
|
|
+ videoError: false,
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -43,57 +46,98 @@
|
|
|
return `${STORAGE_KEY_PREFIX}${this.userId}_${safeUrl}`;
|
|
|
},
|
|
|
},
|
|
|
+ watch: {
|
|
|
+ url(newVal) {
|
|
|
+ if (newVal) {
|
|
|
+ this._stopStallDetector();
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initPlayer();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
mounted() {
|
|
|
this.initPlayer();
|
|
|
},
|
|
|
methods: {
|
|
|
- // 初始化播放器
|
|
|
initPlayer() {
|
|
|
const video = this.$refs.videoRef;
|
|
|
if (!video) return;
|
|
|
|
|
|
- // 1. 优先检测浏览器是否原生支持 HLS (如 Safari, iOS)
|
|
|
+ if (this.hls) {
|
|
|
+ this.hls.destroy();
|
|
|
+ this.hls = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$set(this, 'videoError', false);
|
|
|
+
|
|
|
+ // iOS / Safari 原生支持 HLS
|
|
|
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
|
video.src = this.url;
|
|
|
}
|
|
|
- // 2. 检测是否支持 hls.js (如 Firefox, Chrome)
|
|
|
+ // Chrome / Firefox,用 hls.js 解析
|
|
|
else if (Hls.isSupported()) {
|
|
|
- this.hls = new Hls();
|
|
|
+ this.hls = new Hls({
|
|
|
+ maxBufferLength: 60,
|
|
|
+ maxMaxBufferLength: 120,
|
|
|
+ maxBufferSize: 60 * 1000 * 1000,
|
|
|
+ lowLatencyMode: false,
|
|
|
+ enableWorker: true,
|
|
|
+ });
|
|
|
this.hls.loadSource(this.url);
|
|
|
this.hls.attachMedia(video);
|
|
|
|
|
|
- // 监听 HLS 解析完成事件
|
|
|
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
|
- console.log('HLS 视频源解析完成,准备播放');
|
|
|
+ video.play().catch(() => {});
|
|
|
+ this._startStallDetector(video);
|
|
|
});
|
|
|
|
|
|
- // 监听错误事件
|
|
|
this.hls.on(Hls.Events.ERROR, (event, data) => {
|
|
|
if (data.fatal) {
|
|
|
switch (data.type) {
|
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
|
- console.error('网络错误,尝试恢复...');
|
|
|
this.hls.startLoad();
|
|
|
break;
|
|
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
|
- console.error('媒体解码错误,尝试恢复...');
|
|
|
this.hls.recoverMediaError();
|
|
|
break;
|
|
|
default:
|
|
|
- console.error('发生不可恢复的致命错误');
|
|
|
this.hls.destroy();
|
|
|
+ this.hls = null;
|
|
|
+ this.$set(this, 'videoError', true);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
- // 3. 都不支持
|
|
|
+ // X5 / 老旧 WebView 降级
|
|
|
else {
|
|
|
- console.error('当前浏览器不支持 HLS 视频播放');
|
|
|
+ video.src = this.url;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 卡死检测:有缓冲但 currentTime 不动时,跳过 0.1 秒解除卡死
|
|
|
+ _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) {
|
|
|
+ video.currentTime += 0.1;
|
|
|
+ }
|
|
|
+ this._lastTime = video.currentTime;
|
|
|
+ }, 1000);
|
|
|
+ },
|
|
|
+
|
|
|
+ _stopStallDetector() {
|
|
|
+ if (this._stallTimer) {
|
|
|
+ clearInterval(this._stallTimer);
|
|
|
+ this._stallTimer = null;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 读取上次保存的时间点
|
|
|
getSavedTime() {
|
|
|
try {
|
|
|
const val = localStorage.getItem(this.storageKey);
|
|
|
@@ -103,7 +147,6 @@
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 视频元数据加载完成后跳转到上次位置
|
|
|
onLoadedMetadata() {
|
|
|
const saved = this.getSavedTime();
|
|
|
const video = this.$refs.videoRef;
|
|
|
@@ -112,7 +155,6 @@
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // timeupdate 节流处理
|
|
|
onTimeUpdate() {
|
|
|
if (this._saveTimer) return;
|
|
|
this._saveTimer = setTimeout(() => {
|
|
|
@@ -121,7 +163,6 @@
|
|
|
}, 5000);
|
|
|
},
|
|
|
|
|
|
- // 实际写入 localStorage
|
|
|
saveProgress() {
|
|
|
const video = this.$refs.videoRef;
|
|
|
if (!video) return;
|
|
|
@@ -132,8 +173,8 @@
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 播放结束时清除记录
|
|
|
onEnded() {
|
|
|
+ this._stopStallDetector();
|
|
|
try {
|
|
|
localStorage.removeItem(this.storageKey);
|
|
|
} catch {
|
|
|
@@ -142,15 +183,12 @@
|
|
|
},
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- // 1. 清除保存进度的定时器
|
|
|
+ this._stopStallDetector();
|
|
|
if (this._saveTimer) {
|
|
|
clearTimeout(this._saveTimer);
|
|
|
this._saveTimer = null;
|
|
|
}
|
|
|
- // 2. 销毁前立即保存一次进度
|
|
|
this.saveProgress();
|
|
|
-
|
|
|
- // 3. 【关键】销毁 hls 实例,释放内存并断开网络连接
|
|
|
if (this.hls) {
|
|
|
this.hls.destroy();
|
|
|
this.hls = null;
|
|
|
@@ -162,6 +200,7 @@
|
|
|
<style scoped lang="scss">
|
|
|
.video-component {
|
|
|
width: 100%;
|
|
|
+ position: relative;
|
|
|
|
|
|
.video-player {
|
|
|
width: 1458px;
|
|
|
@@ -170,5 +209,15 @@
|
|
|
background: #000;
|
|
|
border-radius: 6px;
|
|
|
}
|
|
|
+
|
|
|
+ .video-error-text {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ color: #ccc;
|
|
|
+ font-size: 14px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|