使用 WebRTC 构建简单的前端视频通信

欢迎关注个人知乎专栏:https://zhuanlan.zhihu.com/starkwangjavascript

在传统的 Web 应用中,浏览器与浏览器之间是没法直接相互通讯的,必须借助服务器的帮助,可是随着 WebRTC 在各大浏览器中的普及,这一现状获得了改变。css

WebRTC(Web Real-Time Communication,Web实时通讯),是一个支持网页浏览器之间进行实时数据传输(包括音频、视频、数据流)的技术,谷歌于2011年5月开放了工程的源代码,目前在各大浏览器的最新版本中都获得了不一样程度的支持。html

这篇文章里咱们采用 WebRTC 来构建一个简单的视频传输应用。java

1、关于 WebRTC 的一些基本概念

传统的视频推流的技术实现通常是这样的:客户端采集视频数据,推流到服务器上,服务器再根据具体状况将视频数据推送到其余客户端上。web

可是 WebRTC 却大相径庭,它能够在客户端之间直接搭建基于 UDP 的数据通道,通过简单的握手流程以后,能够在不一样设备的两个浏览器内直接传输任意数据。浏览器

这其中的流程包括:安全

  1. 采集视频流数据,建立一个 RTCPeerConnection服务器

  2. 建立一个 SDP offer 和相应的回应websocket

  3. 为双方找到 ICE 候选路径网络

  4. 成功建立一个 WebRTC 链接

下面咱们介绍这其中涉及到的一些关键词:

一、RTCPeerConnection 对象

RTCPeerConnection 对象是 WebRTC API 的入口,它负责建立、维护一个 WebRTC 链接,以及在这个链接中的数据传输。目前新版本的浏览器大都支持了这一对象,可是因为目前 API 还不稳定,因此须要加入各个浏览器内核的前缀,例如 Chrome 中咱们使用 webkitRTCPeerConnection 来访问它。

二、会话描述协议(SDP)

为了链接到其余用户,咱们必需要对其余用户的设备状况有所了解,好比音频视频的编码解码器、使用何种编码格式、使用何种网络、设备的数据处理能力,因此咱们须要一张“名片”来得到用户的全部信息,而 SDP 为咱们提供了这些功能。

一个 SDP 的握手由一个 offer 和一个 answer 组成。

三、网络通讯引擎(ICE)

通讯的两侧可能会处于不一样的网络环境中,有时会存在好几层的访问控制、防火墙、路由跳转,因此咱们须要一种方法在复杂的网络环境中找到对方,而且链接到相应的目标。WebRTC 使用了集成了 STUN、TURN 的 ICE 来进行双方的数据通讯。

2、建立一个 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 文件,先封装一下各浏览器的 userMediaRTCPeerConnection 对象:

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 对象,但在不一样页面中,可能须要一个额外的服务器协助这个交换流程。

3、创建 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 或者其它手段进行这个交换过程。

4、加入视频流

如今咱们已经有了一个可靠的视频数据传输通道了,下一步只须要向这个通道加入数据流便可。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);
        })
    });
}
相关文章
相关标签/搜索