feat:完善监控播放器
This commit is contained in:
325
src/components/HlsPlayer/index.vue
Normal file
325
src/components/HlsPlayer/index.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<div v-show="isActive" class="myVideo-container">
|
||||
<video
|
||||
class="myVideo"
|
||||
ref="videoElement"
|
||||
muted
|
||||
:style="videoStyle"
|
||||
playsinline
|
||||
:controls="false"
|
||||
disablePictureInPicture
|
||||
></video>
|
||||
<div v-if="loading" class="loading-overlay pointer-events-none">
|
||||
<div class="loading-text">{{ loadingText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Hls from 'hls.js'
|
||||
|
||||
export default {
|
||||
name: 'HlsPlayer',
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: '100%'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hls: null,
|
||||
video: null,
|
||||
loading: false,
|
||||
loadingText: '加载中...',
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
isReady: false,
|
||||
playAttempts: 0,
|
||||
maxPlayAttempts: 3,
|
||||
cleanupTimeout: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
videoStyle() {
|
||||
const style = {}
|
||||
if (this.width) {
|
||||
style.width = typeof this.width === 'number' ? `${this.width}px` : this.width
|
||||
}
|
||||
if (this.height) {
|
||||
style.height = typeof this.height === 'number' ? `${this.height}px` : this.height
|
||||
}
|
||||
return style
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isActive: {
|
||||
handler(newValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(this.initializePlayer)
|
||||
} else {
|
||||
this.immediateCleanup()
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
url(newUrl) {
|
||||
if (newUrl && this.isActive) {
|
||||
this.initializePlayer()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.video = this.$refs.videoElement
|
||||
this.registerVideoEvents()
|
||||
},
|
||||
methods: {
|
||||
registerVideoEvents() {
|
||||
const events = ['canplay', 'waiting', 'playing', 'error', 'stalled', 'suspend']
|
||||
|
||||
events.forEach((event) => {
|
||||
this.video.addEventListener(event, this.handleVideoEvent)
|
||||
})
|
||||
},
|
||||
|
||||
handleVideoEvent(event) {
|
||||
switch (event.type) {
|
||||
case 'canplay':
|
||||
this.isReady = true
|
||||
break
|
||||
case 'waiting':
|
||||
this.showLoadingIndicator('缓冲中...')
|
||||
break
|
||||
case 'playing':
|
||||
this.loading = false
|
||||
break
|
||||
case 'error':
|
||||
this.handlePlaybackError()
|
||||
break
|
||||
case 'stalled':
|
||||
this.showLoadingIndicator('播放卡顿...')
|
||||
break
|
||||
case 'suspend':
|
||||
this.cleanupNetworkResources()
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
immediateCleanup() {
|
||||
// 立即停止网络请求
|
||||
if (this.hls) {
|
||||
this.hls.stopLoad()
|
||||
this.hls.detachMedia()
|
||||
}
|
||||
|
||||
// 延迟完全清理以避免播放卡顿
|
||||
this.cleanupTimeout = setTimeout(() => {
|
||||
// 仅在不重新初始化播放器时才调用 fullCleanup
|
||||
if (!this.url || !this.isActive) {
|
||||
this.fullCleanup()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
fullCleanup() {
|
||||
if (this.hls) {
|
||||
this.hls.destroy()
|
||||
this.hls = null
|
||||
}
|
||||
|
||||
if (this.video) {
|
||||
this.video.pause()
|
||||
this.video.removeAttribute('src')
|
||||
this.video.load()
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
this.retryCount = 0
|
||||
this.isReady = false
|
||||
this.playAttempts = 0
|
||||
|
||||
if (this.cleanupTimeout) {
|
||||
clearTimeout(this.cleanupTimeout)
|
||||
}
|
||||
},
|
||||
|
||||
initializePlayer() {
|
||||
if (!this.isActive || !this.url) return
|
||||
|
||||
// 如果是重新初始化播放器,先清理已存在的资源
|
||||
if (this.hls) {
|
||||
this.immediateCleanup()
|
||||
}
|
||||
|
||||
this.hls = new Hls({
|
||||
// 内存优化配置
|
||||
maxBufferSize: 15 * 1000 * 1000, // 降低缓冲区大小(15MB)
|
||||
maxBufferLength: 50, // 更小的缓冲窗口
|
||||
liveSyncDuration: 1, // 紧跟直播点
|
||||
liveMaxLatencyDuration: 5, // 最大延迟5秒
|
||||
liveDurationInfinity: true,
|
||||
lowLatencyMode: false, // 启用低延迟模式
|
||||
maxMaxBufferLength: 60,
|
||||
backBufferLength: 1, // 减少保留的缓冲数据
|
||||
manifestLoadingTimeOut: 10000,
|
||||
manifestLoadingMaxRetry: 5,
|
||||
fragLoadingTimeOut: 5000,
|
||||
fragLoadingMaxRetry: 2,
|
||||
enableWorker: true, // 启用Web Worker
|
||||
recycleVideoFrames: true, // 启用帧回收
|
||||
|
||||
startLevel: -1,
|
||||
autoStartLoad: true,
|
||||
maxBufferHole: 2, // 允许更大的时间缺口
|
||||
highBufferWatchdogPeriod: 4, // 延长监控周期
|
||||
nudgeMaxRetry: 5, // 增加微调重试次数
|
||||
nudgeOffset: 0.05, // 微调步长(秒)
|
||||
jumpGaps: false,
|
||||
stallThreshold: 1000
|
||||
})
|
||||
|
||||
this.hls.attachMedia(this.video)
|
||||
this.hls.loadSource(this.url)
|
||||
|
||||
// 事件处理
|
||||
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
this.loading = false
|
||||
this.retryCount = 0
|
||||
this.safePlay()
|
||||
})
|
||||
|
||||
this.hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
this.handleHlsError(data)
|
||||
})
|
||||
|
||||
this.hls.on(Hls.Events.BUFFER_EOS, () => {
|
||||
this.cleanupNetworkResources()
|
||||
})
|
||||
},
|
||||
|
||||
cleanupNetworkResources() {
|
||||
// 清理已播放的缓冲数据
|
||||
if (this.hls && this.video) {
|
||||
try {
|
||||
const currentTime = this.video.currentTime
|
||||
this.hls.mediaBuffer = null
|
||||
if (this.hls.bufferTimer) {
|
||||
clearInterval(this.hls.bufferTimer)
|
||||
}
|
||||
this.hls.flushBuffer()
|
||||
this.video.currentTime = currentTime
|
||||
} catch (e) {
|
||||
console.warn('Buffer cleanup error:', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleHlsError(data) {
|
||||
console.error('HLS Error:', data)
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
this.retryLoad()
|
||||
break
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
this.hls.recoverMediaError()
|
||||
break
|
||||
default:
|
||||
this.fullCleanup()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
retryLoad() {
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
this.retryCount++
|
||||
this.showLoadingIndicator(`重试第 ${this.retryCount} 次...`)
|
||||
setTimeout(() => this.initializePlayer(), 2000)
|
||||
} else {
|
||||
this.showLoadingIndicator('视频加载失败')
|
||||
this.fullCleanup()
|
||||
}
|
||||
},
|
||||
|
||||
showLoadingIndicator(text) {
|
||||
this.loading = true
|
||||
this.loadingText = text
|
||||
},
|
||||
|
||||
async safePlay() {
|
||||
if (!this.video || this.playAttempts >= this.maxPlayAttempts) return
|
||||
|
||||
try {
|
||||
this.playAttempts++
|
||||
if (this.video.readyState >= 2) {
|
||||
await this.video.play()
|
||||
this.playAttempts = 0
|
||||
} else {
|
||||
setTimeout(this.safePlay, 500)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('播放失败:', error)
|
||||
setTimeout(this.safePlay, 1000)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 移除所有事件监听
|
||||
if (this.video) {
|
||||
const events = ['canplay', 'waiting', 'playing', 'error', 'stalled', 'suspend']
|
||||
events.forEach((event) => {
|
||||
this.video.removeEventListener(event, this.handleVideoEvent)
|
||||
})
|
||||
}
|
||||
this.fullCleanup()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.myVideo-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.myVideo {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
// aspect-ratio: 16/9;
|
||||
/* border: 1px solid #ccc; */
|
||||
border-radius: vw(5);
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: white;
|
||||
padding: vw(10);
|
||||
border-radius: vw(4);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user