508 lines
9.4 KiB
Vue
508 lines
9.4 KiB
Vue
<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> |