127 lines
2.6 KiB
Vue
127 lines
2.6 KiB
Vue
<!-- Author: fruit.com.cn -->
|
||
<!-- 清晰度产物 HLS 预览播放器:Chrome/Edge 用 hls.js,Safari 走原生 -->
|
||
|
||
<script setup lang="ts">
|
||
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||
import type Hls from 'hls.js'
|
||
import { useI18n } from '@/hooks/web/useI18n'
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
url: string
|
||
title: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(event: 'update:modelValue', value: boolean): void
|
||
}>()
|
||
|
||
const { t } = useI18n()
|
||
|
||
const videoRef = ref<HTMLVideoElement>()
|
||
const errorMsg = ref('')
|
||
let hls: Hls | null = null
|
||
|
||
function destroy() {
|
||
hls?.destroy()
|
||
hls = null
|
||
const video = videoRef.value
|
||
if (video) {
|
||
video.pause()
|
||
video.removeAttribute('src')
|
||
video.load()
|
||
}
|
||
}
|
||
|
||
async function start(url: string) {
|
||
await nextTick()
|
||
const video = videoRef.value
|
||
if (!video || !url) return
|
||
errorMsg.value = ''
|
||
destroy()
|
||
|
||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||
video.src = url
|
||
video.play().catch(() => {})
|
||
return
|
||
}
|
||
|
||
const { default: HlsPlayer } = await import('hls.js')
|
||
if (!HlsPlayer.isSupported()) {
|
||
errorMsg.value = t('episodeTranscode.playerUnsupported')
|
||
return
|
||
}
|
||
const instance = new HlsPlayer()
|
||
hls = instance
|
||
instance.on(HlsPlayer.Events.MANIFEST_PARSED, () => {
|
||
video.play().catch(() => {})
|
||
})
|
||
instance.on(HlsPlayer.Events.ERROR, (_event, data) => {
|
||
if (data?.fatal) {
|
||
errorMsg.value = t('episodeTranscode.playerFailed')
|
||
}
|
||
})
|
||
instance.loadSource(url)
|
||
instance.attachMedia(video)
|
||
}
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(visible) => {
|
||
if (visible) {
|
||
start(props.url)
|
||
} else {
|
||
destroy()
|
||
}
|
||
}
|
||
)
|
||
|
||
onBeforeUnmount(destroy)
|
||
</script>
|
||
|
||
<template>
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
:title="title"
|
||
width="720px"
|
||
destroy-on-close
|
||
class="rendition-player-dialog"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
@closed="destroy"
|
||
>
|
||
<div class="rendition-player">
|
||
<video ref="videoRef" class="rendition-player__video" controls playsinline></video>
|
||
<p v-if="errorMsg" class="rendition-player__error">{{ errorMsg }}</p>
|
||
<p class="rendition-player__url" :title="url">{{ url }}</p>
|
||
</div>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.rendition-player {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.rendition-player__video {
|
||
width: 100%;
|
||
max-height: 62vh;
|
||
background: #000;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.rendition-player__error {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
.rendition-player__url {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
word-break: break-all;
|
||
}
|
||
</style>
|