使用小鱼易连Web SDK加入多人音视频会议后,需要展示参会者的画面。SDK 提供两种布局方式:自动布局和自定义布局,分别适用于不同的需求。
布局方式 | 优势 | 局限 |
| 不支持业务扩展布局模式 | |
自定义布局 |
|
|
本文将重点介绍如何使用小鱼易连 Web SDK 进行自定义布局,快速完成多人会议画面的接入和展示。
自定义布局:Web SDK 上报参会者总数,业务层基于数量进行分页,每页最多请求 9 个画面视频流,通过 layout 事件获取最新参会者列表数据,并根据此数据渲染布局。
事件 | 必要性 | 描述 |
必须 |
| |
必须 |
| |
可选 |
| |
可选 |
|
事件 | 描述 |
| |
设置视频播放容器元素,SDK接管控制视频的播放和画面渲染 | |
辅助计算函数,用于计算移动端设备参会者画面旋转角度 |
调用requestNewLayout
方法进行请流时,不同的配置参数会影响请流行为,具体可以分为两种主要的使用场景:指定参会者分页(推荐)和自动填充分页。这两种方式提供了不同的灵活性和控制权,开发者可以根据实际需求选择最适合的方式。
在使用自定义布局时,开发者通过监听bulkRoster
或roster
事件获取到参会者列表数据,并通过指定calluri
的方式来进行分页请流。这种方式更加灵活,支持 People + Content 同时请求画面,适合大部分多方视频会议场景,因此推荐使用此方法。
此方法的核心在于开发者自行维护分页数据,而不是依赖 SDK 内部的分页机制。pageIndex
必须固定为 0,由开发者控制每次请求的参会者列表数据。
注意事项
pageIndex
必须固定为 0,SDK 不参与分页计算;conf-change-info
事件上报contentUri
、主会场Uri
字段;roster
中的endpointedId
即为calluri
字段;举例
假设会议中存在 4 个设备,参会者的roster
列表数据为 [A, B, C, D],若每页最多请求 2 个参会者的视频流,则请求第一页的数据如下:
const reqList = [
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 指定A的设备ID
calluri: 'A'
},
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 指定B的设备ID
calluri: 'B'
}
];
XYClient.requestNewLayout(reqList, 2, 0);
切换第二页时的请求数据如下:
const reqList = [
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 指定C的设备ID
calluri: 'C'
},
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 指定的设备ID
calluri: 'D'
}
];
XYClient.requestNewLayout(reqList, 2, 0);
在使用自定义布局请流时,支持不指定具体的calluri
,可以将calluri
设置为空值 (''
),这表示自动填充画面请流。SDK 会根据参会者的优先级自动排序和分组,进行自动分页请求。开发者只需指定每一页需要显示的最大人数,SDK 会自动计算每一页的参会者并发起请求。
排序规则
SDK 会根据以下优先级进行排序:
注意事项
pageIndex
必须从 1 开始,翻页时pageIndex
递增;conf-change-info
事件上报的参会者总人数(totalEpCount
)进行处理。举例
假设会议中有 4 个设备,conf-change-info
事件上报的totalEpCount
为 4,且每页最多显示 2 个画面,则计算出总页数为 2。第一页的请求数据如下:
const reqList = [
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 自动填充参会者视频流
calluri: ''
},
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 自动填充参会者视频流
calluri: ''
}
];
// 请求第一页数据
XYClient.requestNewLayout(reqList, 2, 1);
切换第二页的请求数据如下:
const reqList = [
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 自动填充参会者视频流
calluri: ''
},
{
resolution: 1,
quality: 1,
mediagroupid: 0,
// 自动填充参会者视频流
calluri: ''
}
];
// 请求第二页的数据
XYClient.requestNewLayout(reqList, 2, 2);
如果需要进行自定义布局,基于指定参会者分页模式的处理流程如下:
在通过 XYRTC.createClient 创建客户端时,可以配置 layout 字段为 CUSTOM,即开启自定义布局模式;
const XYClient = XYRTC.createClient({
...
layout: 'CUSTOM'
});
通过监听 conf-change-info 事件,缓存content calluri,并执行第四步请流处理;
let cacheContentUri = '';
XYClient.on('conf-change-info', (data: IConfInfo) => {
cacheContentUri = data.contentUri;
// 见第四步骤实现
requestLayout();
})
类型定义:IConfInfo
监听 bulkRoster 或者 roster 事件,获取参会者列表数据,并执行第四步请流处理;
let cacheRosterList = [];
XYClient.on('roster', (data: IRoster[]) => {
cacheRosterList = data;
// 见第四步骤实现
requestLayout();
})
layout
事件;
const requestLayout = () => {
const reqList = [];
const totalCount = cacheRosterList.length;
// 每页最大显示4画面
const pageSize = totalCount > 4 ? 4 : totalCount;
// 页码,可以基于totalCount和pageSize算出总页数,并缓存数据作分页处理
// 此处固定配置第一页,仅作演示流程使用
const pageIndex = 1;
// 获取指定页码的参会者数据
const rosters = cacheRosterList.slice(pageIndex - 1, pageSize);
for (let i = 0; i < pageSize; i++) {
const requestConfig = {
mediagroupid: 0,
resolution: 2,
quality: 1,
calluri: rosters[i].endpointId,
};
reqList.push(requestConfig);
}
// 存在共享内容
if (cacheContentUri) {
reqList.push({
// Content内容配置:1
mediagroupid: 1,
// Content分辨率请求1080P
resolution: 4,
// 帧率最高
quality: 2,
calluri: cacheContentUri,
});
}
// 调用请求流接口,注意第三个参数固定配置0
XYClient.requestNewLayout(reqList, 5, 0);
}
回调参数类型:IConfInfo
当请流或者参会者状态变更后,持续监听 layout 事件,此列表数据是最终请求的拉流结果数据,可以使用此数据计算每个参会者的位置、大小、旋转角度等信息,并进行画面渲染。
下方演示代码处理逻辑:
提示
// 安装@xylink/xy-rtc-sdk库即可加载
// 此处使用SDK提供的辅助函数计算旋转角度
import { getLayoutRotateInfo } from './node_modules/@xylink/xy-rtc-sdk/xyrtc.js';
// 参会者布局列表数据
let layoutList = [];
// 缓存列表数据,做Diff使用
let cacheLayoutList = [];
XYClient.on('layout', (e: ILayout[]) => {
console.log('layout list: ', e);
layoutList = e;
// 获取成员列表布局数据,直接渲染使用即可
renderLayoutList();
})
// 渲染Layout布局列表数据
const renderLayoutList = () => {
const meetingContainer = document.getElementById('layout');
diffInvalidLayout();
layoutList
// 批量更新计算移动端画面旋转角度
.map((item) => {
if (item.rotationInfo) {
const rotate = getLayoutRotateInfo(item.rotationInfo, videoContainerWidth, videoContainerHeight);
if (rotate) {
item.rotate = rotate;
}
}
return item;
})
// 下方同自动布局处理流程一致
.forEach((item) => {
const { positionStyle = {}, rotate = {}, id, state } = item;
const { displayName = '' } = item?.roster || {};
const layoutItemId = 'wrap-' + id;
const isExist = document.getElementById(layoutItemId);
const { width, height, left, top } = positionStyle;
// Layout外围容器样式
const layoutItemStyle = `width: ${width}; height: ${height}; left: ${left}; top: ${top}`;
const isPause = state === 'MUTE';
// video样式,包含竖屏旋转样式
let layoutVideoStyle = '';
for (let key in rotate) {
layoutVideoStyle += `${key}: ${rotate[key]};`;
}
if (!isExist) {
// 初始渲染
const layoutItemString = `
<div class="layout_item" style="${layoutItemStyle}" id=${layoutItemId}>
<div class="layout_name">${displayName}</div>
<div class="layout_pause center ${isPause ? 'show' : 'hidden'}">视频暂停</div>
<div class="center layout_video">
<video class="video" style="${layoutVideoStyle}" playsinline autoplay muted></video>
</div>
</div>`;
// 向容器中追加新的Layout画面Dom
meetingContainer.insertAdjacentHTML('beforeend', layoutItemString);
// id不变的情况下,只需要执行一次即可
XYClient.setVideoRenderer(id, layoutItemId);
} else {
// 已渲染,更新样式和状态
const layoutItemEle = document.getElementById(layoutItemId);
const layoutNameEle = layoutItemEle.querySelector('.layout_name');
const layoutVideoEle = layoutItemEle.querySelector('.video');
const layoutPauseEle = layoutItemEle.querySelector('.layout_pause');
layoutItemEle.style.cssText = layoutItemStyle;
layoutNameEle.innerHTML = displayName;
layoutVideoEle.style.cssText = layoutVideoStyle;
layoutPauseEle.className = `center layout_pause ${isPause ? 'show' : 'hidden'}`;
}
});
// 缓存上一组Layout List数据,下一次diff出离开会议的设备并清空DOM
cacheLayoutList = JSON.parse(JSON.stringify(layoutList));
};
// diff出离开会议设备
const diffInvalidLayout = () => {
const filterLayoutList = cacheLayoutList.filter((item) => {
return !layoutList.some(({ id }) => id === item.id);
});
// 清理Video资源
clearInvalidLayout(filterLayoutList);
};
// 清理Video资源和DOM
const clearInvalidLayout = (list) => {
list.forEach(({ id }) => {
const layoutItemId = 'wrap-' + id;
const layoutItemEle = document.getElementById(layoutItemId);
// 清理Video资源
XYClient.removeVideoRenderer(id);
// 移除layoutItemEle元素
layoutItemEle.remove();
});
};
// UI伪代码代码
<div class="meeting center" id="meeting">
<!-- 渲染所有Layout数据 -->
<div id="layout" class="layout"></div>
</div>
实现效果如下:
1. 带宽条件:单个设备与媒体服务间的最大上下行带宽为 3MB;
2. 分辨率和带宽建议
3. 优化策略
提示
如有请求多路高分辨率画面需求,需要联系小鱼易连技术支持高带宽和分辨率配置;
1. 使用指定参会者分页模式
2. 画面质量控制
3. 性能优化