zhgdyunapp/components/my-player-m3u8/my-player-m3u8.vue
2025-07-24 16:53:08 +08:00

508 lines
9.4 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>
<view class="m3u8-player">
<!-- H5平台使用mui-player -->
<view v-if="isH5" class="video-container">
<div ref="muiPlayerContainer" class="mui-player-container"></div>
</view>
<!-- APP平台使用原生video组件 -->
<video
v-else
:hidden="showMask"
:src="src"
:poster="poster"
:controls="true"
:autoplay="autoplay"
:muted="muted"
:loop="loop"
:enable-play-gesture="true"
:enable-progress-gesture="true"
:show-fullscreen-btn="true"
:show-play-btn="true"
:show-center-play-btn="true"
:show-loading="true"
:object-fit="objectFit"
class="video-player"
@loadstart="onLoadStart"
@loadedmetadata="onLoadedMetadata"
@canplay="onCanPlay"
@play="onPlay"
@pause="onPause"
@ended="onEnded"
@error="onError"
@timeupdate="onTimeUpdate"
@fullscreenchange="onFullscreenChange"
></video>
<!-- 加载状态 -->
<view v-if="loading" class="loading-overlay">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 错误状态 -->
<view v-if="error" class="error-overlay">
<text class="error-text">{{ errorMessage }}</text>
<view class="retry-btn" @click="retry">重试</view>
</view>
</view>
</template>
<script>
// 引入mui-player
// #ifdef H5
import Hls from 'hls.js'
import MuiPlayer from 'mui-player'
import 'mui-player/dist/mui-player.min.css'
// #endif
export default {
name: "my-player-m3u8",
props: {
// 视频源
src: {
type: String,
required: true
},
// 控制遮罩层显示隐藏
showMask: {
type: Boolean,
required: false
},
// 封面图
poster: {
type: String,
default: ''
},
// 自动播放
autoplay: {
type: Boolean,
default: false
},
// 静音
muted: {
type: Boolean,
default: false
},
// 循环播放
loop: {
type: Boolean,
default: false
},
// 视频填充模式
objectFit: {
type: String,
default: 'contain'
},
// 播放器主题色
theme: {
type: String,
default: '#b7daff'
},
// 是否显示弹幕
danmaku: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
error: false,
errorMessage: '',
player: null
};
},
computed: {
// 判断是否为H5平台
isH5() {
// #ifdef H5
return true;
// #endif
// #ifdef APP-PLUS
return false;
// #endif
}
},
mounted() {
this.initPlayer();
},
beforeDestroy() {
this.destroyPlayer();
},
watch: {
src(newSrc) {
if (this.player && this.isH5) {
this.player.reloadUrl(newSrc);
}
}
},
methods: {
// 初始化播放器
initPlayer() {
// #ifdef H5
this.initH5Player();
// #endif
// #ifndef H5
// APP平台使用原生video组件无需额外初始化
// #endif
},
// H5平台初始化mui-player
initH5Player() {
// #ifdef H5
this.$nextTick(() => {
this.setupMuiPlayer();
});
// #endif
},
// 设置mui-player播放器
setupMuiPlayer() {
// #ifdef H5
try {
// 先销毁已存在的播放器
if (this.player) {
this.player.destroy();
this.player = null;
}
// 获取容器元素
const container = this.$refs.muiPlayerContainer;
if (!container) {
console.error('找不到播放器容器');
return;
}
// 初始化mui-player播放器
this.player = new MuiPlayer({
container: container,
src: this.src,
poster: this.poster,
autoplay: this.autoplay,
muted: this.muted,
loop: this.loop,
themeColor: this.theme,
// 弹幕配置
danmaku: this.danmaku ? {
enable: true,
api: 'https://api.prprpr.me/dplayer/',
token: 'tokendemo',
maximum: 1000
} : false,
// 播放器配置
showMiniProgress: true,
live: false,
pageHead: true,
// HLS配置
parse: {
type: 'hls',
loader: Hls,
config: {
enableWorker: true,
lowLatencyMode: true
}
},
// 播放器选项
lang: 'zh-cn',
hotkey: true,
preload: 'auto',
volume: 0.7,
mutex: true
});
// 绑定事件
this.player.on('loadstart', this.onLoadStart);
this.player.on('loadedmetadata', this.onLoadedMetadata);
this.player.on('canplay', this.onCanPlay);
this.player.on('play', this.onPlay);
this.player.on('pause', this.onPause);
this.player.on('ended', this.onEnded);
this.player.on('error', this.onError);
this.player.on('timeupdate', this.onTimeUpdate);
console.log('mui-player初始化成功');
} catch (error) {
console.error('mui-player初始化失败:', error);
this.handleError('播放器初始化失败: ' + error.message);
}
// #endif
},
// 销毁播放器
destroyPlayer() {
if (this.player) {
try {
if (this.isH5) {
this.player.destroy();
}
this.player = null;
} catch (error) {
console.error('销毁播放器失败:', error);
}
}
},
// 重试播放
retry() {
this.error = false;
this.errorMessage = '';
this.loading = true;
// 重新初始化播放器
this.$nextTick(() => {
this.initPlayer();
});
},
// 处理错误
handleError(message) {
this.error = true;
this.errorMessage = message;
this.loading = false;
this.$emit('error', { message });
},
// 事件处理方法
onLoadStart() {
this.loading = true;
this.error = false;
this.$emit('loadstart');
},
onLoadedMetadata() {
this.$emit('loadedmetadata');
},
onCanPlay() {
this.loading = false;
this.$emit('canplay');
},
onPlay() {
this.$emit('play');
},
onPause() {
this.$emit('pause');
},
onEnded() {
this.$emit('ended');
},
onError(event) {
console.error('Video error:', event);
this.handleError('视频播放出错');
this.$emit('error', event);
},
onTimeUpdate() {
this.$emit('timeupdate');
},
onFullscreenChange(event) {
this.$emit('fullscreenchange', event);
},
// 公开方法
play() {
if (this.player) {
if (this.isH5) {
this.player.video().play();
}
}
},
pause() {
if (this.player) {
if (this.isH5) {
this.player.video().pause();
}
}
},
seek(time) {
if (this.player) {
if (this.isH5) {
this.player.video().currentTime = time;
}
}
},
toggle() {
if (this.player) {
if (this.isH5) {
const video = this.player.video();
if (video.paused) {
video.play();
} else {
video.pause();
}
}
}
}
}
}
</script>
<style scoped lang="scss">
.m3u8-player {
position: relative;
width: 100%;
height: 100%;
background: #000;
z-index: 1;
.video-container {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
.mui-player-container {
width: 100%;
height: 100% !important;
position: relative;
z-index: 1;
}
}
.video-player {
width: 100%;
height: 100%;
object-fit: contain;
position: relative;
z-index: 1;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top: 4rpx solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
color: #fff;
font-size: 28rpx;
margin-top: 20rpx;
}
}
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.8);
z-index: 10;
.error-text {
color: #fff;
font-size: 28rpx;
margin-bottom: 30rpx;
text-align: center;
}
.retry-btn {
padding: 20rpx 40rpx;
background: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* mui-player样式覆盖 */
:deep(.m-player) {
width: 100% !important;
height: 100% !important;
z-index: 1;
position: relative;
.video-wrapper {
width: 100% !important;
height: 100% !important;
z-index: 1;
position: relative;
}
.video-wrapper video {
width: 100% !important;
height: 100% !important;
z-index: 1;
position: relative;
}
/* 降低mui-player内部元素的层级 */
.mplayer-header,
.mplayer-footer,
.mplayer-loading,
.mplayer-error,
.mplayer-cover,
.mplayer-poster {
z-index: 2;
position: relative;
}
/* 确保控制按钮在正确层级 */
[control] {
z-index: 3;
position: relative;
}
/* 进度条层级 */
.progress-container,
.mini-progress {
z-index: 3;
position: relative;
}
/* 确保播放器容器不会超出父容器 */
#mplayer-media-wrapper {
width: 100% !important;
height: 100% !important;
position: relative;
z-index: 1;
}
/* 防止mui-player的绝对定位元素影响层级 */
.mplayer-sidebar {
z-index: 4;
}
/* 确保toast消息在最高层级 */
.mplayer-toast {
z-index: 15;
}
}
</style>