即时通信 IM 视频布局

本篇文档旨在指导开发者在标准会议无 UI 集成方案中实现视频画面渲染、布局切换、屏幕共享画面展示、移动端布局适配以及视频画面挂件定制。可以以直接使用 RoomView 组件承载标准会议的视频区域,也可以通过 layoutTemplate、renderParticipantView 插槽和 stream-double-click 事件组合出符合业务场景的视频布局体验。

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

RoomView 只负责视频流画面容器和布局编排,不包含完整会议页面、成员列表、底部工具栏或业务弹窗。会议页面通常需要将 RoomView 与房间管理、设备控制、成员管理等能力组合使用。

一、前提条件

·用户已经通过 useLoginState 完成登录鉴权,请参考 接入概览 文档。

·用户已经通过 useRoomState 进入标准会议房间,请参考 房间管理 文档。

·承载 RoomView 的父容器必须具备明确的宽高,否则视频区域可能无法正常计算布局。

二、功能介绍

RoomView 是标准会议的视频布局容器,用于渲染成员摄像头流和屏幕共享流。开发者主要通过布局入参选择画面排列方式,通过插槽定制视频挂件,通过事件响应用户对视频画面的操作。

三、RoomView 组件接口

类型 名称 说明
入参 layoutTemplate?: RoomLayoutTemplate 视频布局模板。默认值为 RoomLayoutTemplate.GridLayout。
视频挂件插槽(Vue3) participantViewUI 自定义视频画面挂件,插槽参数为 { participant, streamType }。
视频挂件渲染函数(React) renderParticipantView 自定义视频画面挂件,插槽参数为 { participant, streamType }。
事件 stream-double-click 用户双击视频画面时触发,事件参数为 { participant, streamType }。

四、内置布局能力总览

标准会议中,RoomView 通过 layoutTemplate 属性控制布局。

布局 枚举值 适用端 推荐场景 表现说明
宫格布局 RoomLayoutTemplate.GridLayout PC Web 多人讨论、日常会议、视频面试 默认布局。最多按 3 x 3 展示当前页画面,超过 9 路时支持翻页。
顶部栏布局 RoomLayoutTemplate.CinemaLayout PC Web 演示、培训、屏幕共享 主画面居中突出,其他成员在顶部栏展示。
侧边栏布局 RoomLayoutTemplate.SidebarLayout PC Web 演讲、汇报、主讲人模式 主画面居中突出,其他成员在侧边栏展示。
移动端布局 RoomLayoutTemplate.MobileLayout H5 移动端会议页面 支持屏幕分享双指放大、滑动分页、主画面、悬浮画面和懒加载。React 暂不支持移动端布局。

五、核心参数说明

参数 说明 常见用途
participant 当前视频画面对应的成员对象,包含 userId、userName、nameCard、role、microphoneStatus、cameraStatus 等状态。 展示昵称、角色标识、麦克风/摄像头状态,或判断当前画面对应的操作目标。
streamType 当前视频画面的流类型,用于区分摄像头流和屏幕共享流。 摄像头流展示成员挂件;屏幕共享流展示“正在共享屏幕”等提示,并避免展示不适合的摄像头状态 UI。

六、场景一:渲染视频区域

完成登录和进房后,如需在会议页面中展示标准会议的视频画面,可直接渲染 RoomView。这是接入视频布局能力的最小使用方式。

七、实现思路

为视频区域准备一个有明确宽高的容器,并在其中渲染 RoomView。如果不传入 layoutTemplate,默认使用 RoomLayoutTemplate.GridLayout。

八、PC Web 示例代码

Vue3React<template> <div class="room-view-wrapper"> <RoomView :layout-template="RoomLayoutTemplate.GridLayout" /> </div></template><script setup lang="ts">import { RoomLayoutTemplate, RoomView } from 'tuikit-atomicx-vue3/room';</script><style scoped>/* 必须为 RoomView 父容器明确设置宽高 */.room-view-wrapper { width: 100%; height: 100%; }</style>import { RoomLayoutTemplate, RoomView } from 'tuikit-atomicx-react/room';export default function ConferenceRoom() { return ( <div style={{ width: '100%', height: '100%' }}> <RoomView layoutTemplate={RoomLayoutTemplate.GridLayout} /> </div> );}

九、H5 示例代码

Vue3React<template> <div class="room-view-wrapper"> <RoomView :layout-template="RoomLayoutTemplate.MobileLayout" /> </div></template><script setup lang="ts">import { RoomLayoutTemplate, RoomView } from 'tuikit-atomicx-vue3/room';</script><style scoped>/* 必须为 RoomView 父容器明确设置宽高 */.room-view-wrapper { width: 100%; height: 100%; }</style>import { RoomLayoutTemplate, RoomView } from 'tuikit-atomicx-react/room';export default function ConferenceRoom() { return ( <div style={{ width: '100%', height: '100%' }}> <RoomView layoutTemplate={RoomLayoutTemplate.MobileLayout} /> </div> );}

十、注意事项

·RoomView 会占满父容器,请确保父容器设置了明确的 width 和 height。

·RoomView 不负责创建或加入房间,也不负责打开本地摄像头或麦克风。设备打开逻辑请参考设备及网络。

十一、场景二:自定义视频区域挂件

如果你需要在视频画面上展示昵称、头像、角色标识、麦克风状态、音量波动、屏幕共享标识或业务标签,可以使用 renderParticipantView 插槽。

十二、实现思路

RoomView 会在每个视频画面上透传 participant 和 streamType。业务侧可以基于成员状态渲染挂件 UI,但不需要接管视频流播放逻辑。

十三、插槽参数

参数 类型 说明
participant RoomParticipant 当前视频画面对应的成员,包含 userId、userName、nameCard、role、microphoneStatus 等状态。
streamType VideoStreamType 当前画面类型,常见值为摄像头流和屏幕共享流。

十四、示例代码

会议页面示例:

Vue3React<template> <RoomView :layout-template="currentLayout"> <template #renderParticipantView="{ participant, streamType }"> <ParticipantViewUI :participant="participant" :stream-type="streamType" /> </template> </RoomView></template><script setup lang="ts">import { ref } from 'vue';import { RoomLayoutTemplate, RoomView,} from 'tuikit-atomicx-vue3/room';import ParticipantViewUI from './ParticipantViewUI.vue';const currentLayout = ref(RoomLayoutTemplate.GridLayout);</script>import { useState } from 'react';import { RoomLayoutTemplate, RoomView,} from 'tuikit-atomicx-react/room';import ParticipantViewUI from './ParticipantViewUI';export default function ConferenceRoom() { const [currentLayout, setCurrentLayout] = useState(RoomLayoutTemplate.GridLayout); return ( <RoomView layoutTemplate={currentLayout} renderParticipantView={({ participant, streamType }) => ( <ParticipantViewUI participant={participant} streamType={streamType} /> )} /> );}

ParticipantViewUI.vue 示例:

Vue3React<template> <div class="participant-view-ui" :class="{ 'camera-off': shouldShowCameraPlaceholder }" > <div v-if="shouldShowCameraPlaceholder" class="camera-placeholder"> {{ displayName }} </div> <div class="participant-info"> <span class="user-name">{{ displayName }}</span> <span v-if="isOwner && isCameraStream" class="role-tag">房主</span> <span v-if="isScreenStream" class="stream-tag">{{ displayName }} 正在屏幕分享</span> <span v-else class="device-tag">{{ isMicrophoneOn ? '麦克风已开' : '麦克风已关' }}</span> </div> </div></template><script setup lang="ts">import { computed } from 'vue';import { DeviceStatus, RoomParticipantRole, VideoStreamType,} from 'tuikit-atomicx-vue3/room';import type { RoomParticipant } from 'tuikit-atomicx-vue3/room';const props = defineProps<{ participant: RoomParticipant; streamType: VideoStreamType;}>();const displayName = computed(() => props.participant.nameCard || props.participant.userName || props.participant.userId);const isOwner = computed(() => props.participant.role === RoomParticipantRole.Owner && props.streamType === VideoStreamType.Camera);const isCameraStream = computed(() => props.streamType === VideoStreamType.Camera);const isScreenStream = computed(() => props.streamType === VideoStreamType.Screen);const isMicrophoneOn = computed(() => props.participant.microphoneStatus === DeviceStatus.On);const shouldShowCameraPlaceholder = computed(() => isCameraStream.value && props.participant.cameraStatus !== DeviceStatus.On);</script><style scoped>/* 必须设置 pointer-events 属性值为 none,否则会拦截底层视频画面的双击、拖拽交互! */.participant-view-ui { position: absolute; inset: 0; pointer-events: none; }.participant-view-ui.camera-off { background: #1f2937; }.camera-placeholder { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: #fff; font-size: 16px; }.participant-info { position: absolute; left: 8px; bottom: 8px; display: flex; align-items: center; gap: 6px; max-width: calc(100% – 16px); padding: 4px 8px; color: #fff; background: rgb(0 0 0 / 50%); border-radius: 12px; }.user-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }</style>import { DeviceStatus, RoomParticipantRole, VideoStreamType, useRoomParticipantState,} from 'tuikit-atomicx-react/room';import type { RoomParticipant } from 'tuikit-atomicx-react/room';interface Props { participant: RoomParticipant; streamType: VideoStreamType;}export default function ParticipantViewUI({ participant, streamType }: Props) { // RoomView internally wraps each renderParticipantView call in a component that // subscribes to participant state, so this component always receives the latest // participant. Call useRoomParticipantState() here only when you need extra // reactive data (e.g. speakingUsers) beyond what participant already provides. const { speakingUsers } = useRoomParticipantState(); const displayName = participant.nameCard || participant.userName || participant.userId; const isOwner = participant.role === RoomParticipantRole.Owner && streamType === VideoStreamType.Camera; const isCameraStream = streamType === VideoStreamType.Camera; const isScreenStream = streamType === VideoStreamType.Screen; const isMicrophoneOn = participant.microphoneStatus === DeviceStatus.On; const shouldShowCameraPlaceholder = isCameraStream && participant.cameraStatus !== DeviceStatus.On; const isSpeaking = (speakingUsers.get(participant.userId) ?? 0) > 0; return ( /* 必须设置 pointer-events: none,否则会拦截底层视频画面的双击、拖拽交互! */ <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', background: shouldShowCameraPlaceholder ? '#1f2937' : undefined, }} > {shouldShowCameraPlaceholder && ( <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%', color: '#fff', fontSize: 16 }}> {displayName} </div> )} <div style={{ position: 'absolute', left: 8, bottom: 8, display: 'flex', alignItems: 'center', gap: 6, maxWidth: 'calc(100% – 16px)', padding: '4px 8px', color: '#fff', background: 'rgb(0 0 0 / 50%)', borderRadius: 12 }}> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{displayName}</span> {isOwner && isCameraStream && <span>房主</span>} {isScreenStream ? <span>{displayName} 正在屏幕分享</span> : <span>{isMicrophoneOn ? (isSpeaking ? '讲话中' : '麦克风已开') : '麦克风已关'}</span> } </div> </div> );}

十五、推荐做法

·建议区分摄像头流和屏幕共享流。屏幕共享画面通常不需要展示麦克风图标,但可以展示“正在共享屏幕”等标识。

·建议为挂件容器设置 pointer-events: none,避免遮挡视频画面的双击、拖拽或其他交互。

十六、场景三:根据会议场景切换布局

不同会议场景对视频布局的要求不同。例如,多人讨论适合宫格布局;屏幕共享、培训或主讲人场景适合顶部栏或侧边栏布局;移动端页面适合移动端布局。

十七、实现思路

在业务层维护当前布局状态,并将其传入 RoomView 的 layoutTemplate。PC Web 可以在宫格、顶部栏、侧边栏之间切换;H5 建议固定使用 RoomLayoutTemplate.MobileLayout。

十八、示例代码

Vue3React<template> <RoomView :layout-template="currentLayout" /></template><script setup lang="ts">import { ref } from 'vue';import { RoomView, RoomLayoutTemplate } from 'tuikit-atomicx-vue3/room';const currentLayout = ref(RoomLayoutTemplate.GridLayout);function switchToGridLayout() { currentLayout.value = RoomLayoutTemplate.GridLayout;}function switchToSpeakerLayout() { currentLayout.value = RoomLayoutTemplate.SidebarLayout;}</script>import { useState } from 'react';import { RoomView, RoomLayoutTemplate } from 'tuikit-atomicx-react/room';export default function ConferenceRoom() { const [currentLayout, setCurrentLayout] = useState(RoomLayoutTemplate.GridLayout); function switchToGridLayout() { setCurrentLayout(RoomLayoutTemplate.GridLayout); } function switchToSpeakerLayout() { setCurrentLayout(RoomLayoutTemplate.SidebarLayout); } return <RoomView layoutTemplate={currentLayout} />;}

十九、注意事项

·PC Web 不建议使用 MobileLayout,H5 不建议使用 PC 的宫格、顶部栏或侧边栏布局。

·如果业务侧提供布局切换按钮,应在 UI 层根据当前平台只展示可用布局。

20、场景四:屏幕共享时自动切换布局

当会议中有人开始屏幕共享时,业务通常希望自动突出共享内容;当共享结束或只剩一个画面时,再恢复到宫格布局。

21、实现思路

通过 useRoomParticipantState 获取 participantWithScreen 和 participantList。当 participantWithScreen 从空变为有值时,将布局切换为 RoomLayoutTemplate.SidebarLayout 或 RoomLayoutTemplate.CinemaLayout;当画面数量恢复到 1 时,切回宫格布局。

22、关键接口 / 状态

状态 说明
participantWithScreen 当前正在屏幕共享的成员。
participantList 当前房间成员列表。
RoomLayoutTemplate.SidebarLayout 侧边栏主画面布局。
RoomLayoutTemplate.GridLayout 宫格布局。

23、示例代码

Vue3React<template> <RoomView :layout-template="currentLayout" /></template><script setup>import { ref, watch } from 'vue';import { RoomLayoutTemplate, useRoomParticipantState,} from 'tuikit-atomicx-vue3/room';const currentLayout = ref(RoomLayoutTemplate.GridLayout);const { participantList, participantWithScreen } = useRoomParticipantState();watch(participantWithScreen, (newVal, oldVal) => { if (newVal && !oldVal) { currentLayout.value = RoomLayoutTemplate.SidebarLayout; }});watch( () => participantList.value.length + (participantWithScreen.value ? 1 : 0), (count) => { if (count === 1) { currentLayout.value = RoomLayoutTemplate.GridLayout; } },);</script>import { useState, useEffect } from 'react';import { RoomLayoutTemplate, RoomView, useRoomParticipantState,} from 'tuikit-atomicx-react/room';export default function ConferenceRoom() { const [currentLayout, setCurrentLayout] = useState(RoomLayoutTemplate.GridLayout); const { participantList, participantWithScreen } = useRoomParticipantState(); useEffect(() => { if (participantWithScreen) { setCurrentLayout(RoomLayoutTemplate.SidebarLayout); } }, [participantWithScreen]); useEffect(() => { const count = participantList.length + (participantWithScreen ? 1 : 0); if (count === 1) { setCurrentLayout(RoomLayoutTemplate.GridLayout); } }, [participantList, participantWithScreen]); return <RoomView layoutTemplate={currentLayout} />;}

24、RoomView 核心逻辑说明

本节用于解释 RoomView 的内部表现,帮助开发者理解“为什么画面这样排序”“为什么某些远端画面没有持续播放”“为什么小窗口画质较低”。如果你只需要完成基础接入,可以先阅读前面的场景章节。

25、RoomView 核心逻辑

核心逻辑 说明 业务侧是否需要处理
视频流排序 根据屏幕共享、房主、本地用户、管理员、摄像头和麦克风状态生成视频流列表。 通常不需要。只有自定义成员列表或业务排序时才需要参考。
非可视区域不播放 视频画面支持懒加载,非可视区域中的远端视频流不会持续播放。 通常不需要。注意不要用自定义样式破坏布局容器的可视区域计算。
小窗口订阅低清流 视频流会根据画面尺寸和页面中的大画面数量选择合适清晰度,小窗口优先使用低清流,大画面和屏幕共享优先使用高清流。 通常不需要。业务侧不建议自行频繁切换远端流清晰度。
只负责排版和缩放 RoomView 负责视频画面的排版、缩放和渲染参数,不负责进房、开关设备、成员会控或业务弹窗。 需要业务侧与房间管理、设备管理、成员管理组合。

26、视频流排序参考

优先级 排序因素 说明
1 屏幕共享流 有屏幕共享时,屏幕共享流会优先进入视频流列表。
2 房主 房主画面优先。
3 本地用户 本地用户画面优先。
4 管理员 管理员画面优先。
5 同时开启摄像头和麦克风 音视频活跃成员优先。
6 开启摄像头 有视频画面的成员优先。
7 开启麦克风 仅音频活跃成员次之。

27、主画面选择规则

布局 主画面规则
宫格布局 不区分主画面,按分页展示视频流。
顶部栏 / 侧边栏布局 优先展示用户双击锁定的画面;否则展示屏幕共享;否则展示第一个开启摄像头的成员;再否则展示本地用户。
移动端布局 优先展示屏幕共享;1-2 人场景突出远端或当前主要画面;多人场景通过分页展示。

注意:说明:

注意:顶部栏和侧边栏布局中,用户双击某个画面后,该画面会成为主画面。若该成员离开房间,或被锁定的屏幕共享结束,主画面会自动回退到默认选择规则。

28、异常排查指引

视频布局本身通常不直接抛出业务错误,但可能因为房间状态、容器尺寸、设备状态或布局配置不正确导致画面不符合预期。建议按以下方式排查。

常见表现 问题类型 建议处理方式
RoomView 没有画面 未进入房间或未打开摄像头 确保在进房接口调用成功后再渲染 RoomView 组件。确认已经打开本地摄像头,设备开启逻辑参考设备及网络。
视频区域不可见或尺寸异常 父容器无尺寸 检查浏览器控制台元素,确认 RoomView 外层 div 的 width 和 height 是否计算为 0,建议设置为 100% 或具体的 px 值。
布局没有按预期切换 布局枚举错误 使用 RoomLayoutTemplate 枚举,不要手写字符串。
PC 端或者移动端交互不符合预期 平台布局不匹配 H5 使用 RoomLayoutTemplate.MobileLayout。
共享内容没有成为主画面 屏幕共享未突出展示 监听 participantWithScreen,在共享开始时切换到顶部栏或侧边栏布局。
双击或屏幕分享双指放大不生效 挂件遮挡交互 挂件层避免拦截事件,必要时设置 pointer-events: none。

29、API 文档

Component / State 功能描述 API 文档
RoomView 标准会议视频布局主容器,负责视频流展示、布局编排、插槽透传和双击事件。 API 文档
useRoomState 获取当前房间信息,用于判断是否已经进入标准会议房间。 API 文档
useRoomParticipantState 提供 participantList、participantWithScreen、participantListWithVideo 等布局辅助状态。 API 文档

31、RoomView 是否会自动打开摄像头和麦克风?

不会。RoomView 负责视频画面渲染和布局编排,打开或关闭本地摄像头、麦克风需要使用设备管理相关接口。

32、RoomView 是否包含默认昵称、麦克风图标和角色标识?

RoomView 只提供 renderParticipantView 插槽,不内置固定业务挂件。你可以根据 participant 和 streamType 自定义昵称、角色、麦克风状态和业务标签。

33、为什么页面中看不到视频布局?

常见原因包括尚未进入房间、父容器没有明确尺寸、当前平台使用了不合适的布局,或本地及远端成员没有打开摄像头。建议先确认 currentRoom、父容器宽高和设备状态。

34、屏幕共享时应该使用哪种布局?

PC Web 推荐使用 RoomLayoutTemplate.SidebarLayout 或 RoomLayoutTemplate.CinemaLayout,让共享内容成为主画面。H5 推荐继续使用 RoomLayoutTemplate.MobileLayout。

35、顶部栏布局和侧边栏布局有什么区别?

两者都属于主画面布局。顶部栏布局将其他成员放在主画面上方,侧边栏布局将其他成员放在主画面侧边。选择哪一种主要取决于业务页面的空间结构和用户习惯。

36、宫格布局最多展示多少路画面?

PC Web 宫格布局单页最多展示 9 路画面,超过后通过翻页展示。移动端布局采用滑动分页方式展示。

37、RoomView 提供的布局均不满足需求,是否可以提供更多的可选布局?

如果 RoomView 内置布局不满足你的需求,请 联系我们 获取更多视频流布局信息。

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