一、核心功能
| 功能分类 | 具体能力 |
| 智能流切换 | LiveView 能够根据当前用户的身份(观众或连麦者)自动切换流类型。观众模式: 组件将播放超低延迟视频流,确保数百万观众都能流畅观看,同时大幅节省流量成本。连线模式: 组件会自动切换到实时音视频流,提供毫秒级的超低延迟,保证连线用户之间实时、清晰的互动体验。 |
| 多人连麦布局 | 自动适配单人与多人连麦场景的视频流布局,包含 Loading 动画和主播离开提示等状态展示。 |
| 内置播放器控制栏 | 开箱即用的播放器控制栏,包含播放/暂停、音量调节、清晰度切换(360P-1080P)、画中画、全屏等功能。 |
| 可定制化 UI | LiveView 提供从轻度到深度的渐进式自定义能力:插槽注入:通过 Slot 在视频流上叠加水印、自定义 Loading、自定义连麦麦位 UI 等。控制栏配置:隐藏/禁用/替换图标等方式定制内置按钮,或添加业务自定义按钮。自定义控制栏 UI:隐藏内置控制栏,通过 useLivePlayerState() 提供的完整响应式状态和控制方法,构建 100% 自定义的播放控制界面。 |
二、效果展示
| 自定义连麦挂件 | 自定义控制栏操作按钮 |
| | |
| 自定义刷新时 Loading 效果 | 自定义播放器控制栏 UI 样式 |
| | |
三、准备工作
在开始调整视频挂件前,请先参考 主播开播 和 观众观看 完成主流程的搭建。
四、组件定制化
LiveView 提供从轻度到深度的渐进式自定义能力,你可以根据业务需求选择合适的定制方式。
五、组件插槽
LiveView 提供以下插槽,用于自定义视频流上层的 UI 内容。
| 名称 | 参数 | 参数类型 | 说明 |
| center-overlay | – | – | 插槽内容会居中叠加在视频流上层,适合放置暂停按钮、水印等自定义元素。 |
| streamViewUI | userInfo | SeatUserInfo | 当前麦位上的用户信息,包含 userId、userName、avatarUrl、microphoneStatus、cameraStatus 等字段。如果麦位为空,则 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 对象包含所有内置按钮(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>
十、添加自定义按钮
通过 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>
十一、监听播放器事件
支持的事件有:
| 事件名 | 事件参数 | 说明 |
| PlayStateChange | isPlaying: boolean | 当播放状态发生变化(播放/暂停)时触发。 |
| VolumeChange | volume: number | 当音量发生变化时触发。 |
| FullscreenChange | isFullscreen: boolean | 当进入或退出全屏模式时触发。 |
| PictureInPictureChange | isPictureInPicture: boolean | 当进入或退出画中画模式时触发。 |
| ResolutionChange | resolution: Resolution | 当清晰度切换时触发。 |
| ControlBarVisibilityChange | 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(); } });}