代码只对海康和大华做了一些适配,仅供学习参考。不同摄像头可能不太一样,可以自行抓包适配。解析消息直接是字符串操作,比较方便,不依赖第三包。
摄像头注册代码
//接收到netty的信息后需要进行gbk解码
String str=packet.content().toString(Charset.forName("gbk"));
//去除空包,公网状态下很多空包 应该是服务器端口被探测
if(str.trim().length()==0){
    return;
}
//打印接收信息
logger.info("-------------");
logger.info(n+str);
logger.info(packet.sender().getAddress().getHostAddress());
logger.info(String.valueOf(packet.sender().getPort()));
logger.info("-------------");

String sendStr=null;

//re(str) 解析收到的字符串
//这里可以验证设备编号 
Map<String,String> map=re(str);
if(!map.get("deviceId").startsWith("3402000000111")){
    return;
}

//getDeviceInfo(map.get("deviceId")) 从redis获取上次保存的最新信息
//获取缓存中得设备信息 里面包含了设备所有保存的信息
DeviceInfo deviceInfo=getDeviceInfo(map.get("deviceId"));
//数据为空就实例化一个
if(deviceInfo==null){
    deviceInfo=new DeviceInfo();
}
//设置最新的ip和端口,公网情况下ip和端口是有时间限制的,所以必须保存最新的ip和端口
deviceInfo.setIp(packet.sender().getAddress().getHostAddress());
deviceInfo.setPort(packet.sender().getPort());
//保存最后通信时间以作过期判断
deviceInfo.setTime(System.currentTimeMillis());

根据上一节收到的数据样式进行简单的解析

private static Map<String,String> re(String str){
   Map<String,String> map=new HashMap<>();
   BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(str.getBytes(Charset.forName("gbk"))), Charset.forName("gbk")));
   //记录读取的行数
   Integer count=0;
   try {
       String line=null;
       //记录是否遇到空行,消息体和消息头中间会有个换行隔开
       boolean flag=false;
       while ((line=br.readLine())!=null){
           count=count+1;
           if("".equals(line)){
               flag=true;
               continue;
           }
           if(flag){
				//获取MESSAGE 对应的CmdType,这里常见的是keeplive(保护心跳)
               if(line.contains("<CmdType>")){
               		//去掉空字符串(测试的情况是海康的有空格,大华没有)
                   line=line.trim();
                   line=line.replace("<CmdType>","");
                   line=line.replace("</CmdType>","");
                   map.put("CmdType",line);
               }

               continue;
           }
           if(count==1){
           		//第一行信息 SIP/2.0结尾的是摄像头请求信息
               if(line.endsWith("SIP/2.0")){
               		//获取消息类型
                   map.put("method",line.split("\\s+")[0].trim());
                   map.put("messageType","REQUEST");
                   continue;
               }
               //SIP/2.0开头的是摄像头响应的信息
               if(line.startsWith("SIP/2.0")){
               		//响应码获取
                   map.put("stateCode",line.split("\\s+")[1]);
                   //消息类型
                   map.put("method",line.split("\\s+")[2].trim());
                   map.put("messageType","RESPONSE");
                   continue;
               }
           }
           //冒号加空格切割请求头属性和值
           String[] s=line.split(": ");
           //第一个是请求头名称,第二个是值
           map.put(s[0].trim(),s[1].trim());
           //From请求头,如果是请求信息,通常设备编号在这里获取
           if("From".equals(s[0])){
               if("REQUEST".equals(map.get("messageType"))){
                   String deviceId=s[1].split(";")[0];
                   deviceId=deviceId.split(":")[1];
                   deviceId=deviceId.replace(">","");
                   deviceId=deviceId.split("@")[0];
                   map.put("deviceId",deviceId);
                   logger.info(deviceId);
               }
           }

           if("To".equals(s[0])){
              	if("RESPONSE".equals(map.get("messageType"))&&!map.get("Via").contains("branchbye")){
                   String deviceId=s[1].split("\\s+")[0];
                   deviceId=deviceId.replaceAll("\"","");
                   map.put("deviceId",deviceId);
                   String deviceLocalIp=s[1].split("\\s+")[1];
                   deviceLocalIp=deviceLocalIp.split(";")[0];
                   deviceLocalIp=deviceLocalIp.split("@")[1];
                   deviceLocalIp=deviceLocalIp.replace(">","");
                   map.put("deviceLocalIp",deviceLocalIp.split(":")[0]);
                   map.put("deviceLocalPort",deviceLocalIp.split(":")[1]);
               }
               //这里区分是否是下达推流结束指令的的响应     
          	if("RESPONSE".equals(map.get("messageType"))&&map.get("Via").contains("branchbye")){
                   String deviceId=s[1].split(";")[0];
                   deviceId=deviceId.split("@")[0];
                   deviceId=deviceId.replace("<sip:","");
                   map.put("deviceId",deviceId);
               }
           }
       }
   }catch (Exception e){
       e.printStackTrace();
   }
	//设备编号处理特殊情况,有时包含空格
   if(map.get("deviceId").split("\\s+").length>1){
       map.put("deviceId",map.get("deviceId").split("\\s+")[1]);
   }
   return  map;

}
定义注册响应模板

{value} 这种格式的字符串用于替换真实的值
realm="3402000000"是填写sip域,这里直接赋值,可根据情况修改

//换行符
private static final String n="\r\n";

//摄像头第一次注册401回复验证
private static final String str_401=
        "SIP/2.0 401 Unauthorized"+"\r\n"+
                "CSeq: 1 REGISTER"+"\r\n"+
                "Call-ID: {Call-ID}"+"\r\n"+
                "From: {From}"+"\r\n"+
                "To: {To}"+"\r\n"+
                "Via: {Via}"+"\r\n"+
                "WWW-Authenticate: Digest realm=\"3402000000\",nonce=\"{nonce}\""+"\r\n"+
                "Content-Length: 0"+"\r\n"+
                "\r\n";

//第二次注册成功回复200 ok
private static final String str_200_ok=
        "SIP/2.0 200 OK"+"\r\n"+
                "CSeq: 2 REGISTER"+"\r\n"+
                "Call-ID: {Call-ID}"+"\r\n"+
                "From: {From}"+"\r\n"+
                "To: {To}"+"\r\n"+
                "Via: {Via}"+"\r\n"+
                "Expires: 3600"+"\r\n"+
                "Date: {Date}"+"\r\n"+
                "Content-Length: 0"+"\r\n"+
                "\r\n";
注册响应
 //摄像头注册处理
if("REGISTER".equals(map.get("method"))){
   //第一次注册,回复401
   if("1 REGISTER".equals(map.get("CSeq"))){
   		//该字符串是用于回复摄像头请求信息
       sendStr=str_401;
       //这里的nonce是一串随机数,随便自己生成
       String nonce = DigestUtils.md5Hex(map.get("Call-ID")+map.get("deviceId"));
       sendStr=sendStr.replace("{nonce}",nonce);
       //大部分的字段回复直接拷贝请求头里面的
       sendStr=sendStr.replace("{Call-ID}",map.get("Call-ID"));
       sendStr=sendStr.replace("{From}",map.get("From"));
       sendStr=sendStr.replace("{To}",map.get("To"));
       sendStr=sendStr.replace("{Via}",map.get("Via"));
       //重置推流状态
       deviceInfo.setLive(false);
   }else if("2 REGISTER".equals(map.get("CSeq"))){//第二次注册,这里可以验证是否是自己的摄像机,我这边直接通过
   		//关于如何验证密码下面再说明,这里直接通过
       sendStr=str_200_ok;
       sendStr=sendStr.replace("{Call-ID}",map.get("Call-ID"));
       sendStr=sendStr.replace("{From}",map.get("From"));
       sendStr=sendStr.replace("{To}",map.get("To"));
       sendStr=sendStr.replace("{Via}",map.get("Via"));
       //生成时间
       sendStr=sendStr.replace("{Date}",getGMT());

       String contact=map.get("Contact");
       contact=contact.split("@")[1];
       contact=contact.replace(">","");
  /* Map<String,String> d=new HashMap<>(2);
   d.put("deviceLocalIp",contact.split(":")[0]);
   d.put("deviceLocalPort",contact.split(":")[1]);*/
       deviceInfo.setLocalIp(contact.split(":")[0]);
       deviceInfo.setLocalPort(contact.split(":")[1]);

       String deviceId=map.get("deviceId");
       //注册成功直接生成ssrc,方便按需推流,这里是取设备编号后四位加前缀,然后取十六进制字符串
       deviceInfo.setSsrc(Utils.getSsrc("010000"+deviceId.substring(deviceId.length()-4)));
		//业务通知事件,可忽略
       Data.putScheduled(new SendTipsTask(deviceId + "注册成功。。。"));
       Data.putScheduled(new SendDataTask(commonService));
       //断流检测
       //bye(map.get("deviceId"));
   }else if("0".equals(map.get("Expires"))){//注销
       String deviceId=map.get("deviceId");
       //业务通知事件,可忽略
       Data.putScheduled(new SendTipsTask(deviceId + "注销。。。"));
       deviceInfo.setLive(false);
       Data.putScheduled(new SendDataTask(commonService));
   }else if(!"0".equals(map.get("Expires"))){//重复注册,直接回复200,如果回复401会造成断流
       sendStr=str_200_ok;
       sendStr=sendStr.replace("{Call-ID}",map.get("Call-ID"));
       sendStr=sendStr.replace("{From}",map.get("From"));
       sendStr=sendStr.replace("{To}",map.get("To"));
       sendStr=sendStr.replace("{Via}",map.get("Via"));
       sendStr=sendStr.replace("{Date}",getGMT());

       String contact=map.get("Contact");
       contact=contact.split("@")[1];
       contact=contact.replace(">","");
  /* Map<String,String> d=new HashMap<>(2);
   d.put("deviceLocalIp",contact.split(":")[0]);
   d.put("deviceLocalPort",contact.split(":")[1]);*/
       deviceInfo.setLocalIp(contact.split(":")[0]);
       deviceInfo.setLocalPort(contact.split(":")[1]);

       String deviceId=map.get("deviceId");
       deviceInfo.setSsrc(Utils.getSsrc("010000"+deviceId.substring(deviceId.length()-4)));
   }
}
注册验证机制

发送过去验证的参数系要SIP域(realm)和一串随机数(nonce)

public static void main(String[] args) {
    //ha1=md5(username:realm:password)
    //ha2=md5(Method:Uri)
    //RESPONSE=md5(HA1:nonce:HA2)
    
    //摄像头id 第二次注册参数会携带
    String username="34020000001110000003";
    //SIP域,要跟摄像头里填写的一致
    String realm="3402000000";
    //存在服务器和摄像头填写的注册密码,验证时必须要一致
    String password="123456";
    //发送给摄像头的随机数,第二次注册时摄像头回复也会携带该参数
    String nonce="962535b552b6e29883ff988c0065ddc2";
    //请求方法,这里直接就是注册
    String Method="REGISTER";
    //uri是第二次注册是的链接,第二次注册时摄像头回复也会携带该参数
    String Uri="sip:34020000002000000001@192.168.1.201:5060";
    //先生成ha1
    String ha1= DigestUtils.md5Hex(username+":"+realm+":"+password);
    //ha2
    String ha2= DigestUtils.md5Hex(Method+":"+Uri);
    //生成最终结果,然后对比摄像头传来的response属性,具体情形可参考上一节的注册信令解读
    //如果md5一致就是说明密码正确,然后验证通过,否则返回404给摄像头,或者不理会,再或者继续回复401要求验证密码等信息
    System.out.println(DigestUtils.md5Hex(ha1+":"+nonce+":"+ha2));
}
下一节心跳保活回复
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐