方案背景
在介紹GB28181接入服務器的方案前,我們先大概給你們介紹一下爲何咱們選擇了用nodejs開發國標GB28181的服務,我大概給不少人介紹過這個方案,大部分都爲之虎軀一震,nodejs在傳統行業的人事看來,就是主要作網站、作業務的,不是作流媒體的,這個其實都是誤解,nodejs大部分時候都是很是給力的,並且下降了開發成本和維護成本,當您感到質疑的時候,不妨先看一下我以前的一篇博客《對EasyDarwin開源項目後續發展的思考:站在巨人的肩膀上再跳上另外一個更高的肩膀》node
GB28181接入服務器是EasyDSS雲平臺提供的接入GB28181設備/平臺的信令交互服務器,GB28181將 SIP定位爲聯網系統的主要信令基礎協議,並利用 SIP協議的有關擴展,實現了對非會話業務的兼顧,例如,對報警業務、歷史視音頻回放、下載等的支持。目前有GB28181-2011和 GB28181-2016兩個版本。
GB28181接入服務器對接入系統的GB28181設備的管理,所有經過一個20位的設備ID號來管理;以SIP協議爲載體,以REGISTER、INVITE、MESSAGE等命令實現與28181設備和GB28181流媒體服務器的交互。linux
隨着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模塊,經過以下命令安裝:redis
npm install sip
node.js擁有強大的包管理工具,能夠經過以下命令搜索咱們可能須要的node.js模塊:npm
npm search xxx
以下圖所示:
其餘node.js相關學習你們感興趣能夠在網上找到十分豐富的資料,好比推薦一本比較好書《深刻淺出node.js》, 固然最好的建議是:看個毛線的書,老夫都是直接擼代碼!編程
國標接入流程
1 接受下級的註冊和註銷
首先,咱們須要創建一個sip服務來檢測和接受下級設備或者平臺的註冊命令的處理,以下代碼所示:json
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服務接收設備端註冊請求,回調函數中進行處理:windows
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:指令碼; 字節五、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 中的 Bit五、Bit4 分別控制鏡頭變倍的縮小和放大,字節4 中的B it三、Bit二、B it一、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
WEB:www.EasyDarwin.org
流媒體技術交流QQ羣:538316953
Copyright © EasyDarwin.org 2012-2018