这篇说说如何通过 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,});
注意:说明:
六、视频流挂件与自定义 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"> <!– 摄像头关闭时展示头像 –> <div v-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);
20、自定义视频流 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; } }