写此文的动力:以前在线教育,虽然使用第三方开发,但是底层使用的是webRTC技术,一直想找时间研究,最近看到相关实现,亲自撸代码实现其中原理。学习到以下函数,并其实现方式:
此文写得比较粗糙,具体实现结合源码理解
主要几步:
- 先启动项目调用createOffer,得到offer
- 再在傀儡端把上面得到的offer传入createAnswer,并调用,得到pc.localDescription,在此函数中还得添加桌面流
- 再在控制端把上面得到的pc.localDescription传入setRemote,并调用,同时监听流的增加
- 实现STUN的过程,看下面
MediaStream API
-
媒体内容的流
-
一个流对象可以包含多轨道,包括音频和视频轨道等
-
能通过 WebRTC 传输
-
通过 标签可以播放
如何捕获桌面/窗口流
async function getScreenStream(){
const sources = await desktopCapturer.getSources({types: ['screen']})
navigator.webkitGetUserMedia({
audio:false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sources[0].id,
maxWidth: window.screen.width,
maxHeight: window.screen.height
}
}
// 捕获成功放在callback中的第一个参数
},(stream)=>{
peer.emit('add-stream', stream)
}, (err)=> {
// 这里必须写,不然报错
console.log(err)
})
}
复制代码
如何播放媒体流对象
var video = document.querySelector('video')
video.srcObject = stream
video.onloadedmetadata = function(e) {
video.play();
}
复制代码
实现桌面流传输
最简单的传输过程
SDP
SDP(Session Description Protocol)是一种会话描述协议,用来描述多媒体 会话,主要用于协商双方通讯过程,传递基本信息。
-
SDP的格式包含多行,每行为
<type>=<value>
-
<type>
:字符,代表特定的属性,比如v,代表版本 -
<value>
:结构化文本,格式与属性类型有关,UTF8编码
实战编码
第一步:创建RTCPeerConnection,它是我们建立P2P连接的一个封装的对象,然后我们会调用RTCPeerConnection的方法去创建一个offer,这个offer它是一个SDP,SDP本质上就是一个协议。等下展开讲SDP,大家可以理解成,我们发起了一个邀请,我们将我们得邀请设置到我们的LocalDescription,大家可以简单理解这三步,其实就是我们在创建一个初始连接,初始的P2P连接。
然后我们需要将我们控制端生成的邀请,也就是我们的offer,通过其他的媒介,比如你可以直接通过微信或者说短信都可以,将邀请的SDP传到给我们的傀儡端,我们的傀儡端拿到了offer之后,它也会去创建一个PeerConnection的对象,然后我们的傀儡端因为要分享我们的画面,在桌面共享的那一节的时候其实我们讲过,我们需要将我们的桌面流捕获完之后添加到我们的PeerConnection里面,随后我们将控制端设置为我们傀儡端的远端。就是我们要传给offer对应的PeerConnection,就调用setRemoteDescription这个方法,然后我们为了表示我们已经确定了,这里面我们会调用一个createAnswer,代表的是一个我确定的SDP,然后我会将SDP同样在傀儡端里设置上,这时候我们的傀儡端也会生成一个SDP,同样我们也可以通过任何的方法,将我们的响应SDP传到控制端,这时候控制端也会将这个SDP设置为它要传输的对象。
到这里,控制端和傀儡端已经互相设置为Remote,这个时候就会建立P2P的连接。简单总结一下,就是我们控制端发起了衣蛾邀请,傀儡端在确定邀请之后,把自己的桌面流添加到P2P的连接当中,然后同样返回一个确定的协议,最后我们控制端将确定的协议也设置上,这样子的话我们就代表着控制端和傀儡端的P2P连接已经可以开始了。
NAT(网络地址转换)
P2P数据交换是需要通过服务器,比如在美团大象,如果你要发信息的话,我们会传到服务端,然后发给对应的用户。如果走P2P连接,肯定会比走服务器来的快而且还更安全。
为什么要做一层服务端的转发,其实答案非常多,其中一个原因就是我们在端到端的通信时,需要知道对方的公网IP和端口号,实际上这不是一件容易的事情,因为我们的网络环境里充斥着NAT技术,NAT是网络地址转换的一个缩写,为什么这个技术会出现呢,如果大家对网络知识有一定了解的话,IPv4地址早就不够用了,它不够用主要有两个原因:
- 第一个原因是IPv4,它本来就是一个32位的整数,理论上只能支持40多亿的地址,这个数远远小于世界的总人口;
- 第二个原因是IP地址在地理位置上得分配不均,美国非常的多,中国是非常稀缺的,中国人均只有0.06个地址,而占据世界人口56%的亚洲只能够分到9%的地址。
于是人类为了解决地址的问题,NAT就出现了,在NAT内每个设备它都会有一个独立的局域网地址,然后它们在跟外网连接的时候会共用同一个公网IP,而NAT它负责维护一个包括本地IP端口和外网IP端口的一个映射表。
怎么获取真正的IP和端口呢?
这里面就会涉及到NAT打洞,基本方法就是由服务端跟其中一方ClientB建立一个连接,这时候NAT里面就会建立一个端口号的内外网的一个映射,之后我们服务端就可以知道ClientB外网的IP和端口,然后传给ClientA,最后ClinetA它就可以直接利用NAT打好了洞,然后跟ClientB进行一个通讯。在webRTC里面已经有一个集成好的机制,就是STUN服务,当ClientA和ClientB要做P2P连接的时候,它首先第一步需要跟我们的服务器做一个穿越打洞,然后将打洞的结果传到ClientB下,同样ClientB也需要做一个类似的操作,这样子我们通过服务器的帮助下,这样ClientA和ClientB就能拿到对方真实公网的IP和端口。
webRTC的NAT穿透是一整个机制,我们管它叫ICE
ICE(Interactive Connectivity Establishment)交互式连接创建
-
优先STUN (Session Traversal Utilities for NAT),NAT会话穿越应用程序
-
备选TURN (Traversal Using Relay NAT) ,中继NAT实现的穿透
-
Full Cone NAT - 完全锥形NAT
-
Restricted Cone NAT - 限制锥形NAT
-
Port Restricted Cone NAT 端口限制锥形NAT
-
Symmetric NAT 对称NAT
-
视频播放,需要进行换址操作
STUN的整个过程
首先我们的控制端,会先发起一个询址,然后我们的STUN服务会将这个洞打好,然后返回给我们的控制端,这个时候控制端就知道自己的外网的IP和端口,随后我们需要通过一定的介质然后给到傀儡端,这里面跟PeerConnection的SDP传输是一样的,你可以通过任何的介质来传输,像邮件、微信什么都可以,傀儡端拿到了IceEvent之后,它会通过addIceCandidate的方法添加我们的代理,这样的话,我们的傀儡端就知道控制端的一个外网IP了,类似的傀儡端也会拿到自己的IP和端口给到控制端,控制端添加ICE代理,这样子,我们的P2P才是真正的建立成功。
信令服务: 就是webRTC之间传递消息的服务器,实现连接两端
信令承载的作用就是各种转发
基于webSocket
建立数据传输 RTCDataChannel过程
//控制端
var pc = new RTCPeerConnection();
let dc = pc.createDataChannel('robotchannel', {reliable: false});
// 建立成功
dc.onopen = function() {
console.log('opened')
peer.on('robot', (type, data) => {
dc.send(JSON.stringify({type, data}))
})
}
// 接收消息
dc.onmessage = function(event) {
console.log('message', event)
}
dc.onerror = (e) => {console.log(e)}
复制代码
//傀儡端
const pc = new window.RTCPeerConnection();
pc.ondatachannel = (e) => {
console.log('data', e)
e.channel.onmessage = (e) => {
console.log('onmessage', e, JSON.parse(e.data))
let {type, data} = JSON.parse(e.data)
console.log('robot', type, data)
if(type === 'mouse') {
data.screen = {
width: window.screen.width,
height: window.screen.height
}
}
ipcRenderer.send('robot', type, data)
}
}
复制代码
具体源码查看自己的git
面试说分3步:
- 获取多媒体数据
- 建立P2P连接和通过信令传输多媒体数据
- 传输数据