基于Vue3+西瓜視頻播放器(xgplayer)移動(dòng)端直播解決方案
基于 Vue3 + 西瓜視頻播放器(xgplayer),打造輕量、流暢、可直接復(fù)用的移動(dòng)端直播組件,適配快速集成場(chǎng)景。

最終效果
- ?? 支持 HLS(m3u8)直播流,播放流暢無(wú)卡頓
- ?? 自定義封面 + 播放按鈕,點(diǎn)擊即啟動(dòng)
- ?? 移動(dòng)端適配(內(nèi)嵌播放、全屏切換、無(wú)滾動(dòng)條)
- ?? 基礎(chǔ)錯(cuò)誤處理,兼容瀏覽器自動(dòng)播放限制
核心代碼實(shí)現(xiàn)
1. 依賴安裝
npm install xgplayer --save
2. 極簡(jiǎn)直播播放器組件(LivePlayer.vue)
<template>
<div :id="id" style="position: relative" :width="props.width" :height="props.height">
<img
v-if="showImage"
style="background-color: black; object-fit: cover"
:width="props.width"
:height="props.height"
:src="props.poster"
@click="clickImage"
/>
<img
v-if="showImage"
class="play-icon"
:src="getAssetsImages(`icon_play.png`)"
alt=""
@click="clickImage"
/>
</div>
</template>
<script setup lang="ts">
import { getAssetsImages } from "@/utils/util.js";
import { ref } from "vue";
import Player from "xgplayer";
import "xgplayer/dist/index.min.css";
const props = defineProps({
id: {
type: String,
required: true,
},
videoUrl: {
type: String,
default: () => "",
},
poster: {
type: String,
default: () => "",
},
playsinline: {
type: Boolean,
default: true,
},
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "100%",
},
});
const showImage = ref(true);
// 定義一個(gè)變量來(lái)存儲(chǔ) player 實(shí)例
let player: Player;
const clickImage = () => {
if (player == null) {
initPlayer();
showImage.value = false;
}
};
// 初始化西瓜視頻
const initPlayer = () => {
player = new Player({
lang: "zh", // 設(shè)置播放器的語(yǔ)言為中文(zh)
volume: 0.5, // 設(shè)置初始音量為50%
id: props.id, // 使用傳入的id屬性,可能是一個(gè)容器或視頻元素的ID
url: props.videoUrl, // 設(shè)置視頻的URL地址
poster: props.poster, // 設(shè)置視頻封面圖
playsinline: props.playsinline, // 是否允許在移動(dòng)設(shè)備上內(nèi)嵌播放(不跳轉(zhuǎn)到全屏)
height: props.height, // 設(shè)置播放器的高度
width: props.width, // 設(shè)置播放器的寬度
isLive: true, // 設(shè)置是否直播
playbackRate: [1], // 倍速展示
defaultPlaybackRate: 1,
cssFullscreen: false, // 設(shè)置是否使用CSS全屏樣式
download: false, // 隱藏下載按鈕
autoplay: false, // 自動(dòng)播放視頻
whitelist: [""], // 設(shè)置白名單,當(dāng)前為空
});
// 添加事件監(jiān)聽(tīng)器,確保播放器準(zhǔn)備好后再播放
player.on("ready", () => {
player.play(); // 播放視頻
});
};
</script>
<style scoped>
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50px;
height: 50px;
}
</style>
3. 頁(yè)面使用示例(LivePage.vue)
<template>
<div class="award-ceremony">
<div class="award-ceremony__content">
<!-- 直播播放器區(qū)域 -->
<div class="video_wrapper">
<VideoPlayer
id="videoPlayer"
:live-url="liveStreamUrl"
:poster="livePoster"
/>
</div>
<!-- 主體內(nèi)容區(qū)(評(píng)論/節(jié)目單) -->
<div class="main_wrapper">
<!-- 切換按鈕 -->
<ul class="btn-list">
<li
v-for="(item, index) in btnList"
:key="index"
:class="index === activeIndex ? 'active' : ''"
@click="handleTabClick(index)"
>
{{ item }}
</li>
</ul>
<!-- 評(píng)論列表 -->
<ul class="comment-list">
<van-pull-refresh v-model="refreshing" success-text="刷新成功" @refresh="onRefresh">
<van-list
v-model:loading="loading"
offset="100"
:finished="finished"
finished-text="沒(méi)有更多了"
@load="onLoad"
>
<li v-for="(item, index) in commentList" :key="index" class="comment-item">
<!-- 評(píng)論用戶信息 -->
<div class="comment-user">
<div class="name">{{ item.userName }}</div>
<div class="from" v-if="item.userType === 'leader'">
{{ item.orgName }}
</div>
<div class="from" v-else>
{{ item.company }}
</div>
<div class="from" v-else-if="item.department">{{ item.department }}</div>
</div>
<!-- 評(píng)論內(nèi)容 -->
<div class="comment-content">
<div class="text">
<span>{{ item.content?.substring(0, 64) }}</span>
<span v-show="item.isExpand">{{ item.content?.substring(64) }}</span>
<span v-if="item.content?.length > 64 && !item.isExpand">...</span>
<span v-if="item.content?.length > 64" class="more" @click="toggleExpand(item)">
{{ item.isExpand ? "收起" : "展開(kāi)" }}
</span>
</div>
<div class="time">{{ formatTime(item.publishTime) }}</div>
</div>
</li>
</van-list>
</van-pull-refresh>
</ul>
<!-- 評(píng)論發(fā)布框 -->
<van-cell-group>
<van-field
v-model="commentInput"
maxlength="100"
rows="1"
autosize
label=""
type="textarea"
placeholder="請(qǐng)輸入(最多100字)"
>
<template #button>
<van-button size="small" :loading="isPublishing" @click="publishComment">發(fā)送</van-button>
</template>
</van-field>
</van-cell-group>
</div>
</div>
<!-- 節(jié)目單彈窗 -->
<van-overlay
z-index="999"
:show="showProgramModal"
:lock-scroll="false"
@click="showProgramModal = false"
>
<div class="program-wrapper">
<div class="program-bg">
<div class="program-detail" @click.stop>
<div class="title text-gradient">標(biāo)題占位</div>
<div class="subtitle text-gradient">企業(yè)20xx年度先進(jìn)頒獎(jiǎng)典禮</div>
<van-list class="program-list">
<div v-for="(item, index) in programList" :key="index">
<div v-if="item.isImportant" class="important-item">
<p>{{ item.programType }}</p>
<p>{{ item.programName }}</p>
<p>{{ item.host }}</p>
</div>
<div v-else class="program-item">
<p>{{ item.programType }}{{ item.programName }}</p>
<p>{{ item.host }}</p>
</div>
<div class="award-item">{{ item.awardDesc }}</div>
</div>
</van-list>
</div>
</div>
</div>
</van-overlay>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import VideoPlayer from "./VideoPlayer.vue"; // 引入播放器組件(下文附簡(jiǎn)化版)
import { VanPullRefresh, VanList, VanCellGroup, VanField, VanButton, VanOverlay, VanToast } from "vant";
import "vant/lib/index.css";
// 直播核心配置(替換為實(shí)際項(xiàng)目配置)
const liveStreamUrl = ref("https://live-stream.example.com/livestream.m3u8"); // 直播流地址(m3u8)
const livePoster = ref("https://picsum.photos/1080/720"); // 直播封面圖
// 標(biāo)簽切換
const btnList = ref(["評(píng)論", "節(jié)目單"]);
const activeIndex = ref(0);
const showProgramModal = ref(false);
// 評(píng)論模塊狀態(tài)
const commentList = ref([]);
const commentInput = ref("");
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const isPublishing = ref(false);
let page = 1;
const pageSize = 5;
// 節(jié)目單數(shù)據(jù)(實(shí)際項(xiàng)目從接口獲取)
const programList = ref([
{
isImportant: true,
programType: "開(kāi)場(chǎng)",
programName: "領(lǐng)導(dǎo)致辭",
host: "張總",
awardDesc: "歡迎各位嘉賓蒞臨本次頒獎(jiǎng)典禮"
},
{
isImportant: false,
programType: "頒獎(jiǎng)",
programName: "年度優(yōu)秀員工",
host: "李經(jīng)理",
awardDesc: "表彰本年度表現(xiàn)突出的優(yōu)秀員工代表"
},
{
isImportant: false,
programType: "表演",
programName: "員工才藝展示",
host: "王主持人",
awardDesc: "員工自發(fā)組織的文藝表演"
},
{
isImportant: true,
programType: "閉幕",
programName: "總結(jié)發(fā)言",
host: "劉總",
awardDesc: "本次頒獎(jiǎng)典禮總結(jié)及未來(lái)展望"
}
]);
/** 切換標(biāo)簽(評(píng)論/節(jié)目單) */
const handleTabClick = (index) => {
activeIndex.value = index;
if (index === 1) {
showProgramModal.value = true;
}
};
/** 加載評(píng)論列表(模擬接口請(qǐng)求) */
const onLoad = async () => {
if (finished.value) return;
loading.value = true;
try {
// 模擬接口請(qǐng)求(實(shí)際項(xiàng)目替換為真實(shí)接口)
await new Promise(resolve => setTimeout(resolve, 800));
// 模擬評(píng)論數(shù)據(jù)
const mockComments = Array.from({ length: pageSize }, (_, i) => ({
userName: `用戶${page * pageSize + i + 1}`,
userType: Math.random() > 0.7 ? "leader" : "normal",
orgName: Math.random() > 0.7 ? "企業(yè)總部" : "分公司",
company: "中國(guó)電信",
department: "技術(shù)部",
content: `恭喜獲獎(jiǎng)的同事!${"直播內(nèi)容非常精彩,為奮斗者點(diǎn)贊~".repeat(Math.floor(Math.random() * 3) + 1)}`,
publishTime: new Date().toISOString(),
isExpand: false
}));
if (refreshing.value) {
commentList.value = mockComments;
refreshing.value = false;
} else {
commentList.value.push(...mockComments);
}
// 模擬加載完成(第3頁(yè)后無(wú)更多數(shù)據(jù))
if (page >= 3) {
finished.value = true;
}
page++;
} catch (error) {
console.error("加載評(píng)論失?。?, error);
VanToast("加載失敗,請(qǐng)稍后重試");
} finally {
loading.value = false;
}
};
/** 刷新評(píng)論列表 */
const onRefresh = () => {
finished.value = false;
commentList.value = [];
page = 1;
loading.value = true;
onLoad();
};
/** 發(fā)布評(píng)論 */
const publishComment = async () => {
const content = commentInput.value.trim();
if (!content) return;
isPublishing.value = true;
try {
// 模擬發(fā)布接口請(qǐng)求
await new Promise(resolve => setTimeout(resolve, 500));
// 發(fā)布成功后添加到列表頭部
commentList.value.unshift({
userName: "當(dāng)前用戶",
userType: "normal",
company: "中國(guó)電信",
department: "市場(chǎng)部",
content: content,
publishTime: new Date().toISOString(),
isExpand: false
});
commentInput.value = "";
VanToast("發(fā)布成功");
} catch (error) {
console.error("發(fā)布評(píng)論失?。?, error);
VanToast("發(fā)布失敗,請(qǐng)稍后重試");
} finally {
isPublishing.value = false;
}
};
/** 展開(kāi)/收起長(zhǎng)評(píng)論 */
const toggleExpand = (item) => {
item.isExpand = !item.isExpand;
};
/** 格式化時(shí)間 */
const formatTime = (timeStr) => {
if (!timeStr) return "";
const date = new Date(timeStr);
return date.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
});
};
/** 監(jiān)聽(tīng)節(jié)目單彈窗,加載數(shù)據(jù)(實(shí)際項(xiàng)目可在此處請(qǐng)求最新節(jié)目單) */
watch(showProgramModal, (isShow) => {
if (isShow) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
});
/** 頁(yè)面掛載時(shí)加載評(píng)論 */
onMounted(() => {
onLoad();
});
</script>
<style lang="scss" scoped>
@font-face {
font-family: "wdch";
src: url("https://cdn.example.com/fonts/wdch.ttf") format("truetype"); // 替換為公開(kāi)字體CDN
}
.award-ceremony {
overflow: hidden;
height: 100vh;
font-size: 12px;
position: relative;
:deep(.van-hairline--top-bottom:after) {
border: none;
}
&__content {
height: 100vh;
background: linear-gradient(180deg, #fff3e6 0%, #ffffff 100%);
.video_wrapper {
width: 100%;
height: 210px;
background: #ca1e00 url("https://picsum.photos/1080/210") no-repeat center bottom; // 替換為公開(kāi)背景圖
background-size: 100% auto;
overflow: hidden;
}
.main_wrapper {
height: calc(100% - 210px);
display: flex;
flex-direction: column;
.btn-list {
flex: none;
display: flex;
justify-content: space-around;
align-items: center;
padding: 4px 0;
background: #fff;
li {
width: 50%;
height: 100%;
line-height: 26px;
text-align: center;
color: #333;
font-size: 14px;
cursor: pointer;
&.active {
font-size: 16px;
color: #e33016;
background: url("https://picsum.photos/200/30") no-repeat center bottom; // 替換為公開(kāi)按鈕背景
background-size: auto 100%;
}
}
}
.comment-list {
flex: auto;
overflow-y: auto;
padding: 0 12px;
box-sizing: border-box;
.comment-item {
margin: 8px 0 0;
padding: 12px 0;
min-height: 96px;
background: linear-gradient(90deg, #ffd3c4, #ffffff, #ffffff);
background-size: 100% 100%;
display: flex;
align-items: center;
border-radius: 4px;
border: 1px solid #ffc9c1;
.comment-user {
padding: 0 8px;
font-size: 14px;
color: #da2a0e;
line-height: 24px;
width: 33.33%;
text-align: center;
.from {
font-size: 12px;
}
}
.comment-content {
height: 100%;
padding: 0 8px;
font-size: 12px;
color: #131415;
line-height: 14px;
width: 66.66%;
border-left: 1px dashed rgba(227, 48, 22, 0.2);
.text {
min-height: 56px;
span {
word-wrap: break-word;
white-space: normal;
}
.more {
color: #666;
margin-left: 4px;
white-space: nowrap;
cursor: pointer;
}
}
.time {
text-align: right;
position: relative;
top: 4px;
color: #999;
}
}
}
}
:deep(.van-cell-group) {
flex: none;
width: 100%;
padding: 8px 12px;
border-top: 1px solid #f2f3f5;
box-shadow: 0 0 5px #f2f3f5;
background: #fff;
.van-field__body {
align-items: flex-end;
}
.van-field__control {
background: rgba(255, 207, 197, 0.9);
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
border-radius: 16px;
padding: 8px;
font-size: 12px;
line-height: 16px;
min-height: 32px;
color: #e53416;
word-wrap: break-word;
white-space: normal;
overflow: hidden;
}
.van-button {
width: 64px;
height: 32px;
background: linear-gradient(180deg, #f35e31 0%, #e33016 100%);
border-radius: 16px;
font-size: 14px;
color: #fffefc;
}
}
}
}
.program-wrapper {
width: 100%;
height: 100%;
padding: 46px 16px 24px; // 適配導(dǎo)航欄高度
.program-bg {
width: 100%;
height: calc(100vh - 46px - 24px);
background: url("https://picsum.photos/1080/720") no-repeat center bottom; // 替換為公開(kāi)節(jié)目單背景
box-sizing: border-box;
background-size: 100% 100%;
text-align: center;
padding: 19vh 12px 4vh;
}
.program-detail {
width: 100%;
height: 100%;
}
.title {
height: 32px;
line-height: 40px;
font-size: 18px;
font-family: "wdch";
}
.subtitle {
line-height: 40px;
font-size: 15px;
font-family: "wdch";
}
.program-list {
height: calc(100% - 72px);
overflow-y: auto;
color: #e33016;
font-size: 14px;
margin: 0 -8px 0 0;
.program-item {
text-align: center;
line-height: 20px;
padding: 4px 0;
}
.award-item {
line-height: 40px;
font-size: 15px;
font-family: "wdch";
background: linear-gradient(180deg, #ff0000 0%, #ff8300 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.important-item {
padding: 4px 0;
> p {
font-size: 15px;
font-family: "wdch";
background: linear-gradient(180deg, #ff0000 0%, #ff8300 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
}
.text-gradient {
background: linear-gradient(180deg, #f0855c 0%, #ef2407 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
}
</style>
總結(jié)
到此這篇關(guān)于基于Vue3+西瓜視頻播放器(xgplayer)移動(dòng)端直播解決方案的文章就介紹到這了,更多相關(guān)Vue3 xgplayer移動(dòng)端直播內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue+Echart柱狀圖實(shí)現(xiàn)疫情數(shù)據(jù)統(tǒng)計(jì)
這篇文章主要介紹了在Vue nuxt項(xiàng)目中,如何使用Echart(百度圖表)柱狀圖來(lái)實(shí)現(xiàn)疫情數(shù)據(jù)統(tǒng)計(jì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2021-12-12
vue.js 實(shí)現(xiàn)圖片本地預(yù)覽 裁剪 壓縮 上傳功能
這篇文章主要介紹了vue.js 實(shí)現(xiàn)圖片本地預(yù)覽裁剪壓縮上傳功能,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-03-03
vue for循環(huán)出來(lái)的數(shù)據(jù)實(shí)現(xiàn)用逗號(hào)隔開(kāi)
HTML(HyperText Markup Language)是用于創(chuàng)建網(wǎng)頁(yè)的標(biāo)準(zhǔn)標(biāo)記語(yǔ)言,正確的HTML編碼和結(jié)構(gòu)對(duì)于網(wǎng)頁(yè)的正確顯示至關(guān)重要,當(dāng)HTML代碼正確無(wú)誤時(shí),網(wǎng)頁(yè)的效果圖將與設(shè)計(jì)師的預(yù)期相符,反之則可能出現(xiàn)布局錯(cuò)亂、樣式失效等問(wèn)題2024-10-10
前端框架React實(shí)現(xiàn)Vue的watch和computed詳解
React 實(shí)現(xiàn)computed的核心是useMemo,通過(guò)依賴數(shù)組控制緩存,完全匹配 Vue 計(jì)算屬性的緩存特性,這篇文章主要介紹了前端框架React實(shí)現(xiàn)Vue的watch和computed的相關(guān)資料,需要的朋友可以參考下2026-05-05
vue3 useRoute無(wú)法獲取get參數(shù)的問(wèn)題解決
本文主要介紹了vue3 useRoute無(wú)法獲取get參數(shù)的問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2026-03-03
Vue實(shí)現(xiàn)拖拽穿梭框功能四種方式實(shí)例詳解
這篇文章主要介紹了Vue實(shí)現(xiàn)拖拽穿梭框功能四種方式,使用原生js實(shí)現(xiàn)拖拽,VUe使用js實(shí)現(xiàn)拖拽穿梭框,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09
Vue?3?表單與后端數(shù)據(jù)交互之查詢并回顯數(shù)據(jù)步驟流程
本文給大家介紹Vue3表單與后端數(shù)據(jù)交互之查詢并回顯數(shù)據(jù)步驟流程,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-12-12
vue?codemirror實(shí)現(xiàn)在線代碼編譯器效果
這篇文章主要介紹了vue-codemirror實(shí)現(xiàn)在線代碼編譯器?,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
vue使用動(dòng)態(tài)組件手寫(xiě)Router View實(shí)現(xiàn)示例
這篇文章主要為大家介紹了vue使用動(dòng)態(tài)組件手寫(xiě)RouterView實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
VUE學(xué)習(xí)之Element-ui文件上傳實(shí)例詳解
今天進(jìn)行了element?文件上傳組件的運(yùn)用,寫(xiě)一下心得,下面這篇文章主要給大家介紹了關(guān)于VUE學(xué)習(xí)之Element-ui文件上傳的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10

