做为一个认为啥都想懂一点的小开发,一直都对WebRTC
很感兴趣,这个兴趣来源于几年前公司但愿作一个即时通信的小功能在APP
上,不过最终因为项目最终需求更改而搁置。虽然如此,可是我仍是了解了一些关于该技术的技术背景,例如P2P
通信、内网打洞等等。经过几个晚上的学习和实验,大致上了解WebRTC
的原理和使用方法,如今分享一下个人学习过程吧。javascript
做为一个文档党,历来都要先看官方文档和文章,这样才能保证本身拿到最新,最好的一手信息。WebRTC
官网文档也还算是比较全面,不过貌似都很久没更新了。推测是,大概好久没有作功能升级了吧。我此次学习,参考了一些官方例子,加上了本身的理解。有错误的地方你们能够指出来呀,一块儿学习。参考的文章会在文章结尾加上。废话很少说了,开始吧。html
WebRTC
是谷歌开发的,目标是创造一个高质量的、可靠的通信框架,从字面的意咱们能够拆分为了Web
跟RTC
两部分,Web
很好理解啊,就是基于网络,而RTC
全称为Real Time Communications
(实时通信),所以它的做用就是让咱们能够利用浏览器(也能用于APP
),进行实时的通信的一个框架。既然是通信媒介固然是多种的,包括视频,语音,文本等多种多媒体信息,甚至你还能利用它来传输各类文件。下面,咱们用最直观的,视频通信来开始咱们的学习吧。前端
用浏览器打开摄像头很简单,咱们能够直接调用JS API
实现。java
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<h1>得到视频流</h1>
<!-- 设置自动播放 -->
<video autoplay playsinline></video>
<script src="js/main.js"></script>
</body>
</html>
复制代码
// 媒体流配置
const mediaStreamConstraints = {
video: true
};
// 得到 video 标签元素
const localVideo = document.querySelector("video");
// 媒体流对象
let localStream;
// 回调保存视频流对象并把流传到 video 标签
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
// handle 错误信息
function handleLocalMediaStreamError(error) {
console.log("打开本地视频流错误: ", error)
}
// fire!!
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream)
.catch(handleLocalMediaStreamError);
复制代码
代码主要分2步git
navigator.mediaDevices.getUserMedia
中得到视频设备。then
的回调中把视频流传到 video
标签。很是简单吧github
值得注意的是,我用的是Chrome
浏览器,新版本的Chrome
增强了获取设备的安全策略。若是你想要打开摄像头等设备,你的域名若是不是本地文件或者 localhost
那必须经过https
访问。web
既然视频流咱们获得了,第二步,咱们来使用WebRTC
的 RTCPeerConnection
来进行本地传输吧。这个Demo
不是真实的使用场景,由于不涉及到真实世界的网络传输,咱们仅仅是在同一个页面,打开了两个 RTCPeerConnection
把一个的内容传输到另外一个,从而进行通信。在贴代码以前,咱们先来简单的描述一下建立链接的过程吧。浏览器
假设如今是A想跟B视频。他们的 offer/answer (申请?/ 应答?), 机制是这样的:安全
1. `A `建立了一个 `RTCPeerConnection` 对象
2. `A` 利用`RTCPeerConnection` 的 `createOffer()` 方法建立了一个 `offer` (一个` SDP` 的会话描述)
3. `A` 在 `offer` 的回调中使用 `setLocalDescription()` 方法存储他的 `offer`
4. `A` 把他的 `offer` 字符串化,而后经过某一种信令机制发给 `B`
5. `B` 收到 `A` 的 `offer` 后用`setRemoteDescription()` 存起来,如此一来他的 `RTCPeerConnection` 就知道了 `A` 的配置。
6. `B` 调用 `createAnswer()` 并用他的成功回调的传送他的本地会话描述:这就是 `B` 的`answer`
7. `B` 用 `setLocalDescription()` 设置了他的 `answer` 到本地的会话描述
8. 而后 `B` 用某一种信令机制把他的 `answer` 字符串化以后返回给 `A`
9. `A` 把 `B` 的 `answer` 利用`setRemoteDescription()`方法存取为远程会话描述
复制代码
过程看上去很麻烦,不过其实他们就作了个事情服务器
SDP
)SDP
)有关 SDP
的格式,能够参看文章后面的连接
下面让咱们看代码,走起
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<h1>RTCPeerConnection 传输视频流</h1>
<!-- 设置自动播放 -->
<video autoplay playsinline id="localVideo"></video>
<video autoplay playsinline id="remoteVideo"></video>
<div>
<button id="startBtn">开始</button>
<button id="callBtn">拨打</button>
<button id="hangupBtn">挂机</button>
</div>
<!-- 垫片,用于统一浏览器 API -->
<script src="js/adapter.js"></script>
<script src="js/main.js"></script>
</body>
</html>
复制代码
HTML 代码比较简单,咱们建立了两个 video
,一个显示远程一个显示本地,而且加入了三个按钮进行模拟拨打。细心的同窗可能已经发现了,咱们引入了一个垫片adapter.js
。常常写前端的同窗对垫片可能熟悉不过了,由于世界上不只仅只有谷歌的浏览器,还有各类各样别的。而后命名,API
也是各类各样,因此咱们会利用各类垫片,统一咱们的API
。再也不忍受兼容之苦。adapter.js
就是这样的存在。他是谷歌官方提供给咱们的。引入它咱们即可以用统一套API
操做。
因为代码比较长,就只贴关键代码了。所有代码连接我会在文章后面贴上。
// 开始按钮,打开本地媒体流
function startAction() {
startButton.disabled = true;
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
trace('本地媒体流打开中...');
}
复制代码
这是响应开始
按钮的函数。跟第一个例子同样,主要是用来打开摄像头,而且把视频流传到id
为localVideo
的视频标签。
// 拨打按钮, 建立 peer connection
function callAction() {
callButton.disabled = true;
hangupButton.disabled = false;
trace("开始拨打...");
startTime = window.performance.now();
// ...
const servers = null; // RTC 服务器配置
// 建立 peer connetcions 并添加事件
localPeerConnection = new RTCPeerConnection(servers);
trace("建立本地 peer connetcion 对象");
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
remotePeerConnection = new RTCPeerConnection(servers);
trace("建立远程 peer connetcion 对象");
remotePeerConnection.addEventListener('icecandidate', handleConnection);
remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
// 添加本地流到链接中并建立链接
localPeerConnection.addStream(localStream);
trace("添加本地流到本地 PeerConnection");
trace("开始建立本地 PeerConnection offer");
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
}
复制代码
这部份是拨打
按钮的响应函数。在这个方法中,咱们作了个事情。
建立了用于通信的一对RTCPeerConnection
对象,localPeerConnection
和remotePeerConnection
分别给两个RTCPeerConnection
对象注册了icecandidate(重要)
和 iceconnectionstatechange
事件的响应函数
给remotePeerConnection
注册了addstream
事件的响应。
把本地视频流添加到localPeerConnection
localPeerConnection
建立offer
这里有一个上面没有说起的东西ICE Candidate
,ICE
是啥呢?哈哈,他的全称是 Interactive Connectivity Establishment
交互式链接的创建。他是一个规范,说白了就是创建链接用的规范,因为咱们的WebRTC
是要进行P2P
链接的,而咱们的网络是很是复杂的,并且大部分都是在内网(须要打洞或者穿越防火墙)。因此咱们须要一个机制来创建内网链接。这个我会在后面的文章详细来讲说。如今,简单理解成就是创建链接用的就行了。而icecandidate
的响应方法,则是当网络可用的状况下,用于存储和交换各类网络信息。
// 定义 RTC peer connection
function handleConnection(event) {
const peerConnection = event.target;
const iceCandidate = event.candidate;
if (iceCandidate) {
const newIceCanidate = new RTCIceCandidate(iceCandidate);
const otherPeer = getOtherPeer(peerConnection);
otherPeer.addIceCandidate(newIceCanidate)
.then(() => {
handleConnectionSuccess(peerConnection);
}).catch((error) => {
handleConnectionFailure(peerConnection, error);
});
trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
`${event.candidate.candidate}.`);
}
}
复制代码
这段代码正是体现了网络信息(ICE candidate
),的保存和交换过程。而保存Candidate
是经过调用RTCPeerConnection
对象的addIceCandidate
方法。这里可能你们有疑问,这里就交换了Candidate
信息了吗?是的getOtherPeer
方法其实就是用于得到对方的RTCPeerConnection
对象,由于咱们的 Demo 是在同一页面建立的。因此不需经过其余载体交换。
好的,说完链接建立,咱们接着说建立offer
。在建立offer
前,咱们已经留意到,其实已经把本地的视频流添加到RTCPeerConnection
对象中了,所以offer
所带的SDP
会话描述,已经带有相关信息。咱们先来createOffer
成功后的回调方法。
// 建立 offer
function createdOffer(description) {
trace(`Offer from localPeerConnection:\n${description.sdp}`);
trace('localPeerConnection setLocalDescription 开始.');
localPeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
trace('remotePeerConnection setRemoteDescription 开始.');
remotePeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
trace('remotePeerConnection createAnswer 开始.');
remotePeerConnection.createAnswer()
.then(createdAnswer)
}
复制代码
简单明了,对于localPeerConnection
来讲是本地,因此就是调用 setLocalDescription
把offer
信息存储。而对于对方就是远程remotePeerConnection
就是用setRemoteDescription
进行存储了。这里跟我章节前说的第4步说的不同,这里没有转成字符串。聪明的同窗可能猜到为何了,由于这里是同一个页面,不须要传输呀。
紧接着立刻remotePeerConnection
就调用createAnswer
建立了一个 answer
,让咱们继续看,
// 建立 answer
function createdAnswer(description) {
trace(`Answer from remotePeerConnection:\n${description.sdp}.`);
trace('remotePeerConnection setLocalDescription 开始.');
remotePeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
trace('localPeerConnection setRemoteDescription 开始.');
localPeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
}
复制代码
这里跟上面的createOffer
回调作的差很少,把answer
存储到双方对应的描述中。
到这里为止双方的链接建好,offer
与 answer
也存储稳当。因为remotePeerConnection
在以前已经已经注册好addStream
的响应方法了gotRemoteMediaStream
,而正如前文说的,由于建立offer
的时候已经把视频流带上了,因此gotRemoteMediaStream
此刻会回调,经过这个方法,把视频流显示在remoteVideo
标签中。
// 回调保存远程媒体流对象并把流传到 video 标签
function gotRemoteMediaStream(event) {
const mediaStream = event.stream;
remoteVideo.srcObject = mediaStream;
remoteStream = mediaStream;
trace("远程节点连接成功,接收远程媒体流中...");
}
复制代码
如今,咱们应该能够看到两个如出一辙的画面了。注意哦,右边那个是经过RTC
传输过来的。撒花~
这一篇先到这里吧,咱们下一篇继续。下一篇会继续继续深刻WebRTC
架构和ICE
,signling
之类的内容。谢谢你们的阅读,毕竟我也是个初学者,若是文中有不对的地方,你们能够评论一下,而后一块儿探讨。再次谢过。
代码和参考文档