本文详细介绍了直播视频组件(LiveView)的接入方式与自定义能力。您可以直接参考本文示例快速集成组件,也可以根据业务需求对控制栏、插槽和播放器 UI 进行深度定制。
核心功能
功能分类 | 具体能力 |
智能流切换 | LiveView 能够根据当前用户的身份(观众或连麦者)自动切换流类型。 观众模式: 组件将播放超低延迟视频流,确保数百万观众都能流畅观看,同时大幅节省流量成本。 连线模式: 组件会自动切换到实时音视频流,提供毫秒级的超低延迟,保证连线用户之间实时、清晰的互动体验。 |
多人连麦布局 | 自动适配单人与多人连麦场景的视频流布局,包含 Loading 动画和主播离开提示等状态展示。 |
内置播放器控制栏 | 开箱即用的播放器控制栏,包含播放/暂停、音量调节、清晰度切换(360P-1080P)、画中画、全屏等功能。 |
可定制化 UI | LiveView 提供从轻度到深度的渐进式自定义能力: 插槽注入:通过 Slot 在视频流上叠加水印、自定义 Loading、自定义连麦麦位 UI 等。 控制栏配置:隐藏/禁用/替换图标等方式定制内置按钮,或添加业务自定义按钮。 自定义控制栏 UI:隐藏内置控制栏,通过 useLivePlayerState() 提供的完整响应式状态和控制方法,构建 100% 自定义的播放控制界面。 |
效果展示
自定义连麦挂件 | 自定义控制栏操作按钮 |
![]() | ![]() |
自定义刷新时 Loading 效果 | 自定义播放器控制栏 UI 样式 |
![]() | ![]() |
准备工作
组件定制化
组件插槽
LiveView 提供以下插槽,用于自定义视频流上层的 UI 内容。
名称 | 参数 | 参数类型 | 说明 |
| - | - | 插槽内容会居中叠加在视频流上层,适合放置暂停按钮、水印等自定义元素。 |
| userInfo | | 当前麦位上的用户信息,包含 userId、userName、avatarUrl、microphoneStatus、cameraStatus 等字段。如果麦位为空,则 userInfo.userId 为空字符串。 |
center-overlay 插槽


基本用法
<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 插槽


基本用法
<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 对象包含所有内置按钮(
play、volume、resolution、pictureInPicture、fullscreen),直接修改即可实时生效,每个按钮支持以下属性( 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>
添加自定义按钮
<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 | 当进入或退出画中画模式时触发。 | |
当清晰度切换时触发。 | ||
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() 是全局单例,在任意组件中调用都共享同一份状态,无需通过 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 | 说明 |
响应式状态 | isPlaying、isMuted、currentVolume、isFullscreen、isPictureInPicture、controlBarVisible、currentResolution、resolutionList、buttons | |
播放控制 | pause()、resume()、refresh() | 暂停、恢复播放、刷新(重新拉流)。 |
音量控制 | setVolume(volume)、mute()、unmute() | 设置音量(0-100)、静音、取消静音。 |
全屏 | requestFullscreen()、exitFullscreen() | 进入/退出全屏模式。 |
画中画 | requestPictureInPicture()、exitPictureInPicture() | 进入/退出画中画模式。 |
清晰度 | switchResolution(resolution) | |
控制栏 | 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();}});}



