72QishierPlayer/src/components/qishier/QishierVideoPlayer.vue
2026-03-11 17:46:15 +08:00

168 lines
3.0 KiB
Vue

<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
function destroyPlayer() {
if (art?.hls) {
art.hls.destroy()
art.hls = undefined
}
if (art) {
art.destroy()
art = null
}
if (playerRef.value) {
playerRef.value.innerHTML = ''
}
}
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,
autoHide: 2000,
mutex: true,
theme: '#f5c542',
moreVideoAttr: {
playsInline: true
} as any,
customType: {
m3u8(video: HTMLVideoElement, url: string, artInstance: Artplayer) {
const artplayer = artInstance as ArtWithHls
if (Hls.isSupported()) {
const hls = new Hls()
artplayer.hls = hls
hls.loadSource(url)
hls.attachMedia(video)
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url
} else {
emit('error', '当前浏览器不支持 m3u8 播放')
}
}
}
}) as ArtWithHls
art.type = 'm3u8'
const restore = () => {
const progress = getEpisodeProgress(episode.id)
if (progress > 0 && art?.video) {
try {
art.video.currentTime = progress
} catch {
// ignore
}
}
}
art.on('video:loadedmetadata', restore)
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,
() => {
// 让 ended 事件读取最新 autoNext 值
}
)
onMounted(async () => {
await initPlayer()
})
onBeforeUnmount(() => {
destroyPlayer()
})
</script>
<style scoped>
.player-wrap {
width: 100%;
background: #000;
}
.artplayer-container {
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
}
:deep(.artplayer-app) {
width: 100%;
height: 100%;
}
</style>