1. 介绍
前面公司做摄像头相关的项目,一开始使用的海康威视提供的相关接口来控制摄像头转动,获取位置等,后面发现由于大华的很多摄像头使用该接口却行不通,后面所以就转用通用的 onvif 协议来做相关的操作。
在网上找了很多篇文章,几乎都是c语言写的,java来做的很少,后面经过自己慢慢的摸索,终于自己用java写了一套比较完整的代码来使用onvif协议控制摄像头转动,获取摄像头位置,及登录授权获取token等接口。
onvif 协议只要摸入门,其实其他请求其接口还是很简单的, 有些人喜欢去下载wsdl文件来生成一个java代码架构,这种方式我也试过,虽然能生成出来,但是其中的代码太多,比如一个PTZ操作就是几十个java文件,
不方便查阅,使用起来困难。我比较推荐使用 (ONVIF Device Test Tool) 这款软件来连接摄像头,让后做相关的控制,它会给你呈现出你发送某个接口的xml文件是什么内容,以及返回的xml文件。拿到xml文件后你就
可以写一个http请求,通过发送xml流的方式来请求摄像头服务就可以了,这样来说,整个过程就相当简单了。
2. 项目结构
这点我先给大家介绍一下我们onvif一个项目结构。主要有两个类,一个 OnvifDeviceInvoker,这个类主要是读取相关的xml,发送http请求, 包含:获取云台位置参数,控制摄像头转动,获取token三个接口。
OnvifUtil: 主要包含用户密码加密,读取xml文件,发送xml流post请求, onvif值 与 osd值相互转化的工具类。 这点我介绍一为什么要做这个转化,因为我们转动摄像的角度通常用0°~360°来记录,而onvif值
却是通常用-1到+1来记录,不同的值标识不通的角度。而且大华和海康威视还对应的不一样。 onvif值 对应 osd值 如图所示:
GetProfiles.xml: 请求获取token的xml文件
GetStatus.xml: 请求获取摄像头位置的xml文件
AbsoluteMove.xml: 请求转动摄像头的xml文件
GetSnapshotUri.xml:请求抓取图片的xml文件。特别说一下: 请求这个xml流之后会返回一个xml文件,
获取里面的图片地址,然后再次请求这个地址,这个地址是需要经过digest认证,
所以在请求后第一次会返回 一个401状态码,获取heder参数 WWW-Authenticate,
用于digest 认证。示例如下:
请求示例:
生成digest加密后的认证字符串方法
拍照暂时不介绍了,具体代码,下面有完整的贴出!
3. 代码展示
CameraBrandEnum 标注品牌的枚举类,主要用于不同品牌的 ONVIF与OSD转化转化的区别
public enum CameraBrandEnum {
DH("大华", "DH"),
HKWS("海康威视", "HKWS");
private String name;
private String value;
CameraBrandEnum(String name, String value) {
this.name = name;
this.value = value;
}
public void setName(String name) {
this.name = name;
}
public void setValue(String value) {
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}
AbsoluteMoveReq, Digest,GetStatusResp,DigestAuthenticateDto:参数实体相关类
@Data
public class AbsoluteMoveReq {
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "摄像头云台垂直位置 y")
private int tiltPos;
@ApiModelProperty(value = "摄像头云台水平位置 x")
private int panPos;
@ApiModelProperty(value = "摄像头云台倍数参数")
private int zoomPos;
}
@Data
public class Digest {
/** 加密后的密码 */
private String password;
/** 随机字符串 */
private String nonce;
/** 时间 2010-09-16T07:50:45Z */
private String date;
/** 用户 */
private String username;
}
@Data
public class GetStatusResp {
@ApiModelProperty(value = "摄像头云台垂直位置 y")
private int tiltPos;
@ApiModelProperty(value = "摄像头云台水平位置 x")
private int panPos;
@ApiModelProperty(value = "摄像头云台倍数参数")
private int zoomPos;
}
/**
* Digest 认证实体
*/
@Data
public class DigestAuthenticateDto {
private String nonce;
private String qop;
private String realm;
}
OnvifUtil: 工具类
package com.zdkj.iot.shared.onvif.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson.JSON;
import com.zdkj.iot.commons.exception.IOTException;
import com.zdkj.iot.shared.onvif.dto.Digest;
import com.zdkj.iot.shared.onvif.dto.DigestAuthenticateDto;
import com.zdkj.iot.shared.onvif.enums.CameraBrandEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
@Slf4j
public class OnvifUtil {
/**
* 获取加密的用户密码
* @param psw 原密码
* @return 加密的密码
*/
public static Digest getEncryptionPassword(String username, String psw) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'",
Locale.getDefault());
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new IOTException("创建SHA-1加密方法失败");
}
// nonce需要用Base64解码一次
String nonce= getNonce();
String date = df.format(new Date());
byte[] b1 = Base64.decode(nonce.getBytes());
// 生成字符字节流
byte[] b2 = date.getBytes(); // "2018-01-10T11:00:00Z";
byte[] b3 = psw.getBytes();
// 根据我们传得值的长度生成流的长度
// 利用sha-1加密字符
md.update(b1, 0, b1.length);
md.update(b2, 0, b2.length);
md.update(b3, 0, b3.length);
Digest digest = new Digest();
digest.setNonce(nonce);
digest.setDate(date);
digest.setUsername(username);
digest.setPassword(Base64.encode(md.digest()).trim());
// 生成最终的加密字符串
return digest;
}
/**
* 替换授权相关的字符串
*/
public static String strRepelce(Digest digest, String str) {
return str.replace("${username}", digest.getUsername()).replace("${password}", digest.getPassword())
.replace("${nonce}", digest.getNonce()).replace("${createDate}", digest.getDate());
}
/**
* 读取文件转换为string
* @param path 路径 com/zdkj/onvif/media/request/GetProfiles.xml
* @return 字符串
*/
public static String read