当前位置: 首页 > backend >正文

vue中使用西瓜播放器xgplayer (封装)+xgplayer-hls 播放.m3u8格式视频

1.西瓜播放器官网

http://h5player.bytedance.com/guide/

2.安装

# 最新稳定版
$ npm install xgplayer对于已有项目也可以通过 CDN 引入,代码如下:
<script src="//unpkg.byted-static.com/xgplayer/2.31.2/browser/index.js" type="text/javascript"></script>

3.封装西瓜播放器组件

<template><div class="video-box"><div ref="playerRef" id="video-player" class="video-player"></div></div></template><script setup>import { ref, onMounted, watch } from 'vue';import Player from 'xgplayer';import 'xgplayer/dist/index.min.css';// 定义propsconst props = defineProps({url: {type: String,default: ''},poster: {type: String,default: ""}});// 定义播放器实例和DOM引用const playerRef = ref(null);const player = ref(null);// 定义emitsconst emit = defineEmits(['triggerEvent', 'timeupdate', 'loadingStateChange', 'playEnd', 'videoClick']);// 判断是否为Apple设备const isAppleDevice = () => {const ua = navigator.userAgent.toLowerCase();return /iphone|ipad|phone|Mac/i.test(ua);};// 初始化播放器const initPlayer = () => {if (!props.url) return console.warn('url is not exist');const config = {el: playerRef.value, // 使用ref代替idurl: props.url,plugins: [window.HlsPlayer],hls: {retryCount: 3, // 重试 3 次,默认值retryDelay: 1000, // 每次重试间隔 1 秒,默认值loadTimeout: 10000, // 请求超时时间为 10 秒,默认值fetchOptions: {// 该参数会透传给 fetch,默认值为 undefinedmode: 'cors'}},fluid: true,// 倍速播放playbackRate: [2],defaultPlaybackRate: 1,volume: 0.7,playsinline: isAppleDevice(), // IOS设备设置'x5-video-player-type': 'h5', // 微信内置浏览器设置'x5-video-orientation': 'portraint',poster: props.poster,// 画中画// pip: true,pipConfig: {bottom: 100,right: 100,width: 320,height: 180},// 初始化首帧// videoInit: true,autoplay: true};// 实例化播放器player.value = new Player(config);if (player.value) {// 新增:单击事件处理const container = player.value.root;container.addEventListener('click', handleContainerClick);// 注册事件监听player.value.on('play', () => {emit('triggerEvent', true);});player.value.on('pause', () => {emit('triggerEvent', false);});player.value.on('ended', () => {emit('playEnd');});// 1. 视频进入等待缓冲状态player.value.on('waiting', () => {// console.log('视频缓冲中...');emit('loadingStateChange', { bool: true, time: player.value.currentTime }); // 通知父组件显示加载状态});player.value.on('canplay', () => {emit('loadingStateChange', { bool: false, time: player.value.currentTime });});// 监听播放进度更新player.value.on('timeupdate', () => {emit('timeupdate', { playerVideo: player.value });});setTimeout(() => {forceVideoSize();}, 100); // 延迟100ms确保播放器完全初始化}};// 生命周期钩子onMounted(() => {initPlayer();});// 监听url变化watch(() => props.url, (newValue) => {if (!player.value) {initPlayer();return;}player.value.src = newValue;});// 处理容器点击事件const handleContainerClick = (e) => {// 排除控制栏区域的点击if (e.target.closest('.xgplayer-control')) return;emit('videoClick', e); // 通知父组件显示加载状态};const forceVideoSize = () => {if (!player.value) return;const videoEl = player.value.root.querySelector('video');const container = player.value.root;if (videoEl) {// 完全重置video标签的样式videoEl.style.cssText = `width: 100% !important;height: 100% !important;max-width: none !important;max-height: none !important;object-fit: cover !important;position: absolute !important;top: 0 !important;left: 0 !important;bottom: 0 !important;right: 0 !important;margin: 0 !important;padding: 0 !important;border: none !important;`;// 设置播放器容器样式container.style.cssText = `width: 100% !important;height: 100% !important;max-width: none !important;max-height: none !important;position: relative !important;overflow: hidden !important;margin: 0 !important;padding: 0 !important;`;}};// 组件卸载时销毁播放器onUnmounted(() => {if (player.value) {player.value.destroy();player.value = null;}});</script><style scoped lang="scss">.video-box {width: 100% !important;height: 100% !important;.video-player {width: 100% !important;height: 100% !important;padding: 0 !important;margin: 0 auto;}}</style>
3.1 .m3u8 格式处理需要在index.html 中引入
3.2 注意 vite项目下,使用依赖import 的方式播放有问题,需要改为全局引入静态min.js
  <!-- 先引入 xgplayer 核心库 -->
<script src="https://unpkg.com/xgplayer@latest/dist/index.min.js"></script> 
<!-- 再引入 xgplayer-hls 插件 -->
<script src="https://unpkg.com/xgplayer-hls@latest/dist/index.min.js"></script> 

4.父组件使用

<template><swiper :circular="state.circular" class="m-tiktok-video-swiper" @change="swiperChange"@animationfinish="animationfinish" :current="state.current" :vertical="true" duration="300"><swiper-item v-for="(item, index) in state.originList" :key="index"><view class="swiper-item"v-if="index == state.current || index + 1 == state.current || index - 1 == state.current"><xgplayerVideo  class="m-tiktok-video-player" :url="item.src":poster="poster"v-if="index == state.current && item.src" @playEnd="ended"@loadingStateChange="onwaiting"@triggerEvent="onTriggerEvent"@timeupdate="onTimeupdate"@videoClick="onVideoClick"></xgplayerVideo><slot :item="item"></slot></view></swiper-item></swiper>
</template><script lang="ts" setup>
import { reactive, ref, getCurrentInstance, watch, nextTick } from "vue";
import type { ComponentInternalInstance, PropType } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import { getPlayUrl } from "@/api/home";
import { trackEvent } from "@/utils/common";
import xgplayerVideo from "./xgplayerVideo.vue"
const _this = getCurrentInstance() as ComponentInternalInstance;export interface IvideoItem {/*** 视频链接*/src: string;/*** 海报封面*/poster?: string;
}const emits = defineEmits(["change","ended","swiperchange","videoClicks"
]);const props = defineProps({poster: {type: String,default: "",},/*** 当前播放剧集(索引值)*/current: {type: [Number, String],default: 0,},/*** 视频列表*/videoList: {type: Array as PropType<IvideoItem[]>,default: () => [],},/*** 是否循环播放一个视频*/loop: {type: Boolean,default: true,},/*** 显示原生控制栏*/controls: {type: Boolean,default: true,},/*** 是否自动播放*/autoplay: {type: Boolean,default: true,},/*** 是否自动滚动播放*/autoChange: {type: Boolean,default: true,},
});const state = reactive({circular: false,originList: [] as any, // 源数据originIndex: 0, // 记录源数据的下标current: 0,videoContexts: [] as any,isFirstLoad: true,isPause: true,bufferStartTime:0
});
const VideoPlayer = ref([])
const videoDomDate = ref({})
const reportedTimes = ref(new Set()); // 记录已上报的时间点const animationfinish = async () => { };function ended() {trackEvent("play", {id: props.videoList[0].videoId,level: props.current,status:'finish'})// 自动切换下一个视频if (props.autoChange) {state.current = state.originIndex + 1;}emits("ended");
}
/*** 初始一个显示的swiper数据* @originIndex 从源数据的哪个开始显示默认0*/
async function initSwiperData(originIndex = state.originIndex) {// 确保索引有效if (originIndex < 0 || originIndex >= state.originList.length) {console.warn("无效的视频索引:", originIndex);return;}const index = originIndex;// 延迟播放当前视频,确保DOM已更新await nextTick();console.log("播放视频:", index, props.videoList[index]);// handleCoverClick(index);// 数据改变emits("change", {index: originIndex,detail: state.originList[originIndex],});}
// 视频缓冲
const onwaiting = (val) => {// state = true 开始缓冲// state = false 缓冲结束if (val.bool) {state.bufferStartTime = Date.now()trackEvent("play_stop", {id: props.videoList[0].videoId,level: props.current,stop: Number(val.time.toFixed(2)),time:0});} else {if (state.bufferStartTime) {const bufferTime = (Date.now() - state.bufferStartTime) / 1000;trackEvent("play_stop", {id: props.videoList[0].videoId,level: props.current,stop: Number(val.time.toFixed(2)),time:bufferTime});}}}
// 播放暂停
const onTriggerEvent = (boo) => {console.log(boo);
}
// 点击屏幕
const onVideoClick = (e) => {emits("videoClicks", e);
}// 每隔10秒上报一次
const onTimeupdate = (val) => {if (!val.playerVideo || val.playerVideo.paused) return;const currentTime = Math.floor(val.playerVideo.currentTime);// 视频总时长const duration = Math.floor(val.playerVideo.duration || 0);// 只处理0秒和10的倍数秒数,且不超过视频总时长if ((currentTime === 0 || currentTime % 10 === 0) && currentTime <= duration && !reportedTimes.value.has(currentTime)) {if (props.current == 1) {trackEvent("play_progress", {id: props.videoList[0].videoId,level: props.current,progress: currentTime});}reportedTimes.value.add(currentTime);// console.log(`上报: ${currentTime}秒`);}}
// var hls = new Hls();
const handleCoverClick = async(index) => {
// if (Hls.isSupported()) {// 创建新的HLS实例// if (hls) {//     hls.destroy()//  }//           // 暂停视频(确保没有播放中的实例)//     if (videoDomDate.value.videoDom) {//         videoDomDate.value.videoDom.pause();//     }//       // 加载.m3u8视频源//         console.log('HLS媒体已附加',state.originList[index].src,state.current);//         if(state.originList[index].src){//             hls.loadSource(state.originList[index].src); // 假设item.src是.m3u8格式的URL//         }//         // 关联HLS实例与视频元素// // 关联HLS实例与视频元素//     if (videoDomDate.value.videoDom) {//         hls.attachMedia(videoDomDate.value.videoDom); // 假设DomVideoPlayer暴露了video元素//       }//         console.log('开始进入',videoDomDate.value);//        hls.on(Hls.Events.MEDIA_ATTACHED, () => {//         console.log(videoDomDate.value.videoDom,',准备播放');//         videoDomDate.value.videoDom.play(); // 开始播放视频//        });VideoPlayer.value.forEach(item => {console.log(item);if (item) {const num = Number(item.$ownerInstance.$el.getAttribute('data-index'))console.log(num,'99');if (num === index) {item.play()} else {state.isPause = trueitem.toSeek(0)item.pause();}}})
// }
};/*** swiper滑动时候*/
async function swiperChange(event: any) {const { current } = event.detail;state.current = current;state.originIndex = current;// 确保视频源已加载// if (!state.originList[current].src) {//     const url = await getVideoUrl(props.videoList[current].videoUrl);//     state.originList[current].src = url;// }initSwiperData();emits("swiperchange", current);console.log("swiper切换:", current);
}
async function getVideoUrl(videoUrl: string) {try {if (videoUrl) {const {data: { url },} = await getPlayUrl({videoUrl,});return url;}} catch (error) {console.error("获取视频URL失败:", error);throw error;}
}
// 监听props.current变化
watch(() => props.current,async () => {console.log(props.current, props.videoList);if (props.videoList?.length) {const i = props.videoList.findIndex((item: any) => item.num == props.current);if (i > -1) {state.current = i;state.originIndex = i;state.originList = props.videoList;const url = await getVideoUrl(props.videoList[i].videoHls);console.log(url, '=============>');state.originList[i].src = url;// 埋点trackEvent("play", {id: props.videoList[0].videoId,level: props.current,status:'start'})// Meta Pixel 用于衡量 Facebook广告 的效果//#ifdef H5window.fbq('track', 'ViewContent',{content_ids :props.videoList[0].videoId});//#endifif (state.isFirstLoad || !state.videoContexts?.length) {initSwiperData();}}}},{immediate: true,deep: true}
);
function jumpToVideo(index) {if (index >= 0 && index < state.originList.length) {state.current = index;state.originIndex = index;// initSwiperData(index);}
}let loadTimer: any = null;
onLoad(() => {// 为了首次只加载一条视频(提高首次加载性能),延迟加载后续视频loadTimer = setTimeout(() => {state.isFirstLoad = false;clearTimeout(loadTimer);}, 5000);
});onUnload(() => {clearTimeout(loadTimer);
});defineExpose({initSwiperData,jumpToVideo,
});
</script><style lang="scss">
.m-tiktok-video-swiper,
.m-tiktok-video-player {width: 100%;height: 100%;background-color: #000;
}.m-tiktok-video-swiper {.swiper-item {width: 100%;height: 100%;position: relative;}.m-tiktok-video-poster {display: block;opacity: 1;visibility: visible;position: absolute;left: 0;top: 0;background-position: center center;background-color: #000;background-size: 100% auto;background-repeat: no-repeat;transition: opacity 0.3s ease, visibility 0.3s ease;pointer-events: none;width: 100%;height: 100%;}.iszan {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 99;}
}
</style>
http://www.xdnf.cn/news/15415.html

相关文章:

  • 【王树森推荐系统】物品冷启05:流量调控
  • Java-72 深入浅出 RPC Dubbo 上手 生产者模块详解
  • 清除 Android 手机 SIM 卡数据的4 种简单方法
  • 网络准入控制系统的作用解析,2025年保障企业入网安全第一道防线
  • OpenVela之开发自测试框架cmocka
  • 【算法训练营Day12】二叉树part2
  • 量产技巧之RK3588 Android12默认移除导航栏状态栏​
  • google浏览器::-webkit-scrollbar-thumb设置容器滚动条滑块不生效
  • Android 性能优化:启动优化全解析
  • C++-linux 7.文件IO(一)系统调用
  • Linux上基于C/C++头文件查找对应的依赖开发库
  • uni-app 选择国家区号
  • CentOS 7服务器上使用Docker部署Notesnook的详细指导说明
  • Spring Cloud分布式配置中心:架构设计与技术实践
  • 链表算法之【获取链表开始入环的节点】
  • 图生生AI模仿裂变:1分钟批量裂变素材图片!
  • MySQL数据库的基础操作
  • C++后端面试八股文
  • 深入解析Hadoop YARN架构设计:从原理到实践
  • 5、qt系统相关
  • LLM表征工程还有哪些值得做的地方
  • linux打包固件shell脚本
  • FOC算法中SIMULINK一些常用模块(1)(个人留存)
  • 多客户端-服务器(select,poll)
  • 第二章 基于新版Onenet搭建云服务(stm32物联网)
  • elementPlus中的el-table实现合并单元格
  • MMKV 存储json list数据(kotlin)
  • 《Linux篇》自动化构建-make/Makefile
  • 自动润滑系统:从 “盲目养护“ 到智能精注的工业运维革命
  • MMaDA:多模态大型扩散语言模型