赞
踩
最新EasyGBS国标GB28181方案改用golang做国标了,Golang+C/C++,采用CGO调用底层计算能力,现在做的效果可以到EasyGBS网站看看:www.easygbs.com
在介绍GB28181接入服务器的方案前,咱们先大概给大家介绍一下为什么我们选择了用nodejs开发国标GB28181的服务,我大概给很多人介绍过这个方案,大部分都为之虎躯一震,nodejs在传统行业的人事看来,就是主要做网站、做业务的,不是做流媒体的,这个其实都是误解,nodejs大部分时候都是非常给力的,而且降低了开发成本和维护成本,当您感到质疑的时候,不妨先看一下我之前的一篇博客《对EasyDarwin开源项目后续发展的思考:站在巨人的肩膀上再跳上另一个更高的肩膀》
GB28181接入服务器是EasyDSS云平台提供的接入GB28181设备/平台的信令交互服务器,GB28181将 SIP定位为联网系统的主要信令基础协议,并利用 SIP协议的有关扩展,实现了对非会话业务的兼顾,例如,对报警业务、历史视音频回放、下载等的支持。目前有GB28181-2011和 GB28181-2016两个版本。
GB28181接入服务器对接入系统的GB28181设备的管理,全部通过一个20位的设备ID号来管理;以SIP协议为载体,以REGISTER、INVITE、MESSAGE等命令实现与28181设备和GB28181流媒体服务器的交互。
随着node.js社区的不断壮大,借助其强大的事件驱动和IO并发能力,已经衍生出了很多很强大的模块(包),实现各种各样的功能,使得服务端程序开发变得非常容易,习惯了 C/C++编程的程序员绝对会感到十分惊讶,因为居然有一种语言开发可以如此高效且简单(PS: 我也就刚学习一个月node.js而已- --!);而本文将要讲解的是一种通过node.js实现接入国标设备以及平台的sip信令服务器的方案。
首先,下载node.js并安装,windows,linux平台均支持; 最好有一个比较强大的JS编辑器或者IDE,我推荐一个十分强大且轻量级的IDE兼编辑神器Visual Studio Code。
然后,熟悉npm管理工具的命令,通过npm安装各个需要依赖的node.js模块,其中最重要的sip模块,通过如下命令安装:
npm install sip
node.js拥有强大的包管理工具,可以通过如下命令搜索我们可能需要的node.js模块:
npm search xxx
如下图所示:
其他node.js相关学习大家感兴趣可以在网上找到十分丰富的资料,比如推荐一本比较好书《深入浅出node.js》, 当然最好的建议是:看个毛线的书,老夫都是直接撸代码!
1 接受下级的注册和注销
首先,我们需要建立一个sip服务来检测和接受下级设备或者平台的注册命令的处理,如下代码所示:
sip.start(option, async request => { switch (request.method) { case common.SIP_REGISTER: this.emit('register', request); break; case common.SIP_MESSAGE: this.emit('message', request); break; case common.SIP_INVITE: this.emit('invite', request); break; case common.SIP_ACK: this.emit('ack', request); break; case common.SIP_BYE: this.emit('bye', request); break; default: console.log('unsupported: ' + request.method); break; } });
然后,sip服务接收设备端注册请求,回调函数中进行处理:
case common.SIP_REGISTER: try { const username = sip.parseUri(request.headers.to.uri).user; const userinfo = config.userinfos[username]; const session = { realm: config.server.realm }; if (!userinfo) { sip.send(digest.challenge(session, sip.makeResponse(request, 401, common.messages[401]))); this.session_.set(username,session); return; } else { if(!this.session_.has(username)){ this.session_.set(username,session); } userinfo.session = userinfo.session || this.session_.get(username); if (!digest.authenticateRequest(userinfo.session, request, { user: username, password: userinfo.password })) { sip.send(digest.challenge(userinfo.session, sip.makeResponse(request, 401, common.messages[401]))); this.session_.set(username,userinfo.session); return; } else { this.session_.delete(username); if(request.headers.expires === '0'){ this.emit('unregister', request); } else{ this.emit('register', request); } let response = sip.makeResponse(request, 200, common.messages[200]); sip.send(response); } } } catch (e) { //输出到控制台 console.log(e); } break;
如上代码所示,根据国标gb28181标准处理逻辑如下:
2 查询设备目录列表信息
根据国标协议标准,查询设备目录,
MESSAGE消息头Content-type头域为Content-type: Application/MANSCDP+xml
查询命令采用MANSCDP协议格式定义,详细国标协议文档。
查询请求命令应包括命令类型(CmdType)、命令序列号(SN)、设备编码(DeviceID), 采用RFC 3428 的MESSAGE 方法的消息体携带。 相关设备在收到MESSAGE消息后,应立即返回应答,应答均无消息体; 一个查询目录XML如下示例:
<?xml version="1.0"?>
<Query>
<SN>1</SN>
<DeviceID>64010000001110000001</DeviceID>
</Query>
查询目录函数GetCatalog函数如下代码所示:
async getCatalog(serial) { const device = await devices.getDevice(serial); if (common.isEmpty(device)) { return {}; } const json = { Query: { CmdType: common.CMD_CATALOG, SN: common.sn(), DeviceID: serial } }; const builder = new xml2js.Builder(); const content = builder.buildObject(json); const options = { method: common.SIP_MESSAGE, serial: serial, contentType: common.CONTENT_MANSCDP, content: content, host: device.host, port: device.port, callId: common.callId(), fromTag: common.tag() }; const response = await uas.send(options); if (common.isEmpty(response)) { return {}; } else { } }
查询设备目录应答"MESSAGE"方法消息,回调函数处理如下:
uas.on('message', async ctx => { const request = ctx.request; if (request.content.length === 0) { return; } const vias = request.headers.via; const via = vias[0]; const json = await this.parseXml(request.content); if(json.hasOwnProperty(common.TYPE_RESPONSE)) { //Response switch (json.Response.CmdType) { case common.CMD_CATALOG: ctx.send(200); if (request.headers['content-length'] === 0) { return ; } let items = json.Response.DeviceList.Item; let allCount = json.Response.SumNum; let itemCount = json.Response.DeviceList.$.Num; let channels = []; let deviceInfo = { host: via.params.received, port: via.params.rport, count: 0, channels: [] }; if(devices.hasDevice(json.Response.DeviceID)){ deviceInfo = devices.getDevice(json.Response.DeviceID); channels = deviceInfo.channels; } else{ return ; } deviceInfo.count = allCount; let id = channels.length; if(itemCount>1) { for (let item of items) { id = channels.length; try { let channel = { channel: id, type: 1, name: item.Name, serial: item.DeviceID, status: item.Status==='ON'?1:0, ability: '10000000', snapurl: '', model: item.Model,//设备型号 brand: 2, version: 'v1.0' }; if(channels.length>0){ for(let ch of channels){ if(ch.serial === item.DeviceID){ id = ch.channel-1; break; } } } channel.channel = id+1; channels[id] = channel; } catch (e) { } } } else { try { const channel = { channel: id, type: 1, name: items.Name, serial: items.DeviceID, status: items.Status==='ON'?1:0, ability: '10000000', snapurl: '', model: items.Model,//设备型号 brand: 2, version: 'v1.0' }; if(channels.length>0){ for(let ch of channels){ if(ch.serial === items.DeviceID){ id = ch.channel-1; break; } } } channel.channel = id+1; channels[id] = channel; } catch (e) { } } deviceInfo.channels = channels; devices.addDevice(json.Response.DeviceID, deviceInfo); //TODO: Add device to redis { try { const infoString = { host: deviceInfo.host, port: deviceInfo.port, serial: json.Response.DeviceID, type: 2,//1=摄像机 2=NVR count: deviceInfo.count, channels: deviceInfo.channels }; let info = infoString; info.serverId = common.serial; await redis.set(`${common.DEVICE}:${json.Response.DeviceID}`, JSON.stringify(info), 'EX', common.DEVICE_EXPIRE); } catch (e) { console.log(e); } } //info.channels = channels; break; default: break; } } ctx.send(200); });
需要注意几点:
(1) 在公网应用时,设备注册上来的sip信令交互中填写的IP和端口很有可能是内网的端口,而实际的传输IP和端口通过via的param中获取:host: via.params.received, port: via.params.rport;
(2) 设备目录查询时,如果摄像机个数比较多,则可能分多次回调Response,这时候需要做相应处理,如上代码所示;
3 实时流媒体点播
实时流媒体点播即拉流,gb28181协议定义的拉流逻辑如下图所示:
从上图我们可以看出,拉流逻辑需要通过一个流媒体服务器进行中转,所以拉流逻辑需要流媒体服务器的配合才能完成,所以,完整的拉流逻辑我会在另一篇博客《node.js实现国标GB28181流媒体点播服务解决方案》中进行详细讲解。
4 设备控制
源设备向目标设备发送设备控制命令,控制命令的类型包括球机/云台控制、远程启动、录像控制、
报警布防/撤防、报警复位等,设备控制采用RFC 3428中的MESSAGE方法实现。 源设备包括SIP客户端,目标设备包括SIP设备或者网关。源设备向目标设备发送球机/云台控制命令、远程启动命令后,目标设备不发送应答命令。(摘录自 《GB+28181国家标准《安全防范视频监控联网系统信息传输、交换、控制技术要求》》)
本文主要讲解云台控制的流程实现,其他设备控制命令类似。
一个云台控制XML消息体示例:
<?xml version="1.0"?> <Control>
<CmdType>DeviceControl</CmdType>
<SN>11</SN>
<DeviceID>64010000041310000345</DeviceID>
<PTZCmd>A50F4D1000001021</PTZCmd>
<Info>
<ControlPriority>5</ControlPriority>
</Info>
</Control>
从上消息体中,我们可以看出主要需要填写的字段就是PTZCmd这个8个字节的头缓冲区。
详细解释如下:(内容摘录自《GB+28181国家标准《安全防范视频监控联网系统信息传输、交换、控制技术要求》》)
(1)表L.1 指令格式
字节 | 字节1 | 字节2 | 字节3 | 字节4 | 字节5 | 字节6 | 字节7 | 字节8 |
---|---|---|---|---|---|---|---|---|
含义 | A5H | 组合码1 | 地址 | 指令 | 数据1 | 数据2 | 组合码2 | 校验码 |
各字节定义如下: 字节1:指令的首字节为A5H; 字节2:组合码1,高4位是版本信息,低4位是校验位。本标准的版本号是1.0,版本信息为0H; 校验位=(字节1的高4位+字节1的低4位+字节2的高4位)%16; 字节3:地址的低8位;字节4:指令码; 字节5、6:数据1和数据2; 字节7:组合码2,高4位是数据3,低4位是地址的高4位;在后续叙述中,没有特别指明的高4位,表示该4位与所指定的功能无关; 字节8:校验码,为前面的第1—7字节的算术和的低8位,即算术和对256取模后的结果; 字节8=(字节1+字节2+字节3+字节4+字节5+字节6+字节7)%256。 地址范围000H—FFFH(即0—4095),其中000H地址作为广播地址。
(2)L.2 PTZ 指令
PTZ指令见表L.2。 表L.2 PTZ 指令 由慢到快为00H-FFH。 注4:字节7的高4位为变焦速度,速度范围由慢到快为0H-FH;低4位为地址的高4位。
字节 | 位 |
---|
| · |Bit7 | Bit6| Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0|
|:— |:------| :------|:-----|:-----|:-----|:-----|:-----|
|字节4 | 0 |0 |镜头变倍(Zoom) |镜头变倍(Zoom) |云台垂直方向控制(Tilt)|云台垂直方向控制(Tilt) | 云台水平方向控制|(Pan) |
|||| 缩小(OUT)| 放大(IN) |上(Up) |下(Down)| 左(Left) | 右(Right) |
字节5 | 水平控制速度相对值 |
---|
字节6 | 垂直控制速度相对值 |
---|
字节7 | 变倍控制速度相对值 | 地址高4 位 |
---|
注1: 字节4 中的 Bit5、Bit4 分别控制镜头变倍的缩小和放大,字节4 中的B it3、Bit2、B it1、Bit0位分别控制云台上、下、左、右方向的转动,相应Bit 位置1 时,启动云台向相应方向转动,相应Bit位清0 时, 停止云台相应方向的转动。云台的转动方向以监视器显示图像的移动方向为准。 注2:Bit5 和Bit4 不应同时为1,Bit3 和Bit2 不应同时为1;Bit1 和Bit0 不应同时为1。镜头变倍指令、云台上下指令、云台左右指令三者可以组合。 注3 :字节5 控制水平方向速度,速度范围由慢到快为00H-FFH;字节6 控制垂直方向速度,速度范围
PTZ指令举例见表L.3。
表L.3 PTZ 指令举例
序号 | 字节4 | 字节5 | 字节6 | 字节7高4位 | 功能描述 |
---|---|---|---|---|---|
1 | 20H | XX | XX | 0H-FH | 镜头以字节7 高4 位的数值变倍缩小 |
2 | 10H | XX | XX | 0H-FH | 镜头以字节7 高4 位的数值变倍放大 |
3 | 08H | 00H-FFH | XX | X | 云台以字节6 给出的速度值向上方向运动 |
4 | 04H | 00H-FFH | XX | X | 云台以字节6 给出的速度值向下方向运动 |
5 | 02H | XX | 00H-FFH | X | 云台以字节5 给出的速度值向左方向运动 |
6 | 01H | XX | 00H-FFH | X | 云台以字节5 给出的速度值向右方向运动 |
7 | 00H | XX | XX | X | PTZ 的所有操作均停止 |
8 | 29H | 00H-FFH | 00H-FFH | 0H-FH | 这是一个PTZ 组合指令的示例: 云台以字节5 给出的速度值向右方向运动,同时以字节6给出的速度值向上方向运动,实际上是斜向右上方向运行;与此同时,镜头以字节7 高4 位的数值变倍缩小 |
通过以上国标协议的详细诠释,我们得以实现云台控制的命令封装,请求函数如下:
async ptzControl(serial, code, callId, command, speed){ const devices = require('gateway/devices'); const device = await devices.getDevice(serial); if (common.isEmpty(device)) { return {}; } //define PTZCmd header 8字节 let cmd = Buffer.alloc(8); cmd[0] = 0xA5;//首字节以05H开头 cmd[1] = 0x0F;//组合码,高4位为版本信息v1.0,版本信息0H,低四位为校验码 // 校验码 = (cmd[0]的高4位+cmd[0]的低4位+cmd[1]的高4位)%16 cmd[2] = 0x01;//地址的低8位???什么地址,地址范围000h ~ FFFh(0~4095),其中000h为广播地址 cmd[3] = common.ptzCMD[command]; //指令码 let ptzSpeed = parseInt(speed); if(ptzSpeed>0xff) ptzSpeed = 0xff; cmd[4] = ptzSpeed; //数据1,水平控制速度、聚焦速度 cmd[5] = ptzSpeed; //数据2,垂直控制速度、光圈速度 cmd[6] = 0x00; //高4位为数据3=变倍控制速度,低4位为地址高4位 if(command === 9||command === 10){ let zoomSpeed = speed; if(zoomSpeed > 0x0F){ zoomSpeed = 0x0F; } cmd[6] = zoomSpeed<<4|0; } else if(command === 16||command === 17||command === 18) { //16: 0x81, //设置预置位 //17: 0x82, //调用预置位 //18: 0x83 //删除预置位 } cmd[7] = (cmd[0]+cmd[1]+cmd[2]+cmd[3]+cmd[4]+cmd[5]+cmd[6])%256; var cmdString = common.Bytes2HexString(cmd); //generate XML const xmlJson = { Control: { CmdType: 'DeviceControl', SN: command, DeviceID: code, PTZCmd: cmdString,//'A50F000800C80084'//cmdString, Info: { ControlPriority: 5 } } }; const builder = new xml2js.Builder(); // JSON->xml //var parser = new xml2js.Parser(); //xml -> json const xml = builder.buildObject(xmlJson); console.log('xml = '+xml); const options = { method: common.SIP_MESSAGE, serial: serial, contentType: common.CONTENT_MANSCDP, content: xml, host: device.host, port: device.port, callId: callId, fromTag: common.tag() }; const response = await uas.send(options); // if (response.status === 200) { // await uas.sendAck(response); // } return response; }
注意:本文中所涉及的GB28181协议最低兼容GB28181协议2011版本,向上兼容2016版本。
##获取更多信息
邮件:support@easydarwin.org
流媒体技术交流QQ群:538316953
Copyright © EasyDarwin.org 2012-2019
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。