该方案将复杂的房间管理、成员状态、设备控制及屏幕共享等核心会议能力,高度抽象为易用的响应式数据、精简接口与事件,并内置了丰富的场景化最佳实践。你在享受响应式开发带来极致便利的同时,拥有对业务 UI 与交互 100% 的自定义控制权,彻底告别设备插拔监听、房间状态同步、视频流懒加载、大小流切换等繁杂的底层技术处理。
注意:推荐使用更高效的 AI 集成助手
注意:我们为你提供了全新的 AI 集成方式,只需要描述你的需求,即可自动生成集成代码,大幅提升开发效率。
注意:点击这里,立即体验 AI 集成
一、适用场景
凭借这套极具弹性的无 UI 集成架构,你可以轻松打破标准化会议产品的界面束缚,将高品质的音视频能力无缝内嵌至现有的业务工作流中。不论是企业内部协作,还是深耕垂直行业的在线服务,都能快速打磨出高度贴合自身品牌调性的互动体验:
| 企业协同 | 医疗问诊/医疗会诊 |
| | |
| 专业咨询 | 在线教育 |
| | |
除上述典型行业外,只要你的业务核心诉求涵盖 2~300 人并发的高质量实时互动、灵活的会中协作以及严密的会议秩序管控,该方案均能为你提供坚实稳定的技术底座,显著缩短产品的上线周期。
二、底层能力概览
尽管企业协同、在线教育、医疗问诊等业务面向不同的行业受众,但其底层产品形态高度一致。基于这套多人会议无 UI 底座,你可以轻松应对以下核心需求:
·高并发实时互动:支持最高 300 人稳定在会,保障低延迟、高流畅的音视频沟通体验。
·严密的会控与秩序管理:提供完善的房主/主持人权限体系,灵活管理成员状态。
·动态的会中协作:支持中途灵活加入、高质量屏幕共享与业务内容演示。
开发者只需基于同一套集成方案快速接入,即可在统一的能力底座上,自由构建贴合自身行业特性的产品体验。
四、步骤1:开通服务
请参考 开通服务 领取 TUIRoomKit 体验版。
五、步骤2:环境准备
·Node.js:≥ 18.19.1 (推荐使用 LTS 版本)。
·Vue:≥ 3.4.21(Vue3 集成)。
·React:≥ 18.2.0(React 集成)。
·现代浏览器:支持 WebRTC APIs 的现代浏览器。
·设备:摄像头、麦克风、扬声器。
六、步骤3:安装 Atomicx 依赖
Vue3
npmpnpmyarnnpm install tuikit-atomicx-vue3@5 @tencentcloud/uikit-base-component-vue3pnpm install tuikit-atomicx-vue3@5 @tencentcloud/uikit-base-component-vue3yarn add tuikit-atomicx-vue3@5 @tencentcloud/uikit-base-component-vue3
React
npmpnpmyarnnpm install tuikit-atomicx-react @tencentcloud/uikit-base-component-reactpnpm install tuikit-atomicx-react @tencentcloud/uikit-base-component-reactyarn add tuikit-atomicx-react @tencentcloud/uikit-base-component-react
七、搭建基础多人房间
下面我们将通过一个最小可用 Demo,快速完成标准会议场景的基础接入。完成本节后,你将获得一个可运行的 Web 会议页面,支持登录鉴权、创建或加入房间、设备控制、房间画面渲染、屏幕分享以及成员列表展示。
八、步骤1:配置全局 UIKitProvider
先在应用根组件中配置 UIKitProvider。它主要用于提供全局主题和语言配置,建议在 App.vue(Vue3)或 App.tsx(React)中完成初始化。
Vue3React<template> <UIKitProvider theme="light" language="zh-CN"> <div id="app-content" /> </UIKitProvider></template><script setup lang="ts">import { UIKitProvider } from '@tencentcloud/uikit-base-component-vue3';</script>import { UIKitProvider } from '@tencentcloud/uikit-base-component-react';export default function App() { return ( <UIKitProvider theme="light" language="zh-CN"> <div id="app-content" /> </UIKitProvider> );}
九、步骤2:登录与身份鉴权
在项目的 src/components 目录下创建 LoginUserInfo.vue(Vue3)或 LoginUserInfo.tsx(React)文件,并拷贝以下代码完成用户登录与退出。
Vue3React<template> <div class="login-user-info"> <template v-if="loginUserInfo"> <span>{{ loginUserInfo.userName || loginUserInfo.userId }}</span> <TUIButton type="primary" @click="handleLogout">退出登录</TUIButton> </template> <template v-else> <TUIInput v-model="sdkAppId" style="width: 180px;" placeholder="请输入 sdkAppId" /> <TUIInput v-model="userId" style="width: 180px;" placeholder="请输入 userId" /> <TUIInput v-model="userName" style="width: 220px;" placeholder="请输入 userName(可选)" /> <TUIInput v-model="userSig" style="width: 420px;" :maxLength="2000" placeholder="请输入 userSig" /> <TUIButton type="primary" @click="handleLogin">登录</TUIButton> </template> </div></template><script setup lang="ts">import { ref } from 'vue';import { TUIInput, TUIButton } from '@tencentcloud/uikit-base-component-vue3';import { useLoginState } from 'tuikit-atomicx-vue3';const { loginUserInfo, login, logout, setSelfInfo } = useLoginState();const sdkAppId = ref('');const userId = ref('');const userName = ref('');const userSig = ref('');async function handleLogin() { if (!sdkAppId.value || !userId.value || !userSig.value) { return; } await login({ sdkAppId: Number(sdkAppId.value), userId: userId.value, userSig: userSig.value, }); if (userName.value) { await setSelfInfo({ userName: userName.value, avatarUrl: '', }); }}async function handleLogout() { await logout();}</script><style scoped>.login-user-info { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }</style>import { useState } from 'react';import { TUIInput, TUIButton, Toast } from '@tencentcloud/uikit-base-component-react';import { useLoginState } from 'tuikit-atomicx-react/room';export default function LoginUserInfo() { const { loginUserInfo, login, logout, setSelfInfo } = useLoginState(); const [sdkAppId, setSdkAppId] = useState(''); const [userId, setUserId] = useState(''); const [userName, setUserName] = useState(''); const [userSig, setUserSig] = useState(''); async function handleLogin() { if (!sdkAppId || !userId || !userSig) return; await login({ sdkAppId: Number(sdkAppId), userId, userSig, }); if (userName) { await setSelfInfo({ userName, avatarUrl: '' }); } } async function handleLogout() { await logout(); } return ( <div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}> {loginUserInfo ? ( <> <span>{loginUserInfo.userName || loginUserInfo.userId}</span> <TUIButton type="primary" onClick={handleLogout}>退出登录</TUIButton> </> ) : ( <> <TUIInput style={{ width: '180px' }} placeholder="请输入 sdkAppId" value={sdkAppId} onChange={e => setSdkAppId(e.target.value)} /> <TUIInput style={{ width: '180px' }} placeholder="请输入 userId" value={userId} onChange={e => setUserId(e.target.value)} /> <TUIInput style={{ width: '220px' }} placeholder="请输入 userName(可选)" value={userName} onChange={e => setUserName(e.target.value)} /> <TUIInput style={{ width: '420px' }} maxLength={2000} placeholder="请输入 userSig" value={userSig} onChange={e => setUserSig(e.target.value)} /> <TUIButton type="primary" onClick={handleLogin}>登录</TUIButton> </> )} </div> );}
为了快速体验,你可以先通过控制台或 UserSig 工具生成测试用 userSig。正式环境请改为由业务服务端签发。
十、步骤3:创建或加入房间
在项目的 src/components 目录下创建 RoomControl.vue(Vue3)或 RoomControl.tsx(React)文件,并拷贝以下代码完成房间生命周期管理。
Vue3React<template> <div class="room-control"> <template v-if="!currentRoom"> <TUIInput v-model="roomId" style="max-width: 200px;" placeholder="请输入房间 ID" /> <TUIButton type="primary" @click="handleCreateRoom">创建房间</TUIButton> <TUIButton type="primary" @click="handleJoinRoom">加入房间</TUIButton> </template> <template v-else> <span>房间 ID:{{ currentRoom.roomId }}</span> <TUIButton type="primary" @click="handleLeaveRoom">离开房间</TUIButton> <TUIButton v-if="localParticipant?.role === RoomParticipantRole.Owner" type="primary" @click="handleEndRoom"> 结束房间 </TUIButton> </template> </div></template><script setup lang="ts">import { ref } from 'vue';import { TUIInput, TUIButton, TUIToast } from '@tencentcloud/uikit-base-component-vue3';import { useRoomState, useRoomParticipantState, RoomParticipantRole } from 'tuikit-atomicx-vue3/room';const { currentRoom, createAndJoinRoom, joinRoom, leaveRoom, endRoom } = useRoomState();const { localParticipant } = useRoomParticipantState();const roomId = ref('');async function handleCreateRoom() { try { await createAndJoinRoom({ roomId: roomId.value, options: { roomName: '临时房间' } }); TUIToast.success({ message: '创建房间成功' }); } catch { TUIToast.error({ message: '创建房间失败,请重试' }); }}async function handleJoinRoom() { try { await joinRoom({ roomId: roomId.value }); TUIToast.success({ message: '加入房间成功' }); } catch { TUIToast.error({ message: '加入房间失败,请重试' }); }}async function handleLeaveRoom() { await leaveRoom();}async function handleEndRoom() { await endRoom();}</script><style scoped>.room-control { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 16px; background: #fff; border: 1px solid #e5e5e5; border-radius: 8px; flex: 1; min-width: 300px; }</style>import { useState } from 'react';import { TUIInput, TUIButton, Toast } from '@tencentcloud/uikit-base-component-react';import { useRoomState, useRoomParticipantState, RoomParticipantRole } from 'tuikit-atomicx-react/room';export default function RoomControl() { const { currentRoom, createAndJoinRoom, joinRoom, leaveRoom, endRoom } = useRoomState(); const { localParticipant } = useRoomParticipantState(); const [roomId, setRoomId] = useState(''); async function handleCreateRoom() { try { await createAndJoinRoom({ roomId, options: { roomName: '临时房间' } }); Toast.success({ message: '创建房间成功' }); } catch { Toast.error({ message: '创建房间失败,请重试' }); } } async function handleJoinRoom() { try { await joinRoom({ roomId }); Toast.success({ message: '加入房间成功' }); } catch { Toast.error({ message: '加入房间失败,请重试' }); } } return ( <div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', padding: '16px', background: '#fff', border: '1px solid #e5e5e5', borderRadius: '8px', flex: 1, minWidth: '300px' }}> {!currentRoom ? ( <> <div style={{ width: '200px', flexShrink: 0 }}> <TUIInput placeholder="请输入房间 ID" value={roomId} onChange={e => setRoomId(e.target.value)} /> </div> <TUIButton type="primary" onClick={handleCreateRoom}>创建房间</TUIButton> <TUIButton type="primary" onClick={handleJoinRoom}>加入房间</TUIButton> </> ) : ( <> <span>房间 ID:{currentRoom.roomId}</span> <TUIButton type="primary" onClick={() => leaveRoom()}>离开房间</TUIButton> {localParticipant?.role === RoomParticipantRole.Owner && ( <TUIButton type="primary" onClick={() => endRoom()}>结束房间</TUIButton> )} </> )} </div> );}
本示例同时提供“创建房间”和“加入房间”两个入口,方便使用两个测试账号完成联调。
十一、步骤4:媒体设备与屏幕分享
在项目的 src/components 目录下创建 DeviceControl.vue(Vue3)或 DeviceControl.tsx(React)文件,并拷贝以下代码完成摄像头、麦克风和屏幕分享控制。
Vue3React<template> <div class="device-control"> <TUIButton type="primary" @click="toggleCamera"> {{ isLocalCameraOpen ? '关闭摄像头' : '打开摄像头' }} </TUIButton> <TUIButton type="primary" @click="toggleMicrophone"> {{ isLocalMicrophoneOpen ? '关闭麦克风' : '打开麦克风' }} </TUIButton> <TUIButton type="primary" @click="toggleScreenShare"> {{ isScreenSharing ? '停止共享' : '共享屏幕' }} </TUIButton> </div></template><script setup lang="ts">import { computed } from 'vue';import { TUIButton, TUIToast } from '@tencentcloud/uikit-base-component-vue3';import { DeviceStatus, useDeviceState } from 'tuikit-atomicx-vue3/room';const { cameraStatus, microphoneStatus, screenStatus, openLocalCamera, closeLocalCamera, openLocalMicrophone, closeLocalMicrophone, startScreenShare, stopScreenShare,} = useDeviceState();const isLocalCameraOpen = computed(() => cameraStatus.value === DeviceStatus.On);const isLocalMicrophoneOpen = computed(() => microphoneStatus.value === DeviceStatus.On);const isScreenSharing = computed(() => screenStatus.value === DeviceStatus.On);async function toggleCamera() { try { if (isLocalCameraOpen.value) { await closeLocalCamera(); } else { await openLocalCamera(); } } catch { TUIToast.error({ message: '摄像头操作失败,请检查设备权限' }); }}async function toggleMicrophone() { try { if (isLocalMicrophoneOpen.value) { await closeLocalMicrophone(); } else { await openLocalMicrophone(); } } catch { TUIToast.error({ message: '麦克风操作失败,请检查设备权限' }); }}async function toggleScreenShare() { try { if (isScreenSharing.value) { await stopScreenShare(); } else { await startScreenShare({ screenAudio: true }); } } catch { TUIToast.error({ message: '屏幕共享失败,请检查浏览器权限' }); }}</script><style scoped>.device-control { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 16px; background: #fff; border: 1px solid #e5e5e5; border-radius: 8px; flex: 1; min-width: 300px; }</style>import { TUIButton, Toast } from '@tencentcloud/uikit-base-component-react';import { DeviceStatus, useDeviceState } from 'tuikit-atomicx-react/room';export default function DeviceControl() { const { cameraStatus, microphoneStatus, screenStatus, openLocalCamera, closeLocalCamera, openLocalMicrophone, closeLocalMicrophone, startScreenShare, stopScreenShare, } = useDeviceState(); const isLocalCameraOpen = cameraStatus === DeviceStatus.On; const isLocalMicrophoneOpen = microphoneStatus === DeviceStatus.On; const isScreenSharing = screenStatus === DeviceStatus.On; async function toggleCamera() { try { if (isLocalCameraOpen) { await closeLocalCamera(); } else { await openLocalCamera(); } } catch { Toast.error({ message: '摄像头操作失败,请检查设备权限' }); } } async function toggleMicrophone() { try { if (isLocalMicrophoneOpen) { await closeLocalMicrophone(); } else { await openLocalMicrophone(); } } catch { Toast.error({ message: '麦克风操作失败,请检查设备权限' }); } } async function toggleScreenShare() { try { if (isScreenSharing) { await stopScreenShare(); } else { await startScreenShare({ screenAudio: true }); } } catch { Toast.error({ message: '屏幕共享失败,请检查浏览器权限' }); } } return ( <div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', padding: '16px', background: '#fff', border: '1px solid #e5e5e5', borderRadius: '8px', flex: 1, minWidth: '300px' }}> <TUIButton type="primary" onClick={toggleCamera}> {isLocalCameraOpen ? '关闭摄像头' : '打开摄像头'} </TUIButton> <TUIButton type="primary" onClick={toggleMicrophone}> {isLocalMicrophoneOpen ? '关闭麦克风' : '打开麦克风'} </TUIButton> <TUIButton type="primary" onClick={toggleScreenShare}> {isScreenSharing ? '停止共享' : '共享屏幕'} </TUIButton> </div> );}
十二、步骤5:视频区域及成员列表
在项目的 src/components 目录下创建 ConferenceRoom.vue(Vue3)或 ConferenceRoom.tsx(React)文件,并拷贝以下代码完成会议主区域渲染。
Vue3React<template> <div v-if="currentRoom" class="conference-room"> <div class="conference-content"> <div class="room-stage"> <RoomView> <template #participantViewUI="{ participant, streamType }"> <div v-if="participant" :class="['participant-overlay', { 'participant-overlay–camera-off': isCameraOff(participant, streamType) }]" > <Avatar v-if="isCameraOff(participant, streamType)" class="participant-avatar" size="xxl" :src="participant.avatarUrl" :user-id="participant.userId" /> <div v-if="isLocalScreenStream(participant, streamType)" class="screen-share-mask"> 正在屏幕分享中 </div> <div class="participant-tag"> <span>{{ participant.userName || participant.userId }}</span> <span v-if="participant.microphoneStatus !== DeviceStatus.On && !isScreen(streamType)">麦克风已关闭</span> <span v-if="isScreen(streamType)">屏幕共享中</span> </div> </div> </template> </RoomView> </div> <aside class="member-panel"> <div class="member-title"> 参会者({{ currentRoom.participantCount || participantList.length }}) </div> <div v-for="participant in participantList" :key="participant.userId" class="member-item"> <div class="member-name"> <span>{{ participant.userName || participant.userId }}</span> <span v-if="isRoomOwner(participant.userId)" class="member-role-tag">房主</span> </div> <div class="member-status"> <span>{{ participant.cameraStatus === DeviceStatus.On ? '开摄像头' : '关摄像头' }}</span> <span>{{ participant.microphoneStatus === DeviceStatus.On ? '开麦克风' : '关麦克风' }}</span> </div> </div> </aside> </div> </div> <div v-else class="empty-state"> 请先创建房间或加入已有房间 </div></template><script setup lang="ts">import { watch } from 'vue';import { Avatar, DeviceStatus, RoomView, useRoomParticipantState, useRoomState, VideoStreamType,} from 'tuikit-atomicx-vue3/room';const { currentRoom } = useRoomState();const { participantList, getParticipantList, localParticipant } = useRoomParticipantState();watch( () => currentRoom.value?.roomId, async (roomId, oldRoomId) => { if (!oldRoomId && roomId) { await getParticipantList({ cursor: '' }); } },);function isScreen(streamType: VideoStreamType) { return streamType === VideoStreamType.Screen;}function isCameraOff(participant: { cameraStatus?: DeviceStatus }, streamType: VideoStreamType) { return !isScreen(streamType) && participant.cameraStatus !== DeviceStatus.On;}function isLocalScreenStream(participant: { userId?: string }, streamType: VideoStreamType) { return isScreen(streamType) && participant.userId === localParticipant.value?.userId;}function isRoomOwner(userId?: string) { return userId === currentRoom.value?.roomOwner.userId;}</script><style scoped>.conference-room { display: flex; flex-direction: column; gap: 16px; min-height: 0; }.conference-content { display: flex; gap: 16px; min-height: 520px; }.room-stage { flex: 1; background: #fff; border: 1px solid #e5e5e5; border-radius: 8px; overflow: hidden; }.member-panel { width: 280px; padding: 16px; background: #fff; border: 1px solid #e5e5e5; border-radius: 8px; overflow-y: auto; }.member-title { margin-bottom: 12px; font-weight: 600; }.member-item { padding: 10px 0; border-bottom: 1px solid #f5f5f5; }.member-name { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #222; margin-bottom: 4px; }.member-role-tag { padding: 2px 6px; border-radius: 10px; background: rgba(0, 82, 217, 0.12); color: #0052d9; font-size: 12px; line-height: 1; }.member-status { display: flex; gap: 8px; font-size: 12px; color: #888; }.participant-overlay { position: absolute; inset: 0; }.participant-overlay–camera-off { background: #f3f4f6; }.participant-avatar { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }.screen-share-mask { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.45); color: #fff; font-size: 18px; font-weight: 600; }.participant-tag { position: absolute; left: 12px; bottom: 12px; display: flex; gap: 8px; padding: 6px 10px; border-radius: 14px; background: rgba(0, 0, 0, 0.65); color: #fff; font-size: 12px; }.empty-state { min-height: 520px; display: flex; align-items: center; justify-content: center; color: #999; background: #fff; border: 1px dashed #d9d9d9; border-radius: 8px; }</style>import { useEffect, useRef } from 'react';import { Avatar, DeviceStatus, RoomView, useRoomParticipantState, useRoomState, VideoStreamType,} from 'tuikit-atomicx-react/room';export default function ConferenceRoom() { const { currentRoom } = useRoomState(); const { participantList, getParticipantList, localParticipant } = useRoomParticipantState(); const prevRoomIdRef = useRef<string | undefined>(undefined); useEffect(() => { const roomId = currentRoom?.roomId; if (!prevRoomIdRef.current && roomId) { getParticipantList({ cursor: '' }); } prevRoomIdRef.current = roomId; }, [currentRoom?.roomId]); function isRoomOwner(userId?: string) { return userId === currentRoom?.roomOwner.userId; } if (!currentRoom) { return ( <div style={{ minHeight: '520px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999', background: '#fff', border: '1px dashed #d9d9d9', borderRadius: '8px' }}> 请先创建房间或加入已有房间 </div> ); } return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', minHeight: 0 }}> <div style={{ display: 'flex', gap: '16px', minHeight: '520px' }}> <div style={{ flex: 1, background: '#fff', border: '1px solid #e5e5e5', borderRadius: '8px', overflow: 'hidden' }}> <RoomView renderParticipantView={({ participant, streamType }) => { const isScreen = streamType === VideoStreamType.Screen; const isCameraOff = !isScreen && participant.cameraStatus !== DeviceStatus.On; const isLocalScreen = isScreen && participant.userId === localParticipant?.userId; return ( <div style={{ position: 'absolute', inset: 0, background: isCameraOff ? '#f3f4f6' : undefined }}> {isCameraOff && ( <Avatar style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} size="xxl" src={participant.avatarUrl} /> )} {isLocalScreen && ( <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.45)', color: '#fff', fontSize: '18px', fontWeight: 600 }}> 正在屏幕分享中 </div> )} <div style={{ position: 'absolute', left: '12px', bottom: '12px', display: 'flex', gap: '8px', padding: '6px 10px', borderRadius: '14px', background: 'rgba(0,0,0,0.65)', color: '#fff', fontSize: '12px' }}> <span>{participant.userName || participant.userId}</span> {!isScreen && participant.microphoneStatus !== DeviceStatus.On && <span>麦克风已关闭</span>} {isScreen && <span>屏幕共享中</span>} </div> </div> ); }} /> </div> <aside style={{ width: '280px', padding: '16px', background: '#fff', border: '1px solid #e5e5e5', borderRadius: '8px', overflowY: 'auto' }}> <div style={{ marginBottom: '12px', fontWeight: 600 }}> 参会者({currentRoom.participantCount || participantList.length}) </div> {participantList.map(participant => ( <div key={participant.userId} style={{ padding: '10px 0', borderBottom: '1px solid #f5f5f5' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: '#222', marginBottom: '4px' }}> <span>{participant.userName || participant.userId}</span> {isRoomOwner(participant.userId) && ( <span style={{ padding: '2px 6px', borderRadius: '10px', background: 'rgba(0,82,217,0.12)', color: '#0052d9', fontSize: '12px' }}>房主</span> )} </div> <div style={{ display: 'flex', gap: '8px', fontSize: '12px', color: '#888' }}> <span>{participant.cameraStatus === DeviceStatus.On ? '开摄像头' : '关摄像头'}</span> <span>{participant.microphoneStatus === DeviceStatus.On ? '开麦克风' : '关麦克风'}</span> </div> </div> ))} </aside> </div> </div> );}
RoomView 负责房间中的布局组织与视频画面渲染,你可以通过视频挂件接口补充成员名称、设备状态、本地屏幕分享遮罩等自定义 UI(Vue3 使用 participantViewUI 插槽,React 使用 renderParticipantView prop)。同时,你也可以基于 useRoomParticipantState 获取成员数据,自行渲染成员面板。
十三、步骤6:组装完整页面
完成以上组件后,你可以回到 src/App.vue(Vue3)或 src/App.tsx(React)中将它们组装为一个完整页面。
Vue3React<template> <UIKitProvider theme="light" language="zh-CN"> <div class="app-container"> <header class="app-header"> <h1 class="app-title">Conference Demo</h1> <LoginUserInfo /> </header> <main v-if="loginUserInfo" class="app-main"> <div class="control-panel"> <RoomControl /> <DeviceControl /> </div> <ConferenceRoom /> </main> <div v-else class="welcome-screen"> <div class="welcome-content"> <h2>欢迎使用多人会议 Demo</h2> <p>请先完成登录,然后创建房间或加入房间。</p> </div> </div> </div> </UIKitProvider></template><script setup lang="ts">import { UIKitProvider } from '@tencentcloud/uikit-base-component-vue3';import { useLoginState } from 'tuikit-atomicx-vue3';import LoginUserInfo from './components/LoginUserInfo.vue';import DeviceControl from './components/DeviceControl.vue';import RoomControl from './components/RoomControl.vue';import ConferenceRoom from './components/ConferenceRoom.vue';const { loginUserInfo } = useLoginState();</script><style>* { box-sizing: border-box; }html, body, #app { width: 100%; height: 100%; margin: 0; }body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: #f5f5f5; }.app-container { width: 100%; height: 100%; display: flex; flex-direction: column; }.app-header { padding: 16px 24px; background: #fff; border-bottom: 1px solid #e5e5e5; display: flex; align-items: center; justify-content: space-between; gap: 16px; }.app-title { margin: 0; font-size: 20px; font-weight: 600; color: #222; }.app-main { flex: 1; display: flex; flex-direction: column; gap: 16px; padding: 20px; min-height: 0; width: 100%; max-width: 1400px; margin: 0 auto; }.control-panel { display: flex; gap: 16px; flex-wrap: wrap; }.welcome-screen { flex: 1; display: flex; align-items: center; justify-content: center; }.welcome-content { text-align: center; color: #666; }</style>import { UIKitProvider } from '@tencentcloud/uikit-base-component-react';import { useLoginState } from 'tuikit-atomicx-react/room';import LoginUserInfo from './components/LoginUserInfo';import DeviceControl from './components/DeviceControl';import RoomControl from './components/RoomControl';import ConferenceRoom from './components/ConferenceRoom';export default function App() { const { loginUserInfo } = useLoginState(); return ( <UIKitProvider theme="light" language="zh-CN"> <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}> <header style={{ padding: '16px 24px', background: '#fff', borderBottom: '1px solid #e5e5e5', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px' }}> <h1 style={{ margin: 0, fontSize: '20px', fontWeight: 600, color: '#222' }}>Conference Demo</h1> <LoginUserInfo /> </header> {loginUserInfo ? ( <main style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '16px', padding: '20px', minHeight: 0, width: '100%', maxWidth: '1400px', margin: '0 auto' }}> <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}> <RoomControl /> <DeviceControl /> </div> <ConferenceRoom /> </main> ) : ( <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ textAlign: 'center', color: '#666' }}> <h2>欢迎使用多人会议 Demo</h2> <p>请先完成登录,然后创建房间或加入房间。</p> </div> </div> )} </div> </UIKitProvider> );}
到这里,一个最小可用的标准会议页面就已经组装完成。
十四、步骤7:运行和联调验证
启动开发服务器。
npmpnpmyarnnpm run devpnpm run devyarn run dev
十五、准备两个测试账号
在腾讯云 控制台 获取 sdkAppId 信息并生成两个不同的 userId 和对应的 userSig(例如 user_test_A、user_test_B)。
十六、打开两个浏览器标签页
·窗口 A:使用 user_test_A 登录。
·窗口 B:使用 user_test_B 登录。
十七、测试流程
·在窗口 A 中:输入房间 ID(例如 "test_room_A"),点击创建房间。
·在窗口 A 中:点击打开摄像头和打开麦克风。
·在窗口 B 中:输入相同的房间 ID("test_room_A"),点击加入房间。
·在窗口 B 中:点击打开摄像头和打开麦克风。
·验证两个窗口能否看到对方的视频画面并听到声音。
完成以上验证后,你已经成功跑通一个最小可用的标准会议 Demo,可以继续进入专题文档扩展更复杂的业务能力。
十八、下一步
当你完成了基础的多人音视频房间功能后,你可以参考以下功能指南,进一步丰富和扩展你的特色业务能力。
十九、核心功能
| 多人音视频房间功能 | 功能介绍 | 实现指南 |
| 房间管理 | 支持客户端及服务端发起会议,加入会议。 | 房间管理 |
| 设备管理 | 支持用户切换麦克风、摄像头和扬声器设备。 | 设备及网络 |
| 视频布局 | 提供宫格模式和演讲者模式的视频布局。 | 视频布局 |
| 屏幕分享 | 支持用户分享屏幕、窗口或浏览器标签页。 | 屏幕分享 |
| 成员管理 | 房主/管理员可进行踢人、转让房主等权限操作。 | 成员管理 |
20、附加功能
| 多人音视频房间功能 | 功能介绍 | 实现指南 |
| 会中聊天 | 实现参会者间的实时文本消息通信。 | 会中聊天 |
| 会中呼叫 | 支持房间中用户邀请其他用户加入当前房间。 | 会中呼叫 |
| 预定房间 | 提供房间的提前预约和列表管理功能。 | 预定房间 |
| 虚拟背景 | 支持用户设置背景模糊或替换为虚拟背景图。 | 虚拟背景 |
| 基础美颜 | 支持用户设置磨皮、美白、红润的美颜效果。 | 基础美颜 |
21、API 参考
| State Hook 名称 | 核心作用与场景 | API 文档 |
| useLoginState | 登录鉴权。 管理用户登录生命周期和身份信息。 | API 文档 |
| useDeviceState | 设备与网络。 管理麦克风、摄像头、屏幕共享设备,以及网络质量。 | API 文档 |
| useRoomState | 房间管理。 管理实时房间的生命周期(创建、加入、结束)和核心信息。 | API 文档 |
| useRoomParticipantState | 成员管理。 管理房间内所有参会者信息、角色和权限。 | API 文档 |
| useMessageListState | 聊天消息。 管理消息收发和展示。 | API 文档 |
| useMessageInputState | 聊天输入。管理消息的发送 | API 文档 |
23、本地开发时音视频功能正常,但部署到线上环境后无法正常采集用户的摄像头或麦克风设备?
·原因分析:浏览器出于安全和隐私保护的考虑,对音视频设备(麦克风、摄像头)的采集有着严格限制。只有在安全环境下,采集操作才会被允许。安全环境协议包括:https://、localhost、file:// 等。HTTP 协议被视为不安全,浏览器会默认禁止其访问媒体设备。
·解决方案:若你在本地(localhost)测试一切正常,但部署后出现采集失败,请立即检查你的网页是否部署在 HTTP 协议上。你必须使用 HTTPS 协议部署你的网页,并确保具备有效的安全证书。
·相关资源:更多关于 URL 域名及协议的限制详情,请参见 URL 域名及协议限制说明。
24、是否支持使用 iframe 集成?
支持。使用 iframe 集成无 UI SDK 时, 需要在 iframe 标签中配置 allow 属性以授予必要的浏览器权限(麦克风、摄像头、屏幕共享、全屏等),示例如下:
// 开启麦克风、摄像头、屏幕分享、全屏权限<iframe allow="microphone; camera; display-capture; display; fullscreen;">
25、联系我们
如果你在接入或使用过程中有任何疑问或者建议,欢迎 联系我们 提交反馈。