该 SDK 是基于 WebRTC 技术实现的一款多人实时音视频通话 SDK
本文档为开发者文档,阅读本文档前请确认
- 有 Web 开发经验
- 有 HTML5 媒体 API 的基本知识
- 拥有七牛账户并且开通直播云服务
开发者需要通过 Server-API 的说明,在后台搭建一个后台服务。前端通过请求搭建好的后台服务获取到一个房间的 roomToken
后,就开始进入该 SDK 工作流程。
请使用 Chrome 56 以上版本进行开发以支持 WebRTC
我们为开发者提供了 2 种引用方式
npm install --save pili-rtc-web
import { QNRTCSession } from 'pili-rtc-web';
const myRTC = new QNRTCSession();
// 或者使用 default export
import QNRTC from 'pili-rtc-web';
const myRTC = new QNRTC.QNRTCSession();
<script src="./build/pili-rtc-web.js"></script>
<script>
var myRTC = new QNRTC.QNRTCSession();
</script>
基于 Chrome 已经基本支持 async/await(或者使用 babel 等工具),其语法在复杂异步场景下逻辑处理更有优势,所以下面的代码将会用这种方式编写
当然,实际开发过程中,你也可以根据自己的喜好使用 Promise 的方案来编写
如果你对 async/await 不了解,可以参考 async 介绍
这里教程我们会使用 async/await 的方式来编写,请确保下面的代码运行在一个 async 函数包裹之下。比如 async iife。
(async () => {
// code goes here
})();
如果你想使用 promise,可以对下面的代码做一些简单的替换
try {
const a = await some_method();
} catch(e) {
console.log(e);
}
// promise 写法
some_method().then(a => {
}).catch(e => {
console.log(e);
})
好了,准备就绪之后,让我们准备开始进行第一次发布吧。 首先准备一个带 <video>
标签的页面,我们将会把摄像头预览放在这个元素中
<body>
<video id="localplayer" />
</body>
在开始发布之前,我们必须先加入一个房间。
因为发布/订阅/控制等操作都是对应一个房间完成的,也就是说我们在一个房间内开始发布,房间的其他用户都可以获取到我们的数据流。
在这里我们需要 1 个已知参数,roomToken
, 关于这个参数的获取请参照上文提到的 Server-API
const myRTC = new QNRTC.QNRTCSession(); // 初始化我们的 SDK (QNRTC的引入方式见上)
try {
const users = await myRTC.joinRoomWithToken(roomToken); // 加入房间
// 因为 await 的特性,当代码执行到这里的时候,joinRoomWithToken 这个异步请求已经完成
// 如果过程中出现错误,会直接 throw 出来,如果需要处理只要 try/catch 就好
// 这里的 users 表示该房间中已经存在的用户,具体可以参照 API 文档
// 你也随时可以通过 myRTC.users 获取当前的用户列表
console.log('current users', users);
} catch (e) {
// 加入房间失败,关于错误处理可以参考下文的 错误处理 一节
console.log('join room error!', e);
}
加入房间完成,开始发布吧
在发布之前,我们需要通过本机的媒体设备采集本地的媒体数据
// 使用内置的 deviceManager 对象采集本地媒体数据
const stream = await QNRTC.deviceManager.getLocalStream();
// 页面上的 vide 元素
const localVideo = document.getElementById('localplayer');
// 拿到 stream 对象后,调用 play 就可以播放了
// 参数为想要播放在哪个 video 元素上
// 这里第二个参数代表用 静音模式 来播放,本地预览的时候一般我们会设置成静音
stream.play(localVideo, true);
此时,我们应该已经可以在页面上预览到我们本地的媒体流了。下一步,就是将这个媒体流发布到房间中。
// 发布自己本地的流
try {
await myRTC.publish(stream)
} catch (e) {
console.log('publish error!', e);
}
// done! 代码执行到这里说明发布成功
// 我们可以根据这个特性很方便地设定 publishState 等参数
如果没有报错的话说明你的媒体流已经顺利发布到这个房间,下一步,让我们订阅这个刚刚发布的流
SDK不允许用户自己订阅自己,所以这里我们需要另一个用户来做测试,让我们新准备一个页面
这里页面也需要一个 video 元素,用于播放我们订阅的流
<video id="remoteplayer"></video>
重复上面加入房间的步骤,注意这里必须要保证 2 个页面的用户名不同。
加入房间后,让我们开始订阅吧。
... // 加入房间后
const remoteVideo = document.getElementById('remoteplayer');
// 获取当前房间所有用户
const users = myRTC.users;
for (let i = 0; i < users.length>; i +=1) {
const user = users[i];
// 如果这个用户正在发布,我们就订阅他
if (user.published) {
try {
// 通过用户的 userId 订阅目标用户
// 这里返回和我们最初从本地获取媒体流时的返回格式一样
// 都是封装后的 Stream 对象
const remoteStream = await myRTC.subscribe(user.userId);
// 同样,调用 play 方法,选择页面上的 video 元素,就可以播放啦
remoteStream.play(remoteVideo);
} catch (e) {
console.log('subscribe error!', e);
}
}
}
无论手动还是模块引入,SDK 都会暴露一个 QNRTC 对象,该对象整合了 SDK 所有的独立模块
模块名称 | 用途 |
---|---|
Stream | 包装了媒体流数据的对象 |
QNRTCSession | 核心类,创建 QNRTC 实例 |
deviceManager | 设备管理模块,一般用来捕获本地的媒体数据 |
log | 控制打印的 log 信息 |
在介绍主要的房间/发布/订阅操作相关的 API 前,我想先介绍 Stream 这个基础类
媒体流是包含了媒体数据(视频,音频)的数据流,也就是我们在连麦过程中需要最经常操作的一种数据。
Stream 将媒体流进行了包装并结合了一些实际的业务数据
name | 类型 | 介绍 |
---|---|---|
userId | string | 标记当前这个流属于哪个用户 |
isDestroyed | boolean | 这个流是否已经被销毁(发布者取消发布,发布者失去连接, 取消订阅...) |
muteAudio | boolean | 表示该流是否被静音 |
muteVideo | boolean | 表示该流是否被黑屏 |
enableAudio | boolean | 表示该流是否有音频轨道 |
enableVideo | boolean | 表示该流是否有视频轨道 |
参数 | 类型 | 备注 |
---|---|---|
mediaElement | HTMLVideoElement/HTMLAudioElement | video/audio 元素的 DOM 对象 |
isMute | boolean | 是否用 静音模式 播放 |
指定媒体流在一个 video/audio 元素上播放
纯音频连麦请使用 audio 元素,其余情况使用 video 元素
const mediaElement = document.get...
stream.play(mediaElement);
参数 | 类型 | 备注 |
---|---|---|
callback | (buffer: Float32Array) => any | 接收音频数据的回调 |
bufferSize | number | 每次获取的音频数据长度,默认为 4096 (只能为 2 的 n 次方,且处于 256 - 16384 之间) |
通过指定的 callback 获取音频数据
stream.onAudioBuffer(buffer => {
// buffer 为一个长度为 2048 的 Float32Array
console.log('get audio buffer data', buffer);
}, 2048)
通常用于音频可视化的数据,可以配合 requestAnimationFrame 和 canvas
实现绘制声波图
返回: 尺寸为 2048 的 Unit8Array
示例代码参考 AudioWave
通常用于音频可视化的数据,可以配合 requestAnimationFrame 和 canvas
实现绘制音域图
返回: 尺寸为 2048 的 Unit8Array
示例代码参考 AudioWave
包括丢包率,实时码率等信息
返回的具体格式参考下面的返回类型介绍
Safari 暂时不支持此功能
返回: StatsReport
const report = stream.getStats();
console.log("音频丢包数", report.audioPacketLoss);
name | 类型 | 介绍 |
---|---|---|
users | User[] | 房间当前的用户信息 |
subscribedUsers | User[] | 房间当前的已经订阅的用户信息 |
创建 QNRTC 实例
const myRTC = new QNRTC.QNRTCSession()
加入房间
参数 | 类型 | 介绍 |
---|---|---|
roomToken | string | 房间 token,获取方式请阅读 Server-API 文档 |
返回 | 类型 | 介绍 |
---|---|---|
users | User[] | 返回房间当前的 User 信息,User 详细见类型介绍 |
try {
const users = await myRTC.joinRoomWithToken(...);
} catch(e) {
console.log('joinRoomWithToken Error!', e);
}
// or use Promise
myRTC.joinRoomWithToken(...).then((users) => {
console.log('join success!');
}).catch(e => {
console.log('joinRoomWithToken Error!', e);
})
发布一个流到当前房间
参数 | 类型 | 介绍 |
---|---|---|
stream | Stream | 流对象,从 deviceManager 中获取 |
try {
const stream = await QNRTC.deviceManager.getLocalStream();
await myRTC.publish(stream);
} catch(e) {
console.log('publish Error!', e);
}
// or use Promise
QNRTC.deviceManager.getLocalStream()
.then(stream => myRTC.publish(stream))
.then(() => {
console.log('publish success');
}).catch(e => {
console.log('publish Error!', e);
});
停止向房间推流,取消发布
await myRTC.unpublish();
// or use Promise
myRTC.unpublish().then(() => console.log('un publish')).catch(console.error)
订阅一名用户,获取该用户发布的流
参数 | 类型 | 介绍 |
---|---|---|
userId | string | 该用户的用户 id |
返回 | 类型 | 介绍 |
---|---|---|
stream | Stream | 订阅的流对象,调用 play 方法来播放 |
const video = document.getElementById('video');
try {
const stream = await myRTC.subscribe(userId);
stream.play(video);
} catch(e) {
console.log('subscribe Error!', e);
}
// or use Promise
myRTC.subscribe(userId).then((stream) => {
stream.play(video);
console.log('subscribe success!');
}).catch(e => {
console.log('subscribe Error!', e);
})
取消订阅一名用户
参数 | 类型 | 介绍 |
---|---|---|
userId | string | 该用户的用户 id |
try {
await myRTC.unsubscribe(userId);
} catch(e) {
console.log('subscribe Error!', e);
}
// or use Promise
myRTC.unsubscribe(userId).then(() => {
console.log('unsubscribe success!');
}).catch(e => {
console.log('unsubscribe Error!', e);
})
将发布中的视频流静音或者黑屏
参数 | 类型 | 介绍 |
---|---|---|
muteAudio | boolean | 是否静音 |
muteVideo | boolean | 是否黑屏(默认 false) |
// 黑屏
myRTC.mute(false, true);
// 静音
myRTC.mute(true);
将用户踢出房间(如果调用者没有管理权限会抛出错误)
参数 | 类型 | 介绍 |
---|---|---|
userId | string | 该用户的用户 id |
try {
await myRTC.kickoutUser(userId);
} catch(e) {
console.log('kickoutUser error', e);
}
// or use Promise
myRTC.kickoutUser(userId).then(() => {}).catch(e => {
console.log('kickoutUser error', e);
})
离开当前房间
myRTC.leaveRoom();
通过 QNRTCSession 实例可以给各种事件挂钩子,具体的事件实现参考 EventEmitter 具体的事件列表参考下文
// 监听事件
myRTC.on('user-join', handleUserJoin);
// 只监听一次
myRTC.once('user-join', handleUserJoin);
// 取消监听
myRTC.off('user-join', handleUserJoin);
// 取消所有监听
myRTC.removeAllListeners('user-join');
deviceManager 是一个用来管理本地媒体设备的对象,一般用来捕获本地的媒体流
name | 类型 | 介绍 |
---|---|---|
deviceInfo | MediaDeviceInfo[] | 房间当前的设备列表 |
audioDevice | MediaDeviceInfo | 当前使用的音频设备, @default 代表使用系统默认设备 |
videoDevice | MediaDeviceInfo | 当前使用的视频设备, @default 代表使用系统默认设备 |
获取本地的媒体数据
参数 | 类型 | 备注 |
---|---|---|
recordConfig |
{ audio: { enabled: boolean, bitrate?: number }, video: { enabled: boolean, frameRate?: number, height?: number, width?: number, bitrate?: number, } } |
带 ? 的为可选项 enabled 代表是否开启视频/音频轨道 bitrate 代表码率(kb/s),默认音频 64, 视频 512 frameRate 代表视频帧率 width/height 代表视频分辨率 |
返回 | 类型 | 备注 |
---|---|---|
stream | Stream | 本地的 stream 对象 |
try {
const localStream = await QNRTC.deviceManager.getLocalStream({
audio: { enabled: true },
video: { enabled: true, bitrate: 1024, frameRate: 30 },
});
} catch (e) {
console.log('getLocalStream Error!', e);
}
// or use promise
QNRTC.deviceManager.getLocalStream({
audio: { enabled: true },
video: { enabled: true, bitrate: 1024, frameRate: 30 },
}).then(localStream => {
...
}).catch(e => {
console.log('getLocalStream Error!', e);
})
参数 | 类型 | 备注 |
---|---|---|
value | number | 基于当前音量的增益数值,1 不改变,0 静音 |
Safari 暂时不支持该方法
QNRTC.deviceManager.setVolume(10);
QNRTC.deviceManager.on("device-update", () => {
console.log("current devices", QNRTC.deviceManager.deviceInfo);
});
参数 | 类型 | 备注 |
---|---|---|
type | string | "audio" 或者 "video" |
deviceId | string | 设备 id |
使用指定的 deviceId 采集, 可以在发布中途调用
如果在发布中途调用, safari 11+/chrome 65+ 会进行流的热替换
其余版本会用新的流自动重新发布一次
QNRTC.deviceManager.changeDevice("video", "your device id");
用于控制 SDK 打印在控制台上的信息
设置打印等级,一共有 4 个等级 disable
warning
debug
log
设置为 disable
后将不会打印数据, 默认为 log
import { log } from 'pili-rtc-web';
// 关闭 SDK 的 console 打印
log.setLevel("disable");
事件名称 | 描述 | 参数 | 备注 |
---|---|---|---|
user-leave | 有用户离开房间 | (user: User) | user: 离开的用户 |
user-join | 有用户加入房间 | (user: User) | user: 加入的用户 |
user-publish | 有用户开始发布 | (user: User) | user: 发布的用户 |
user-unpublish | 有用户取消发布 | (user: User) | user: 取消发布的用户 |
room-state-change | 房间状态改变 | (state: RoomState) | 表示房间状态的number数字,具体见类型介绍 |
mute | 房间用户修改静音/黑屏状态 | ({userId : string, streamId: string, muteAudio: boolean, muteVideo: boolean}) | |
disconnect | 和房间失去连接 | 无 | 房间被关闭/被踢出房间都会触发 |
kicked | 被踢出房间 | (userId: string) | 执行踢出命令的用户 id |
error | 错误 | (error: QNRTCError) | 非正常流程抛出的错误,一般出现在自动断线重连过程中 |
User {
userId: string
published: boolean // 如果 true 表示该用户正在推流
}
StatsReport {
bandwidth: number; // 带宽
videoBitrate: number; // 视频码率
audioBitrate: number; // 音频码率
videoBytes: number; // 视频传输字节
videoPackets: number; // 视频传输包数量
videoPacketLoss: number; // 视频丢包数
videoPacketLossRate: number; // 视频丢包率
audioBytes: number; // 音频传输字节
audioPackets: number; // 音频传输包数
audioPacketLoss: number; // 音频丢包数
audioPacketLossRate: number; // 音频丢包率
}
enum RoomState {
Idle = 0, // 未连接
Connecting = 1, // 连接中
Connected = 2, // 已连接到房间
Reconnecting = 3, // 正在尝试重新连接
}
所有SDK处理的错误都会抛出一个QNRTCError
,包含code
和message
信息
try {
await myRTC.kickoutUser('xxx')
} catch(e) {
switch (e.code) {
case 10051:
console.log('没有权限踢人!');
break;
default:
console.log('其他错误', e.code, e.message);
break;
}
}
代码 | 描述 | 可能触发的函数 |
---|---|---|
10001 | roomToken 错误 | joinRoomWithToken |
10002 | roomToken 过期 | joinRoomWithToken |
10003 | room 已经被关闭 | joinRoomWithToken |
10011 | room 人满 | joinRoomWithToken |
10012 | room 不存在 | joinRoomWithToken |
10021 | 用户不存在 | publish unpublish subscribe unsubscribe kickoutUser |
10031 | 发布流不存在 | unpublish subscribe unsubscribe |
10032 | 发布流信息不匹配 | unpublish |
10041 | 订阅流不存在 | unsubscribe |
10042 | 订阅流信息不匹配 | subscribe |
10044 | 无法订阅自己 | subscribe |
10051 | 没有权限 | publish kickoutUser |
10052 | server 端不可用 | publish subscribe |
11000 | unexpected error | |
11001 | 获取 AuthToken 失败 | joinRoomWithToken |
11002 | 推流 ice 失败 | publish |
11003 | 订阅 ice 失败 | subscribe |
11004 | SDK 端无法获取目标订阅用户的流 | subscribe |
11005 | 无法创建订阅 p2p 连接 | subscribe |
11006 | 无法创建发布 p2p 连接 | publish |
11007 | 不支持远端的媒体格式 | publish subscribe |