HTML5视频监控技术预研

原文 https://blog.gmem.cc/research-on-html5-video-surveillance

 

引言

安防类项目中通常都有视频监控方面的需求。视频监控客户端主要是Native应用的形式,在Web端需要利用NPAPI、ActiveX之类的插件技术实现。

但是,IE式微,Chrome也放弃了NPAPI,另一方面,监控设备硬件厂商的视频输出格式则逐渐标准化。这让基于开放、标准化接口的Web视频监控成为可能。

本文讨论以HTML5及其衍生技术为基础的B/S架构实时视频监控解决方案。主要包括两方面的内容:

  1. 视频编码、流媒体基础知识,以及相关的库、框架的介绍
  2. 介绍可以用于视频监控的HTML5特性,例如媒体标签、MSE、WebRTC,以及相关的库、框架

本文仅仅简介若干种备选的解决方案,本站其它文章进行了更加深入的探讨:

  1. H.264学习笔记
  2. 实时通信协议族
  3. 基于Kurento搭建WebRTC服务器
  4. 基于Broadway的HTML5视频监控

音视频编码

音频、视频的编码(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包括以下组件:

  1. MPEG-TS分流器(demuxer)。muxer负责把视频、音频、字幕打包成一种容器格式,demuxer则作相反的工作
  2. MPEG1视频解码器
  3. MP2音频解码器
  4. WebGL渲染器、Canvas2D渲染器
  5. 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/

# 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编码器。特性包括:

  1. 提供一流的性能、压缩比。特别是性能方面,可以在普通PC上并行编码4路或者更多的1080P流
  2. 提供最好的视频质量,具有最高级的心理视觉优化
  3. 支持多种不同应用程序所需要的特性,例如电视广播、蓝光低延迟视频应用、Web视频

流媒体技术

有了上面介绍的HTML5标签、合理编码的视频格式,就可以实现简单的监控录像回放了。但是,要进行实时监控画面预览则没有这么简单,必须依赖流媒体技术实现。

流媒体

所谓多媒体(Multimedia)是指多种内容形式 —— 文本、音频、视频、图片、动画等的组合。

所谓流媒体,就是指源源不断的由提供者产生,并持续的被终端用户接收、展示的多媒体,就像水流一样。现实世界中的媒体,有些天生就是流式的,例如电视、广播,另外一些则不是,例如书籍、CD。

流媒体技术(从传递媒体角度来看)可以作为文件下载的替代品。

流媒体技术关注的是如何传递媒体,而不是如何编码媒体,具体的实现就是各种流媒体协议。封装后的媒体比特流(容器格式)由流媒体服务器递送到流媒体客户端。流媒体协议可能对底层容器格式、编码格式有要求,也可能没有任何要求。

直播

直播流(Live streaming)和静态文件播放的关键差异:

  1. 点播的目标文件通常位于服务器上,具有一定的播放时长、文件大小。浏览器可以使用渐进式下载,一边下载一边播放
  2. 直播不存在播放起点、终点。它表现为一种流的形式,源源不断的从视频采集源通过服务器,传递到客户端
  3. 直播流通常是自适应的(adaptive),其码率随着客户端可用带宽的变化,可能变大、变小,以尽可能消除延迟

流媒体技术不但可以用于监控画面预览,也可以改善录像播放的用户体验,比起简单的静态文件回放,流式回放具有以下优势:

  1. 延迟相对较低,播放能够尽快开始
  2. 自适应流可以避免卡顿

流协议

主流的用于承载视频流的流媒体协议包括:

协议 说明
HLS

HTTP实时流(HTTP Live Streaming),由苹果开发,基于HTTP协议

HLS的工作原理是,把整个流划分成一个个较小的文件,客户端在建立流媒体会话后,基于HTTP协议下载流片段并播放。客户端可以从多个服务器(源)下载流。

在建立会话时,客户端需要下载extended M3U (m3u8) 播放列表文件,其中包含了MPEG-2 TS(Transport Stream)容器格式的视频的列表。在播放完列表中的文件后,需要再次下载m3u8,如此循环

此协议在移动平台上支持较好,目前的Android、iOS版本都支持

此协议的重要缺点是高延迟(5s以上通常),要做到低延迟会导致频繁的缓冲(下载新片段)并对服务器造成压力,不适合视频监控

播放HLS流的HTML代码片段:

XHTML

1

RTMP

实时消息协议(Real Time Messaging Protocol),由Macromedia(Adobe)开发。此协议实时性很好,需要Flash插件才能在客户端使用,但是Adobe已经打算在不久的将来放弃对Flash的支持了

有一个开源项目HTML5 FLV Player,它支持在没有Flash插件的情况下,播放Flash的视频格式FLV。此项目依赖于MSE,支持以下特性:

  1. 支持H.264 + AAC/MP3编码的FLV容器格式的播放
  2. 分段(segmented)视频播放
  3. 基于HTTP的FLV低延迟实时流播放
  4. 兼容主流浏览器
  5. 资源占用低,可以使用客户端的硬件加速
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主要有三个职责:

  1. 捕获客户端音视频,对应接口MediaStream(也就是getUserMedia)
  2. 音视频传输,对应接口RTCPeerConnection
  3. 任意数据传输,对应接口RTCDataChannel

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的软件包括:

  1. Live555媒体服务器,完整的RTSP服务器
  2. openRTSP,一个命令行程序,支持提供RTSP流、接收RTSP流、把RTSP流中的媒体录像到磁盘
  3. playSIP,可以进行VoIP通话
  4. liveCaster,支持组播的MP3流媒体服务

其它

流媒体服务实现有很多,它们中的一些在最初针对特定的流协议,大部分都走向多元化。例如,Red5是一个RTMP流媒体服务器,Wowza是一个综合的流媒体服务器,支持WebRTC的流媒体服务在后面的章节介绍。

HTML5媒体标签

HTML5支持 

此标签用于在浏览器中创建一个纯音频播放器。播放静态文件的示例:

XML

1

2

3

4

5

6

7

    

    

    

    

    download audio

此标签用于在浏览器中创建一个视频播放器。播放静态文件的示例:

XHTML

1

2

3

4

5

6

7

8

9

10

  

  

  

  

  

  

  download video

在画布中,你可以进行任意的图形绘制,当然可以去逐帧渲染视频内容。

编程方式创建

音频、视频播放器标签也可以利用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你可以:

  1. 通过JavaScript来构建媒体流,不管媒体是如何捕获的
  2. 处理自适应码流、广告插入、时间平移(time-shifting,回看)、视频编辑等应用场景
  3. 最小化JavaScript中处理媒体解析的代码

MSE定义支持的(你生成的)媒体格式,只有符合要求的容器格式、编码格式才能被MSE处理。通常容器格式是ISO BMFF(MP4),也就是说你需要生成MP4的片断,然后Feed给MSE进行播放。

MediaSource对象作为video/audio元素的媒体来源,它可以具有多个SourceBuffer对象。应用程序把数据片段(segment)附加到SourceBuffer中,并可以根据系统性能对数据片段的质量进行适配。SourceBuffer中包含多个track buffer —— 分别对应音频、视频、文本等可播放数据。这些数据被音频、视频解码器解码,然后在屏幕上显示、在扬声器中播放:

 HTML5视频监控技术预研_第1张图片

要把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处理视频流的流程如下:HTML5视频监控技术预研_第2张图片

WebRTC

WebRTC是一整套API,其中一部分供Web开发者使用,另外一部分属于要求浏览器厂商实现的接口规范。WebRTC解决诸如客户端流媒体发送、点对点通信、视频编码等问题。桌面浏览器对WebRTC的支持较好,WebRTC也很容易和Native应用集成。

使用MSE时,你需要自己构建视频流。使用WebRTC时则可以直接捕获客户端视频流。

使用WebRTC时,大部分情况下流量不需要依赖于服务器中转,服务器的作用主要是:

  1. 在信号处理时,转发客户端的数据
  2. 配合实现NAT/防火墙穿透
  3. 在点对点通信失败时,作为中继器使用

架构

HTML5视频监控技术预研_第3张图片

流捕获

捕获视频

主要是捕获客户端摄像头、麦克风。在视频监控领域用处不大,这里大概了解一下。流捕获通过navigator.getUserMedia调用实现: 

XHTML

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

三个调用参数分别是:

  1. 约束条件,你可以指定媒体类型、分辨率、帧率 
  2. 成功后的回调,你可以在回调中解析出URL提供给video元素播放
  3. 失败后的回调

捕获音频

捕获音频类似:

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对象提供以下方法:

  1. getAudioTracks(),音轨列表
  2. 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),信号处理牵涉到三类信息:

  1. 会话控制信息:初始化、关闭通信,报告错误
  2. 网络配置:对于其它端点来说,本机的IP和端口是什么
  3. 媒体特性:本机能够处理什么音视频编码、多高的分辨率。本机发送什么样的音视频编码

WebRTC没有对信号处理规定太多,我们可以通过Ajax/WebSocket通信,以SIP、Jingle、ISUP等协议完成信号处理。点对点连接设立后,流的传输并不需要服务器介入。信号处理的示意图如下:

HTML5视频监控技术预研_第4张图片

示例代码

下面的代表片段包含了一个视频电话的信号处理过程:

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来说是透明的: 

  1. 执行解码
  2. 屏蔽丢包的影响
  3. 点对点通信:WebRTC引入流交互式连接建立(Interactive Connectivity Establishment,ICE)框架。ICE负责建立点对点链路的建立:
    1. 首先尝试直接
    2. 不行的话尝试STUN(Session Traversal Utilities for NAT)协议。此协议通过一个简单的保活机制确保NAT端口映射在会话期间有效
    3. 仍然不行尝试TURN(Traversal Using Relays around NAT)协议。此协议依赖于部署在公网上的中继服务器。只要端点可以访问TURN服务器就可以建立连接
  4. 通信安全
  5. 带宽适配
  6. 噪声抑制
  7. 动态抖动缓冲(dynamic jitter buffering),抖动是由于网络状况的变化,缓冲用于收集、存储数据,定期发送

任意数据交换

通过RTCDataChannel完成,允许点对点之间任意的数据交换。RTCPeerConnection连接创建后,不但可以传输音视频流,还可以打开多个信道(RTCDataChannel)进行任意数据的交换。RTCDataChanel的特点是:

  1. 类似于WebSocket的API
  2. 支持带优先级的多通道
  3. 超低延迟,因为不需要通过服务器中转
  4. 支持可靠/不可靠传输语义。支持SCTP、DTLS、UDP几种传输协议
  5. 内置安全传输(DTLS)
  6. 内置拥塞控制

使用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和消息。特殊化的功能有服务器端插件完成

官网地址:https://janus.conf.meetecho.com

Kurento

这是一个开源的WebRTC媒体服务器

备选方案一:从RTSP开始

我们首先尝试的方案是直接使用RTSP源,原因是海康、大华主流厂商的较新的IP摄像头均支持暴露标准化的RTSP流。

尝试播放

使用VLC播放器,打开网络串流:rtsp://admin:[email protected]:554/ch1/main/av_stream,视频源为公司门口的海康摄像头的主码流(main,子码流为sub)。

发现可以正常播放,说明视频格式应该是标准的。VLC菜单 Tool ⇨ Codec Info查看,编码格式为H264。

浏览器无法直接使用RTSP协议,因此,需要有服务器端来处理视频源的RTSP,将其转换为:

  1. 通过WebSocket发送的视频片断,由客户端的:
    1. JSMpeg/Broadway直接解码,渲染到画布
    2. 或者,构造MP4片断Feed给MSE播放
  2. 或者,通过WebRTC网关,转换后提供给客户端的WebRTC代码处理
  3. 或者,使用浏览器插件机制,例如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:[email protected]: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:[email protected]: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

} ); 

渲染截图:

HTML5视频监控技术预研_第5张图片

小结

这种方式客户端解码压力较大,同时开9画面的352x288视频,我的机器上CPU占用率大概到40%左右,画面变化较为剧烈的时候会出现卡顿现象。

实现方式三:Broadway

与JSMpeg类似,Broadway也是JavaScript解码工具。关键之处是,Broadway支持的视频编码是H.264,意味着可能免去消耗服务器资源的视频重编码。

最初的尝试并不顺利,根据IP摄像头的RTSP Describe应答(SDP),我们推断其H.264 Profile为Baseline,但是不转码的情况下Broadway根本无法播放。后来查看ffmpeg的日志输出,发现其实际上使用的Profile是Main。进一步尝试,发现摄像头是可以配置为Baseline的:

HTML5视频监控技术预研_第6张图片

只需要把编码复杂度设置为低,H.264的Profile就从Main变为Baseline。

设置完毕后,仍然基于h264-live-player的Demo进行测试,使用如下命令行抽取原始H.264帧:

Shell

1

ffmpeg -i rtsp://admin:[email protected]: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入手。主要原因是:

  1. 容易扩展的模块化设计
  2. 提供Java客户端、JS客户端
  3. 可以在服务器端合成多画面,这样可以减轻客户端解码压力,特别是那些低配置的客户端
  4. 内置对RTSP协议的支持

基于Kurento搭建WebRTC服务器一文详细讨论了这种实现方式。

备选方案二:从设备SDK开始

这里的设备,主要包括:网络硬盘录像机(NVR)、视频服务器、IP摄像头。为了便于二次开发,硬件厂商都为这些设备配置的相应的SDK套件。这些SDK通常都提供了:实时码流预览、录像文件回放、播放控制(如:暂停、单帧前进、单帧后退)、获取码流基本信息、播放截图等功能。

我们的基本目标是,通过SDK得到标准化的码流,例如H264格式。具体如何操作,得看厂商的SDK,但是思路基本是:

  1. 如果SDK直接支持获取标准格式的流,例如RTSP,那么备选方案一就可以直接用上
  2. 如果SDK支持获取标准编码的视频帧,例如H264,那我们只需要将其包装为合适的容器格式,再通过RTSP/HTTP的方式发送出去
  3. 如果SDK支持获取解码后的原始图像数据,例如RGB、YV12,我们可以基于H264再次编码,然后按第2步方式处理。这种方式对服务器性能要求比较高,CPU压力较大,PC机处理不了多少个通道
  4. 如果都不支持,只提供了封装好的播放控件 —— 这个就比较悲催了,不过通过OS底层API,例如Windows的GDI应该也是可以实现,否则那些屏幕录像软件怎么做的呢?

海康SDK

根据Linux版本的海康设备网络编程指南的描述,我们应该可以:

  1. 调用NET_DVR_Init进行SDK初始化
  2. 调用NET_DVR_Login登陆到目标设备
  3. 调用NET_DVR_RealPlay进行播放,此时返回一个实时播放句柄
    1. 如果设备支持RTSP协议取流:针对上述句柄调用NET_DVR_SetStandardDataCallBack,可以设置一个标准的数据回调函数,此回调会接受到标准码流,这对应上面的第1种思路
    2. 如果设备不支持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

#include

#include

#include

 

// 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协议的媒体流,回调函数会反复被调用:

  1. 第一次调用为40字节的头,不太清楚有什么用
  2. 第二次调用传递了SDP
  3. 后续调用传递标准音视频数据,其内容是RTP封包

总结

基于HTM5的视频监控,媒体流从采集设备到浏览器,主要路径如下图所示:

HTML5视频监控技术预研_第7张图片

对上图的说明如下:

  1. 在设备层,需要以某种方式获得码流,以流协议的方式发送出去。最常用的方式是RTSP/RTP。流的可能获取路径为:
    1. 设备直接暴露RTSP协议端点,并且发送标准码流
    2. 设备SDK允许获取标准码流,需要自己以RTSP协议发送
    3. 设备SDK允许获得解码后的逐帧,需要直接编码为H264,然后以RTSP发送
  2. 流媒体层通常需要引入专门的流媒体服务器,这类服务器能够在内部进行各种流协议的转换,可以解除客户端对特定流协议的依赖
  3. 客户端和服务器端的传输方式,可以有TCP、HTTP、P2P(WebRTC)、WebSocket等多种。其中
    1. 直接的TCP协议浏览器是不支持的,这意味着RTSP/RTMP等协议,在浏览器端必须要有插件才可以使用
    2. WebSocket通常配合JSMpeg或者MSE使用,由程序向JSMpeg/MSE不断Feed视频帧
  4. 客户端解码展示的技术主要有三类:
    1. 浏览器内置的解码能力,主要通过video标签,MSE属于此类
    2. JavaScript软解码,主要是JSMpeg、Broadway
    3. 插件机制,例如Chrome的NaCl

能够免于引入流媒体层的方案,需要:设备能直接暴露标准码流的RTSP端点,并且安装浏览器插件。缺点也很明显,一个是设备的访问密码暴露给了客户端,第二个是目前没有成熟、开源的插件可用。我相信主要原因是合理技术方向不在于此,没人愿意去开发。

直接使用设备层的RTSP端点,可能存在兼容性问题。一个是它发送的码流是否标准化,第二个是市场上有多少设备没有暴露RTSP端点。

客户端方面,JSMpeg是兼容性较好的方案,WebRTC/MSE都有部分平台不支持(但是桌面级的浏览器大部分支持)。JSMpeg的缺点是:

  1. 如果基于WebGL渲染,受限于浏览器WebGL上下文最大数量,多画面可能无法渲染。某些流媒体服务器支持在服务器端合成多画面Grid,可以规避此缺点
  2. 如果基于Canvas2D渲染,画质较差(我的机器上还有莫名其妙的斜线)
  3. 对码流格式要求严格,仅仅支持MPEG-TS,此格式压缩比差,网络带宽占用大
  4. 性能相对较差,尽管使用了MPEG-TS这种简单的视频格式,基于JavaScript解码渲染仍然使客户端压力较大。我的机器(i7-4940MX / Quadro K5100M / Ubuntu 14.04 LTS)上会出现卡顿情况

和JSMpeg类似的库是Broadway,后者能够进行Baseline的H.264解码。如果设备支持Baseline H.264输出,使用Broadway可以很好的解决服务器端转码导致的资源消耗问题。

附录

参考资料

  1. Audio and Video Delivery
  2. W3C Recommendation - Media Source Extensions™
  3. WebRTC Project Home
  4. HTML5 视频直播(三)

你可能感兴趣的:(HTML5视频监控技术预研)