WebRTC获取GB28181监控摄像头实时音视频流的实现方法
我写了一个网关,核心是打通GB28181和WebRTC,通过浏览器获取监控摄像头的实时音视频流。完全用C/C++实现,依赖极少,效率极高。还封装了一个前端js类,只需要简单几条语句就能在web上拉取摄像头的流。
一、目标和分析
1、在浏览器上直接播放,不用安装任何插件
只能用WebRTC。
如果采用厂商(如海康)的SDK,意味着要安装插件,只能限制在windows下运行。 而WebRTC是现代浏览器的标准。
2、实时性,不能感觉到延时
这里的问题就变成,用什么方式获取摄像头的音视频流?
目前有三种常见方式:
a、厂家专用sdk 本质上私有协议,兼容性差。
b、RTSP协议 优点是接入简单,缺点是几乎没有浏览器可以显示实时 RTSP 协议
c、GB/T.28181即所谓的国标,本质上是SIP协议
这个最佳,媒体采用rtp传送,实时性强。
GB基于sip协议可以充分利用以前开发软交换的经验。 国标开放性也最好,国产视频监控摄像头强制必须支持该标准。
3、在任意操作系统上使用
在某些行业领域,强制安装国信麒麟等国产操作系统, 因此展示监控音视频流的客户端,要求能够在各种操作系统上运行,无论桌面操作系统windows、Linux(包括国信麒麟)、MacOS还是手机操作系统安卓或iOS。
支持WebRTC的现代浏览器,确实可以在任意操作系统上使用。 所以问题变成了,我们实现的网关可以在任意操作系统上部署。跨平台,对第三方库的依赖尽可能少。
4、支持H.264或H.265
最新版的浏览器 Chrome(版本136+以上)、Safari等已经直接支持H.265,Firefox还只能支持H.264。
网关收到呼叫时,如果摄像头的流是H.265,须判断前端浏览器是否支持,如果不支持,则可进行转码。 转码带来的问题是比较费cpu,而且通常会带来2、3秒的延时。
应该尽量避免转码。除了使用支持h.265的浏览器,也可以设置摄像头编码方式为h.264。
5、网关的效率
除了前面讨论的应尽量避免转码,还应支持流分发:
多个前端获取同一摄像头时,应该采用流分发,而非软交换sip通常习惯的发起多个呼叫。 这也是GB/T.28181对媒体服务器的要求。 特别是在转码的情况下,避免了相同流的重复解复用和转码,对资源的节约是很大的。
要能支持多个浏览器拉取多个摄像头的流,网关服务器的cpu占用不高,高效率并发。
二、实现
系统结构:
web浏览器 <--WebRTC--> 网关服务器 <--GB28181--> 监控管理平台(如海康) <--GB28181--> 摄像头
或直接挂摄像头:
web浏览器 <--WebRTC--> 网关(软交换)服务器 <--GB28181--> 摄像头
下面总结一些要点:
1、ps解包
和普通软交换不同,大部分监控摄像头使用GB28181只支持ps封包,ps可以将音频视频封在一起,因此要手写一个高效的解包模块。信令部分也要对摄像头的sdp和信令部分进行处理,以符合国标。
2、WebRTC的音视频流是分开的
采用不同rtp载荷,虽然端口可能复用。
3、发往WebRTC的流封装
对于H.264,须按rfc6184进行封装;
对于h.265,须按rfc7798进行封装(RTP Payload Format for High Efficiency Video Coding (HEVC))。
4、推到浏览器的流要注意时间戳
视频简单起见可以套用ps流的时间戳间隔。
要准确发送rtcp给浏览器。
传送视频帧的rtp,其mark位要准确,简单来说就是相同时间戳的最后一个rtp包mark必须置1,其它都置0。否则浏览器将无法正常播放。
音频的时间戳,应根据采样进行处理,通常都是8000采样率,所以每次加160即可。
5、浏览器信令选择:SIP
WebRTC没有规定控制信令,我采用wss承载的sip协议,sip是久经考验的标准,和GB相同类型。
支持wss-sip协议有开源的jssip,是js实现的完整sip协议栈,很成熟。
6、网关实现尽量减少依赖
网关的sip协议栈,媒体处理都是我自己实现的。
ws/wss协议栈、STUN协议栈、DTLS协议处理也全都自己实现,仅使用必要的库,这些库往往操作系统自带,如openssl、srtp。
如果需使用h.265到h.264的转码,我也避免使用极其臃肿的FFmpeg,而是直接使用短小精悍的libde265和libx264,这两个库很多操作系统也有预装。(实际上FFmpeg底层也会套娃般使用这两个库)
7、配置简单
只使用一个ini文件。
三、浏览器前端和效果
我对jssip进行了封装,呼叫摄像头就一条语句:
rtcMonitor.makeCall(摄像头id);
只有一个参数,即摄像头id,是一个符合 GB./T28181 标准的20位字符串,通常可从监控管理平台(如海康)获取。
下面是前端调用的demo说明:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title> WebRTC 获取 GB28181 摄像头音视频 </title><style>body { font-family: Arial, sans-serif; text-align: center; margin-top: 20px; background-color: #f4f4f4; }.container { width: 90%; max-width: 800px; margin: auto; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }input, button { padding: 10px; margin: 5px; width: 90%; box-sizing: border-box; }.status { margin-top: 10px; font-weight: bold; }.videos { display: flex; justify-content: space-around; margin-top: 20px; }.videos video { width: 45%; max-width: 350px; border: 1px solid #ccc; background-color: #000; }#localVideo { transform: scaleX(-1); } /* 镜像本地视频 */.video-container {width: 100%; /* 父容器宽度设为100% */}.video-container video {max-width: 100%; /* 视频宽度不超过父容器的最大宽度 */height: auto; /* 高度自动,以保持原始宽高比 */}</style><script src="./RtcMonitor.js" type="module"></script>
</head>
<body><div class="container"><h1>WebRTC 获取 GB28181摄像头 音视频</h1><div class="status" id="status">未连接...</div><hr><h2>SIP 账户设置</h2><input type="text" id="sip_id" placeholder="5001" value="5001"><input type="password" id="password" placeholder="密码" value="password123"><button id="connectBtn">初始化并连接</button><hr><h2>呼叫摄像头</h2><input type="text" id="target_id" placeholder="摄像头id,20位国标编号" value="34020000001320000001"><button id="callBtn">呼叫</button><button id="hangupBtn">挂断</button><div class="video-container"><video id="remoteVideo" autoplay playsinline></video></div></div><script type="module">import RtcMonitor from './RtcMonitor.js'var rtcMonitor = null;const connectBtn = document.getElementById('connectBtn');const callBtn = document.getElementById('callBtn');const hangupBtn = document.getElementById('hangupBtn');// 按钮事件监听connectBtn.addEventListener('click', connect);callBtn.addEventListener('click', makeCall);hangupBtn.addEventListener('click', hangupCall);// 初始化callBtn.disabled = true;hangupBtn.disabled = true;// 1. 连接 SIP 服务器function connect() {const sipIdEl = document.getElementById('sip_id').value.toString();const passwordEl = document.getElementById('password').value.toString();var remoteVideoEl = document.getElementById('remoteVideo');var statusEl = document.getElementById("status");// 创建一个 RtcMonitor 类,5个参数:// sipId - 如果有多台或者多个窗口需显示监控画面,需不同的sipId// password - 对应sipId的密码。网关服务器校验// wss_uri - 网关服务器url,必须是合法的 wss 地址// remoteVideoEl - video 元素,用来显示监控流的音视频// statusEl - 用来显示状态的文字信息,可以为null,表示不显示状态rtcMonitor = new RtcMonitor(sipIdEl, passwordEl, 'wss://192.168.0.103:4536/wss', remoteVideoEl, statusEl);rtcMonitor.register();connectBtn.disabled = true;callBtn.disabled = false;}// 2. 发起呼叫function makeCall() {const targetIdEl = document.getElementById('target_id').value.toString();// 向网关服务器发起呼叫,请求摄像头的音频视频流// 只有一个参数,即摄像头id,是一个符合 GB./T28181 标准的 20位字符串,通常可从监控管理平台(如海康)获取rtcMonitor.makeCall(targetIdEl);hangupBtn.disabled = false;callBtn.disabled = true;}// 3. 挂断function hangupCall() {// 如果需要切换看其它摄像头,需调用本函数挂断当前会话,再调用 makeCall(另一个摄像头id)rtcMonitor.hangup();hangupBtn.disabled = true;callBtn.disabled = false;}</script>
</body>
</html>
安卓手机chrome浏览器跑上面demo的效果(使用自签名证书,所以地址栏显示感叹号):