迎接关注我的知乎专栏:https://zhuanlan.zhihu.com/starkwang
在传统的 Web 运用中,浏览器与浏览器之间是没法直接互相通讯的,必需借助服务器的辅佐,然则跟着 WebRTC 在各大浏览器中的提高,这一近况得到了转变。
WebRTC(Web Real-Time Communication,Web及时通讯),是一个支撑网页浏览器之间举行及时数据传输(包含音频、视频、数据流)的手艺,谷歌于2011年5月开放了工程的源代码,如今在各大浏览器的最新版本中都得到了差别水平的支撑。
这篇文章里我们采纳 WebRTC 来构建一个简朴的视频传输运用。
一、关于 WebRTC 的一些基本概念
传统的视频推流的手艺完成平常是如许的:客户端收集视频数据,推流到服务器上,服务器再根据具体状况将视频数据推送到其他客户端上。
然则 WebRTC 却判然差别,它能够在客户端之间直接搭建基于 UDP 的数据通道,经由简朴的握手流程以后,能够在差别装备的两个浏览器内直接传输恣意数据。
这个中的流程包含:
收集视频流数据,竖立一个 RTCPeerConnection
竖立一个 SDP offer 和响应的回应
为两边找到 ICE 候选途径
胜利竖立一个 WebRTC 衔接
下面我们引见这个中涉及到的一些关键词:
1、RTCPeerConnection 对象
RTCPeerConnection
对象是 WebRTC API 的进口,它担任竖立、保护一个 WebRTC 衔接,以及在这个衔接中的数据传输。如今新版本的浏览器多数支撑了这一对象,然则因为如今 API 还不稳固,所以须要到场各个浏览器内核的前缀,比方 Chrome 中我们运用 webkitRTCPeerConnection
来接见它。
2、会话形貌协定(SDP)
为了衔接到其他用户,我们必须要对其他用户的装备状况有所相识,比方音频视频的编码解码器、运用何种编码花样、运用何种收集、装备的数据处理才能,所以我们须要一张“手刺”来取得用户的一切信息,而 SDP 为我们供应了这些功用。
一个 SDP 的握手由一个 offer 和一个 answer 构成。
3、收集通讯引擎(ICE)
通讯的两侧能够会处于差别的收集环境中,有时会存在好几层的接见掌握、防火墙、路由跳转,所以我们须要一种要领在庞杂的收集环境中找到对方,而且衔接到响应的目的。WebRTC 运用了集成了 STUN、TURN 的 ICE 来举行两边的数据通讯。
二、竖立一个 RTCPeerConnection
起首我们的目的是在统一个页面中竖立两个及时视频,一个的数据直接来自你的摄像头,另一个的数据来自当地竖立的 WebRTC 衔接。看起来是如许的:
图图图。。。。。。。
起首我们竖立一个简朴的 HTML 页面,含有两个 video
标签:
<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css">
#theirs{
position: absolute;
top: 100px;
left: 100px;
width: 500px;
}
#yours{
position: absolute;
top: 120px;
left: 480px;
width: 100px;
z-index: 9999;
border:1px solid #ddd;
}
</style>
</head>
<body>
<video id="yours" autoplay></video>
<video id="theirs" autoplay></video>
</body>
<script type="text/javascript" src="./main.js"></script>
</html>
下面我们竖立一个 main.js
文件,先封装一下各浏览器的 userMedia
和 RTCPeerConnection
对象:
function hasUserMedia() {
navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
return !!navigator.getUserMedia;
}
function hasRTCPeerConnection() {
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;
return !!window.RTCPeerConnection;
}
然后我们须要浏览器挪用体系的摄像头 API getUserMedia
取得媒体流,注重要翻开浏览器的摄像头限定。Chrome因为平安的题目,只能在 https 下或许 localhost 下翻开摄像头。
var yourVideo = document.getElementById("yours");
var theirVideo = document.getElementById("theirs");
var yourConnection, theirConnection;
if (hasUserMedia()) {
navigator.getUserMedia({ video: true, audio: false },
stream => {
yourVideo.src = window.URL.createObjectURL(stream);
if (hasRTCPeerConnection()) {
// 稍后我们完成 startPeerConnection
startPeerConnection(stream);
} else {
alert("没有RTCPeerConnection API");
}
},
err => {
console.log(err);
}
)
}else{
alert("没有userMedia API")
}
没有不测的话,如今应当能在页面中看到一个视频了。
下一步是完成 startPeerConnection
要领,竖立传输视频数据所须要的 ICE 通讯途径,这里我们以 Chrome 为例:
function startPeerConnection(stream) {
//这里运用了几个大众的stun协定服务器
var config = {
'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
};
yourConnection = new RTCPeerConnection(config);
theirConnection = new RTCPeerConnection(config);
yourConnection.onicecandidate = function(e) {
if (e.candidate) {
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
theirConnection.onicecandidate = function(e) {
if (e.candidate) {
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
}
我们运用这个函数竖立了两个衔接对象,在 config
里,你能够恣意指定 ICE 服务器,虽然有些浏览器内置了默许的 ICE 服务器,能够不必设置,但照样发起加上这些设置。下面,我们举行 SDP 的握手。
因为是在统一页面中举行的通讯,所以我们能够直接交流两边的 candidate
对象,但在差别页面中,能够须要一个分外的服务器辅佐这个交流流程。
三、竖立 SDP Offer 和 SDP Answer
浏览器为我们封装好了响应的 Offer 和 Answer 要领,我们能够直接运用。
function startPeerConnection(stream) {
var config = {
'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
};
yourConnection = new RTCPeerConnection(config);
theirConnection = new RTCPeerConnection(config);
yourConnection.onicecandidate = function(e) {
if (e.candidate) {
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
theirConnection.onicecandidate = function(e) {
if (e.candidate) {
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
//本方发生了一个offer
yourConnection.createOffer().then(offer => {
yourConnection.setLocalDescription(offer);
//对方接收到这个offer
theirConnection.setRemoteDescription(offer);
//对方发生一个answer
theirConnection.createAnswer().then(answer => {
theirConnection.setLocalDescription(answer);
//本方接收到一个answer
yourConnection.setRemoteDescription(answer);
})
});
}
和 ICE 的衔接一样,因为我们是在统一个页面中举行 SDP 的握手,所以不须要借助任何其他的通讯手腕来交流 offer 和 answer,直接赋值即可。假如须要在两个差别的页面中举行交流,则须要借助一个分外的服务器来辅佐,能够采纳 websocket 或许别的手腕举行这个交流历程。
四、到场视频流
如今我们已经有了一个牢靠的视频数据传输通道了,下一步只须要向这个通道到场数据流即可。WebRTC 直接为我们封装好了到场视频流的接口,当视频流增加时,另一方的浏览器会经由过程 onaddstream
来示知用户,通道中有视频流到场。
yourConnection.addStream(stream);
theirConnection.onaddstream = function(e) {
theirVideo.src = window.URL.createObjectURL(e.stream);
}
以下是完全的 main.js
代码:
function hasUserMedia() {
navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
return !!navigator.getUserMedia;
}
function hasRTCPeerConnection() {
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;
return !!window.RTCPeerConnection;
}
var yourVideo = document.getElementById("yours");
var theirVideo = document.getElementById("theirs");
var yourConnection, theirConnection;
if (hasUserMedia()) {
navigator.getUserMedia({ video: true, audio: false },
stream => {
yourVideo.src = window.URL.createObjectURL(stream);
if (hasRTCPeerConnection()) {
startPeerConnection(stream);
} else {
alert("没有RTCPeerConnection API");
}
},
err => {
console.log(err);
})
} else {
alert("没有userMedia API")
}
function startPeerConnection(stream) {
var config = {
'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
};
yourConnection = new RTCPeerConnection(config);
theirConnection = new RTCPeerConnection(config);
yourConnection.onicecandidate = function(e) {
if (e.candidate) {
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
theirConnection.onicecandidate = function(e) {
if (e.candidate) {
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
theirConnection.onaddstream = function(e) {
theirVideo.src = window.URL.createObjectURL(e.stream);
}
yourConnection.addStream(stream);
yourConnection.createOffer().then(offer => {
yourConnection.setLocalDescription(offer);
theirConnection.setRemoteDescription(offer);
theirConnection.createAnswer().then(answer => {
theirConnection.setLocalDescription(answer);
yourConnection.setRemoteDescription(answer);
})
});
}