基于Vue3+西瓜視頻播放器(xgplayer)移動端直播解決方案
更新時間:2026年02月12日 10:56:52 作者:胖虎265
在現(xiàn)代前端開發(fā)中,視頻播放功能越來越常見,從在線教育到娛樂平臺都離不開高質(zhì)量的視頻播放器,這篇文章主要介紹了基于Vue3+西瓜視頻播放器(xgplayer)移動端直播的相關資料,需要的朋友可以參考下
基于 Vue3 + 西瓜視頻播放器(xgplayer),打造輕量、流暢、可直接復用的移動端直播組件,適配快速集成場景。

最終效果
- ?? 支持 HLS(m3u8)直播流,播放流暢無卡頓
- ?? 自定義封面 + 播放按鈕,點擊即啟動
- ?? 移動端適配(內(nèi)嵌播放、全屏切換、無滾動條)
- ?? 基礎錯誤處理,兼容瀏覽器自動播放限制
核心代碼實現(xiàn)
1. 依賴安裝
npm install xgplayer --save
2. 極簡直播播放器組件(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);
// 定義一個變量來存儲 player 實例
let player: Player;
const clickImage = () => {
if (player == null) {
initPlayer();
showImage.value = false;
}
};
// 初始化西瓜視頻
const initPlayer = () => {
player = new Player({
lang: "zh", // 設置播放器的語言為中文(zh)
volume: 0.5, // 設置初始音量為50%
id: props.id, // 使用傳入的id屬性,可能是一個容器或視頻元素的ID
url: props.videoUrl, // 設置視頻的URL地址
poster: props.poster, // 設置視頻封面圖
playsinline: props.playsinline, // 是否允許在移動設備上內(nèi)嵌播放(不跳轉(zhuǎn)到全屏)
height: props.height, // 設置播放器的高度
width: props.width, // 設置播放器的寬度
isLive: true, // 設置是否直播
playbackRate: [1], // 倍速展示
defaultPlaybackRate: 1,
cssFullscreen: false, // 設置是否使用CSS全屏樣式
download: false, // 隱藏下載按鈕
autoplay: false, // 自動播放視頻
whitelist: [""], // 設置白名單,當前為空
});
// 添加事件監(jiā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. 頁面使用示例(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ū)(評論/節(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>
<!-- 評論列表 -->
<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="沒有更多了"
@load="onLoad"
>
<li v-for="(item, index) in commentList" :key="index" class="comment-item">
<!-- 評論用戶信息 -->
<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>
<!-- 評論內(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 ? "收起" : "展開" }}
</span>
</div>
<div class="time">{{ formatTime(item.publishTime) }}</div>
</div>
</li>
</van-list>
</van-pull-refresh>
</ul>
<!-- 評論發(fā)布框 -->
<van-cell-group>
<van-field
v-model="commentInput"
maxlength="100"
rows="1"
autosize
label=""
type="textarea"
placeholder="請輸入(最多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">標題占位</div>
<div class="subtitle text-gradient">企業(yè)20xx年度先進頒獎典禮</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"; // 引入播放器組件(下文附簡化版)
import { VanPullRefresh, VanList, VanCellGroup, VanField, VanButton, VanOverlay, VanToast } from "vant";
import "vant/lib/index.css";
// 直播核心配置(替換為實際項目配置)
const liveStreamUrl = ref("https://live-stream.example.com/livestream.m3u8"); // 直播流地址(m3u8)
const livePoster = ref("https://picsum.photos/1080/720"); // 直播封面圖
// 標簽切換
const btnList = ref(["評論", "節(jié)目單"]);
const activeIndex = ref(0);
const showProgramModal = ref(false);
// 評論模塊狀態(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ù)(實際項目從接口獲?。?
const programList = ref([
{
isImportant: true,
programType: "開場",
programName: "領導致辭",
host: "張總",
awardDesc: "歡迎各位嘉賓蒞臨本次頒獎典禮"
},
{
isImportant: false,
programType: "頒獎",
programName: "年度優(yōu)秀員工",
host: "李經(jīng)理",
awardDesc: "表彰本年度表現(xiàn)突出的優(yōu)秀員工代表"
},
{
isImportant: false,
programType: "表演",
programName: "員工才藝展示",
host: "王主持人",
awardDesc: "員工自發(fā)組織的文藝表演"
},
{
isImportant: true,
programType: "閉幕",
programName: "總結發(fā)言",
host: "劉總",
awardDesc: "本次頒獎典禮總結及未來展望"
}
]);
/** 切換標簽(評論/節(jié)目單) */
const handleTabClick = (index) => {
activeIndex.value = index;
if (index === 1) {
showProgramModal.value = true;
}
};
/** 加載評論列表(模擬接口請求) */
const onLoad = async () => {
if (finished.value) return;
loading.value = true;
try {
// 模擬接口請求(實際項目替換為真實接口)
await new Promise(resolve => setTimeout(resolve, 800));
// 模擬評論數(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: "中國電信",
department: "技術部",
content: `恭喜獲獎的同事!${"直播內(nèi)容非常精彩,為奮斗者點贊~".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頁后無更多數(shù)據(jù))
if (page >= 3) {
finished.value = true;
}
page++;
} catch (error) {
console.error("加載評論失敗:", error);
VanToast("加載失敗,請稍后重試");
} finally {
loading.value = false;
}
};
/** 刷新評論列表 */
const onRefresh = () => {
finished.value = false;
commentList.value = [];
page = 1;
loading.value = true;
onLoad();
};
/** 發(fā)布評論 */
const publishComment = async () => {
const content = commentInput.value.trim();
if (!content) return;
isPublishing.value = true;
try {
// 模擬發(fā)布接口請求
await new Promise(resolve => setTimeout(resolve, 500));
// 發(fā)布成功后添加到列表頭部
commentList.value.unshift({
userName: "當前用戶",
userType: "normal",
company: "中國電信",
department: "市場部",
content: content,
publishTime: new Date().toISOString(),
isExpand: false
});
commentInput.value = "";
VanToast("發(fā)布成功");
} catch (error) {
console.error("發(fā)布評論失?。?, error);
VanToast("發(fā)布失敗,請稍后重試");
} finally {
isPublishing.value = false;
}
};
/** 展開/收起長評論 */
const toggleExpand = (item) => {
item.isExpand = !item.isExpand;
};
/** 格式化時間 */
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)聽節(jié)目單彈窗,加載數(shù)據(jù)(實際項目可在此處請求最新節(jié)目單) */
watch(showProgramModal, (isShow) => {
if (isShow) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
});
/** 頁面掛載時加載評論 */
onMounted(() => {
onLoad();
});
</script>
<style lang="scss" scoped>
@font-face {
font-family: "wdch";
src: url("https://cdn.example.com/fonts/wdch.ttf") format("truetype"); // 替換為公開字體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; // 替換為公開背景圖
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; // 替換為公開按鈕背景
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; // 適配導航欄高度
.program-bg {
width: 100%;
height: calc(100vh - 46px - 24px);
background: url("https://picsum.photos/1080/720") no-repeat center bottom; // 替換為公開節(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>
總結
到此這篇關于基于Vue3+西瓜視頻播放器(xgplayer)移動端直播解決方案的文章就介紹到這了,更多相關Vue3 xgplayer移動端直播內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:
相關文章
vue項目中請求數(shù)據(jù)特別多導致頁面卡死的解決
這篇文章主要介紹了vue項目中請求數(shù)據(jù)特別多導致頁面卡死的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
vue3數(shù)據(jù)監(jiān)聽watch/watchEffect的示例代碼
我們都知道監(jiān)聽器的作用是在每次響應式狀態(tài)發(fā)生變化時觸發(fā),在組合式?API?中,我們可以使用?watch()函數(shù)和watchEffect()函數(shù),下面我們來看下vue3如何進行數(shù)據(jù)監(jiān)聽watch/watchEffect,感興趣的朋友一起看看吧2023-02-02
Vue組件通信$attrs、$listeners實現(xiàn)原理解析
這篇文章主要介紹了Vue組件通信$attrs、$listeners實現(xiàn)原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-09-09
Vue實現(xiàn)鼠標經(jīng)過文字顯示懸浮框效果的示例代碼
這篇文章主要介紹了Vue實現(xiàn)鼠標經(jīng)過文字顯示懸浮框效果,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-10-10
Vue.js項目實戰(zhàn)之多語種網(wǎng)站的功能實現(xiàn)(租車)
這篇文章主要介紹了Vue.js項目實戰(zhàn)之多語種網(wǎng)站(租車)的功能實現(xiàn) ,需要的朋友可以參考下2019-08-08

