HTML5 视频直播(三)

连续写了两篇有关视频直播的文章之后,有同学问我为什么没有 WebRTC 相关内容。实际上一开始我就说过,我的需求是在移动 WEB 端上直播视频,而移动端浏览器现阶段对「WebRTC 的支持度」非常不乐观,所以我就直接无视它了。但我一时为了标题美观,活生生地把「移动 WEB 端」写成了「HTML5」,所以为了严谨我还是补上这一篇吧。

WebRTC(Web Real-Time Communication),中文一般翻译为「Web 实时通信」。它由一组标准、协议和 JavaScript API 组成,用于实现端到端的音视频及数据共享。与其他浏览器通信机制不同,WebRTC 通过 UDP 传输数据,而我们早已熟知的 XMLHttpRequest、WebSocket 都基于 TCP。

纵观整个浏览器市场,其实只有 Google 和 Mozilla 两家公司对 WebRTC 比较上心,Firefox 22 和 Chrome 23 就开始就支持了它;Microsoft 是搞了自己的一套标准,后续可能会跟 WebRTC 融合,但至少 IE 不会支持现阶段的 WebRTC 标准;Apple 也许是因为有 FaceTime 可以很好地实现 Apple 设备间的多媒体通讯,压根就没打算在 Safari 中增加对 WebRTC 的支持;至于 Opera,换内核后基本等同于 Chrome,这下更要被人无视了。

初识 WebRTC

WebRTC 涉及到很多复杂技术,不过好在浏览器已经把大多数复杂工作抽象成为下面三个 API:

  • MediaStream:获取音频和视频流;
  • RTCPeerConnection:音频和视频数据通信;
  • RTCDataChannel:任意应用数据通信;

MediaStream 对应的是 JS 里的 navigator.getUserMedia() 方法,它负责从底层平台获取音视频流。音视频流经过 WebRTC 音视频引擎的自动优化、编码和解码,就可以直接用或传输到各种目的地用。这里有个 Demo,就是用 getUserMedia 获取视频流,再把每一帧都转成 ASCII 字符播放。总之 MediaStream API 设计得很简单,使用起来也很方便。

RTCPeerConnection 用来建立和维护端到端连接,并提供高效的音视频流传输。整个 WebRTC 提供的 API 中,要数这个最复杂:

首先,要建立端到端连接,不可避免要解决 NAT 穿透问题,RTCPeerConnection 为此引入了 ICE(Interactive Connectivity Establishment)框架。ICE 致力于在端之间建立一条有效的通道,优先直连,其次用 STUN 协商,再不行只能用 TURN 转发。

STUN(Session Traversal Utilities for NAT)协议,解决了三个问题:1)获得外网 IP 和端口;2)在 NAT 中建立路由条目,绑定外网端口,使得到达外网 IP 和端口的入站分组能找到应用程序,不被丢弃;3)定义了一个简单的 keep-alive 机制,保证 NAT 路由条目不会因为超时而被删除。STUN 服务器必须架设在公网上,可以自己搭建,也可以使用第三方提供的公开服务,例如 Google 的「stun:stun.l.google.com:19302」。

TURN(Traversal Using Relays around NAT)协议,依赖外网中继设备在两端之间传递数据。简单说就是通过两端都可以访问的 TURN 服务转发消息,间接把两端连起来。TURN 还会尝试使用 TCP 建立,而不仅仅是 UDP,可靠性大大增强,带宽成本也随着大幅提升。根据 Google 的统计,UDP 服务中,有 8% 左右的情况下需要 TURN。

其次,要建立端到端的信道,还是需要借助服务端来交换和协商一些信息,这个过程被称之为 Signaling。WebRTC 并没有规则 Signaling 必须使用某种协议,而把选择权交给了应用程序。我们可以选用不同方式(XMLHttpRequest、WebSocket),采用已有的 SIP、Jingle、ISUP 等发信协议,来建立信道。

通常,在 WebRTC 应用中,建立信道这一步都是优先走 WebSocket,并支持降级为 HTTP。一来支持 WebRTC 的浏览器肯定都支持 WebSocket;二来 WebSocket 实时性更好一些。特别需要注意的是,WebSocket 只用来辅助建立端到端连接,一旦连接建立,信源在端到端之间的传输就完全不需要服务端了(当然 TURN 这种中继模式就另当别论)。

RTCDataChannel 用来支持端到端的任意应用数据交换。建立 RTCPeerConnection 连接之后,除了可以传输音视频流,还可以打开一个或多个信道用来传输任何文本或二进制内容,这就是 RTCDataChanel。DataChannel API 在使用上跟 WebSocket 非常类似,功能上都可以用来在端到端之间传输数据,但是本质上他们还是有区别的:

首先,WebRTC 端与端之间是对等的,DataChannel 可以由任何一方发起;这与 WebSocket 连接只能由客户端发起不同; 其次,WebSocket 的会话层协议 TLS 是可选的;而 WebRTC 的会话层协议 DTLS 是必须的,这表明通过 WebRTC 传输的数据一定会被加密; 再者,WebSocket 运行在 TCP 之上,每条消息天然有序并可靠;而 DataChannel 可以通过 SCTP 的交付属性选项来指定消息是有序还是乱序,是可靠还是部分可靠,部分可靠时还可以指定使用超时重传还是计数重传策略。

现阶段 DataChannel 运行在下列协议之上:

  • SCTP(Stream Control Transmission Protocol),流控制传输协议,提供了一些与 TCP 类似的特性;
  • DTLS(Datagram Transport Layer Security),传输内容加密,UDP 版的 TLS;
  • UDP(User Datagram Protocol),用户数据报协议,整个 WebRTC 的基础;

这里我并不打算完整地介绍如何从零开始使用 WebRTC,类似的文章网上大把。这里推荐几篇文章,后几篇中文的出自同一个作者,写得比较通俗易懂:

另外还推荐《Web 性能权威指南》这本书,它的第 3 章「UDP 的构成」和第 18 章「WebRTC」对 UDP 内网穿透和 WebRTC 有比较详细的介绍。

一对多直播

前面说过,WebRTC 是用来解决端到端的实时通信问题,也就是说它很适合用在网络电话这种需要双向视频通话的场景上。网上大部分 WebRTC 的 Demo 也都是在页面上放两个 Video,分别来播 localStream 和 RemoteStream。那么究竟 WebRTC 能否用来实现单向一对多直播呢?当然可以,而且貌似还很简单:

  • 首先必须有一个专门负责调用 getUserMedia 采集音视频的页面,我称之为信源服务;
  • 打开直播页面时,建立到信源服务的 PeerConnection,并通过 DataChannel 通知信源服务;
  • 信源服务收到通知后,通过对应 PeerConnection 的 addStream 方法提供直播流;
  • 直播页面监听 PeerConnection 的 onaddstream 事件,将获得的直播流用丢给 Video 播放;

为了方便,我使用了 PeerJS 这个开源项目来验证上面这个过程。PeerJS 对 WebRTC Api 进行了封装,使用更简单。它还提供了用来辅助建立连接的 Signaling 服务,在官网注册一个 Api Key 就能用。也可以通过 PeerJS Server 搭建自己的服务,只需要通过 npm install peer 装好 peer 后,再通过下面这行命令启动就可以了:

peerjs --port 9000 --key peerjs

启动好 Peer Server,在页面中引入 peer.js 就可以开始玩了。首先实现信源服务:

//由于其它端都要连它,指定一个固定的 ID
var peer = new Peer('Server', {
    host: 'qgy18.qgy18.com', 
    port: 9003, 
    path: '/',
    config: {
        'iceServers': [
              { url: 'stun:stun.l.google.com:19302' }
        ]
    }
});

navigator.getUserMedia({ audio: false, video: true }, function(stream) {
    window.stream = stream;
}, function() { /*...*/ });

peer.on('connection', function(conn) {
    conn.on('data', function(clientId){
        var call = peer.call(clientId, window.stream);

        call.on('close', function() { /*...*/ });
    });
});

然后就是直播服务:

//随机生成一个 ID
var clientId = (+new Date).toString(36) + '_' + (Math.random().toString()).split('.')[1];

var peer = new Peer(clientId, {
    host: 'qgy18.qgy18.com', 
    port: 9003, 
    path: '/',
    config: {
        'iceServers': [
              { url: 'stun:stun.l.google.com:19302' }
        ]
    }
});

var conn = peer.connect('Server');

conn.on('open', function() {
    conn.send(clientId);
});

peer.on('call', function(call) {
    call.answer();
    call.on('stream', function(remoteStream) {
        var video = document.getElementById('video');
        video.src = window.URL.createObjectURL(remoteStream);
    });

    call.on('close', function() { /*...*/ });
});

直播页面通过指定 ID 的方式跟信源服务建立端到端连接,然后通过 DataChannel 告诉信源服务自己的 ID,信源服务收到消息后,主动把直播流发过来,直播页面应答后播放就可以了。整个过程原理就这么简单,这里有一个「完整的 Demo」。

看完上面的 Demo,你也许会想原来使用 WebRTC 直播这么简单,随便找台带摄像头的电脑,开个浏览器就能提供直播服务,那还搞 HLS、RTMP 什么的干嘛。

实际上,现实并没有那么美好,这个 Demo 也就玩玩儿还可以,真正使用起来问题还大着呢!

首先,虽然说在 WebRTC 直播方案中,服务端只扮演桥梁的工作,实际数据传输直接发生在端到端之间,但前面说过仍然会有 8% 的情况完全不能直连。要保证服务的高可用性,还是得考虑部署 TURN 这种复杂而昂贵的中转服务。

其次,Chrome 对每个 Tab 允许连接的终端数有限制,最多 256 个。实际上,在我最新的 Retina Macbook Pro 上,差不多有 10 个连接时,Chrome 就开始变得无比卡,风扇呼呼地转,内存被吃掉 6G,CPU 一直跑满,网络吞吐开始忙不过来,直播服务也开始变得极其不稳定。

所以实际使用方案中,一般还是需要 Media Server 的支持,把「端到多端」变成「端到 Media Server 到多端」的架构。Media Server 可以有更好的性能和带宽,可以自己实现 WebRTC 协议,也就有了支持更多用户的可能。

我找到一个名为 Janus 的 WebRTC Gateway,这个开源项目用 C 语言实现了对 WebRTC 的支持。Janus 自身实现得很简单,提供插件机制来支持不同的业务逻辑,配合官方自带插件就可以用来实现高效的 Media Server 服务。

Janus 官方提供的 Demo 在这里,我也尝试在我的 VPS 上部署了一套。Janus 有个 Streaming 插件,可以接受 GStreamer 推送的音视频流,然后通过 PeerConnection 推送给所有的用户。由于 GStreamer 可以直接读摄像头,也就不用再走 WebRTC 的 MediaStream 获取视频,这样架构就变成了传统的服务器到端了。整个过程比较复杂和曲折,这里不写了,有兴趣的同学可以单独找我讨论。

本文链接:参与评论 »

--EOF--

提醒:本文最后更新于 583 天前,文中所描述的信息可能已发生改变,请谨慎使用。

专题「好玩的 HTML5」的其他文章 »

Comments