72QishierPlayer/src/components/qishier/QishierVideoPlayer.vue
2026-03-12 13:10:11 +08:00

241 lines
4.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="player-wrap">
<div ref="playerRef" class="artplayer-container"></div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import Artplayer from 'artplayer'
import Hls from 'hls.js'
import type { EpisodeData } from '@/api/qishier'
import { getEpisodeProgress, saveEpisodeProgress } from '@/utils/playHistory'
type ArtWithHls = Artplayer & {
hls?: Hls
}
const props = defineProps<{
episode: EpisodeData | null
autoNext: boolean
}>()
const emit = defineEmits<{
error: [message: string]
endedNext: []
}>()
const playerRef = ref<HTMLDivElement | null>(null)
let art: ArtWithHls | null = null
let playRetryTimer: number | null = null
function clearPlayRetryTimer() {
if (playRetryTimer !== null) {
window.clearTimeout(playRetryTimer)
playRetryTimer = null
}
}
function destroyPlayer() {
clearPlayRetryTimer()
if (art?.hls) {
art.hls.destroy()
art.hls = undefined
}
if (art) {
art.destroy()
art = null
}
if (playerRef.value) {
playerRef.value.innerHTML = ''
}
}
function safeAutoPlay() {
clearPlayRetryTimer()
playRetryTimer = window.setTimeout(() => {
void art?.video?.play().catch(() => {
// 某些浏览器自动播放会被拦截,这里静默处理
})
}, 180)
}
function buildPlayer(episode: EpisodeData) {
if (!playerRef.value) return
art = new Artplayer({
container: playerRef.value,
url: episode.videoUrl,
autoplay: true,
autoSize: true,
fullscreen: true,
fullscreenWeb: true,
pip: true,
setting: true,
playbackRate: true,
hotkey: true,
mutex: true,
theme: '#f5c542',
moreVideoAttr: {
playsInline: true,
preload: 'auto'
} as any,
customType: {
m3u8(video: HTMLVideoElement, url: string, artInstance: Artplayer) {
const artplayer = artInstance as ArtWithHls
if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: false,
backBufferLength: 30,
maxBufferLength: 30,
maxMaxBufferLength: 60,
maxBufferHole: 1,
highBufferWatchdogPeriod: 2,
nudgeOffset: 0.1,
nudgeMaxRetry: 5
})
artplayer.hls = hls
hls.loadSource(url)
hls.attachMedia(video)
hls.on(Hls.Events.ERROR, (_event, data) => {
console.error('HLS Error:', data)
if (!data.fatal) {
return
}
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
console.warn('HLS NETWORK_ERROR尝试恢复')
hls.startLoad()
emit('error', '网络异常,正在尝试恢复')
return
}
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
console.warn('HLS MEDIA_ERROR尝试恢复')
hls.recoverMediaError()
emit('error', '播放异常,正在尝试恢复')
return
}
emit('error', '视频播放失败')
})
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url
} else {
emit('error', '当前浏览器不支持 m3u8 播放')
}
}
}
}) as ArtWithHls
art.type = 'm3u8'
const restoreProgress = () => {
const progress = getEpisodeProgress(episode.id)
if (progress > 0 && art?.video) {
try {
art.video.currentTime = progress
} catch {
// ignore
}
}
safeAutoPlay()
}
art.on('video:loadedmetadata', restoreProgress)
art.on('video:canplay', () => {
// 某些设备 loadedmetadata 后还没准备好canplay 时再兜底一次
safeAutoPlay()
})
art.on('video:waiting', () => {
// 这里不报错,只作为缓冲状态
console.warn('视频缓冲中...')
})
art.on('video:timeupdate', () => {
if (art?.video) {
saveEpisodeProgress(episode.id, art.video.currentTime)
}
})
art.on('video:ended', () => {
if (props.autoNext) {
emit('endedNext')
}
})
art.on('error', () => {
emit('error', '视频播放失败')
})
}
async function initPlayer() {
destroyPlayer()
if (!props.episode) return
await nextTick()
buildPlayer(props.episode)
}
watch(
() => props.episode?.id,
async () => {
await initPlayer()
}
)
watch(
() => props.autoNext,
() => {
// 保持响应式依赖
}
)
onMounted(async () => {
await initPlayer()
})
onBeforeUnmount(() => {
destroyPlayer()
})
</script>
<style scoped>
.player-wrap {
width: 100%;
background: #000;
border-radius: 12px;
overflow: hidden;
}
.artplayer-container {
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
}
:deep(.artplayer-app) {
width: 100%;
height: 100%;
}
:deep(.art-video-player) {
background: #000;
}
</style>