原文 https://blog.gmem.cc/research-on-html5-video-surveillance
引言
安防类项目中通常都有视频监控方面的需求。视频监控客户端主要是Native应用的形式,在Web端需要利用NPAPI、ActiveX之类的插件技术实现。
但是,IE式微,Chrome也放弃了NPAPI,另一方面,监控设备硬件厂商的视频输出格式则逐渐标准化。这让基于开放、标准化接口的Web视频监控成为可能。
本文讨论以HTML5及其衍生技术为基础的B/S架构实时视频监控解决方案。主要包括两方面的内容:
- 视频编码、流媒体基础知识,以及相关的库、框架的介绍
- 介绍可以用于视频监控的HTML5特性,例如媒体标签、MSE、WebRTC,以及相关的库、框架
本文仅仅简介若干种备选的解决方案,本站其它文章进行了更加深入的探讨:
音视频编码
音频、视频的编码(Codec,压缩)算法有很多,不同浏览器对音视频的编码算法的支持有差异。H264这样的监控设备常用的视频编码格式,主流浏览器都有某种程度的支持。
常见的音频编码算法包括: MP3, Vorbis, AAC;常见的视频编码算法包括: H.264, HEVC, VP8, VP9。
编码后的音频、视频通常被封装在一个比特流容器格式(container)中,这些格式中常见的有: MP4, FLV, WebM, ASF, ISMA等。
JSMpeg
视频解码工作通常由浏览器本身负责,配合video实现视频播放。
现代浏览器的JS引擎性能较好,因此出现了纯粹由JS实现的解码器JSMpeg,它能够解码视频格式MPEG1、音频格式MP2。支持通过Ajax加载静态视频文件,支持低延迟(小于50ms)的流式播放(通过WebSocket)。JSMpeg包括以下组件:
- MPEG-TS分流器(demuxer)。muxer负责把视频、音频、字幕打包成一种容器格式,demuxer则作相反的工作
- MPEG1视频解码器
- MP2音频解码器
- WebGL渲染器、Canvas2D渲染器
- WebAudio音频输出组件
JSMpeg的优势在于兼容性好,几乎所有现代浏览器都能运行JSMpeg。
性能
JSMpeg不能使用硬件加速。在iPhone 5S这样的设备上,JSMpeg能够处理720p@30fps视频。
比起现代解码器,MPEG1压缩率较低,因而需要更大的带宽。720p的视频大概占用250KB/s的带宽。
示例
下面我们尝试利用ffmpeg编码本地摄像头视频,并通过JSMpeg播放。
创建一个NPM项目,安装依赖:
JavaScript
1 2 | npm install jsmpeg --save npm install ws --save |
JSMpeg提供了一个中继器,能够把基于HTTP的MPEG-TS流转换后通过WebSocket发送给客户端。此脚本需要到Github下载。 下面的命令启动一个中继器:
Shell
1 2 3 4 | node ./app/websocket-relay.js 12345 8800 8801 # Listening for incomming MPEG-TS Stream on http://127.0.0.1:8800/<secret> # Awaiting WebSocket connections on ws://127.0.0.1:8801/ # 实际上在所有网络接口上监听,并非仅仅loopback |
下面的命令捕获本地摄像头(Linux),并编码为MPEG1格式,然后发送到中继器:
Shell
1 2 3 4 5 | # 从摄像头/dev/video0以480的分辨率捕获原始视频流 ffmpeg -s 640x480 -f video4linux2 -i /dev/video0 \ # 输出为原始MPEG-1视频(JSMpeg可用),帧率30fps,比特率800kbps -f mpegts -codec:v mpeg1video -b 800k -r 30 http://127.0.0.1:8800/12345 # 在我的机器上,上述ffmpeg私有内存占用18MB |
上述命令执行后,中继器控制台上打印:
Shell
1 | Stream Connected: ::ffff:127.0.0.1:42399 |
客户端代码:
JavaScript
1 2 3 4 | var player = new JSMpeg.Player( 'ws://127.0.0.1:8801/', { canvas: document.getElementById( 'canvas' ), autoplay: true } ); |
Broadway
Broadway是一个基于JavaScript的H.264解码器,其源码来自于Android的H.264解码器,利用Emscripten转译成了JavaScript,之后利用Google的Closure编译器优化,并针对WebGL进一步优化。
注意:Broadway仅仅支持Baseline这个H.264 Profile。
h264-live-player是基于Broadway实现的播放器,允许通过WebSocket来传输NAL单元(原始H.264帧),并在画布上渲染。我们运行一下它的示例应用:
Shell
1 2 3 | git clone https://github.com/131/h264-live-player.git cd h264-live-player npm install |
因为我的机器是Linux,所以修改h264-live-player/lib/ffmpeg.js, 把ffpmeg的参数改为:
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var args = [ "-f", "video4linux2", "-i", "/dev/video0" , "-framerate", this.options.fps, "-video_size", this.options.width + 'x' + this.options.height, '-pix_fmt', 'yuv420p', '-c:v', 'libx264', '-b:v', '600k', '-bufsize', '600k', '-vprofile', 'baseline', '-tune', 'zerolatency', '-f' ,'rawvideo', '-' ]; |
然后运行 node server-ffmpeg,打开http://127.0.0.1:8080/,可以看到自己摄像头传来的H.264码流,效果还不错。
服务器端技术
ffpmeg
老牌的编解码库,支持很多的音频、视频格式的编解码,支持多种容器格式,支持多种流协议。关于ffpmeg的详细介绍参见Linux命令知识集锦。
ffpmeg除了提供开发套件之外,还有一个同名的命令行工具,直接使用它就可以完成很多编解码、流转换的工作。
类似的库是libav,ffpmeg和它的功能非常相似,特性更多一些。
x264
官网自称是最好的H.264编码器。特性包括:
- 提供一流的性能、压缩比。特别是性能方面,可以在普通PC上并行编码4路或者更多的1080P流
- 提供最好的视频质量,具有最高级的心理视觉优化
- 支持多种不同应用程序所需要的特性,例如电视广播、蓝光低延迟视频应用、Web视频
流媒体技术
有了上面介绍的HTML5标签、合理编码的视频格式,就可以实现简单的监控录像回放了。但是,要进行实时监控画面预览则没有这么简单,必须依赖流媒体技术实现。
流媒体
所谓多媒体(Multimedia)是指多种内容形式 —— 文本、音频、视频、图片、动画等的组合。
所谓流媒体,就是指源源不断的由提供者产生,并持续的被终端用户接收、展示的多媒体,就像水流一样。现实世界中的媒体,有些天生就是流式的,例如电视、广播,另外一些则不是,例如书籍、CD。
流媒体技术(从传递媒体角度来看)可以作为文件下载的替代品。
流媒体技术关注的是如何传递媒体,而不是如何编码媒体,具体的实现就是各种流媒体协议。封装后的媒体比特流(容器格式)由流媒体服务器递送到流媒体客户端。流媒体协议可能对底层容器格式、编码格式有要求,也可能没有任何要求。
直播
直播流(Live streaming)和静态文件播放的关键差异:
- 点播的目标文件通常位于服务器上,具有一定的播放时长、文件大小。浏览器可以使用渐进式下载,一边下载一边播放
- 直播不存在播放起点、终点。它表现为一种流的形式,源源不断的从视频采集源通过服务器,传递到客户端
- 直播流通常是自适应的(adaptive),其码率随着客户端可用带宽的变化,可能变大、变小,以尽可能消除延迟
流媒体技术不但可以用于监控画面预览,也可以改善录像播放的用户体验,比起简单的静态文件回放,流式回放具有以下优势:
- 延迟相对较低,播放能够尽快开始
- 自适应流可以避免卡顿
流协议
主流的用于承载视频流的流媒体协议包括:
协议 | 说明 | ||
HLS | HTTP实时流(HTTP Live Streaming),由苹果开发,基于HTTP协议 HLS的工作原理是,把整个流划分成一个个较小的文件,客户端在建立流媒体会话后,基于HTTP协议下载流片段并播放。客户端可以从多个服务器(源)下载流。 在建立会话时,客户端需要下载extended M3U (m3u8) 播放列表文件,其中包含了MPEG-2 TS(Transport Stream)容器格式的视频的列表。在播放完列表中的文件后,需要再次下载m3u8,如此循环 此协议在移动平台上支持较好,目前的Android、iOS版本都支持 此协议的重要缺点是高延迟(5s以上通常),要做到低延迟会导致频繁的缓冲(下载新片段)并对服务器造成压力,不适合视频监控 播放HLS流的HTML代码片段: XHTML
| ||
RTMP | 实时消息协议(Real Time Messaging Protocol),由Macromedia(Adobe)开发。此协议实时性很好,需要Flash插件才能在客户端使用,但是Adobe已经打算在不久的将来放弃对Flash的支持了 有一个开源项目HTML5 FLV Player,它支持在没有Flash插件的情况下,播放Flash的视频格式FLV。此项目依赖于MSE,支持以下特性:
| ||
RTSP | 实时流协议(Real Time Streaming Protocol),由RealNetworks等公司开发。此协议负责控制通信端点(Endpoint)之间的媒体会话(media sessions) —— 例如播放、暂停、录制。通常需要结合:实时传输协议(Real-time Transport Protocol)、实时控制协议(Real-time Control Protocol)来实现视频流本身的传递 大部分浏览器没有对RTSP提供原生的支持 RTSP 2.0版本目前正在开发中,和旧版本不兼容 | ||
MPEG-DASH | 基于HTTP的动态自适应流(Dynamic Adaptive Streaming over HTTP),它类似于HLS,也是把流切分为很小的片段。DASH为支持为每个片段提供多种码率的版本,以满足不同客户带宽 协议的客户端根据自己的可用带宽,选择尽可能高(避免卡顿、重新缓冲)的码率进行播放,并根据网络状况实时调整码率 DASH不限制编码方式,你可以使用H.265, H.264, VP9等视频编码算法 Chrome 24+、Firefox 32+、Chrome for Android、IE 10+支持此格式 类似于HLS的高延迟问题也存在 | ||
WebRTC | WebRTC是一整套API,为浏览器、移动应用提供实时通信(RealTime Communications)能力。它包含了流媒体协议的功能,但是不是以协议的方式暴露给开发者的 WebRTC支持Chrome 23+、Firefox 22+、Chrome for Android,提供Java / Objective-C绑定 WebRTC主要有三个职责:
WebRTC内置了点对点的支持,也就是说流不一定需要经过服务器中转 |
服务器端技术
视频监控通常都是CS模式(而非P2P),在服务器端,你需要部署流媒体服务。
GStreamer
这是一个开源的跨平台多媒体框架。通过它你可以构建各种各样的媒体处理组件,包括流媒体组件。通过插件机制,GStreamer支持上百种编码格式,包括MPEG-1, MPEG-2, MPEG-4, H.261, H.263, H.264, RealVideo, MP3, WMV, FLV
Kurento、Flumotion是基于GStreamer构建的流媒体服务器软件。
Live555
Live555是流媒体服务开发的基础库,支持 RTP/RTCP/RTSP/SIP等协议,适合在硬件资源受限的情况下使用(例如嵌入式设备)。
基于Live555的软件包括:
- Live555媒体服务器,完整的RTSP服务器
- openRTSP,一个命令行程序,支持提供RTSP流、接收RTSP流、把RTSP流中的媒体录像到磁盘
- playSIP,可以进行VoIP通话
- liveCaster,支持组播的MP3流媒体服务
其它
流媒体服务实现有很多,它们中的一些在最初针对特定的流协议,大部分都走向多元化。例如,Red5是一个RTMP流媒体服务器,Wowza是一个综合的流媒体服务器,支持WebRTC的流媒体服务在后面的章节介绍。
HTML5媒体标签
HTML5支持 <audio>和 <video>标签(两者都对应了HTMLMediaElement的子类型)以实现视频、音频的播放。
<audio>
此标签用于在浏览器中创建一个纯音频播放器。播放静态文件的示例:
XML
1 2 3 4 5 6 7 | <audio controls preload="auto"> <source src="song.mp3" type="audio/mpeg"> <!-- 备选格式,如果浏览器不支持mp3 --> <source src="song.ogg" type="audio/ogg"> <!-- 如果浏览器不支持audio标签,显示下面的连接 --> <a href="audiofile.mp3">download audio</a> </audio> |
<video>
此标签用于在浏览器中创建一个视频播放器。播放静态文件的示例:
XHTML
1 2 3 4 5 6 7 8 9 10 | <!-- poster指定预览图,autoplay自动播放,muted静音 --> <video controls width="640" height="480" poster="movie.png" autoplay muted> <source src="movie.mp4" type="video/mp4"> <!-- 备选格式,如果浏览器不支持mp4 --> <source src="movie.webm" type="video/webm"> <!-- 可以附带字幕 --> <track src="subtitles_en.vtt" kind="subtitles" srclang="en" label="English"> <!-- 如果浏览器不支持video标签,显示下面的连接 --> <a href="videofile.mp4">download video</a> </video> |
<canvas>
在画布中,你可以进行任意的图形绘制,当然可以去逐帧渲染视频内容。
编程方式创建
音频、视频播放器标签也可以利用JavaScript编程式的创建,示例代码:
JavaScript
1 2 3 4 5 6 7 8 9 | var video = document.createElement( 'video' ); if ( video.canPlayType( 'video/mp4' ) ) { video.setAttribute( 'src', 'movie.mp4' ); } else if ( video.canPlayType( 'video/webm' ) ) { video.setAttribute( 'src', 'movie.webm' ); } video.width = 640; video.height = 480; |
MSE
媒体源扩展(Media Source Extensions,MSE)是一个W3C草案,桌面浏览器对MSE的支持较好。MSE扩展流video/audio元素的能力,允许你通过JavaScript来生成(例如从服务器抓取)媒体流供video/audio元素播放。使用MSE你可以:
- 通过JavaScript来构建媒体流,不管媒体是如何捕获的
- 处理自适应码流、广告插入、时间平移(time-shifting,回看)、视频编辑等应用场景
- 最小化JavaScript中处理媒体解析的代码
MSE定义支持的(你生成的)媒体格式,只有符合要求的容器格式、编码格式才能被MSE处理。通常容器格式是ISO BMFF(MP4),也就是说你需要生成MP4的片断,然后Feed给MSE进行播放。
MediaSource对象作为video/audio元素的媒体来源,它可以具有多个SourceBuffer对象。应用程序把数据片段(segment)附加到SourceBuffer中,并可以根据系统性能对数据片段的质量进行适配。SourceBuffer中包含多个track buffer —— 分别对应音频、视频、文本等可播放数据。这些数据被音频、视频解码器解码,然后在屏幕上显示、在扬声器中播放:
要把MediaSource提供给video/audio播放,调用:
JavaScript
1 | video.src = URL.createObjectURL(mediaSource); |
基于MSE的框架
wfs
wfs是一个播放原始H.264帧的HTML5播放器,它的工作方式是把H.264 NAL单元封装为 ISO BMFF(MP4)片,然后Feed给MSE处理。
flv.js
flv.js是一个HTML5 Flash视频播放器,基于纯JS,不需要Flash插件的支持。此播放器将FLV流转换为ISO BMFF(MP4)片断,然后把MP4片断提供给video元素使用。
flv.js支持Chrome 43+, FireFox 42+, Edge 15.15048+以上版本的直播流 。
Streamedian
Streamedian是一个HTML5的RTSP播放器。实现了RTSP客户端功能,你可以利用此框架直接播放RTSP直播流。此播放器把RTP协议下的H264/AAC在转换为ISO BMFF供video元素使用。Streamedian支持Chrome 23+, FireFox 42+, Edge 13+,以及Android 5.0+。不支持iOS和IE。
在服务器端,你需要安装Streamedian提供的代理(此代理收费),此代理将RTSP转换为WebSocket。Streamedian处理视频流的流程如下:
WebRTC
WebRTC是一整套API,其中一部分供Web开发者使用,另外一部分属于要求浏览器厂商实现的接口规范。WebRTC解决诸如客户端流媒体发送、点对点通信、视频编码等问题。桌面浏览器对WebRTC的支持较好,WebRTC也很容易和Native应用集成。
使用MSE时,你需要自己构建视频流。使用WebRTC时则可以直接捕获客户端视频流。
使用WebRTC时,大部分情况下流量不需要依赖于服务器中转,服务器的作用主要是:
- 在信号处理时,转发客户端的数据
- 配合实现NAT/防火墙穿透
- 在点对点通信失败时,作为中继器使用
架构
流捕获
捕获视频
主要是捕获客户端摄像头、麦克风。在视频监控领域用处不大,这里大概了解一下。流捕获通过navigator.getUserMedia调用实现:
XHTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <script type="text/javascript"> navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.getUserMedia; var success = function ( stream ) { var video = document.getElementById( 'camrea' ); // 把MediaStream对象转换为Blob URL,提供给video播放 video.src = URL.createObjectURL( stream ); video.play(); } var error = function ( err ) { console.log( err ) } // 调用成功后,得到MediaStream对象 navigator.getUserMedia( { video: true, audio: true }, success, error ); </script> <video id="camrea" width="640" height="480"/> |
三个调用参数分别是:
- 约束条件,你可以指定媒体类型、分辨率、帧率
- 成功后的回调,你可以在回调中解析出URL提供给video元素播放
- 失败后的回调
捕获音频
捕获音频类似:
JavaScript
1 2 3 4 5 6 7 8 9 | navigator.getUserMedia( { audio: true }, function ( stream ) { var audioContext = new AudioContext();
// 从捕获的音频流创建一个媒体源管理 var streamSource = audioContext.createMediaStreamSource( stream );
// 把媒体源连接到目标(默认是扬声器) streamSource.connect( audioContext.destination ); }, error ); |
MediaStream
MediaStream对象提供以下方法:
- getAudioTracks(),音轨列表
- getVideoTracks(),视轨列表
每个音轨、视轨都有个label属性,对应其设备名称。
Camera.js
Camera.js是对getUserMedia的简单封装,简化了API并提供了跨浏览器支持:
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | camera.init( { width: 640, height: 480, fps: 30, // 帧率 mirror: false, // 是否显示为镜像 targetCanvas: document.getElementById( 'webcam' ), // 默认null,如果设置了则在画布中渲染
onFrame: function ( canvas ) { // 每当新的帧被捕获,调用此回调 },
onSuccess: function () { // 流成功获取后 },
onError: function ( error ) { // 如果初始化失败 },
onNotSupported: function () { // 当浏览器不支持camera.js时 } } ); // 暂停 camera.pause(); // 恢复 camera.start(); |
掠食者视觉是基于Camera实现的一个好玩的例子(移动侦测)。
信号处理
在端点之间(Peer)发送流之前,需要进行通信协调、发送控制消息,即所谓信号处理(Signaling),信号处理牵涉到三类信息:
- 会话控制信息:初始化、关闭通信,报告错误
- 网络配置:对于其它端点来说,本机的IP和端口是什么
- 媒体特性:本机能够处理什么音视频编码、多高的分辨率。本机发送什么样的音视频编码
WebRTC没有对信号处理规定太多,我们可以通过Ajax/WebSocket通信,以SIP、Jingle、ISUP等协议完成信号处理。点对点连接设立后,流的传输并不需要服务器介入。信号处理的示意图如下:
示例代码
下面的代表片段包含了一个视频电话的信号处理过程:
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | // 信号处理通道,底层传输方式和协议自定义 var signalingChannel = createSignalingChannel(); var conn;
// 信号通过此回调送达本地,可能分多次送达 signalingChannel.onmessage = function ( evt ) { if ( !conn ) start( false );
var signal = JSON.parse( evt.data ); // 会话描述协议(Session Description Protocol),用于交换媒体配置信息(分辨率、编解码能力) if ( signal.sdp ) // 设置Peer的RTCSessionDescription conn.setRemoteDescription( new RTCSessionDescription( signal.sdp ) ); else // 添加Peer的Candidate信息 conn.addIceCandidate( new RTCIceCandidate( signal.candidate ) ); };
// 调用此方法启动WebRTC,获取本地流并显示,侦听连接上的事件并处理 function start( isCaller ) { conn = new RTCPeerConnection( { /**/ } );
// 把地址/端口信息发送给其它Peer。所谓Candidate就是基于ICE框架获得的本机可用地址/端口 conn.onicecandidate = function ( evt ) { signalingChannel.send( JSON.stringify( { "candidate": evt.candidate } ) ); };
// 当远程流到达后,在remoteView元素中显示 conn.onaddstream = function ( evt ) { remoteView.src = URL.createObjectURL( evt.stream ); };
// 获得本地流 navigator.getUserMedia( { "audio": true, "video": true }, function ( stream ) { // 在remoteView元素中显示 localView.src = URL.createObjectURL( stream ); // 添加本地流,Peer将接收到onaddstream事件 conn.addStream( stream );
if ( isCaller ) // 获得本地的RTCSessionDescription conn.createOffer( gotDescription ); else // 针对Peer的RTCSessionDescription生成兼容的本地SDP conn.createAnswer( conn.remoteDescription, gotDescription );
function gotDescription( desc ) { // 设置自己的RTCSessionDescription conn.setLocalDescription( desc ); // 把自己的RTCSessionDescription发送给Peer signalingChannel.send( JSON.stringify( { "sdp": desc } ) ); } } ); }
// 通信发起方调用: start( true ); |
流转发
主要牵涉到的接口是RTCPeerConnection,上面的例子中已经包含了此接口的用法。WebRTC在底层做很多复杂的工作,这些工作对于JavaScript来说是透明的:
- 执行解码
- 屏蔽丢包的影响
- 点对点通信:WebRTC引入流交互式连接建立(Interactive Connectivity Establishment,ICE)框架。ICE负责建立点对点链路的建立:
- 首先尝试直接
- 不行的话尝试STUN(Session Traversal Utilities for NAT)协议。此协议通过一个简单的保活机制确保NAT端口映射在会话期间有效
- 仍然不行尝试TURN(Traversal Using Relays around NAT)协议。此协议依赖于部署在公网上的中继服务器。只要端点可以访问TURN服务器就可以建立连接
- 通信安全
- 带宽适配
- 噪声抑制
- 动态抖动缓冲(dynamic jitter buffering),抖动是由于网络状况的变化,缓冲用于收集、存储数据,定期发送
任意数据交换
通过RTCDataChannel完成,允许点对点之间任意的数据交换。RTCPeerConnection连接创建后,不但可以传输音视频流,还可以打开多个信道(RTCDataChannel)进行任意数据的交换。RTCDataChanel的特点是:
- 类似于WebSocket的API
- 支持带优先级的多通道
- 超低延迟,因为不需要通过服务器中转
- 支持可靠/不可靠传输语义。支持SCTP、DTLS、UDP几种传输协议
- 内置安全传输(DTLS)
- 内置拥塞控制
使用RTCDataChannel可以很好的支持游戏、远程桌面、实时文本聊天、文件传输、去中心化网络等业务场景。
adapter.js
WebRTC adapter是一个垫片库,使用它开发WebRTC应用时,不需要考虑不同浏览器厂商的API前缀差异。
WebRTC示例
本节列出一些WebRTC的代码示例,这些例子都使用adapter.js。
限定分辨率
JavaScript
1 2 3 4 5 6 7 8 | // 指定分辨率 // adapter.js 支持Promise navigator.mediaDevices.getUserMedia( { video: { width: { exact: 640 }, height: { exact: 480 } } } ).then( stream => { let video = document.createElement( 'video' ); document.body.appendChild( video ); video.srcObject = stream; video.play(); } ).catch( err => console.log( err ) ); |
在画布中截图
JavaScript
1 2 | // video为video元素 canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); |
WebRTC框架
框架 | 说明 |
PeerJS | 简化WebRTC的点对点通信、视频、音频调用 提供云端的PeerServer,你也可以自己搭建服务器 |
Sharefest | 基于Web的P2P文件共享 |
webRTC.io | WebRTC的一个抽象层,同时提供了客户端、服务器端Node.js组件。服务器端组件抽象了STUN 类似的框架还有SimpleWebRTC、easyrtc |
OpenWebRTC | 允许你构建能够和遵循WebRTC标准的浏览器进行通信的Native应用程序,支持Java绑定 |
NextRTC | 基于Java实现的WebRTC信号处理服务器 |
Janus | 这是一个WebRTC网关,纯服务器端组件,目前仅仅支持Linux环境下安装 Janus本身实现了到浏览器的WebRTC连接机制,支持以JSON格式交换数据,支持在服务器端应用逻辑 - 浏览器之间中继RTP/RTCP和消息。特殊化的功能有服务器端插件完成 |
Kurento | 这是一个开源的WebRTC媒体服务器 |
备选方案一:从RTSP开始
我们首先尝试的方案是直接使用RTSP源,原因是海康、大华主流厂商的较新的IP摄像头均支持暴露标准化的RTSP流。
尝试播放
使用VLC播放器,打开网络串流:rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream,视频源为公司门口的海康摄像头的主码流(main,子码流为sub)。
发现可以正常播放,说明视频格式应该是标准的。VLC菜单 Tool ⇨ Codec Info查看,编码格式为H264。
浏览器无法直接使用RTSP协议,因此,需要有服务器端来处理视频源的RTSP,将其转换为:
- 通过WebSocket发送的视频片断,由客户端的:
- JSMpeg/Broadway直接解码,渲染到画布
- 或者,构造MP4片断Feed给MSE播放
- 或者,通过WebRTC网关,转换后提供给客户端的WebRTC代码处理
- 或者,使用浏览器插件机制,例如Chrome的NaCl
实现方式一:MSE
Streamedian的服务器端需要授权,我们选用了另外一个实现。
H5S是一个基于live555实现的开源的HTML5 RTSP网关,支持将RTSP/H264流输入转换为HTML5 MSE支持的H264,客户端基于MSE。
服务器
尝试在容器中运行H5S:
Shell
1 2 3 4 5 6 7 8 9 10 11 12 | docker create --name ubuntu-16.04 -h ubuntu-16 --network local --dns 172.21.0.1 --ip 172.21.0.6 -it docker.gmem.cc/ubuntu:16.04 bash docker start ubuntu-16.04 docker exec -it ubuntu-16.04 bash
apt update && apt install wget wget https://raw.githubusercontent.com/veyesys/release/master/h5stream/H5S-r1.0.1128.16-Ubuntu-16.04-64bit.tar.gz tar xzf H5S-r1.0.1128.16-Ubuntu-16.04-64bit.tar.gz && mv H5S-r1.0.1128.16-Ubuntu-16.04-64bit h5s-1.0
cd h5s-1.0 export LD_LIBRARY_PATH=`pwd`/lib/:$LD_LIBRARY_PATH # 指定两次密码,可能H5S存在bug,不这样报身份验证失败 ./h5ss rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream admin 12345 |
客户端
使用H5S自带的基于MSE的客户端代码 + Chrome 49,播放后发现画面静止。控制它查看发现解码错误。打开chrome://media-internals/,发现错误Media segment did not begin with key frame. Support for such segments will be available in a future version。看样子是提供给SourceBuffer的数据不是以关键帧开始导致,未来版本的Chrome可能取消此限制。
换成Chrome 50,可以正常播放,但是流畅度较差,播放一段时间后出现卡死的情况。
小结
H5S实现不完善,在不修改源码的情况下,服务器端只能接入一路视频输入。客户端也存在不流畅、卡死的问题,不适合生产环境。
实现方式二:JSMpeg
转码进程
在上文中我们已经成功尝试了利用JSMpege + WebSocket的方式,在网页中显示摄像头捕获的视频。ffmpeg转换RTSP也是非常简单的:
Shell
1 | ffmpeg -i rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream -s 427x240 -f mpegts -vcodec mpeg1video -b 800k -r 30 http://127.0.0.1:8800/12345 |
服务器
可以使用JSMpeg自带的简单Node.js服务器测试:
Shell
1 | node ./app/websocket-relay.js 12345 8800 8801 |
客户端
下面是客户端代码,默认JSMpeg会基于WebGL渲染,但是我的机器最多开到8画面,开9画面时出现警告:
Too many active WebGL contexts. Oldest context will be lost,且第一画面丢失,简单的通融方法是,第9画面使用Canvas2D渲染:
JavaScript
1 2 3 4 5 6 | new JSMpeg.Player( 'ws://127.0.0.1:8801/', { canvas: document.getElementById( 'canvas9' ), autoplay: true, // 浏览器对WebGL context的数量有限制 disableGl: true } ); |
渲染截图:
小结
这种方式客户端解码压力较大,同时开9画面的352x288视频,我的机器上CPU占用率大概到40%左右,画面变化较为剧烈的时候会出现卡顿现象。
实现方式三:Broadway
与JSMpeg类似,Broadway也是JavaScript解码工具。关键之处是,Broadway支持的视频编码是H.264,意味着可能免去消耗服务器资源的视频重编码。
最初的尝试并不顺利,根据IP摄像头的RTSP Describe应答(SDP),我们推断其H.264 Profile为Baseline,但是不转码的情况下Broadway根本无法播放。后来查看ffmpeg的日志输出,发现其实际上使用的Profile是Main。进一步尝试,发现摄像头是可以配置为Baseline的:
只需要把编码复杂度设置为低,H.264的Profile就从Main变为Baseline。
设置完毕后,仍然基于h264-live-player的Demo进行测试,使用如下命令行抽取原始H.264帧:
Shell
1 | ffmpeg -i rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream -c:v copy -f rawvideo - |
即可免转码的进行实时视频预览了。
此实现方式更多细节信息请参考基于Broadway的HTML5视频监控。
实现方式四:NaCl
Chrome放弃NPAPI之后,插件开发需要使用PPAPI /NaCl。目前能找到的实现有VXG Chrome Plugin,这是一个商业产品,需要授权。除了RTSP之外,还支持RTMP、HLS等协议。
插件方案的缺点是,需要安装,而且仅仅针对单种浏览器。优势则是灵活性高,理论上性能可以做的很好。
实现方式五:WebRTC
WebRTC相关的框架非常多,经过简单的比较,我们决定从Kurento入手。主要原因是:
- 容易扩展的模块化设计
- 提供Java客户端、JS客户端
- 可以在服务器端合成多画面,这样可以减轻客户端解码压力,特别是那些低配置的客户端
- 内置对RTSP协议的支持
基于Kurento搭建WebRTC服务器一文详细讨论了这种实现方式。
备选方案二:从设备SDK开始
这里的设备,主要包括:网络硬盘录像机(NVR)、视频服务器、IP摄像头。为了便于二次开发,硬件厂商都为这些设备配置的相应的SDK套件。这些SDK通常都提供了:实时码流预览、录像文件回放、播放控制(如:暂停、单帧前进、单帧后退)、获取码流基本信息、播放截图等功能。
我们的基本目标是,通过SDK得到标准化的码流,例如H264格式。具体如何操作,得看厂商的SDK,但是思路基本是:
- 如果SDK直接支持获取标准格式的流,例如RTSP,那么备选方案一就可以直接用上
- 如果SDK支持获取标准编码的视频帧,例如H264,那我们只需要将其包装为合适的容器格式,再通过RTSP/HTTP的方式发送出去
- 如果SDK支持获取解码后的原始图像数据,例如RGB、YV12,我们可以基于H264再次编码,然后按第2步方式处理。这种方式对服务器性能要求比较高,CPU压力较大,PC机处理不了多少个通道
- 如果都不支持,只提供了封装好的播放控件 —— 这个就比较悲催了,不过通过OS底层API,例如Windows的GDI应该也是可以实现,否则那些屏幕录像软件怎么做的呢?
海康SDK
根据Linux版本的海康设备网络编程指南的描述,我们应该可以:
- 调用NET_DVR_Init进行SDK初始化
- 调用NET_DVR_Login登陆到目标设备
- 调用NET_DVR_RealPlay进行播放,此时返回一个实时播放句柄
- 如果设备支持RTSP协议取流:针对上述句柄调用NET_DVR_SetStandardDataCallBack,可以设置一个标准的数据回调函数,此回调会接受到标准码流,这对应上面的第1种思路
- 如果设备不支持RTSP协议取流:针对上述句柄调用NET_DVR_SetRealDataCallBack,然后通过PlayM4播放库中的PlayM4_SetDecCallBack回调得到yv12格式的原始图像。这对应上面的第3种思路
示例代码
cmake构建配置:
CMakeLists.txt
1 2 3 4 5 6 7 8 | cmake_minimum_required(VERSION 3.6) project(hikvision)
include_directories(/home/alex/CPP/lib/hcnedsdk/include)
set(SOURCE_FILES getstream.cpp) add_executable(getstream ${SOURCE_FILES}) target_link_libraries(getstream /home/alex/CPP/lib/hcnedsdk/lib/libhcnetsdk.so) |
C++代码:
getstream.cpp
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | #include <HCNetSDK.h> #include <stdio.h> #include <cstring> #include <unistd.h>
// RTSP协议取流 void CALLBACK cbStdData( LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, DWORD dwUser ) { switch ( dwDataType ) { case NET_DVR_SYSHEAD: // 系统头数据,回调的第一个包是40字节的文件头 break; case NET_DVR_STREAMDATA: // 基于私有协议时:视频流数据(包括复合流和音视频分开的视频流数据) break; case NET_DVR_STD_VIDEODATA: // 基于标准协议时:标准视频流数据(RTP包) break; case NET_DVR_STD_AUDIODATA: // 基于标准协议时:标准音频流数据 break; case NET_DVR_SDP: // SDP信息(RTSP传输时有效) break; case NET_DVR_PRIVATE_DATA: // 私有数据,包括智能信息叠加等 break; } }
int main() { // SDK初始化 BOOL result = NET_DVR_Init(); if ( !result ) return 1;
// 同步登陆 NET_DVR_USER_LOGIN_INFO struLoginInfo = { 0 }; struLoginInfo.bUseAsynLogin = 0; strcpy( struLoginInfo.sDeviceAddress, "192.168.0.196" ); struLoginInfo.wPort = 8000; strcpy( struLoginInfo.sUserName, "admin" ); strcpy( struLoginInfo.sPassword, "12345" ); NET_DVR_DEVICEINFO_V40 struDevInfo = { 0 }; LPNET_DVR_DEVICEINFO_V30 lpDevInfo30; long lUserID = NET_DVR_Login_V40( &struLoginInfo, &struDevInfo ); if ( lUserID < 0 ) { printf( "登陆失败,错误码 %d\n", NET_DVR_GetLastError()); NET_DVR_Cleanup(); return 1; } else { lpDevInfo30 = &struDevInfo.struDeviceV30; printf( "成功登陆到设备:%s\n", lpDevInfo30->sSerialNumber ); printf( "SDK字符串编码方式(1 GB2312,2 GBK,3 BIG5,6 UTF-8):%d\n", struDevInfo.byCharEncodeType ); printf( "设备类型(31 高清网络摄像机):%d\n", lpDevInfo30->wDevType ); printf( "模拟通道起始号:%d,模拟通道个数%d,数字通道起始号:%d,数字通道个数%d\n", lpDevInfo30->byStartChan, lpDevInfo30->byChanNum, lpDevInfo30->byStartDChan, lpDevInfo30->byIPChanNum + lpDevInfo30->byHighDChanNum << 8 ); printf( "主码流是否支持RTSP方式:%s,子码流是否支持RTSP方式:%s\n", lpDevInfo30->byMainProto > 0 ? "是" : "否", lpDevInfo30->bySubProto > 0 ? "是" : "否" ); }
// 启动预览 NET_DVR_PREVIEWINFO struPrevInfo = { 0 }; struPrevInfo.hPlayWnd = NULL; // Linux 64 位系统不支持软解码功能 struPrevInfo.lChannel = 1; // 预览通道号 struPrevInfo.dwStreamType = 0; // 0-主码流, 1-子码流, 2-码流 3, 3-码流 4,以此类推 struPrevInfo.dwLinkMode = 0; // 0- TCP 方式, 1- UDP 方式, 2- 组播方式, 3- RTP 方式, 4-RTP/RTSP, 5-RSTP/HTTP struPrevInfo.bBlocked = 1; // 0- 非阻塞取流, 1- 阻塞取流 struPrevInfo.byProtoType = 1; // 应用层取流协议使用RTSP LONG lRealHandle = NET_DVR_RealPlay_V40( lUserID, &struPrevInfo, NULL, NULL ); if ( lRealHandle == -1 ) { printf( "启动预览失败,错误码 %d\n", NET_DVR_GetLastError()); NET_DVR_Logout( lUserID ); NET_DVR_Cleanup(); return 1; }
if ( lpDevInfo30->byMainProto ) { printf( "设置获取标准码流的回调\n" ); // 仅支持对 支持RTSP协议取流的设备的 标准码流回调 NET_DVR_SetStandardDataCallBack( lRealHandle, cbStdData, NULL ); }
sleep( 120 ); // 停止预览 NET_DVR_StopRealPlay( lRealHandle ); // 登出 NET_DVR_Logout( lUserID ); // SDK清理 NET_DVR_Cleanup(); return 0; } |
运行脚本:
Shell
1 2 3 | export HKLIB_HOME=/home/alex/CPP/lib/hcnedsdk/lib export LD_LIBRARY_PATH=$HKLIB_HOME:$HKLIB_HOME/HCNetSDKCom ./getstream |
此程序运行后,会自动获取到基于RTSP协议的媒体流,回调函数会反复被调用:
- 第一次调用为40字节的头,不太清楚有什么用
- 第二次调用传递了SDP
- 后续调用传递标准音视频数据,其内容是RTP封包
总结
基于HTM5的视频监控,媒体流从采集设备到浏览器,主要路径如下图所示:
对上图的说明如下:
- 在设备层,需要以某种方式获得码流,以流协议的方式发送出去。最常用的方式是RTSP/RTP。流的可能获取路径为:
- 设备直接暴露RTSP协议端点,并且发送标准码流
- 设备SDK允许获取标准码流,需要自己以RTSP协议发送
- 设备SDK允许获得解码后的逐帧,需要直接编码为H264,然后以RTSP发送
- 流媒体层通常需要引入专门的流媒体服务器,这类服务器能够在内部进行各种流协议的转换,可以解除客户端对特定流协议的依赖
- 客户端和服务器端的传输方式,可以有TCP、HTTP、P2P(WebRTC)、WebSocket等多种。其中
- 直接的TCP协议浏览器是不支持的,这意味着RTSP/RTMP等协议,在浏览器端必须要有插件才可以使用
- WebSocket通常配合JSMpeg或者MSE使用,由程序向JSMpeg/MSE不断Feed视频帧
- 客户端解码展示的技术主要有三类:
- 浏览器内置的解码能力,主要通过video标签,MSE属于此类
- JavaScript软解码,主要是JSMpeg、Broadway
- 插件机制,例如Chrome的NaCl
能够免于引入流媒体层的方案,需要:设备能直接暴露标准码流的RTSP端点,并且安装浏览器插件。缺点也很明显,一个是设备的访问密码暴露给了客户端,第二个是目前没有成熟、开源的插件可用。我相信主要原因是合理技术方向不在于此,没人愿意去开发。
直接使用设备层的RTSP端点,可能存在兼容性问题。一个是它发送的码流是否标准化,第二个是市场上有多少设备没有暴露RTSP端点。
客户端方面,JSMpeg是兼容性较好的方案,WebRTC/MSE都有部分平台不支持(但是桌面级的浏览器大部分支持)。JSMpeg的缺点是:
- 如果基于WebGL渲染,受限于浏览器WebGL上下文最大数量,多画面可能无法渲染。某些流媒体服务器支持在服务器端合成多画面Grid,可以规避此缺点
- 如果基于Canvas2D渲染,画质较差(我的机器上还有莫名其妙的斜线)
- 对码流格式要求严格,仅仅支持MPEG-TS,此格式压缩比差,网络带宽占用大
- 性能相对较差,尽管使用了MPEG-TS这种简单的视频格式,基于JavaScript解码渲染仍然使客户端压力较大。我的机器(i7-4940MX / Quadro K5100M / Ubuntu 14.04 LTS)上会出现卡顿情况
和JSMpeg类似的库是Broadway,后者能够进行Baseline的H.264解码。如果设备支持Baseline H.264输出,使用Broadway可以很好的解决服务器端转码导致的资源消耗问题。
附录
参考资料