一、说明

GB28181设备在注册到服务端之后,服务端通过发送Catalog命令向设备端发送请求命令指令。

范例说明(来自GB28181国标文档):

M.11设备目录查询消息消息示范例

M.11.1 1.1和2.1

  message  sip:目的设备 编码 @目的域名或IP地址端口 SIP/2.0 
To: <sip:目的设备编码@目的域名>;tag=852843529 
Content-Length: 消息实体的字节长度
CSeq: 2 MESSAGE 
Call-ID: wlss-3a65dfb6-7ee86970ea84c5b2284ad158b3d4fdee@172.18.16.5 
Via: SIP/2.0/ UDP 源域名或IP地址
From: <sip:源设备编码@源域名>;tag=e40807c0 
Content-Type: Application/MANSCDP+xml 
Max-Forwards: 70 
<?xml version="1.0"?> 
<Query> 
<CmdType> Catalog </CmdType> 
<SN>17430</SN> 
<DeviceID>64010000001110000001</DeviceID> 
</Query> 
123456789101112131415  

M.11.2 1.2和2.2

 SIP/2.0 200 OK 
Via: SIP/2.0/UDP 源域名或IP地址
From: <sip:源设备编码@源域名>;tag=e40807c0 
To: <sip:目的设备编码@目的域名>;tag=852843529 
Call-ID: wlss-3a65dfb6-7ee86970ea84c5b2284ad158b3d4fdee@172.18.16.5 
CSeq: 2 MESSAGE 
Content-Length: 0 
1234567  

M.11.3 3.1和4.1

 MESSAGE sip:目的设备编码@目的域名或IP地址端口 SIP/2.0 
Via: SIP/2.0/UDP 源域名或IP地址
From: <sip:源设备编码@源域名>;tag=852843529 
To: <sip:目的设备编码@目的域名>;tag=e40807c0 
Call-ID: wlss-3a65dfb6-7ee86970ea84c5b2284ad158b3d4fdee@172.18.16.5 
CSeq: 2 MESSAGE 
Max-Forwards: 70 
Content-Type: Application/MANSCDP+xml 
Content-Length: 消息实体的字节长度
<?xml version="1.0"?> 
<Response> 
<CmdType> Catalog </CmdType> 
<SN>17430</SN> 
<DeviceID>64010000001110000001</DeviceID> 
<DeviceList Num=2> 
<Item>  
<DeviceID>64010000001110000001</DeviceID> 
<Name>Camera</ Name > 
<Status >OK</ Status > 
<Longitude> 71.3</ Longitude > 
<Latitude>34.2</ Latitude > 
<Item>  
<Item>  
<DeviceID>64010000001110000001</DeviceID> 
<Name >Camera</ Name > 
<Status > OK </Status> 
<Longitude > 171.3</Longitude> 
<Latitude > 34.2 </Latitude> 
<Item>  
</DeviceList> 
</Response>
12345678910111213141516171819202122232425262728293031  
二、抓包截图:

发给设备的查询指令

收到的目录信息

三、Demo源码

依赖:

 npm install log4js
npm install log4js-extend
npm install xml2js
npm install fast-xml-parser  

源码

 
var log4js = require('log4js');

log4js.configure({
  appenders: {
    out: { type: 'stdout' },
    app: { type: 'file', filename: 'application.log' }
  },
  categories: {
    // getLogger 参数为空时,默认使用该分类
    default: { appenders: [ 'out', 'app' ], level: 'debug' }
  }
});

var logger = log4js.getLogger();
const log4js_extend = require("log4js-extend");
log4js_extend(log4js, {
  path: __dirname + "/a.log",
  format: "at @name (@file:@line:@column)"
});

var parseString = require('xml2js').parseString;
var xml2js = require('xml2js');
var sip = require('sip');
var digest = require('sip/digest');

var server_account = '34020000002000000001';
// 注册的设备列表
var devices=[];

// 设备信息
var registry = {
  '34020000001110000001': {password: 'yourpass'}
};

debugger;
var realm ='3402000000'; 

logger.info('localhost name='+realm);
sip.start({
  logger: { 
    send:  function ( MESSAGE ,  address ) { 
    logger.info("==send==:" , message,address); 
  },
    recv: function(message, address) {
      logger.info("==recv==:" , message,address); 
    }
  }
},
function(rq) {
  try {
    if(rq.method ==='REGISTER') {  
      var username = sip.parseUri(rq.headers.to.uri).user;
      var userinfo = registry[username];

      if(!userinfo) {
        // 没有登记的用户,这里直接禁止授权
        //logger.error('没有登记的用户,这里直接禁止授权:' , username);
        var session = {realm: realm};
        sip.send(digest.challenge(session, sip.makeResponse(rq, 401, 'Unauthorized')));
        return;
      }
      else {
        userinfo.session = userinfo.session || {realm: realm};
        if(!digest.authenticateRequest(userinfo.session, rq, {user: username, password: userinfo.password})) {
          sip.send(digest.challenge(userinfo.session, sip.makeResponse(rq, 401, 'Unauthorized')));
        }
        else {
          // 完成授权
          userinfo. contact  = rq.headers.contact;
          var rs = sip.makeResponse(rq, 200, 'Ok');
          sip.send(rs);
          logger.info('请求设备目录');
          command.catalog(rq,username);
        }
      }
    }
    else if(rq.method === 'INVITE') {
      var username = sip.parseUri(rq.uri).user;
      var userinfo = registry[username]
      
      if(userinfo && Array.isArray(userinfo.contact) && userinfo.contact.length > 0) {
        var rs = sip.makeResponse(rq, 302, 'Moved');
        rs.headers.contact = userinfo.contact;
        sip.send(rs);
      }
      else {
        logger.error(userinfo);
        sip.send(sip.makeResponse(rq, 404, 'Not Found'));
      }
    }
    else if(rq.method==='MESSAGE'){
      parseString(rq.content,function(err,result){
        if(result.Notify){
          var CmdType = result.Notify.CmdType[0];
          if(CmdType==='Keepalive'){
            // 心跳
            var rs = sip.makeResponse(rq,200,'OK');
            sip.send(rs);
          }else{
            
      
          }
        }else if(result.Response){
          var CmdType = result.Response.CmdType;
          if(CmdType=='Catalog'){
            // 获取到的设备列表
            if(result.Response.DeviceList){
            
              for(var s in  result.Response.DeviceList[0].Item){
                var temp = result.Response.DeviceList[0].Item[s];
                var exists = devices.indexOf(temp)>-1;
                if(!exists) {
                  devices.push(temp);
                }
              }
              logger.info('----------devices---------',devices);
              sip.send(sip.makeResponse(rq,200,'OK'));
            }
          }
        }
        
      });
      
    }

  } catch(e) {
    logger.error(e);
    sip.send(sip.makeResponse(rq, 500, "Server Internal Error"));
  }
});

var SN=1;

/**
 * 发送目录命令
 * @param {request} rq 获取到的参数
 */var command={
  // 请求目录 
  catalog: function(rs,deviceId){
    const json = {
      Query: {
            CmdType: 'Catalog',
            SN: SN,
            DeviceID: deviceId
        }
    };
    const builder = new xml2js.Builder();
    const content = builder.buildObject(json);

    const username = sip.parseUri(rs.headers.to.uri).user;
    // 从via解析摄像头的ip 端口
    const via = sip.parseUri(rs.headers.via)[0];
    var cseq = rs.headers.cseq;
    cseq.method = 'MESSAGE';
    var res = {
      method:'MESSAGE',
      uri: 'sip:' + username + '@' + via.host + ':' + via.port,
      version:rs.version,
      headers:{
        via        : rs.headers.via,
        from      : rs.headers.from, 
        to        : rs.headers.to, 
        'call-id' : rs.headers['call-id'],
        cseq:cseq,
        'Content-Type':'Application/MANSCDP+xml',
        'expires':'3600',
        'Max-Forwards':70
      },
      content:content
    };

    logger.info(res);
    sip.send(res);

  }
};