直播视频组件 (Web Vue3)

最近更新时间:2026-04-28 09:57:23

我的收藏
本文详细介绍了直播视频组件(LiveView)的接入方式与自定义能力。您可以直接参考本文示例快速集成组件,也可以根据业务需求对控制栏、插槽和播放器 UI 进行深度定制。

核心功能

功能分类
具体能力
智能流切换
LiveView 能够根据当前用户的身份(观众或连麦者)自动切换流类型。
观众模式: 组件将播放超低延迟视频流,确保数百万观众都能流畅观看,同时大幅节省流量成本。
连线模式: 组件会自动切换到实时音视频流,提供毫秒级的超低延迟,保证连线用户之间实时、清晰的互动体验。
多人连麦布局
自动适配单人与多人连麦场景的视频流布局,包含 Loading 动画和主播离开提示等状态展示。
内置播放器控制栏
开箱即用的播放器控制栏,包含播放/暂停、音量调节、清晰度切换(360P-1080P)、画中画、全屏等功能。
可定制化 UI
LiveView 提供从轻度到深度的渐进式自定义能力:
插槽注入:通过 Slot 在视频流上叠加水印、自定义 Loading、自定义连麦麦位 UI 等。
控制栏配置:隐藏/禁用/替换图标等方式定制内置按钮,或添加业务自定义按钮。
自定义控制栏 UI:隐藏内置控制栏,通过 useLivePlayerState() 提供的完整响应式状态和控制方法,构建 100% 自定义的播放控制界面。

效果展示

自定义连麦挂件
自定义控制栏操作按钮






自定义刷新时 Loading 效果
自定义播放器控制栏 UI 样式







准备工作

在开始调整视频挂件前,请先参考 主播开播观众观看 完成主流程的搭建。

组件定制化

LiveView 提供从轻度到深度的渐进式自定义能力,您可以根据业务需求选择合适的定制方式。

组件插槽

LiveView 提供以下插槽,用于自定义视频流上层的 UI 内容。
名称
参数
参数类型
说明

-
-
插槽内容会居中叠加在视频流上层,适合放置暂停按钮、水印等自定义元素。

userInfo

当前麦位上的用户信息,包含 userIduserNameavatarUrlmicrophoneStatuscameraStatus 等字段。如果麦位为空,则 userInfo.userId 为空字符串。

center-overlay 插槽

使用 center-overlay 插槽可以在视频流上层叠加自定义内容,不影响底层视频播放。适合放置水印、暂停按钮、视频刷新时 Loading 等覆盖层元素。



基本用法
<LiveView>
<template #center-overlay>
<div class="watermark-container">
<div class="watermark-grid">
<span v-for="n in 20" :key="n" class="watermark-item">Live</span>
</div>
</div>
</template>
</LiveView>

<style scoped>.watermark-container{position:absolute;inset:0;overflow:hidden;pointer-events:none;user-select:none}.watermark-grid{position:absolute;top:-50%;left:-50%;width:200%;height:200%;display:flex;flex-wrap:wrap;align-content:center;justify-content:center;gap:60px 80px;transform:rotate(-30deg)}.watermark-item{color:rgba(255,255,255,.15);font-size:18px;font-weight:700;letter-spacing:2px;white-space:nowrap}</style>
自定义刷新 Loading 示例
结合 useLivePlayerState()refresh 方法,实现带品牌 Logo 的刷新加载动画:
<template>
<LiveView>
<template #center-overlay>
<div v-if="isRefreshing" class="refresh-overlay">
<img class="refresh-logo" src="./logo.svg" alt="logo" />
<div class="refresh-spinner" />
<span class="refresh-text">加载中...</span>
</div>
<div v-else class="watermark">LIVE</div>
</template>
</LiveView>
<button @click="handleRefresh">刷新</button>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { LiveView, useLivePlayerState } from 'tuikit-atomicx-vue3';

const { refresh } = useLivePlayerState();
const isRefreshing = ref(false);

async function handleRefresh() {
isRefreshing.value = true;
await refresh();
isRefreshing.value = false;
}
</script>

<style scoped>.refresh-overlay{display:flex;flex-direction:column;align-items:center;gap:12px;pointer-events:none}.refresh-logo{width:40px;height:40px;object-fit:contain}.refresh-spinner{width:32px;height:32px;border:3px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.refresh-text{color:rgba(255,255,255,.8);font-size:14px}.watermark{color:rgba(255,255,255,.3);font-size:24px;font-weight:700;letter-spacing:2px;pointer-events:none;user-select:none;text-shadow:0 2px 8px rgba(0,0,0,.3)}</style>

streamViewUI 插槽

使用 streamViewUI 插槽可以完全接管每个麦位的 UI 渲染。您将获得当前麦位的用户信息,可自由实现主播/观众标识、设备状态展示、自定义布局等。



基本用法
<template>
<LiveView @empty-seat-click="handleApplyForSeat">
<template #streamViewUI="{ userInfo }">
<div class="custom-stream-ui">
<span class="user-name">{{ userInfo.userName || '未知用户' }}</span>
</div>
</template>
</LiveView>
</template>

<script setup lang="ts">
import { LiveView } from 'tuikit-atomicx-vue3';

const handleApplyForSeat = (seatIndex: number) => {
console.log('点击了空麦位,索引:', seatIndex);
// 在此处发起连麦申请
};
</script>

<style scoped>.custom-stream-ui{position:absolute;bottom:0;left:0;right:0;padding:8px 12px;background:linear-gradient(transparent,rgba(0,0,0,.6));pointer-events:none}.custom-stream-ui .user-name{color:#fff;font-size:14px;font-weight:500}</style>
自定义连麦 UI 示例
<template>
<LiveView>
<template #streamViewUI="{ userInfo }">
<div class="stream-overlay">
<div class="top-bar">
<div class="role-badge" :class="{ 'anchor-badge': isAnchor(userInfo) }">
<span class="badge-dot" />
{{ isAnchor(userInfo) ? '主播' : '观众' }}
</div>
</div>
<div class="bottom-bar">
<div class="user-info">
<span class="user-name-text">{{ userInfo.userName || '未知用户' }}</span>
</div>
<div class="device-icons">
<div class="icon-wrapper" :class="{ off: !isMicOn(userInfo) }">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" fill="currentColor" />
<path d="M19 11a7 7 0 0 1-14 0" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M12 18v3M9 21h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
<svg v-if="!isMicOn(userInfo)" class="slash-line" viewBox="0 0 24 24">
<line x1="5" y1="2" x2="19" y2="22" stroke="#ff4d4f" stroke-width="2" stroke-linecap="round" />
</svg>
</div>
<div class="icon-wrapper" :class="{ off: !isCameraOn(userInfo) }">
<svg viewBox="0 0 24 24" fill="none">
<rect x="2" y="5.5" width="14.5" height="13" rx="2.5" fill="currentColor" />
<path d="M17.5 10l3.5-2.2v8.4L17.5 14" fill="currentColor" />
</svg>
<svg v-if="!isCameraOn(userInfo)" class="slash-line" viewBox="0 0 24 24">
<line x1="5" y1="2" x2="19" y2="22" stroke="#ff4d4f" stroke-width="2" stroke-linecap="round" />
</svg>
</div>
</div>
</div>
</div>
</template>
</LiveView>
</template>

<script setup lang="ts">
import { LiveView, useLiveListState, DeviceStatus } from 'tuikit-atomicx-vue3';
import type { SeatUserInfo } from 'tuikit-atomicx-vue3';

const { currentLive } = useLiveListState();

const isAnchor = (userInfo: SeatUserInfo) => {
return userInfo.userId === currentLive.value?.liveOwner?.userId;
};

const isMicOn = (userInfo: SeatUserInfo) => {
return userInfo.microphoneStatus === DeviceStatus.On;
};

const isCameraOn = (userInfo: SeatUserInfo) => {
return userInfo.cameraStatus === DeviceStatus.On;
};
</script>

<style scoped>.stream-overlay{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;box-sizing:border-box;border-radius:6px;overflow:hidden}.top-bar{position:absolute;top:6px;left:6px;z-index:2}.role-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:500;line-height:16px;background:rgba(0,0,0,.45);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);color:rgba(255,255,255,.85)}.anchor-badge{background:linear-gradient(135deg,#ff6b35,#e8461e);color:#fff}.badge-dot{width:5px;height:5px;border-radius:50%;background:currentColor;flex-shrink:0}.anchor-badge .badge-dot{background:#fff;animation:pulse-dot 2s ease-in-out infinite}@keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.4}}.bottom-bar{position:absolute;bottom:0;left:0;right:0;z-index:2;display:flex;align-items:flex-end;justify-content:space-between;padding:28px 8px 6px;background:linear-gradient(to top,rgba(0,0,0,.6) 0%,rgba(0,0,0,.2) 50%,transparent 100%)}.user-info{flex:1;min-width:0}.user-name-text{display:block;color:#fff;font-size:11px;font-weight:500;line-height:16px;text-shadow:0 1px 3px rgba(0,0,0,.7);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.device-icons{display:flex;align-items:center;gap:3px;flex-shrink:0;margin-left:6px}.icon-wrapper{position:relative;width:16px;height:16px;color:rgba(255,255,255,.9);transition:color .2s ease}.icon-wrapper.off{color:rgba(255,255,255,.3)}.icon-wrapper svg{position:absolute;top:0;left:0;width:100%;height:100%}.icon-wrapper .slash-line{z-index:1}</style>

自定义控制栏操作按钮

LiveView 内置了播放器控制栏(播放/暂停、音量、清晰度、画中画、全屏等按钮)。通过 useLivePlayerState() ,您可以 控制播放行为、定制控制栏按钮、添加自定义按钮、监听播放器事件




隐藏/禁用内置按钮

buttons 对象包含所有内置按钮(playvolumeresolutionpictureInPicturefullscreen),直接修改即可实时生效,每个按钮支持以下属性( ButtonState ):
属性
类型
说明
visible
boolean
是否显示按钮。
disabled
boolean
是否禁用按钮。
icon
Component | (() => VNode)
自定义按钮默认态图标。
Play 按钮:播放时显示(即“暂停”图标,点击可暂停)。
Volume 按钮:未静音时显示(即“喇叭”图标)。
PictureInPicture 按钮:非画中画时显示。
Fullscreen 按钮:非全屏时显示。
activeIcon
Component | (() => VNode)

自定义按钮激活态图标。
Play 按钮:暂停时显示(即“播放”图标,点击可恢复播放)。
Volume 按钮:静音时显示(即“静音”图标)。
PictureInPicture 按钮:画中画模式下显示。
Fullscreen 按钮:全屏模式下显示。
tooltip
string
鼠标悬浮时的提示文案。
<script setup lang="ts">
import { useLivePlayerState } from 'tuikit-atomicx-vue3';
import MyPauseIcon from './icons/MyPauseIcon.vue';
import MyPlayIcon from './icons/MyPlayIcon.vue';
import MyVolumeOnIcon from './icons/MyVolumeOnIcon.vue';
import MyVolumeOffIcon from './icons/MyVolumeOffIcon.vue';

const { buttons } = useLivePlayerState();

// 隐藏/禁用按钮
buttons.pictureInPicture.visible = false; // 隐藏画中画按钮
buttons.fullscreen.disabled = true; // 禁用全屏按钮

// 自定义图标
buttons.play.icon = MyPauseIcon; // 默认:播放时显示(暂停按钮图标)
buttons.play.activeIcon = MyPlayIcon; // 激活:暂停时显示(恢复播放图标)
buttons.volume.icon = MyVolumeOnIcon; // 默认值:未静音时显示
buttons.volume.activeIcon = MyVolumeOffIcon; // 激活:静音时显示

// 自定义工具提示
buttons.resolution.tooltip = 'Switch Quality';
</script>

添加自定义按钮

通过 addCustomButtons 在控制栏添加业务按钮(例如分享、刷新):
<script setup lang="ts">
import { useLivePlayerState } from 'tuikit-atomicx-vue3';
import ShareIcon from './icons/ShareIcon.vue';

const { addCustomButtons, refresh } = useLivePlayerState();

addCustomButtons([
{
id: 'share',
icon: ShareIcon,
onClick: () => navigator.clipboard.writeText(window.location.href),
tooltip: 'Share',
position: 'end', // 'start' | 'end' | { slot: 'left'|'center'|'right' } | { anchor: ButtonId, position: 'before'|'after' }
},
]);
</script>

监听播放器事件

支持的事件有:
事件名
事件参数
说明
isPlaying: boolean
当播放状态发生变化(播放/暂停)时触发。
volume: number
当音量发生变化时触发。
isFullscreen: boolean
当进入或退出全屏模式时触发。
isPictureInPicture: boolean
当进入或退出画中画模式时触发。
resolution: Resolution
当清晰度切换时触发。
visible: boolean
当控制栏可见性发生变化(显示/隐藏)时触发。
<script setup lang="ts">
import { ref } from 'vue';
import { useLivePlayerState, PlayerControlEvent } from 'tuikit-atomicx-vue3';

const { subscribeEvent } = useLivePlayerState();

const titleVisible = ref(true);
const roomNameVisible = ref(true);

// 根据全屏状态显示/隐藏直播间标题
subscribeEvent(PlayerControlEvent.FullscreenChange, (isFullscreen: boolean) => {
titleVisible.value = !isFullscreen;
});

// 根据控制栏显隐状态同步显示/隐藏房间名称
subscribeEvent(PlayerControlEvent.ControlBarVisibilityChange, (visible: boolean) => {
roomNameVisible.value = visible;
});
</script>

自定义控制栏 UI

当内置控制栏的样式和布局无法满足您的业务需求时,您可以隐藏内置控制栏,使用 useLivePlayerState 提供的全部响应式状态控制方法构建 100% 自定义的播放器界面。
说明:useLivePlayerState() 是全局单例,在任意组件中调用都共享同一份状态,无需通过 props 层层传递。




步骤一:隐藏内置控制栏

<script setup lang="ts">
import { useLivePlayerState } from 'tuikit-atomicx-vue3';

const { buttons } = useLivePlayerState();

// 隐藏所有内置按钮,内置控制栏将不再渲染
Object.values(buttons).forEach(btn => (btn.visible = false));
</script>

步骤二:用响应式状态和控制方法驱动自定义 UI

<template>
<div class="my-player" @mouseenter="showControls" @mouseleave="startAutoHide" @mousemove="showControls">
<LiveView />
<div class="my-controls" :class="{ hidden: !controlsVisible }">
<div class="controls-left">
<button class="ctrl-btn" @click="isPlaying ? pause() : resume()">
<svg v-if="!isPlaying" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
<svg v-else viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" rx="1" /><rect x="14" y="4" width="4" height="16" rx="1" /></svg>
</button>
<button class="ctrl-btn" @click="refresh()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M1 4v6h6" /><path d="M23 20v-6h-6" /><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10M23 14l-4.64 4.36A9 9 0 0 1 3.51 15" /></svg>
</button>
<div class="volume-group">
<button class="ctrl-btn" @click="isMuted ? unmute() : mute()">
<svg v-if="!isMuted" viewBox="0 0 24 24" fill="currentColor"><path d="M11 5L6 9H2v6h4l5 4V5z" /><path d="M15.54 8.46a5 5 0 0 1 0 7.07" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
<svg v-else viewBox="0 0 24 24" fill="currentColor"><path d="M11 5L6 9H2v6h4l5 4V5z" /><line x1="23" y1="9" x2="17" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" /><line x1="17" y1="9" x2="23" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
</button>
<input class="volume-slider" type="range" :value="currentVolume" min="0" max="100"
@input="setVolume(Number(($event.target as HTMLInputElement).value))" />
</div>
</div>
<div class="controls-right">
<select v-if="resolutionList.value.length > 0" class="resolution-select"
:value="currentResolution.value?.value"
@change="switchResolution(resolutionList.value.find(r => r.value === Number(($event.target as HTMLSelectElement).value)))">
<option v-for="r in resolutionList.value" :key="r.value" :value="r.value">{{ r.label }}</option>
</select>
<button class="ctrl-btn" @click="isPictureInPicture ? exitPictureInPicture() : requestPictureInPicture()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" /><rect v-if="!isPictureInPicture" x="11" y="9" width="9" height="6" rx="1" fill="currentColor" stroke="none" /><path v-else d="M15 19l-3-3m0 0l3-3m-3 3h8" /></svg>
</button>
<button class="ctrl-btn" @click="isFullscreen ? exitFullscreen() : requestFullscreen()">
<svg v-if="!isFullscreen" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" /></svg>
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 14h4v4M20 10h-4V6M14 10h4V6M4 14h4v4" /></svg>
</button>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { LiveView, useLivePlayerState } from 'tuikit-atomicx-vue3';

const {
isPlaying, currentVolume, isMuted, isFullscreen,
isPictureInPicture, currentResolution, resolutionList,
buttons,
pause, resume, refresh,
setVolume, mute, unmute,
requestFullscreen, exitFullscreen,
requestPictureInPicture, exitPictureInPicture,
switchResolution,
} = useLivePlayerState();

// 隐藏所有内置按钮
Object.values(buttons).forEach(btn => (btn.visible = false));

// 控制栏自动显隐
const controlsVisible = ref(true);
let autoHideTimer: ReturnType<typeof setTimeout> | null = null;

function showControls() {
controlsVisible.value = true;
startAutoHide();
}

function startAutoHide() {
if (autoHideTimer) clearTimeout(autoHideTimer);
autoHideTimer = setTimeout(() => { controlsVisible.value = false; }, 3000);
}
</script>

<style scoped>.my-player{position:relative;width:100%;height:100%}.my-controls{position:absolute;bottom:0;left:0;right:0;z-index:20;pointer-events:auto;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:linear-gradient(to top,rgba(0,0,0,.75) 0%,rgba(0,0,0,.3) 70%,transparent 100%);transition:opacity .3s ease,transform .3s ease}.my-controls.hidden{opacity:0;transform:translateY(8px);pointer-events:none}.controls-left,.controls-right{display:flex;align-items:center;gap:4px}.ctrl-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:none;border-radius:6px;background:transparent;color:rgba(255,255,255,.85);cursor:pointer;transition:background .15s ease,color .15s ease}.ctrl-btn:hover{background:rgba(255,255,255,.12);color:#fff}.ctrl-btn:active{background:rgba(255,255,255,.2)}.ctrl-btn svg{width:18px;height:18px}.volume-group{display:flex;align-items:center;gap:2px}.volume-slider{width:70px;height:3px;-webkit-appearance:none;appearance:none;background:rgba(255,255,255,.25);border-radius:2px;outline:none;cursor:pointer}.volume-slider::-webkit-slider-thumb{-webkit-appearance:none;width:10px;height:10px;border-radius:50%;background:#fff;cursor:pointer;box-shadow:0 0 4px rgba(0,0,0,.3)}.resolution-select{height:28px;padding:0 8px;border:1px solid rgba(255,255,255,.15);border-radius:6px;background:rgba(0,0,0,.4);color:rgba(255,255,255,.85);font-size:11px;cursor:pointer;outline:none}.resolution-select:hover{border-color:rgba(255,255,255,.35)}.resolution-select option{background:#1a1a1a;color:#fff}</style>

播放控制 API 汇总

以下 API 均通过 useLivePlayerState() 获取:
分类
API
说明
响应式状态
isPlaying、isMuted、currentVolume、isFullscreen、isPictureInPicture、controlBarVisible、currentResolution、resolutionList、buttons
可直接绑定到模板的响应式状态。buttons 可通过赋值控制内置按钮的显隐、禁用和图标。
播放控制
pause()、resume()、refresh()
暂停、恢复播放、刷新(重新拉流)。
音量控制
setVolume(volume)、mute()、unmute()
设置音量(0-100)、静音、取消静音。
全屏
requestFullscreen()、exitFullscreen()
进入/退出全屏模式。
画中画
requestPictureInPicture()、exitPictureInPicture()
进入/退出画中画模式。
清晰度
switchResolution(resolution)
切换清晰度,参数为 Resolution 对象(含 label 和 value)。
控制栏
showControlBar()、hideControlBar()、setAutoHideDelay(ms)
显示/隐藏控制栏、设置自动隐藏延迟。
自定义按钮
addCustomButtons(buttons)
添加自定义按钮到控制栏,相同 id 自动更新。
事件订阅
subscribeEvent(event, callback), unsubscribeEvent(event, callback)
订阅/取消订阅播放器事件。建议在进入直播间前完成注册。

常见问题

如何解决浏览器自动播放受限导致的黑屏问题?

出于用户体验考虑,现代浏览器对网页自动播放功能实施了限制性策略。您可以参考如下代码自定义播放受限弹窗,引导用户与页面交互后恢复播放。
import TUIRoomEngine, { TUIAutoPlayCallbackInfo, TUIRoomEvents } from '@tencentcloud/tuiroom-engine-js';
import { TUIMessageBox } from '@tencentcloud/uikit-base-component-vue3';
import { useRoomEngine } from 'tuikit-atomicx-vue3';

// LiveView 组件的播放能力由底层的 TUIRoomEngine 提供支持。您可以监听该引擎的事件来处理更底层的播放问题
const roomEngine = useRoomEngine();

TUIRoomEngine.once('ready', () => {
roomEngine.instance?.on(TUIRoomEvents.onAutoPlayFailed, handleAutoPlayFailed);
});

function handleAutoPlayFailed(event: TUIAutoPlayCallbackInfo) {
// 可以自行弹窗引导用户与页面发生交互,交互后调用 event.resume() 方法恢复播放
TUIMessageBox.alert({
content: 'Content is ready. Click the button to start playback',
confirmText: 'Play',
callback: () => {
event.resume();
}
});
}