node.js實現國標GB28181設備接入sip服務器解決方案EasyGBS國標視頻平臺

GB28181接入服務器是EasyGBS接入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] = 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版本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章