168 lines
3.0 KiB
Vue
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>
|