Web Real-Time Communication(Web实时通讯,WebRTC)由一组标准、协议和JavaScript API组成,用于实现浏览器之间(端到端)的音频、视频及数据共享。javascript
WebRTC使得实时通讯变成一种标准功能,任何Web应用都无需借助第三方插件和专有软件,而是经过简单地JavaScript API便可完成。java
在WebRTC中,有三个主要的知识点,理解了这三个知识点,也就理解了WebRTC的底层实现原理。这三个知识点分别是:web
如上所说,MediaStream主要是用于获取音频和视频流。其JS实现也比较简单,代码以下:浏览器
'use strict'; navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; var constraints = { // 音频、视频约束 audio: true, // 指定请求音频Track video: { // 指定请求视频Track mandatory: { // 对视频Track的强制约束条件 width: {min: 320}, height: {min: 180} }, optional: [ // 对视频Track的可选约束条件 {frameRate: 30} ] } }; var video = document.querySelector('video'); function successCallback(stream) { if (window.URL) { video.src = window.URL.createObjectURL(stream); } else { video.src = stream; } } function errorCallback(error) { console.log('navigator.getUserMedia error: ', error); } navigator.getUserMedia(constraints, successCallback, errorCallback);
在JS中,咱们经过getUserMedia函数来处理音频和视频,该函数接收三个参数,分别是音视频的约束,成功的回调以及失败的回调。安全
在底层,浏览器经过音频和视频引擎对捕获的原始音频和视频流加以处理,除了对画质和音质加强以外,还得保证音频和视频的同步。服务器
因为音频和视频是用来传输的,所以,发送方还要适应不断变化的带宽和客户端之间的网络延迟调整输出的比特率。网络
对于接收方来讲,则必须实时解码音频和视频流,并适应网络抖动和时延。其工做原理以下图所示:并发
如上成功回调的stream对象中携带者一个或多个同步的Track,若是你同时在约束中设置了音频和视频为true,则在stream中会携带有音频Track和视频Track,每一个Track在时间上是同步的。ide
stream的输出能够被发送到一或多个目的地:本地的音频或视频元素、后期处理的JavaScript代理,或者远程另外一端。以下图所示:函数
在获取到音频和视频流后,下一步要作的就是将其发送出去。但这个跟client-server模式不一样,这是client-client之间的传输,所以,在协议层面就必须解决NAT穿透问题,不然传输就无从谈起。
另外,因为WebRTC主要是用来解决实时通讯的问题,可靠性并非很重要,所以,WebRTC使用UDP做为传输层协议:低延迟和及时性才是关键。
在更深刻讲解以前,咱们先来思考一下,是否是只要打开音频、视频,而后发送UDP包就搞定了?
固然没那么简单,除了要解决咱们上面说的NAT穿透问题以外,还须要为每一个流协商参数,对用户数据进行加密,而且须要实现拥塞和流量控制。
咱们来看一张WebRTC的分层协议图:
ICE、STUN和TURN是经过UDP创建并维护端到端链接所必需的;SDP 是一种数据格式,用于端到端链接时协商参数;DTLS用于保障传输数据的安全;SCTP和SRTP属于应用层协议,用于在UDP之上提供不一样流的多路复用、拥塞和流量控制,以及部分可靠的交付和其余服务。
ICE(Interactive Connectivity Establishment,交互链接创建):因为端与端之间存在多层防火墙和NAT设备阻隔,所以咱们须要一种机制来收集两端之间公共线路的IP,而ICE则是干这件事的好帮手。
WebRTC使用SDP(Session Description Protocol,会话描述协议)描述端到端链接的参数。
SDP不包含媒体自己的任何信息,仅用于描述"会话情况",表现为一系列的链接属性:要交换的媒体类型(音频、视频及应用数据)、网络传输协议、使用的编解码器及其设置、带宽及其余元数据。
DTLS对TLS协议进行了扩展,为每条握手记录明确添加了偏移字段和序号,这样就知足了有序交付的条件,也能让大记录能够被分段成多个分组并在另外一端再进行组装。
DTLS握手记录严格按照TLS协议规定的顺序传输,顺序不对就报错。最后,DTLS还要处理丢包问题:两端都是用计时器,若是预约时间没有收到应答,就重传握手记录。
为保证过程完整,两端都要生成本身签名的证书,而后按照常规的TLS握手协议走。但这样的证书不能用于验证身份,由于没有要验证的信任链。所以,在必要状况下,
应用必须本身参与各端的身份验证:
SRTP为经过IP网络交付音频和视频定义了标准的分组格式。SRTP自己并不对传输数据的及时性、可靠性或数据恢复提供任何保证机制,
它只负责把数字化的音频采样和视频帧用一些元数据封装起来,以辅助接收方处理这些流。
SCTP是一个传输层协议,直接在IP协议上运行,这一点跟TCP和UDP相似。不过在WebRTC这里,SCTP是在一个安全的DTLS信道中运行,而这个信道又运行在UDP之上。
因为WebRTC支持经过DataChannel API在端与端之间传输任意应用数据,而DataChannel就依赖于SCTP。
以上讲了这么多,终于到咱们的主角RTCPeerConnection,RTCPeerConnection接口负责维护每个端到端链接的完整生命周期:
咱们来看一下示例代码:
var signalingChannel = new SignalingChannel(); var pc = null; var ice = { "iceServers": [ { "url": "stun:stun.l.google.com:19302" }, //使用google公共测试服务器 { "url": "turn:user@turnserver.com", "credential": "pass" } // 若有turn服务器,可在此配置 ] }; signalingChannel.onmessage = function (msg) { if (msg.offer) { // 监听并处理经过发信通道交付的远程提议 pc = new RTCPeerConnection(ice); pc.setRemoteDescription(msg.offer); navigator.getUserMedia({ "audio": true, "video": true }, gotStream, logError); } else if (msg.candidate) { // 注册远程ICE候选项以开始链接检查 pc.addIceCandidate(msg.candidate); } } function gotStream(evt) { pc.addstream(evt.stream); var local_video = document.getElementById('local_video'); local_video.src = window.URL.createObjectURL(evt.stream); pc.createAnswer(function (answer) { // 生成描述端链接的SDP应答并发送到对端 pc.setLocalDescription(answer); signalingChannel.send(answer.sdp); }); } pc.onicecandidate = function (evt) { if (evt.candidate) { signalingChannel.send(evt.candidate); } } pc.onaddstream = function (evt) { var remote_video = document.getElementById('remote_video'); remote_video.src = window.URL.createObjectURL(evt.stream); } function logError() { ... }
DataChannel支持端到端的任意应用数据交换,就像WebSocket同样,可是是端到端的。
创建RTCPeerConnection链接以后,两端能够打开一或多个信道交换文本或二进制数据。
其示例demo以下:
var ice = { 'iceServers': [ {'url': 'stun:stun.l.google.com:19302'}, // google公共测试服务器 // {"url": "turn:user@turnservera.com", "credential": "pass"} ] }; // var signalingChannel = new SignalingChannel(); var pc = new RTCPeerConnection(ice); navigator.getUserMedia({'audio': true}, gotStream, logError); function gotStream(stram) { pc.addStream(stram); pc.createOffer().then(function(offer){ pc.setLocalDescription(offer); }); } pc.onicecandidate = function(evt) { // console.log(evt); if(evt.target.iceGatheringState == 'complete') { pc.createOffer().then(function(offer){ // console.log(offer.sdp); // signalingChannel.send(sdp); }) } } function handleChannel(chan) { console.log(chan); chan.onerror = function(err) {} chan.onclose = function() {} chan.onopen = function(evt) { console.log('established'); chan.send('DataChannel connection established.'); } chan.onmessage = function(msg){ // do something } } // 以合适的交付语义初始化新的DataChannel var dc = pc.createDataChannel('namedChannel', {reliable: false}); handleChannel(dc); pc.onDataChannel = handleChannel; function logError(){ console.log('error'); }