首页前端开发Node.jsnode.js实现国标GB28181设备接入sip服务器解决方案SkeyeVSS国标视频云平台

node.js实现国标GB28181设备接入sip服务器解决方案SkeyeVSS国标视频云平台

时间2023-04-23 08:00:01发布访客分类Node.js浏览1302
导读:GB28181接入服务器是SkeyeVSS接入GB28181设备/平台的信令交互服务器,GB28181将 SIP定位为联网系统的主要信令基础协议,并利用 SIP协议的有关扩展,实现了对非会话业务的兼顾,例如,对报警业务、历史视音频回放、下载...

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位。

字节

| · |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] = zoomSpeed4|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版本。

声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!

sipnode.js

若转载请注明出处: node.js实现国标GB28181设备接入sip服务器解决方案SkeyeVSS国标视频云平台
本文地址: https://pptw.com/jishu/6118.html
使用Python发送邮件 Go内存对齐详解

游客 回复需填写必要信息