本篇文档将介绍如何使用小鱼易连Web SDK集成多人音视频通话服务到简单的HTML+Javascrip项目中,开发者按照步骤执行即可完成集成操作,直接运行HTML文件可实时查看效果;
通过此篇文档,开发者将熟悉集成Web SDK的流程和关键操作方法,以便于更好的集成到自己的项目中,请务必详细阅读;
在开始之前,请确认您已经完成相应的准备工作;
提示
此文档适用于小鱼易连Web SDK v2/v3版本;
集成小鱼易连Web SDK时,仅需极少数的步骤即可完成音视频通话功能,具体的流程如下:
下面展示基础的通话流程步骤,请按顺序添加代码,当前流程使用了HTML、Javascript语法,其他框架使用方式相同;
提示
推荐项目中使用框架开发,例如React或者Vue等,实现效率更高效,可参考下载-Demo示例程序项目
新建一个测试项目:video-call
,进入文件夹后执行初始化命令:
$ npm init -y
执行下面终端命名,通过npm安装SDK库v3版本的最新版本文件, 安装完成后,项目目录生成了一个node_modules
文件夹,包含了一个@xylink
的目录;
$ npm install @xylink/xy-rtc-sdk@3 -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 v3.9 简单示例程序</title>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div class="app">
<div class="header">
<button onclick="startCall()">加入会议</button>
<button onclick="endCall()">离开会议</button>
<button onclick="switchCamera()">开启/关闭摄像头</button>
<button onclick="switchMicrophone()">开启/关闭麦克风</button>
<button onclick="startShareContent()">开启共享</button>
<button onclick="stopShareContent()">结束共享</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>
const startCall = () => {};
const endCall = () => {};
const switchCamera = () => {};
const switchMicrophone = () => {};
const startShareContent = () => {};
const stopShareContent = () => {};
</script>
</html>
注:index.html
文件中已加载node_modules
目录下已经安装好SDK库文件,直接使用即可;
终端运行如下命令,启动本地端口运行html文件,注意此处需要使用http://localhost:{port}
形式访问。例如:http://localhost:8080
$ npx live-server
加入会议前,调用 XYRTC.checkSupportWebRTC 检测兼容方法,判断浏览器是否支持WebRTC能力,建议在正式环境处理浏览器兼容,给用户正确的兼容提示和引导;
在加入会议函数(startCall)下添加检测代码,并做提示:
const startCall = async () => {
const response = await XYRTC.checkSupportWebRTC();
if (!response.result) {
alert('不支持WebRTC,请更换支持的浏览器');
return;
}
// 继续执行后续代码
}
检测通过后,调用 XYRTC.createClient 函数创建 XYClient,其中所需的clientId、clientSecret、extId参考准备工作内容;
提示
此处需要填充自己的clientId、clientSecret、extId账号信息
const startCall = async () => {
...
XYClient = XYRTC.createClient({
clientId: 'xxx',
clientSecret: 'xxx',
extId: 'xxx',
container: {
elementId: 'meeting',
},
});
...
}
创建XYClient模块后,执行绑定监听事件,可以根据业务功能,按需监听,此处所列必须的监听函数:
后续会在监听函数中添加逻辑,请勿重复添加监听事件;
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);
});
};
监听事件注册完成后,开始第三方企业账号登录,注意此步骤调用成功后,会返回Token数据,下一步需要使用此数据;
const startCall = async () => {
...
const result await XYClient.loginExternalAccount({
displayName: 'xxx',
extUserId: 'xxx',
extId: 'xxx',
});
const token = result.detail.access_token;
...
}
登录完成后,调用 XYClient.makeCall 即可开始加入云会议室操作,此处配置了小鱼易连客服坐席云会议室号,业务中使用开发者需要创建SDK会议室号加入会议;
const startCall = async () => {
...
await XYClient.makeCall({
token,
confNumber: '188188',
password: '',
displayName: '测试WebSDK',
muteVideo,
muteAudio,
});
...
}
发起呼叫后,调用 XYClient.createStream() 创建音视频轨道并执行采集音视频流操作:
提示
执行 XYStream.init 采集方法时,如果开启了麦克风和摄像头配置,浏览器会申请使用设备的权限提示,请允许采集权限
const startCall = async () => {
...
XYStream = XYRTC.createStream();
await XYStream.init({});
...
}
调用 XYClient.publish 方法推送音视频流到服务器:
const startCall = async () => {
...
XYClient.publish(XYStream, { isSharePeople: true });
}
此步骤执行完毕后,入会流程执行完成;接下来我们实现渲染音视频画面和操作设备功能;
入会成功后,audio-track 事件会上报所有声音通道数据,业务上需调用 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;
}
};
调用 XYClient.destroy 挂断会议时,需要先释放Dom资源,并执行销毁方法,完成后即可离开会议页面;同时,监听到 disconnected 消息时,也需要执行退会处理;
在上文的监听事件中添加处理函数:
XYClient.on('disconnected', (e) => {
console.log('disconnected: ', e);
// 新增处理逻辑
endCall();
});
const endCall = async () => {
if (XYClient) {
layoutList = [];
clearInvalidLayout(cacheLayoutList);
clearInvalidAudios();
XYStream.close();
XYClient.destroy();
console.log('离开会议成功');
}
};
完整文件完整代码如下:
index.css
文件
div,
body {
padding: 0;
margin: 0;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.app {
padding: 20px;
}
.header {
margin-bottom: 20px;
}
.meeting {
width: 460px;
height: 325px;
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;
}
index.html
文件如下:
提示
请在index.html
中配置第三方登录所需的:clientId、clientSecret、extId
<!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 v3.9 简单示例程序</title>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div class="app">
<div class="header">
<button onclick="startCall()">加入会议</button>
<button onclick="endCall()">离开会议</button>
<button onclick="switchCamera()">开启/关闭摄像头</button>
<button onclick="switchMicrophone()">开启/关闭麦克风</button>
<button onclick="startShareContent()">开启共享</button>
<button onclick="stopShareContent()">结束共享</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
*
* 当前使用XYLink Web SDK:v3.9.10
* 产品介绍: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 XYStream = null;
let contentTrack = null;
// 参会者布局列表数据
let layoutList = [];
// 缓存列表数据,做Diff使用
let cacheLayoutList = [];
// 声音通道数据
let audioList = [];
// 关闭摄像头
let muteVideo = true;
// 关闭麦克风
let muteAudio = true;
/**
* 配置信息,请填写!!!
* 详细见文档:https://openapi.xylink.com/common/meeting/api/description_2?platform=web
*/
// 网关应用ID
const clientId = '';
// 网关Secret
const clientSecret = '';
// 企业ID
const extId = '';
const server = 'cloudapi.xylink.com';
// 入会会议号
const confNumber = '';
const initSetting = () => {
XYClient = null;
XYStream = null;
layoutList = [];
cacheLayoutList = [];
audioList = [];
muteVideo = true;
muteAudio = true;
};
const startCall = async () => {
try {
initSetting();
const XYRTC = xyRTC.default;
const response = await XYRTC.checkSupportWebRTC();
if (!response.result) {
alert('不支持WebRTC,请更换支持的浏览器');
return;
}
XYRTC.logger.setLogLevel('INFO');
XYClient = XYRTC.createClient({
clientId,
clientSecret,
extId,
server,
container: {
elementId: 'meeting',
},
});
initEvent();
const result = await XYClient.loginExternalAccount({
displayName: '测试',
// 企业账号登录,填写第三方用户ID,如果没有请填写随机ID
extUserId: 'XXX',
extId,
});
const token = result.detail.access_token;
await XYClient.makeCall({
token,
// 输入会议号
confNumber,
// 入会密码,如果没有则不填写
password: '',
// 入会名称
displayName: '测试88',
muteVideo,
muteAudio,
});
XYStream = XYRTC.createStream();
await XYStream.init();
XYClient.publish(XYStream, { isSharePeople: true });
} 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数据,下一次对比出离开会议的参会者并清空DOM
cacheLayoutList = JSON.parse(JSON.stringify(layoutList));
};
// 对比出离开会议的参会者并清理资源
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();
XYClient.destroy();
console.log('离开会议成功');
}
};
// 清理Video资源和DOM
const clearInvalidLayout = (list) => {
list.forEach(({ id }) => {
const layoutItemId = 'wrap-' + id;
const layoutItemEle = document.getElementById(layoutItemId);
// 移除layoutItemEle元素
layoutItemEle.remove();
});
};
// 清理Audio列表数据
const clearInvalidAudios = () => {
document.getElementById('audios').innerHTML = '';
};
// 开始共享
const startShareContent = async () => {
try {
// screenAudio: true
const result = await XYStream.createContentStream({ screenAudio: true });
if (result) {
XYStream.on('start-share-content', () => {
// 推送 ContentTrack 模块
XYClient.publish(XYStream, { isShareContent: true });
});
XYStream.on('stop-share-content', () => {
// 停止分享
stopShareContent();
});
}
} catch (error) {
stopShareContent();
}
};
// 结束共享
const stopShareContent = async () => {
XYClient.stopShareContent();
};
</script>
</html>
提示
获取完整的项目资源,可以查看下载文档了解详情;