TUIRoomKit 默认提供完整的多人视频会议能力,包括多种视频布局模式、视频画面的挂件层 UI(姓名条、麦克风状态、角色标识等),以及屏幕共享时的自动布局切换。本文介绍如何通过
conference.setFeatureConfig 切换视频布局,以及如何通过 participantViewUI 插槽自定义每一路视频画面的挂件层 UI,以满足差异化的业务场景需求。
前提条件
视频布局与切换
TUIRoomKit 默认内置了以下四种常用的视频流布局,以满足不同场景下的沟通需求:
平台 | 布局类型 | 枚举值 | 适用场景 |
PC | 网格布局 | RoomLayoutTemplate.GridLayout | 多人平等交流,所有画面等大平铺。 |
侧边栏布局 | RoomLayoutTemplate.SidebarLayout | 教学/演讲,主讲人大窗,学员在右侧。 | |
顶部栏布局 | RoomLayoutTemplate.CinemaLayout | 影院模式,主画面上方显示其他成员画面。 | |
H5 | 移动端布局 | RoomLayoutTemplate.MobileLayout | 针对手机屏幕比例优化的大小窗或六宫格布局。 |
说明:
通过界面控件手动切换布局
在会议进行中,用户可以通过顶部工具栏内置的布局切换控件(LayoutWidget)自由切换视频布局,无需开发者进行额外开发。如需在特定场景下隐藏该控件,可通过
conference.setWidgetVisible 接口设置该组件不可见。import { conference, BuiltinWidget } from '@tencentcloud/roomkit-web-vue3';conference.setWidgetVisible({[BuiltinWidget.LayoutWidget]: false,});
说明:
H5 端及研讨会(Webinar)模式下,默认不显示该视频布局切换控件。
内部自动切换布局策略
在标准会议(Standard)模式下,为了保证最佳的会议体验,TUIRoomKit 会在以下两种特定场景下自动调整布局,无需手动干预:
触发条件 | 自动切换目标 | 说明 |
有参与者开始屏幕共享 | RoomLayoutTemplate.SidebarLayout(侧边栏布局) | 屏幕共享画面作为主画面展示,其余参与者画面缩略显示于右侧。 |
视频流数量降至 1 路 | RoomLayoutTemplate.GridLayout(网格布局) | 当前房间仅剩一路视频流时,自动切换为网格布局。 |
说明:
自动切换机制的优先级较高,会覆盖用户在界面上的选择或代码指定的布局。例如:用户在他人屏幕共享期间手动切换了布局,当共享结束后且视频流数量降至 1 路时,系统仍会自动触发切换回
RoomLayoutTemplate.GridLayout。通过 API 切换布局
除了用户的界面操作和系统的自动切换,开发者还可以通过
conference.setFeatureConfig 接口精确控制视频布局。该接口既支持在加入房间前(挂载组件前)设置初始值,也支持在会议进行中随时动态调用以强制切换:import { conference, RoomLayoutTemplate } from '@tencentcloud/roomkit-web-vue3';// 宫格布局conference.setFeatureConfig({layoutTemplate: RoomLayoutTemplate.GridLayout,});// 侧边栏布局conference.setFeatureConfig({layoutTemplate: RoomLayoutTemplate.SidebarLayout,});// 顶部栏布局conference.setFeatureConfig({layoutTemplate: RoomLayoutTemplate.CinemaLayout,});
说明:
覆盖行为:通过代码在运行中切换布局,将会直接覆盖用户此前在界面上手动选择的布局状态。
H5 端:固定使用
MobileLayout,暂不支持动态配置 layoutTemplate 。研讨会模式:不支持
layoutTemplate 配置,目前固定展示主持人的摄像头及屏幕分享画面,且顶部工具栏的布局切换控件会自动隐藏。视频流挂件与自定义 UI
在 TUIRoomKit 中,参与者的实时视频画面(包含摄像头画面与屏幕共享画面)由 RoomKit 内部负责渲染和排版,开发者无需手动管理复杂的音视频轨道。
默认视频流挂件
在基础的视频画面之上,TUIRoomKit 默认提供了一套完整的视频挂件层 UI(如下图所示),包含:
占位展示:当用户关闭摄像头时,居中显示该用户的头像占位图。
状态标签:在画面左下角悬浮显示用户姓名、角色标识(房主/管理员)以及实时的麦克风开关状态。

自定义视频流挂件
为了满足不同业务场景的个性化视觉需求,TUIRoomKit 允许开发者完全接管并替换视频画面上层的挂件层 UI。通过自定义,可以实现:
重塑视觉风格:重新设计头像、姓名栏、状态图标的样式、颜色和位置,使其与您的业务 APP 风格保持一致。
展示扩展数据:结合业务逻辑,在画面上叠加额外的信息标签(例如:老师/学生标识、VIP 等级、付费信息、自定义水印等)。
差异化 UI:根据视频流的类型(屏幕共享流 vs 摄像头流),呈现完全不同的界面交互。

实现方案:participantViewUI 插槽
ConferenceMainView(PC 端)和 ConferenceMainViewH5(H5 端)均提供了 participantViewUI 具名插槽,允许开发者完全替换视频画面上层的视频流挂件 UI。插槽参数
参数名 | 类型 | 说明 |
participant | RoomParticipant | 参与者信息对象,详见下方字段说明。 |
streamType | VideoStreamType | VideoStreamType.Camera(摄像头流)VideoStreamType.Screen(屏幕分享流)。 |
RoomParticipant 字段说明
participant 对象包含以下关键字段:字段名 | 类型 | 说明 |
userId | string | 用户唯一标识。 |
userName | string | 用户名称。 |
avatarUrl | string | 用户头像 URL。 |
nameCard | string | 用户名片(房间内昵称)。 |
role | RoomParticipantRole | 用户角色: Owner(房主)/ Admin(管理员)/ GeneralUser(普通用户)。 |
cameraStatus | DeviceStatus | 摄像头状态: On(开启)/ Off(关闭)。 |
microphoneStatus | DeviceStatus | 麦克风状态: On(开启)/ Off(关闭)。 |
screenShareStatus | DeviceStatus | 屏幕共享状态: On(共享中)/ Off(未共享)。 |
isMessageDisabled | boolean | 是否被禁言。 |
metaData | Record<string, string> | 用户自定义元数据。 |
自定义示例
PC 端
<template><ConferenceMainView><template #participantViewUI="{ participant, streamType }"><MyParticipantView :participant="participant" :stream-type="streamType" /></template></ConferenceMainView></template><script setup lang="ts">import { ConferenceMainView } from '@tencentcloud/roomkit-web-vue3';import MyParticipantView from './MyParticipantView.vue';</script>
H5 端
<template><ConferenceMainViewH5><template #participantViewUI="{ participant, streamType }"><MyParticipantView :participant="participant" :stream-type="streamType" /></template></ConferenceMainViewH5></template><script setup lang="ts">import { ConferenceMainViewH5 } from '@tencentcloud/roomkit-web-vue3';import MyParticipantView from './MyParticipantView.vue';</script>
自定义视频流组件示例(MyParticipantView.vue)
<template><div class="custom-participant-view"><!-- 摄像头关闭时展示头像 --><divv-if="streamType === VideoStreamType.Camera && participant.cameraStatus === DeviceStatus.Off"class="avatar-container"><img :src="participant.avatarUrl" :alt="displayName" class="avatar" /></div><!-- 用户信息区域 --><div class="user-info-overlay"><!-- 角色标识 --><div v-if="showRoleIcon" class="role-icon" :class="roleClass"><IconUser /></div><!-- 麦克风状态 --><div v-if="!isScreenStream" class="mic-status"><IconMicOff v-if="participant.microphoneStatus === DeviceStatus.Off" /><IconMicOn v-else /></div><!-- 用户名称 --><span class="user-name">{{ displayName }}</span><!-- 屏幕分享的提示 --><span v-if="isScreenStream" class="screen-indicator">正在分享屏幕</span></div></div></template><script setup lang="ts">import { computed } from 'vue';import {VideoStreamType,DeviceStatus,RoomParticipantRole,type RoomParticipant,} from 'tuikit-atomicx-vue3/room';import { IconUser, IconMicOff, IconMicOn } from '@tencentcloud/uikit-base-component-vue3';interface Props {participant: RoomParticipant;streamType: VideoStreamType;}const props = defineProps<Props>();const displayName = computed(() => props.participant.nameCard || props.participant.userName || props.participant.userId);const isScreenStream = computed(() => props.streamType === VideoStreamType.Screen);const showRoleIcon = computed(() => {return ((props.participant.role === RoomParticipantRole.Owner ||props.participant.role === RoomParticipantRole.Admin) &&props.streamType === VideoStreamType.Camera);});const roleClass = computed(() => {return props.participant.role === RoomParticipantRole.Owner ? 'owner' : 'admin';});</script><style lang="scss" scoped>.custom-participant-view {/* 满铺且取消指针事件,防误触底层交互 */position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;.avatar-container {position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);.avatar { width: 96px; height: 96px; border-radius: 50%; }}.user-info-overlay {position: absolute; bottom: 8px; left: 8px; display: flex; align-items: center; gap: 8px;padding: 4px 12px; background-color: rgba(0, 0, 0, 0.6); border-radius: 16px; color: white;pointer-events: auto; /* 恢复内部元素的点击交互 */.role-icon {width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center;&.owner { background-color: #1890ff; }&.admin { background-color: #faad14; }}.user-name { font-size: 14px; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.screen-indicator { font-size: 12px; color: #1890ff; }}}</style>
常见问题
如何判断当前画面是否为本地用户?
在自定义挂件层组件中,通过
useRoomParticipantState 获取 localParticipant 并与插槽传入的 participant.userId 进行比较:import { computed } from 'vue';import { useRoomParticipantState, type RoomParticipant } from 'tuikit-atomicx-vue3/room';const props = defineProps<{ participant: RoomParticipant }>();const { localParticipant } = useRoomParticipantState();const isLocal = computed(() => props.participant.userId === localParticipant.value?.userId);
如何获取用户的音量信息?
通过
useRoomParticipantState 提供的 speakingUsers Map 获取,键为 userId,值为当前音量(0 - 100):import { computed } from 'vue';import { useRoomParticipantState, type RoomParticipant } from 'tuikit-atomicx-vue3/room';const props = defineProps<{ participant: RoomParticipant }>();const { speakingUsers } = useRoomParticipantState();const volume = computed(() => speakingUsers.value.get(props.participant.userId) ?? 0);
如何处理屏幕共享流的特殊显示?
插槽参数
streamType 标识当前视频流类型,通过与 VideoStreamType.Screen 比较即可判断是否为屏幕共享流:import { computed } from 'vue';import { VideoStreamType } from 'tuikit-atomicx-vue3/room';const props = defineProps<{ streamType: VideoStreamType }>();const isScreenStream = computed(() => props.streamType === VideoStreamType.Screen);
自定义视频流 UI 的定位和层级如何处理?
自定义 UI 容器应该使用绝对定位填充整个父容器;
自定义 UI 的最外层容器应设置
pointer-events: none;,使点击事件可以穿透到下层的视频渲染区域;自定义 UI 内部需要点击的元素(按钮、图标、链接等),必须显式设置
pointer-events: auto;,使其恢复交互能力;.custom-participant-view {position: absolute;top: 0;left: 0;width: 100%;height: 100%;pointer-events: none;.user-info-overlay {pointer-events: auto;}}