1.RTCPeerConnection 开启视频通话过程
1.1 获取本地音视频流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
...
//创建 RTCPeerConnection 对象
let localPeerConnection = new RTCPeerConnection(servers);
...
//调用 getUserMedia API 获取音视频流
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
then(gotLocalMediaStream).
catch(handleLocalMediaStreamError);
//如果 getUserMedia 获得流,则会回调该函数
//在该函数中一方面要将获取的音视频流展示出来
//另一方面是保存到 localSteam
function gotLocalMediaStream(mediaStream) {
...
localVideo.srcObject = mediaStream;
localStream = mediaStream;
...
}
...
//将音视频流添加到 RTCPeerConnection 对象中
localPeerConnection.addStream(localStream);
...
|
1.2 交换媒体描述信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
...
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
...
//当创建 offer 成功后,会调用该函数
function createdOffer(description) {
...
//将 offer 保存到本地
localPeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
...
//远端 pc 将 offer 保存起来
remotePeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
...
//远端 pc 创建 answer
remotePeerConnection.createAnswer()
.then(createdAnswer)
.catch(setSessionDescriptionError);
}
//当 answer 创建成功后,会回调该函数
function createdAnswer(description) {
...
//远端保存 answer
remotePeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
//本端pc保存 answer
localPeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
}
|
1.3 端与端建立连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
...
localPeerConnection.onicecandidate= handleConnection(event);
...
...
function handleConnection(event) {
//获取到触发 icecandidate 事件的 RTCPeerConnection 对象
//获取到具体的Candidate
const peerConnection = event.target;
const iceCandidate = event.candidate;
if (iceCandidate) {
//创建 RTCIceCandidate 对象
const newIceCandidate = new RTCIceCandidate(iceCandidate);
//得到对端的RTCPeerConnection
const otherPeer = getOtherPeer(peerConnection);
//将本地获到的 Candidate 添加到远端的 RTCPeerConnection对象中
otherPeer.addIceCandidate(newIceCandidate)
.then(() => {
handleConnectionSuccess(peerConnection);
}).catch((error) => {
handleConnectionFailure(peerConnection, error);
});
...
}
}
...
|
1.4 显示远端媒体流
1
2
3
4
5
6
7
8
9
|
...
localPeerConnection.onaddstream = handleRemoteStreamAdded;
...
function handleRemoteStreamAdded(event) {
console.log('Remote stream added.');
remoteStream = event.stream;
remoteVideo.srcObject = remoteStream;
}
...
|
2. 如何控制传输速率
音视频服务质量因素
- 网络质量,包括物理链路的质量、带宽的大小、传输速率的控制等;
- 数据,包括音视频压缩码率、分辨率大小、帧率等。
2.1 网络质量
物理链路层
抖动,延迟,丢包等因素引起
带宽
充分利用带宽
传输速率
音视频压缩率码率:是有损压缩还是无损压缩,压缩率大小
传输码率:发包速度和包体积决定
2.2 数据
分辨率
可以通过调整分辨率来降低传输大小,比如将1280 x 720 变成 640 * 360来缩减视频体积
帧率
降低帧率其实并不能很好缩减体积,在一个GOP中,其实关键帧I所在比重较大,比P帧和B帧多的多,通过减少P帧和B帧并不能降低多少体积大小
2.2 webrtc 控制传输速率的方法
可以通过以下两种方式来控制传输速率。第一种是通过压缩码率这种“曲线救国”的方式进行控制;第二种则是更直接的方式,通过控制传输速度来控制速率。
webrtc中,网络部分的控制手段都是已经封装好,所以我们多是从压缩码率来改变传输速率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
....
var vsender = null; //定义 video sender 变量
var senders = pc.getSenders(); //从RTCPeerConnection中获得所有的sender
//遍历每个sender
senders.forEach( sender => {
if(sender && sender.track.kind === 'video'){ //找到视频的 sender
vsender = sender;
}
});
var parameters = vsender.getParameters(); //取出视频 sender 的参数
if(!parameters.encodings){ //判断参数里是否有encoding域
return;
}
//通过 在encoding中的 maxBitrate 可以限掉传输码率
parameters.encodings[0].maxBitrate = bw * 1000;
//将调整好的码率重新设置回sender中去,这样设置的码率就起效果了。
vsender.setParameters(parameters)
.then(()=>{
console.log('Successed to set parameters!');
}).catch(err => {
console.error(err);
})
...
|
3.打开/关闭音频
3.1 声音静音的操作
播放端控制
video静音
1
2
3
4
|
...
var remotevideo = document.querySelector('video#remote');
remotevideo.muted = false;
...
|
去掉audio 流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
...
var remoteVideo = document.querySelector('video#remote');
...
{
//创建与远端连接的对象
pc = new RTCPeerConnection(pcConfig);
...
//当有远端流过来时,触发该事件
pc.ontrack = getRemoteStream;
...
}
...
function getRemoteStream(e){
//得到远端的音视频流
remoteStream = e.streams[0];
//找到所有的音频流
remoteStream.getAudioTracks().forEach((track)=>{
if (track.kind === 'audio') { //判断 track 是类型
//从媒体流中移除音频流
remoteStream.removeTrack(track);
}
});
//显示视频
remoteVideo.srcObject = e.streams[0];
}
...
|
发送端控制
不采集音频
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
...
//获取本地音视频流
function gotStream(stream) {
localStream = stream;
localVideo.srcObject = stream;
}
//获得采集音视频数据时限制条件
function getUserMediaConstraints() {
var constraints = {
"audio": false,
"video": {
"width": {
"min": "640",
"max": "1280"
},
"height": {
"min": "360",
"max": "720"
}
}
};
return constraints;
}
...
//采集音视频数据
function captureMedia() {
...
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
...
//采集音视频数据的 API
navigator.mediaDevices.getUserMedia(getUserMediaConstraints())
.then(gotStream)
.catch(e => {
...
});
}
...
|
关闭通道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
...
var localStream = null;
//创建peerconnection对象
var pc = new RTCPeerConnection(server);
...
//获得流
function gotStream(stream){
localStream = stream;
}
...
//peerconnection 与 track 进行绑定
function bindTrack() {
//add all track into peer connection
localStream.getTracks().forEach((track)=>{
if(track.kink !== 'audio') {
pc.addTrack(track, localStream);
}
});
}
...
|
- 在关闭本地视频时不是直接在采集的时候就不采集视频数据,而是不将视频数据与 RTCPeerConnection 对象进行绑定,因为自己本地还需要预览
4. 数据统计
4.1 信息获取
webrtc 已经已经提供了一个比较全面的数据监控工具,在chrome中通过如下链接可以直接看到
1
|
chrome://webrtc-internals |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
...
//获得速个连接的统计信息
pc.getStats().then(
//在一个连接中有很多 report
reports => {
//遍历每个 report
reports.forEach( report => {
//将每个 report 的详细信息打印出来
console.log(report);
});
}).catch( err=>{
console.error(err);
});
);
...
|
- getStats API 是 RTCPeerConnecton 对象的方法,用于获取各种统计信息
- 参数可以为null,返回全部信息;参数为MediaStreamTrack,指定特定部分
具体输出如下
- id:对象的唯一标识,是一个字符串。
- timestamp:时间戳,用来标识该条 Report 是什么时间产生的。
- type:类型,是 RTCStatsType 类型
三种获取stats的方法
- RTCPeerConnection 对象的 getStats 方法获取的是所有的统计信息,除了收发包的统计信息外,还有候选者、证书、编解码器等其他类型的统计信息。
- RTCRtpSender 对象的 getStats 方法只统计与发送相关的统计信息。
- RTCRtpReceiver 对象的 getStats 方法则只统计与接收相关的统计信息
4.2 信息渲染
- 引入第三方库 graph.js;
- 启动一个定时器,每秒钟绘制一次图形;
- 在定时器的回调函数中,读取 RTCStats 统计信息,转化为可量化参数,并将其传给 graph.js 进行绘制。
5. 文本发送
1
2
3
|
dc = pc.createDataChannel('sendDataChannel', dataConstraint); //一端主动创建 RTCDataChannel
...
dc.onmessage = receivedMessage; //当有文本数据来时,回调该函数。
|
使用RTCPeerConnection创建RTCDataChannel,注册RTCDataChannel的回调方法onmessage,消息会从onmessage中传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
var servers = {'iceServers': [{
'urls': 'turn:youdomain:3478',
'credential': "passwd",
'username': "username"
}]
};
pc = new RTCPeerConnection(servers, pcConstraint);
pc.onicecandidate = handleIceCandidate; //收集候选者
pc.ondatachannel = onDataChannelAdded; //当对接创建数据通道时会回调该方法。
function onDataChannelAdded(event) {
dc = event.channel;
dc.onmessage = receivedMessage;
...
}
|
接受端在创建RTCPeerConnection时,注册一个ondatachannel方法,通信双方完成了媒体协商、交换了 SDP 之后,另一端收到发送端的消息,ondatachannel 事件就会被触发。此时就会调用它的回调函数 onDataChannelAdded ,通过 onDataChannelAdded 函数的参数 event 你就可以获取到另一端的 RTCDataChannel 对象
5. 传输文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
...
function sendData(){
var offset = 0; //偏移量
var chunkSize = 16384; //每次传输的块大小
var file = fileInput.files[0]; //要传输的文件,它是通过HTML中的file获取的
...
//创建fileReader来读取文件
fileReader = new FileReader();
...
fileReader.onload = e => { //当数据被加载时触发该事件
...
dc.send(e.target.result); //发送数据
offset += e.target.result.byteLength; //更改已读数据的偏移量
...
if (offset < file.size) { //如果文件没有被读完
readSlice(offset); // 读取数据
}
}
var readSlice = o => {
const slice = file.slice(offset, o + chunkSize); //计算数据位置
fileReader.readAsArrayBuffer(slice); //读取 16K 数据
};
readSlice(0); //开始读取数据
}
...
|
通过FileReader每次读取固定大小的数据,读取成功,触发onload方法,然后将读取的数据通过RTCDataChnanel发送出去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
...
var receiveBuffer = []; //存放数据的数组
var receiveSize = 0; //数据大小
...
onmessage = (event) => {
//每次事件被触发时,说明有数据来了,将收到的数据放到数组中
receiveBuffer.push(event.data);
//更新已经收到的数据的长度
receivedSize += event.data.byteLength;
//如果接收到的字节数与文件大小相同,则创建文件
if (receivedSize === fileSize) { //fileSize 是通过信令传过来的
//创建文件
var received = new Blob(receiveBuffer, {type: 'application/octet-stream'});
//将buffer和 size 清空,为下一次传文件做准备
receiveBuffer = [];
receiveSize = 0;
//生成下载地址
downloadAnchor.href = URL.createObjectURL(received);
downloadAnchor.download = fileName;
downloadAnchor.textContent =
`Click to download '${fileName}' (${fileSize} bytes)`;
downloadAnchor.style.display = 'block';
}
}
|
当有数据来临时,会触发RTCDataChannel的onmessage方法,然后我们将数据读出,同时记录数据块大小,当数据块累计大小等于文件大小时,通过
blob来实现下载。
上述为文件实体传输,我们还需要将文件信息传输,这样方便传输计算(比如文件大小对比)
可以通过socket.io来事先传给接收方文件的数据信息
1
2
3
4
|
fileName = file.name;
fileSize = file.size;
fileType = file.type;
lastModifyTime = file.lastModified;
|
6.安全
- SDP 传输对方的 ice-ufrag、ice-pwd 和 fingerprint 信息,有了这些信息后,就可验证对方是否是一个合法用户了。
- 紧接着,A 通过 STUN 协议(底层使用 UDP 协议)进行身份认证。如果 STUN 消息中的用户名和密码与交换的 SDP 中的用户名和密码一致,则说明是合法用户
- 确认用户为合法用户后,则需要进行 DTLS 协商,交换公钥证书并协商密码相关的信息。同时还要通过 fingerprint 对证书进行验证,确认其没有在传输中被窜改。
- 最后,再使用协商后的密码信息和公钥对数据进行加密,开始传输音视频数据