GB28181接入服务器是SkeyeVSS接入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规范解决逻辑如下:
1) SIP 代理向SIP 服务器发送REGISTER 申请,申请中未蕴含Authorization 字段;SIP 服务器向SIP 代理发送响应401,并在响应的音讯头WWW_Authenticate 字段中给
出适宜SIP 代理的认证体制和参数;
2) SIP 代理从新向SIP 服务器发送REGISTER 申请,在申请的Authorization 字段给出信赖书,蕴含认证信息;SIP 服务器对申请进行验证,如果查看出SIP 代理身份非法,向SIP 代理发送胜利响应200 OK ,如果身份不非法则发送拒绝服务应答。
值得注意的是,有些国标设施接入并不遵循以上注册逻辑,这种多见于老旧的国标设施或者平台,其注册甚至都不会携带认证信息,而是通过双向注册实现验证。

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位。
|字节 | 位 |
|:- |:------|

·Bit7Bit6Bit5Bit4Bit3Bit2Bit1Bit0
字节400镜头变倍(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位性能形容
120HXXXX0H-FH镜头以字节7 高4 位的数值变倍放大
210HXXXX0H-FH镜头以字节7 高4 位的数值变倍放大
308H00H-FFHXXX云台以字节6 给出的速度值向上方向静止
404H00H-FFHXXX云台以字节6 给出的速度值向下方向静止
502HXX00H-FFHX云台以字节5 给出的速度值向左方向静止
601HXX00H-FFHX云台以字节5 给出的速度值向右方向静止
700HXXXXXPTZ 的所有操作均进行
829H00H-FFH00H-FFH0H-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版本。

SkeyeVSS技术交换QQ群:102644504