代码只对海康和大华做了一些适配,仅供学习参考。不同摄像头可能不太一样,可以自行抓包适配。解析消息直接是字符串操作,比较方便,不依赖第三包。
摄像头注册代码
//接收到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));
}