webrtc实现单向的视频监控

3,771 阅读10分钟

项目地址:webrtc实现单向的视频监控

什么是webRTC

WebRTC源于瑞典GIPS公司,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API。Google 在 2011年6月1日收购了GIPS,并将webRTC开源。随后webRTC 在Google、Mozilla、Opera支持下被纳入W3C推荐标准。

简单来说,WebRTC 是一个可以在 Web 应用程序中实现音频,视频和数据的实时通信的开源项目。在实时通信中,音视频的采集和处理是一个很复杂的过程。比如音视频流的编解码、降噪和回声消除等,但是在 WebRTC 中,这一切都交由浏览器的底层封装来完成。我们可以直接拿到优化后的媒体流,然后将其输出到本地屏幕和扬声器,或者转发给其对等端。

pic1

webRTC的音视频处理API详细框架:

pic2
通常来说,我们一般直接在浏览器端通过编写js脚本,而不通过第三方插件的情况下,就能实现webRTC在一个浏览器到另一个浏览器之间点对点连接。

不过,虽然浏览器给我们解决了大部分音视频处理问题,但是从浏览器请求音频和视频时,我们还是需要特别注意流的大小和质量。因为即便硬件能够捕获高清质量流,CPU 和带宽也不一定可以跟上,这也是我们在建立多个对等连接时,不得不考虑的问题。上面的框架可能看得云里雾里的,但是总结起来,主要需要的功能完成如下:

  1. 音视频采集
  2. RTCPeerConnection
  3. 信令交换
  4. 通信候选地址交换
  5. 音视频发送,接收

音视频采集

浏览器端的音视频采集算是比较老套的话题了。一般来说两种方法,第一种方法采用回调函数。navigator.getUserMedia(constraints, successCallback, errorCallback); 第二种方法用promise,navigator.mediaDevices.getUserMedia(constraints) .then(successCallback).catch(errorCallback)。MDN上已经明确表示回调函数的api已经逐渐弃用,建议采用promise类型的api。然是为了考虑兼容性问题,可以选择一些适配器,如adapter.js。

这里简单提一下,对于 constraints 约束对象,我们可以用来指定一些和媒体流有关的属性。比如指定是否获取某种流:

    // 只需要视频流,不要音频
    navigator.mediaDevices.getUserMedia({ audio: false, video: true });

当然也可以指定视频流的宽高、帧率以及理想值:

    // 获取指定宽高,这里需要注意:在改变视频流的宽高时,
    // 如果宽高比和采集到的不一样,会直接截掉某部分
    { audio: false, 
      video: { width: 1280, height: 720 } 
    }
    // 设定理想值、最大值、最小值
    {
      audio: true,
      video: {
        width: { min: 1024, ideal: 1280, max: 1920 },
        height: { min: 776, ideal: 720, max: 1080 }
      }
    }

对于移动设备来说,还可以指定获取前摄像头,或者后置摄像头:

    { audio: true, video: { facingMode: "user" } } // 前置
    { audio: true, video: { facingMode: { exact: "environment" } } } // 后置
    // 也可以指定设备 id,
    // 通过 navigator.mediaDevices.enumerateDevices() 可以获取到支持的设备
    { video: { deviceId: myCameraDeviceId } }

还有一个比较有意思的就是设置视频源为屏幕,但是目前只有火狐支持了这个属性。

    { audio: true, video: {mediaSource: 'screen'} } 

最后,如果某台设备没有音频设备,但是constrain的audio设置为true的话,执行getUserMedia会出错!我一直想有没有什么方法让浏览器事先知道目前这台设备的音视频设备接入情况,然后对应的去判断并选择不同的constrains设置。当然给浏览器这么高的权限也确实不太安全,还是直接写死吧。

RTCPeerConnection接口

RTCPeerConnection是负责信令交换,通信候选地址交换,音视频发送、接收的基础。在点对点连接中,不管是发送方还是接收方,都需要初始化这个接口并实现其基本功能。

我们虽然把 WebRTC 称之为点对点的连接,但并不代表在实现过程中不需要服务器的参与。相反,在点对点的信道建立起来之前,二者之间是没有办法通信的。这也就意味着,在信令阶段,我们需要一个通信服务来帮助我们建立起这个连接。WebRTC 本身没有指定某一个信令服务,所以,我们可以但不限于使用 XMPP、XHR、Socket 等来做信令交换所需的服务。本例使用webSocke的ws模块来实现了信令交换,下文会详细说明。因为各浏览器差异,RTCPeerConnection 一样需要加上前缀。

let PeerConnection = window.RTCPeerConnection ||
                     window.mozRTCPeerConnection ||
                     window.webkitRTCPeerConnection;
let peer = new PeerConnection(iceServers);

我们看见 RTCPeerConnection 也同样接收一个参数 — iceServers,先来看看它长什么样:

  iceServers: [
    { url: "stun:stun.l.google.com:19302"}, // 谷歌的公共服务
    {
      url: "turn:***",
      username: ***, // 用户名
      credential: *** // 密码
    }
  ]

参数配置了两个 url,分别是 STUN 和 TURN,这便是 WebRTC 实现点对点通信的关键,也是一般 P2P 连接都需要解决的问题:NAT穿越。

NAT(Network Address Translation,网络地址转换)简单来说就是为了解决 IPV4 下的 IP 地址匮乏而出现的一种技术,也就是一个 公网 IP 地址一般都对应 n 个内网 IP。这样也就会导致不是同一局域网下的浏览器在尝试 WebRTC 连接时,无法直接拿到对方的公网 IP 也就不能进行通信,所以就需要用到 NAT 穿越(也叫打洞)。以下为 NAT 穿越基本流程:

pic3

一般情况下会采用 ICE 协议框架进行 NAT 穿越,ICE 的全称为 Interactive Connectivity Establishment,即交互式连接建立。它使用 STUN 协议以及 TURN 协议来进行穿越。实际上一般情况用谷歌的公用STUN服务器就够用了。TURN主要适用于存在企业级防火墙的网络环境中。在一般情况下,介于复杂的网络环境,turn服务器是需要的。因为端到端通信的一个主要问题是,在许多情况下,这些端点并不在公共互联网中,而是位于网络(和端口)地址转换器(NAT)后面的专用地址空间中。但是本项目仅仅是一个演示系统,就先去掉了turn,多增加了几个能用的google stun。嘻嘻。配置如下:

const configuration = {
    "iceServers": [{
        'urls': [
            'stun:stun.l.google.com:19302',
            'stun:stun1.l.google.com:19302',
            'stun:stun2.l.google.com:19302',
            'stun:stun.l.google.com:19302?transport=udp',
        ]
    }]
};

信令交换

如下图所示,信令交换的流程其实就是createOffer,createAnswer。信令交换是WebRTC通信中的关键环节,交换的信息包括编解码器、网络协议、候选地址等。对于如何进行信令交换,WebRTC并没有明确说明,而是交给应用自己来决定,比如可以采用WebSocket的ws模块。 另外要注意的是,在交换信令的过程中,还需注册消息处理程序。即在服务器中添加处理来自远程计算机的消息处理程序。如果消息包含RTCSessionDescription对象,则应该使用setLocalDescription()/setRemoteDescription()方法将其添加到RTCPeerConnection对象。如果消息包含RTCIceCandidate对象,则应该使用addIceCandidate()方法将其添加到RTCPeerConnection对象。

pic4

通信候选地址交换

注册onicecandidate处理程序实现。信令交换过程中,还会涉及到ice通信候选地址的交换,如下图所示:

pic5

可以看到,发送方在收到接收方的发回来的offer之后,立马就回去向stun服务器请求自己的ipaddress,然后stun服务器返回一个icecandidate事件,触发onicecandidate处理程序,添加icecandidate。然后发送方A再把刚刚收到的candidate发给B。B收到后同样也会向stun服务器请求自己的ipaddress,得到自己的icecandidate,并发给A。这样就实现了通信候选地址(icecandidate)交换。

音视频发送、接收

注册onaddstream处理程序实现。信令交换了,通信地址也交换了之后,就可以开始音视频发送接收了。

下面是有关RTCPeerConnection流程: 注册onicecandidate处理程序,它将任何ICE候选发送给其他对等方。注册onaddstream处理程序,它负责处理从远程计算机接收到的视频流的显示。注册消息处理程序。您的信令服务器还应该有一个处理来自远程计算机的消息处理程序。如果消息包含RTCSessionDescription对象,则应该使用RTCSessionDescription()方法将其添加到RTCPeerConnection对象。如果消息包含RTCIceCandidate对象,则应该使addIceCandidate()方法将其添加到RTCPeerConnection对象。使用getUserMedia()设置本地媒体流,并使用addstream()方法将其添加到RTCPeerConnection对象。开始提供/回答协商过程,这是呼叫者的流量不同于被呼叫者的唯一步骤。调用者使用createoffer( )方法开始协商,并注册一个收到rtcsessiondescription对象的回调。然后,这个回调应该使用setlocaldescription( )将这个rtcsessiondescription对象添加到rtcpeerconnection对象中。最后,调用者应该使用信令服务器将这个rtcsessiondescription发送到远程计算机。另一方面,被呼叫者,在createanswer()方法中注册相同的回调。请注意,只有在从调用者收到通知后,才会启动流。

单向视频监控实现思路

其他通过webrtc做的项目的一般都是用来做视频对话,本文思路不同于其他,要实现的是一个单向视频传输。webrtc要求createOffer方一定要有音视频设备。但是假设监控方本身没有视频设备怎么办呢,难不成还要强行装一个啊,那太搞笑了。我采取了这样的方式,用被监控者的客户端去创建CreateOffer。所以思路是监控者A去发一个命令,让被监控者B去创建createOffer。而B正好又是有音视频设备的。这样就ok了。有了上面webrtc的基础,实现起来也就简单了。

getUserMedia在实际生产环境中,需要服务器支持https协议。本文只是个测试,搭建https需要ssl证书等流程,就不折腾了,所以本例将采集音视频的一方的html直接放到本地,这样就不会受到https影响。而监控端的代码monitor.js和被监控端source.js类似,只是监控端不在本地端直接打开html,而是直接通过http从服务端去获取。

git项目具体使用方法:

目前仅用谷歌和火狐测试过,项目地址:webrtc实现单向的视频监控

服务端

mediago,用node.js的express框架部署。服务器的9090和9091端口要打开。因为本项目http用的9090,ws用的9091。当然你也可以自己去改。

客户端

找两台电脑(通一台上测试也行),有摄像头的一台用谷歌浏览器打开mediasource下的html,输入点击进入输入你自己搭建的服务器的ip地址和用户名,进去后打开摄像头,这个是被监控端。另外一台作为监控端,直接浏览器输入你自己搭建的服务器的ip地址,端口是9090(ip:9090)。然后输入监控端登录用户名,再输入被监控端的用户名,即可。注意,客户端打开之前要保证自己的电脑同时有音视频输入设备。下图是我测试的效果:

pic6
markdown插入视频好麻烦呀。。。就这样吧,嘻嘻。做的比较糙,不喜勿喷