视频布局及挂件(Web)

最近更新时间:2026-04-15 18:06:41

我的收藏
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">
<!-- 摄像头关闭时展示头像 -->
<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);

自定义视频流 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;
}
}