ufutx-love-h5-public/src/components/HlsPlayer.vue

288 lines
6.7 KiB
Vue

<template>
<div class="hls-player" ref="playerContainer">
<video
ref="videoRef"
class="video-element"
playsinline
webkit-playsinline
x5-playsinline
:muted="muted"
:poster="poster"
@click="togglePlay"
></video>
<!-- 控制条 -->
<div class="controls" v-if="controls">
<button @click.stop="togglePlay" class="control-btn">
{{ playing ? '暂停' : '播放' }}
</button>
<button @click.stop="toggleFullscreen" class="control-btn">
{{ isFullscreen ? '退出全屏' : '全屏' }}
</button>
<span class="time">{{ currentTimeDisplay }}</span>
</div>
<!-- 加载中或错误 -->
<div v-if="loading" class="loading">加载中...</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, computed, nextTick } from 'vue'
import Hls from 'hls.js'
const props = defineProps({
src: {
type: String,
required: true,
},
poster: {
type: String,
default: '',
},
muted: {
type: Boolean,
default: false,
},
controls: {
type: Boolean,
default: true,
},
autoplay: {
type: Boolean,
default: false,
},
})
const videoRef = ref<HTMLVideoElement | null>(null)
const playerContainer = ref<HTMLElement | null>(null)
let hls: Hls | null = null
const playing = ref(false)
const loading = ref(true)
const error = ref<string | null>(null)
const currentTime = ref(0)
const duration = ref(0)
// 全屏状态
const isFullscreen = ref(false)
// 播放/暂停
const togglePlay = () => {
if (!videoRef.value) return
if (playing.value) {
videoRef.value.pause()
} else {
videoRef.value.play().catch(err => {
// 自动播放被拒绝
console.warn('Play failed:', err)
})
}
}
// 全屏切换
const toggleFullscreen = () => {
if (!playerContainer.value) return
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
// 进入全屏
const el = playerContainer.value
if (el.requestFullscreen) {
el.requestFullscreen()
} else if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen()
} else {
// 降级:尝试 video 原生全屏 (仅 iOS)
if (videoRef.value && videoRef.value.webkitEnterFullscreen) {
videoRef.value.webkitEnterFullscreen()
} else {
alert('您的浏览器不支持全屏')
}
}
} else {
// 退出全屏
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
}
}
}
// 监听全屏变化
const onFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement || !!document.webkitFullscreenElement
}
// 更新播放状态
const updatePlayState = () => {
if (videoRef.value) {
playing.value = !videoRef.value.paused
}
}
// 时间格式化
const formatTime = (seconds: number) => {
if (isNaN(seconds)) return '00:00'
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}
const currentTimeDisplay = computed(() => formatTime(currentTime.value))
// 初始化播放器
const initPlayer = () => {
if (!videoRef.value || !props.src) return
const video = videoRef.value
// 清理旧实例
if (hls) {
hls.destroy()
hls = null
}
loading.value = true
error.value = null
// 检查 HLS 支持
if (Hls.isSupported()) {
hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
})
hls.loadSource(props.src)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
loading.value = false
if (props.autoplay) {
video.play().catch(err => {
console.warn('Autoplay prevented:', err)
// 显示播放按钮让用户交互
})
}
})
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
error.value = '播放出错,请刷新重试'
loading.value = false
}
})
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// 原生 HLS 支持 (iOS Safari)
video.src = props.src
video.addEventListener('loadedmetadata', () => {
loading.value = false
if (props.autoplay) {
video.play().catch(() => {})
}
})
video.addEventListener('error', () => {
error.value = '视频加载失败'
loading.value = false
})
} else {
error.value = '您的浏览器不支持播放此视频'
loading.value = false
}
}
// 监听 src 变化
watch(() => props.src, () => {
initPlayer()
}, { immediate: false })
// 生命周期
onMounted(() => {
initPlayer()
// 绑定事件
const video = videoRef.value
if (video) {
video.addEventListener('play', updatePlayState)
video.addEventListener('pause', updatePlayState)
video.addEventListener('timeupdate', () => {
currentTime.value = video.currentTime
})
video.addEventListener('durationchange', () => {
duration.value = video.duration
})
}
// 全屏事件监听
document.addEventListener('fullscreenchange', onFullscreenChange)
document.addEventListener('webkitfullscreenchange', onFullscreenChange)
})
onBeforeUnmount(() => {
if (hls) {
hls.destroy()
hls = null
}
const video = videoRef.value
if (video) {
video.removeEventListener('play', updatePlayState)
video.removeEventListener('pause', updatePlayState)
video.removeEventListener('timeupdate', () => {})
video.removeEventListener('durationchange', () => {})
}
document.removeEventListener('fullscreenchange', onFullscreenChange)
document.removeEventListener('webkitfullscreenchange', onFullscreenChange)
})
// 暴露方法 (供父组件调用)
defineExpose({
play: () => videoRef.value?.play(),
pause: () => videoRef.value?.pause(),
togglePlay,
toggleFullscreen,
})
</script>
<style scoped>
.hls-player {
position: relative;
width: 100%;
background: #000;
overflow: hidden;
}
.video-element {
width: 100%;
display: block;
background: #000;
}
.controls {
position: absolute;
bottom: 10px;
left: 10px;
right: 10px;
display: flex;
align-items: center;
gap: 10px;
background: rgba(0,0,0,0.5);
padding: 8px 12px;
border-radius: 4px;
color: #fff;
}
.control-btn {
background: transparent;
border: none;
color: #fff;
font-size: 16px;
cursor: pointer;
padding: 4px 8px;
}
.time {
margin-left: auto;
font-size: 14px;
}
.loading, .error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
background: rgba(0,0,0,0.6);
padding: 10px 20px;
border-radius: 4px;
}
.error {
background: rgba(255,0,0,0.6);
}
</style>