腾讯云技术社区-掘金主页持续为你们呈现云计算技术文章,欢迎你们关注!javascript
做者:villainthrjava
摘自:villainhrgit
WebRTC 全称为:Web Real-Time Communication
。它是为了解决 Web 端没法捕获音视频的能力,而且提供了 peer-to-peer(就是浏览器间)的视频交互。实际上,细分看来,它包含三个部分:github
但一般,peer-to-peer 的场景实际上应用不大。对比与去年火起来的直播
业务,这应该才是 WebRTC 经常应用到的地方。那么对应于 Web 直播来讲,咱们一般须要两个端:web
这里,我就不谈观众端了,后面另写一篇文章介绍(由于,这是在是太多了)。这里,主要谈一下会用到 WebRTC 的主播端。
简化一下,主播端应用技术简单能够分为:录制视频,上传视频。你们先记住这两个目标,后面咱们会经过 WebRTC 来实现这两个目标。api
WebRTC 主要由两个组织来制定。浏览器
固然,咱们初级目标是先关心基本浏览器定义的 API 是啥?以及怎么使用?
而后,后期目标是学习期内部的相关协议,数据格式等。这样按部就班来,比较适合咱们的学习。安全
WebRTC 对于音视频的处理,主要是交给 Audio/Vidoe Engineering 处理的。处理过程为:性能优化
降噪
,消除回音
,抖动/丢包隐藏
,编码
。图像加强
,同步
,抖动/丢包隐藏
,编码
。最后经过 mediaStream Object 暴露给上层 API 使用。也就是说 mediaStream 是链接 WebRTC API 和底层物理流的中间层。因此,为了下面更好的理解,这里咱们先对 mediaStream 作一些简单的介绍。服务器
MS(MediaStream)是做为一个辅助对象存在的。它承载了音视频流的筛选,录制权限的获取等。MS 由两部分构成: MediaStreamTrack 和 MediaStream。
会声会影
的话,应该对轨道
这个词不陌生。通俗来说,你能够认为二者就是等价的。MediaStreamTrack
。它主要的做用就是确保几个轨道是同时播放的。例如,声音须要和视频画面同步。这里,咱们不说太深,讲讲基本的 MediaStream
对象便可。一般,咱们使用实例化一个 MS 对象,就能够获得一个对象。
// 里面还须要传递 track,或者其余 stream 做为参数。
// 这里只为演示方便
let ms = new MediaStream();复制代码
咱们能够看一下 ms
上面带有哪些对象属性:
它的原型链上还挂在了其余方法,我挑几个重要的说一下。
前面说了,MS 还能够其余筛选的做用,那么它是如何作到的呢?
在 MS 中,还有一个重要的概念叫作: Constraints
。它是用来规范当前采集的数据是否符合须要。由于,咱们采集视频时,不一样的设备有不一样的参数设置。经常使用的为:
{
"audio": true, // 是否捕获音频
"video": { // 视频相关设置
"width": {
"min": "381", // 当前视频的最小宽度
"max": "640"
},
"height": {
"min": "200", // 最小高度
"max": "480"
},
"frameRate": {
"min": "28", // 最小帧率
"max": "10"
}
}
}复制代码
那我怎么知道个人设备支持的哪些属性的调优呢?
这里,能够直接使用 navigator.mediaDevices.getSupportedConstraints()
来获取能够调优的相关属性。不过,这通常是对 video 进行设置。了解了 MS 以后,咱们就要开始真正接触 WebRTC 的相关 API。咱们先来看一下 WebRTC 基本API。
WebRTC 的经常使用 API 以下,不过因为浏览器的缘故,须要加上对应的 prefix:
W3C Standard Chrome Firefox
--------------------------------------------------------------
getUserMedia webkitGetUserMedia mozGetUserMedia
RTCPeerConnection webkitRTCPeerConnection RTCPeerConnection
RTCSessionDescription RTCSessionDescription RTCSessionDescription
RTCIceCandidate RTCIceCandidate RTCIceCandidate复制代码
不过,你能够简单的使用下列的方法来解决。不过嫌麻烦的可使用 adapter.js 来弥补
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia复制代码
这里,咱们按部就班的来学习。若是想进行视频的相关交互,首先应该是捕获音视频。
在 WebRTC 中捕获音视频,只须要使用到一个 API,即,getUserMedia()
。代码其实很简单:
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
var constraints = { // 设置捕获的音视频设置
audio: false,
video: true
};
var video = document.querySelector('video');
function successCallback(stream) {
window.stream = stream; // 这就是上面提到的 mediaStream 实例
if (window.URL) {
video.src = window.URL.createObjectURL(stream); // 用来建立 video 能够播放的 src
} else {
video.src = stream;
}
}
function errorCallback(error) {
console.log('navigator.getUserMedia error: ', error);
}
// 这是 getUserMedia 的基本格式
navigator.getUserMedia(constraints, successCallback, errorCallback);复制代码
详细 demo 能够参考:WebRTC。不过,上面的写法比较古老,若是使用 Promise 来的话,getUserMedia 能够写为:
navigator.mediaDevices.getUserMedia(constraints).
then(successCallback).catch(errorCallback);复制代码
上面的注释大概已经说清楚基本的内容。须要提醒的是,你在捕获视频的同时,必定要清楚本身须要捕获的相关参数。
有了本身的视频以后,那如何与其余人共享这个视频呢?(能够理解为直播的方式)
在 WebRTC 中,提供了 RTCPeerConnection
的方式,来帮助咱们快速创建起链接。不过,这仅仅只是创建起 peer-to-peer 的中间一环。这里包含了一些复杂的过程和额外的协议,咱们一步一步的来看下。
WebRTC 利用的是 UDP 方式来进行传输视频包。这样作的好处是延迟性低,不用过分关注包的顺序。不过,UDP 仅仅只是做为一个传输层协议而已。WebRTC 还须要解决不少问题
整个架构以下:
上面那些协议,例如,ICE/STUN/TURN 等,咱们后面会慢慢讲解。先来看一下,二者是如何进行信息协商的,一般这一阶段,咱们叫作 signaling
。
signaling 其实是一个协商过程。由于,两端进不进行 WebRTC 视频交流之间,须要知道一些基本信息。
master key
用来确保安全链接。不过,signaling 这个过程并非写死的,即,无论你用哪一种协议,只要能确保安全便可。为何呢?由于,不一样的应用有着其自己最适合的协商方法。好比:
咱们本身也能够模拟出一个 signaling 通道。它的原理就是将信息进行传输而已,一般为了方便,咱们能够直接使用 socket.io 来创建 room
提供信息交流的通道。
假定,咱们如今已经经过 socket.io
创建起了一个信息交流的通道。那么咱们接下来就能够进入 RTCPeerConnection
一节,进行链接的创建。咱们首先应该利用 signaling
进行基本信息的交换。那这些信息有哪些呢?
WebRTC 已经在底层帮咱们作了这些事情-- Session Description Protocol (SDP)
。咱们利用 signaling
传递相关的 SDP,来确保双方都能正确匹配,底层引擎会自动解析 SDP (是 JSEP 帮的忙),而不须要咱们手动进行解析,忽然感受世界好美妙。。。咱们来看一下怎么传递。
// 利用已经建立好的通道。
var signalingChannel = new SignalingChannel();
// 正式进入 RTC connection。这至关于建立了一个 peer 端。
var pc = new RTCPeerConnection({});
navigator.getUserMedia({ "audio": true })
.then(gotStream).catch(logError);
function gotStream(stream) {
pc.addStream(stream);
// 经过 createOffer 来生成本地的 SDP
pc.createOffer(function(offer) {
pc.setLocalDescription(offer);
signalingChannel.send(offer.sdp);
});
}
function logError() { ... }复制代码
那 SDP 的具体格式是啥呢?
看一下格式就 ok,这不用过多了解:
v=0
o=- 1029325693179593971 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:nHtT
a=ice-pwd:cuwglAha5fBmGljFXWntH1VN
a=fingerprint:sha-256 24:63:EB:DD:18:1B:BB:5E:B3:E8:C5:D7:92:F7:0B:44:EC:22:96:63:64:76:1A:56:64:DE:6B:CE:85:C6:64:78
a=setup:active
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=inactive
a=rtcp-mux
...复制代码
上面的过程,就是 peer-to-peer 的协商流程。这里有两个基本的概念,offer
,answer
。
具体过程为:
不过,上面只是简单确立了两端的链接信息而已,尚未涉及到视频信息的传输,也就是说 UDP 传输。UDP 传输原本就是一个很是让人蛋疼的活,若是是 client-server 的模型话还好,直接传就能够了,但这恰恰是 peer-to-peer 的模型。想一想,你如今是要把你的电脑当作一个服务器使用,中间还须要经历若是突破防火墙,若是找到端口,如何跨网段进行?因此,这里咱们就须要额外的协议,即,STUN/TURN/ICE ,来帮助咱们完成这样的传输任务。
在 UDP 传输中,咱们不可避免的会碰见 NAT
(Network address translator)服务器。即,它主要是将其它网段的消息传递给它负责网段内的机器。不过,咱们的 UDP 包在传递时,通常只会带上 NAT 的 host
。若是,此时你没有目标机器的 entry
的话,那么该次 UDP 包将不会被转发成功。不过,若是你是 client-server 的形式的话,就不会碰见这样的问题。但,这里咱们是 peer-to-peer 的方式进行传输,没法避免的会碰见这样的问题。
为了解决这样的问题,咱们就须要创建 end-to-end 的链接。那办法是什么呢?很简单,就是在中间设立一个 server
用来保留目标机器在 NAT 中的 entry
。经常使用协议有 STUN, TURN 和 ICE
。那他们有什么区别吗?
NAT traversal
服务器,保留指定机器的 entry
因此,上面三者一般是结合在一块儿使用的。它们在 PeerConnection 中的角色以下图:
若是,涉及到 ICE 的话,咱们在实例化 Peer Connection 时,还须要预先设置好指定的 STUN/TRUN 服务器。
var ice = {"iceServers": [
{"url": "stun:stun.l.google.com:19302"},
// TURN 通常须要本身去定义
{
'url': 'turn:192.158.29.39:3478?transport=udp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
},
{
'url': 'turn:192.158.29.39:3478?transport=tcp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
}
]};
var signalingChannel = new SignalingChannel();
var pc = new RTCPeerConnection(ice); // 在实例化 Peer Connection 时完成。
navigator.getUserMedia({ "audio": true }, gotStream, logError);
function gotStream(stream) {
pc.addStream(stream); // 将流添加到 connection 中。
pc.createOffer(function(offer) {
pc.setLocalDescription(offer);
});
}
// 经过 ICE,监听是否有用户链接
pc.onicecandidate = function(evt) {
if (evt.target.iceGatheringState == "complete") {
local.createOffer(function(offer) {
console.log("Offer with ICE candidates: " + offer.sdp);
signalingChannel.send(offer.sdp);
});
}
}
...复制代码
在 ICE 处理中,里面还分为 iceGatheringState
和 iceConnectionState
。在代码中反应的就是:
pc.onicecandidate = function(e) {
evt.target.iceGatheringState;
pc.iceGatheringState
};
pc.oniceconnectionstatechange = function(e) {
evt.target.iceConnectionState;
pc.iceConnectionState;
};复制代码
固然,起主要做用的仍是 onicecandidate
。
不过,这里为了更好的讲解 WebRTC 创建链接的基本过程。咱们使用单页的链接来模拟一下。如今假设,有两个用户,一个是 pc1,一个是 pc2。pc1 捕获视频,而后,pc2 创建与 pc1 的链接,完成伪直播的效果。直接看代码吧:
var servers = null;
// Add pc1 to global scope so it's accessible from the browser console
window.pc1 = pc1 = new RTCPeerConnection(servers);
// 监听是否有新的 candidate 加入
pc1.onicecandidate = function(e) {
onIceCandidate(pc1, e);
};
// Add pc2 to global scope so it's accessible from the browser console
window.pc2 = pc2 = new RTCPeerConnection(servers);
pc2.onicecandidate = function(e) {
onIceCandidate(pc2, e);
};
pc1.oniceconnectionstatechange = function(e) {
onIceStateChange(pc1, e);
};
pc2.oniceconnectionstatechange = function(e) {
onIceStateChange(pc2, e);
};
// 一旦 candidate 添加成功,则将 stream 播放
pc2.onaddstream = gotRemoteStream;
// pc1 做为播放端,先将 stream 加入到 Connection 当中。
pc1.addStream(localStream);
pc1.createOffer(
offerOptions
).then(
onCreateOfferSuccess,
error
);
function onCreateOfferSuccess(desc) {
// desc 就是 sdp 的数据
pc1.setLocalDescription(desc).then(
function() {
onSetLocalSuccess(pc1);
},
onSetSessionDescriptionError
);
trace('pc2 setRemoteDescription start');
// 省去了 offer 的发送通道
pc2.setRemoteDescription(desc).then(
function() {
onSetRemoteSuccess(pc2);
},
onSetSessionDescriptionError
);
trace('pc2 createAnswer start');
pc2.createAnswer().then(
onCreateAnswerSuccess,
onCreateSessionDescriptionError
);
}复制代码
看上面的代码,你们估计有点迷茫,来点实的,你们能够参考 单页直播。在查看该网页的时候,能够打开控制台观察具体进行的流程。会发现一个现象,即,onaddstream
会在 SDP
协商还未完成以前就已经开始,这也是,该 API 设计的一些不合理之处,因此,W3C 已经将该 API 移除标准。不过,对于目前来讲,问题不大,由于仅仅只是做为演示使用。整个流程咱们一步一步来说解下。
pc2.addIceCandidate
方法将 pc1 添加进去。oniceconnectionstatechange
检查 pc1 远端 candidate 的状态。当为 completed
状态时,则会触发 pc2 onicecandidate
事件。此外,还有另一个概念,RTCDataChannel
我这里就不过多涉及了。若是有兴趣的能够参阅 webrtc,web 性能优化 进行深刻的学习。
相关推荐:
【腾讯云的1001种玩法】 Laravel 整合微视频上传管理能力,轻松打造视频App后台
阐述腾讯云直播视频解决方案