本篇文档将介绍如何使用小鱼易连Web SDK集成多人音视频通话服务到简单的HTML+Javascrip项目中,开发者按照步骤执行即可完成集成操作,直接运行HTML文件可实时查看效果;
通过此篇文档,开发者将熟悉集成Web SDK的流程和关键操作方法,以便于更好的集成到自己的项目中,请务必详细阅读;
在开始之前,请确认您已经完成相应的准备工作;
提示
此文档适用于小鱼易连Web SDK v4版本,整体集成流程更简洁,功能更丰富,建议项目升级;
集成小鱼易连Web SDK时,仅需极少数的步骤即可完成音视频通话功能,具体的流程如下:
下面展示基础的通话流程步骤,请按顺序添加代码,当前流程使用了HTML、Javascript语法,其他框架使用方式相同;
提示
推荐项目中使用框架开发,例如React或者Vue等,实现效率更高效,可参考下载-Demo示例程序项目
新建一个测试项目:video-call
,进入文件夹后执行初始化命令:
$ npm init -y
执行下面终端命名,通过npm安装SDK库文件, 安装完成后,项目目录生成了一个node_modules
文件夹,包含了一个@xylink
的目录;
$ npm install @xylink/xy-rtc-sdk -S
在项目跟目录创建一个index.html
测试文件,添加如下代码,包含加入会议、离开会议、开启/关闭摄像头、开启/关闭麦克风四个按钮和事件函数,以及相应的简单css内容(代码建议详细阅读,方便后续添加集成代码):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XYLink Web SDK v4.0 简单示例程序</title>
<style>
div,
body {
padding: 0;
margin: 0;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.app {
padding: 20px;
}
.header {
margin-bottom: 20px;
}
.meeting {
width: 760px;
height: 525px;
border: 1px solid #dadada;
}
.layout {
position: relative;
width: 100%;
height: 100%;
}
.layout_item {
position: absolute;
}
.layout_pause {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #c4c4c4;
z-index: 10;
}
.layout_name {
position: absolute;
bottom: 0;
left: 0;
z-index: 20;
}
.layout_video {
width: 100%;
height: 100%;
}
.video {
height: 100%;
width: 100%;
object-fit: contain;
}
.hidden {
display: none;
}
.show {
display: flex;
}
</style>
</head>
<body>
<div class="app">
<div class="header">
<button onclick="startCall()">加入会议</button>
<button onclick="endCall()">离开会议</button>
<button onclick="switchCamera()">开启/关闭摄像头</button>
<button onclick="switchMicrophone()">开启/关闭麦克风</button>
</div>
<div class="meeting center" id="meeting">
<!-- 渲染所有Layout数据 -->
<div id="layout" class="layout"></div>
<!-- audio元素需要隐藏显示,会议中不需要显示 -->
<div id="audios"></div>
</div>
</div>
</body>
<script src="./node_modules/@xylink/xy-rtc-sdk/lib/xyrtc.umd.js"></script>
<script>
/**
* 如果是初始接入,请一定详细阅读文档后再集成小鱼易连WebSDK
*
* 产品介绍:https://openapi.xylink.com/common/meeting/doc/description?platform=web
* 集成文档:https://openapi.xylink.com/common/meeting/doc/video_call?platform=web
* API文档:https://openapi.xylink.com/common/meeting/api/description?platform=web
*/
// XYRTCClient模块
let XYClient = null;
// 参会者布局列表数据
let layoutList = [];
// 缓存列表数据,做Diff使用
let cacheLayoutList = [];
// 声音通道数据
let audioList = [];
// 关闭摄像头
let muteVideo = true;
// 关闭麦克风
let muteAudio = true;
const startCall = async () => {};
const switchCamera = async () => {};
const switchMicrophone = async () => {};
const endCall = async () => {};
</script>
</html>
注:index.html
文件中已加载node_modules
目录下已经安装好SDK库文件,直接使用即可;
提示
使用浏览器运行index.html
文件即可查看代码效果,后续步骤添加完成后刷新页面即可;
加入会议前,调用 XYRTC.checkSupportWebRTC 检测兼容方法,判断浏览器是否支持WebRTC能力,建议在正式环境处理浏览器兼容,给用户正确的兼容提示和引导;
在加入会议函数(startCall)下添加检测代码,并做提示:
const startCall = async () => {
const response = await XYRTC.checkSupportWebRTC();
if (!response.result) {
alert('不支持WebRTC,请更换支持的浏览器');
return;
}
// 继续执行后续代码
}
检测通过后,调用 XYRTC.createClient 函数创建 XYRTCClient,其中所需的clientId、clientSecret、extId参考准备工作内容;
提示
此处需要填充自己的clientId、clientSecret、extId账号信息
const startCall = async () => {
...
XYClient = XYRTC.createClient({
clientId: 'xxx',
clientSecret: 'xxx',
extId: 'xxx',
container: {
elementId: 'meeting',
},
});
...
}
创建XYRTCClient模块后,执行绑定监听事件,可以根据业务功能,按需监听,此处所列必须的监听函数:
后续会在监听函数中添加逻辑,请勿重复添加监听事件;
const startCall = async () => {
...
initEvent();
...
}
// 增加监听事件函数
const initEvent = () => {
// 参会成员布局列表数据,包含参会者基本信息、位置、尺寸、旋转等数据
XYClient.on('layout', (e) => {
console.log('layout: ', e);
});
// 布局容器尺寸和位置信息
XYClient.on('screen-info', (e) => {
console.log('screen info: ', e);
});
// 音频轨道Tracks数据
XYClient.on('audio-track', (e) => {
console.log('audio track: ', e);
});
// 会议呼叫状态事件
XYClient.on('call-status', (e) => {
console.log('call state: ', e);
});
// 强制挂断会议消息
XYClient.on('disconnected', (e) => {
console.log('disconnected: ', e);
});
};
监听事件注册完成后,开始第三方企业账号登录:
const startCall = async () => {
...
await XYClient.loginExternalAccount({
displayName: 'xxx',
extUserId: 'xxx',
extId: 'xxx',
});
...
}
登录完成后,调用 XYRTCClient.makeCall 即可开始加入云会议室操作,此处配置了小鱼易连客服坐席云会议室号,业务中使用开发者需要创建SDK会议室号加入会议;
const startCall = async () => {
...
await XYClient.makeCall({
confNumber: '188188',
password: '',
displayName: '测试WebSDK',
muteVideo,
muteAudio,
});
...
}
发起呼叫后,调用 XYRTCClient.createVideoAudioTrack 创建音视频轨道并执行采集音视频流操作:
提示
执行 capture 采集方法时,如果开启了麦克风和摄像头配置,浏览器会申请使用设备的权限提示,请允许采集权限
const startCall = async () => {
...
const peopleTrack = await XYClient.createVideoAudioTrack();
await peopleTrack.capture();
...
}
调用 XYRTCClient.publish 方法推送音视频流到服务器:
const startCall = async () => {
...
XYClient.publish(peopleTrack);
}
此步骤执行完毕后,入会流程执行完成;接下来我们实现渲染音视频画面和操作设备功能;
入会成功后,audio-track 事件会上报最多14路声音通道数据,业务上需要调用 setAudioRenderer 方法播放所有通道声音,并由SDK接管播放声音操作;
在上文中的监听事件中添加数据处理逻辑:
XYClient.on('audio-track', (e) => {
console.log('audio track: ', e);
// 新增处理逻辑
audioList = e;
// 获取到audio list数据后,直接渲染并播放即可
renderAudioList();
});
// 新增处理函数
const renderAudioList = () => {
const audioContainer = document.getElementById('audios');
audioList.forEach((item) => {
const muted = item.status === 'local';
const streamId = item.rest.streamId;
const isExist = document.getElementById(streamId);
if (!isExist) {
const newAudio = document.createElement('audio');
// 本地声音mute处理
newAudio.muted = muted;
newAudio.autoplay = true;
newAudio.id = streamId;
audioContainer.appendChild(newAudio);
// 每个Audio Track只需执行一次setAudioRenderer即可
XYClient.setAudioRenderer(streamId, newAudio);
}
});
};
SDK默认是自动布局,在人数变化时会自动按照人数排列不同的布局效果,由于参会者的画面比例都是16:9,所以当人数变化时,外围布局容器为了适应铺满效果,需要动态计算相对应的宽高数据;同时,当布局容器的尺寸或者屏幕大小变化时,需要重新计算布局容器的宽高信息;
因此SDK内部会自动处理这些变化,并通过 screen-info 监听事件上报最终的布局容器样式,业务需要监听并更新对应的容器样式即可;
在上文中的监听事件中添加处理函数:
XYClient.on('screen-info', (e) => {
console.log('screen info: ', e);
// 新增处理逻辑
updateLayoutContainerStyle(e);
});
// 新增处理函数
const updateLayoutContainerStyle = (style) => {
const container = document.getElementById('layout');
container.style.width = `${style.rateWidth}px`;
container.style.height = `${style.rateHeight}px`;
};
SDK通过 layout 事件实时上报远端和本地参会者的布局数据,布局数据包含参会者的基本信息、位置信息、尺寸信息、状态信息、旋转信息等;当触发事件后,业务侧需要渲染Layout列表数据,并在首次调用 setVideoRenderer 方法渲染画面,更新布局UI;
后续当同一参会者(基于id判断)数据更新时,只需更新样式和状态即可,无需再执行渲染方法;
由于 layout 事件上报的是当前分页下全量的参会者数据,所以当部分参会者退会后,需要对比新旧Layout数据,进行Layout的UI更新;
在上文中的监听函数中添加处理函数:
XYClient.on('layout', (e) => {
console.log('layout: ', e);
// 新增处理逻辑
layoutList = e;
// 获取成员列表布局数据,直接渲染使用即可
renderLayoutList();
});
// 新增处理函数
const renderLayoutList = () => {
const meetingContainer = document.getElementById('layout');
// 收到新的数据开始对比差异
diffInvalidLayout();
layoutList.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);
});
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();
});
};Ï
此时,完整的加入会议和参会者布局容器渲染就已经处理完成,加入会议验证效果如下:
调用媒体管理相关方法即可:
// 开关摄像头
const switchCamera = async () => {
if (XYClient) {
if (muteVideo) {
await XYClient.unmuteVideo();
} else {
await XYClient.muteVideo();
}
muteVideo = !muteVideo;
}
};
// 开关麦克风
const switchMicrophone = async () => {
if (XYClient) {
if (muteAudio) {
await XYClient.unmuteAudio();
} else {
await XYClient.muteAudio();
}
muteAudio = !muteAudio;
}
};
调用 XYRTCClient.destroy 挂断会议时,需要先释放Dom资源,并执行销毁方法,完成后即可离开会议页面;同时,监听到 disconnected 消息时,也需要执行退会处理;
在上文的监听事件中添加处理函数:
XYClient.on('disconnected', (e) => {
console.log('disconnected: ', e);
// 新增处理逻辑
endCall();
});
const endCall = async () => {
// 补充函数处理内容
if (XYClient) {
layoutList = [];
clearInvalidLayout(cacheLayoutList);
clearInvalidAudios();
await XYClient.destroy();
console.log('离开会议成功');
}
};
index.html
文件完整代码如下:
提示
1、注意在createClient方法中配置clientId、clientSecret、extId企业账号信息
2、直接使用浏览器打开即可,本地环境下可以通过file协议运行查看效果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XYLink Web SDK v4.0 简单示例程序</title>
<style>
div,
body {
padding: 0;
margin: 0;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.app {
padding: 20px;
}
.header {
margin-bottom: 20px;
}
.meeting {
width: 760px;
height: 525px;
border: 1px solid #dadada;
}
.layout {
position: relative;
width: 100%;
height: 100%;
}
.layout_item {
position: absolute;
}
.layout_pause {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #c4c4c4;
z-index: 10;
}
.layout_name {
position: absolute;
bottom: 0;
left: 0;
z-index: 20;
}
.layout_video {
width: 100%;
height: 100%;
}
.video {
height: 100%;
width: 100%;
object-fit: contain;
}
.hidden {
display: none;
}
.show {
display: flex;
}
</style>
</head>
<body>
<div class="app">
<div class="header">
<button onclick="startCall()">加入会议</button>
<button onclick="endCall()">离开会议</button>
<button onclick="switchCamera()">开启/关闭摄像头</button>
<button onclick="switchMicrophone()">开启/关闭麦克风</button>
</div>
<div class="meeting center" id="meeting">
<!-- 渲染所有Layout数据 -->
<div id="layout" class="layout"></div>
<!-- audio元素需要隐藏显示,会议中不需要显示 -->
<div id="audios"></div>
</div>
</div>
</body>
<script src="./node_modules/@xylink/xy-rtc-sdk/lib/xyrtc.umd.js"></script>
<script>
/**
* 如果是初始接入,请一定详细阅读文档后再集成小鱼易连WebSDK
*
* 产品介绍:https://openapi.xylink.com/common/meeting/doc/description?platform=web
* 集成文档:https://openapi.xylink.com/common/meeting/doc/video_call?platform=web
* API文档:https://openapi.xylink.com/common/meeting/api/description?platform=web
*/
// XYRTCClient模块
let XYClient = null;
// 参会者布局列表数据
let layoutList = [];
// 缓存列表数据,做Diff使用
let cacheLayoutList = [];
// 声音通道数据
let audioList = [];
// 关闭摄像头
let muteVideo = true;
// 关闭麦克风
let muteAudio = true;
const initSetting = () => {
XYClient = null;
layoutList = [];
cacheLayoutList = [];
audioList = [];
muteVideo = true;
muteAudio = true;
};
const startCall = async () => {
try {
initSetting();
const response = await XYRTC.checkSupportWebRTC();
if (!response.result) {
alert('不支持WebRTC,请更换支持的浏览器');
return;
}
// XYRTC.logger.setLogLevel('INFO');
XYClient = XYRTC.createClient({
clientId: 'xxx',
clientSecret: 'xxx',
extId: 'xxx',
container: {
elementId: 'meeting',
},
});
initEvent();
await XYClient.loginExternalAccount({
displayName: '测试WebSDK',
extUserId: 'xxx123123x',
});
await XYClient.makeCall({
confNumber: '188188',
password: '',
displayName: '测试WebSDK',
muteVideo,
muteAudio,
});
const peopleTrack = await XYClient.createVideoAudioTrack();
await peopleTrack.capture();
XYClient.publish(peopleTrack);
} catch (err) {
console.warn('呼叫失败,请检查:', err);
alert(err.msg);
}
};
const initEvent = () => {
// 参会成员布局列表数据,包含参会者基本信息、位置、尺寸、旋转等数据
XYClient.on('layout', (e) => {
console.log('layout: ', e);
layoutList = e;
// 获取成员列表布局数据,直接渲染使用即可
renderLayoutList();
});
// 布局容器尺寸和位置信息
XYClient.on('screen-info', (e) => {
console.log('screen info: ', e);
// 给会议容器设置Layout容器最佳比例宽高信息
updateLayoutContainerStyle(e);
});
// 音频轨道Tracks数据
XYClient.on('audio-track', (e) => {
console.log('audio track: ', e);
audioList = e;
// 获取到audio list数据后,直接渲染并播放即可
renderAudioList();
});
// 会议呼叫状态事件
XYClient.on('call-status', (e) => {
// 呼叫状态处理
console.log('call state: ', e);
});
// 强制挂断会议消息
XYClient.on('disconnected', (e) => {
console.log('disconnected: ', e);
// 退会事件
endCall();
});
};
const updateLayoutContainerStyle = (style) => {
const container = document.getElementById('layout');
container.style.width = `${style.rateWidth}px`;
container.style.height = `${style.rateHeight}px`;
};
const renderLayoutList = () => {
const meetingContainer = document.getElementById('layout');
diffInvalidLayout();
layoutList.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);
};
const renderAudioList = () => {
const audioContainer = document.getElementById('audios');
audioList.forEach((item) => {
const muted = item.status === 'local';
const streamId = item.rest.streamId;
const isExist = document.getElementById(streamId);
if (!isExist) {
const newAudio = document.createElement('audio');
// 本地声音mute处理
newAudio.muted = muted;
newAudio.autoplay = true;
newAudio.id = streamId;
audioContainer.appendChild(newAudio);
// 每个Audio Track只需执行一次setAudioRenderer即可
XYClient.setAudioRenderer(streamId, newAudio);
}
});
};
const switchCamera = async () => {
if (XYClient) {
if (muteVideo) {
await XYClient.unmuteVideo();
} else {
await XYClient.muteVideo();
}
muteVideo = !muteVideo;
}
};
const switchMicrophone = async () => {
if (XYClient) {
if (muteAudio) {
await XYClient.unmuteAudio();
} else {
await XYClient.muteAudio();
}
muteAudio = !muteAudio;
}
};
// 离开会议
const endCall = async () => {
if (XYClient) {
layoutList = [];
clearInvalidLayout(cacheLayoutList);
clearInvalidAudios();
await XYClient.destroy();
console.log('离开会议成功');
}
};
// 清理Video资源和DOM
const clearInvalidLayout = (list) => {
list.forEach(({ id }) => {
const layoutItemId = 'wrap-' + id;
const layoutItemEle = document.getElementById(layoutItemId);
// 清理Video资源
XYClient.removeVideoRenderer(id);
// 移除layoutItemEle元素
layoutItemEle.remove();
});
};
// 清理Audio列表数据
const clearInvalidAudios = () => {
audioList.forEach((item) => {
const streamId = item.rest.streamId;
// 清理Audio资源
XYClient.removeAudioRender(streamId);
});
document.getElementById('audios').innerHTML = '';
};
</script>
</html>