即时通信 IM 视频布局及挂件(Web)

TUIRoomKit 默认提供完整的多人视频会议能力,包括多种视频布局模式、视频画面的挂件层 UI(姓名条、麦克风状态、角色标识等),以及屏幕共享时的自动布局切换。

相关产品
即时通信 IM 产品页,含版本规格、价格与购买入口。

这篇说说如何通过 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; } }

赞 (0)
上一篇 4天前
下一篇 4天前