分享一下在工作中Java来使用ONVIF协议控制摄像设备的心得
概述
onvif协议简述
onvif协议最开始由索尼、博世和安讯士制定,是一个消息层的协议,它是基于soap(一种消息格式为xml的消息协议)的网络视频设备消息格式规范的协议,它的的传输层为http协议,简单来说Onvif协议就是一种全球标准的消息格式为xml标签格式的网络视频设备http请求协议,它的制定目的就是通过标准化接口和通信协议,使不同厂商的网络视频设备能够更方便地互相兼容和集成
ps:我为什么会研究这个的原因就是我们项目的摄像头品牌型号五花八门的。。。。。。
http请求响应示例
onvif协议http请求示例:
onvif协议响应示例:
具体实现
由于版本兼容等问题,在实现的时候选择了两个开源项目来使用
一、高封装onvif请求Java项目:
GitHub - fpompermaier/onvif: Java ONVIF libraryJava ONVIF library. Contribute to fpompermaier/onvif development by creating an account on GitHub.https://github.com/fpompermaier/onvif 该库Maven没有收录需要手动打包进本地仓
这个库其他博主有专门介绍,没啥好说的,作者封装的很好,问题就是对低版本的ONVIF协议设备不兼容无法连接上(用ONVIF协议16的设备正常调用,2.20版本的报鉴权错误)
-
简单示例(云台相对运动控制)
// 通过Onvif协议连接至云台
OnvifDevice onvifDevice = new OnvifDevice(ip, user, passWord);
// 获取云台操作对象
PTZ ptz = onvifDevice.getPtz();
// 获取profileToken
List<Profile> profiles = onvifDevice.getMedia().getProfiles();
String profileToken = profiles.get(0).getToken();
// 云台持以相对坐标进行运动
// 创建具体移动对象
PTZVector ptzVector = new PTZVector();
// 赋予移动对象ZOOM摄像头对焦缩放参数
Vector1D vector1D = new Vector1D();
vector1D.setX(zCoordinate.floatValue());
ptzVector.setZoom(vector1D);
// 赋予具体云台偏移xy值
Vector2D vector2D = new Vector2D();
// 设置x属性的值
vector2D.setX(xCoordinate.floatValue());
// 设置y属性的值
vector2D.setY(yCoordinate.floatValue());
ptzVector.setPanTilt(vector2D);
// 云台相对移动
ptz.relativeMove(profileToken,ptzVector, null);
log.info("ip:"+ip+" 设备云台用户"+user+"+正在移动,x:"+xCoordinate+",y:"+yCoordinate+",z:"+zCoordinate);
-
项目问题
该项目胜在方便,但是问题也不少,已经发现的有:
1.onvif低版本报错不可用
2.某些摄像头组件固件版本无法使用
3.在进行new OnvifDevice()时会进行发起onvif请求,建议使用单例模式,否则在调用量高的情况下会导致内存占用激增
二、通用自定义onvif请求Java项目
-
Maven坐标:
<dependency>
<groupId>be.teletask.onvif</groupId>
<artifactId>onvif</artifactId>
<version>1.0.2</version>
</dependency>
-
简单示例(连接设备显示设备信息)
值得注意的是,该库的API基本都是NIO的形式,是以自定义监听的方式来获取ONVIF协议的返 回结果
// ONVIF设备的IP地址、用户名和密码
String ipAddress = "设备IP地址";
String username = "用户名";
String password = "密码";
// 创建ONVIF管理器
OnvifManager onvifManager = new OnvifManager();
// 设置响应监听器,以便获取设备信息和媒体配置文件
onvifManager.setOnvifResponseListener(new OnvifResponseListener() {
@Override
public void onResponse(OnvifDevice onvifDevice, OnvifResponse response) {
// 在这里处理响应
}
@Override
public void onError(OnvifDevice onvifDevice, int errorCode, String errorMessage) {
// 在这里处理错误
}
});
// 创建ONVIF设备对象
OnvifDevice device = new OnvifDevice(ipAddress, username, password);
// onvifManager.sendOnvifRequest(device,new CustomPlaybackRequest());
// 获取设备信息
onvifManager.getDeviceInformation(device, new OnvifDeviceInformationListener() {
// 指定连接成功后的操作
@Override
public void onDeviceInformationReceived(OnvifDevice onvifDevice, OnvifDeviceInformation onvifDeviceInformation) {
// 对连接设备信息进行打印到控制台
System.out.println(onvifDeviceInformation.getModel());
}
});
对于ONVIF的其他API,该库基本没有进行封装,但是库提供了自定义请求类的方式,然后通过ONVIF设备管理器对象的发送请求来实现
-
自定义onvif请求类实示例
下面以获取回放设置的onvif请求为例自定义实现一个OnvifRequest类
// 实现库所提供的ONVIF请求接口并实现
public class GetReplayConfigurationRequest implements OnvifRequest {
@Override
public String getXml() {
// 构造请求的XML内容,此处可以访问ONVIF协议官网根据官方文档提供的api信息进行填写
return "<GetServiceCapabilities xmlns=\"http://www.onvif.org/ver10/replay/wsdl\"></GetServiceCapabilities>";
}
@Override
public OnvifType getType() {
// 指定请求类型 ,此处涉及到发送请求时的命名空间,根据所需进行设置
// 高版本协议貌似取消了命名空间的作用,因此使用OnvifType.CUSTOM也可以,低版本必须指定正确的命名空间
return OnvifType.GET_Repaly_URI;
}
}
-
发送自定义请求并处理响应(获取回放数据)
// 发送自定义请求并处理响应
// 创建监听器
OnvifResponseListener onvifResponseListener =new OnvifResponseListener() {
@Override
public void onResponse(OnvifDevice onvifDevice, OnvifResponse onvifResponse) {
System.out.println("onvifResponse.getXml() = " + onvifResponse.getXml());
}
@Override
public void onError(OnvifDevice onvifDevice, int i, String s) {
System.out.println(s);
System.out.println("失败");
}
};
// 将设置Onvif管理器监听
onvifManager.setOnvifResponseListener(onvifResponseListener);
// 调用方法发送请求
onvifManager.sendOnvifRequest(device,new GetReplayConfigurationRequest());
如果只是获取数据相关,只要实现OnvifRequest就能够实现,如果需要云台调用等需要携带参数的onvif请求,那么需要更改源码,原因是该库作者在封装的时候没有做SOAP的请求体封装,也就是说无法携带请求体参数
- 自定义源码修改示例(云台停止操作)
修改源码的关键在于需要修改请求体的构建,而该项目的请求体构建在于OnvifXMLBuilder类
修改OnvifXMLBuilder类
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package be.teletask.onvif;
public class OnvifXMLBuilder {
public static final String TAG = OnvifXMLBuilder.class.getSimpleName();
public OnvifXMLBuilder() {
}
public static String getSoapHeader() {
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" ><soap:Body>";
}
public static String getEnvelopeEnd() {
return "</soap:Body></soap:Envelope>";
}
public static String getDiscoverySoapHeader() {
return "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:tns=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\"><soap:Header><wsa:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action><wsa:MessageID>urn:uuid:%s</wsa:MessageID>\n<wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>\n</soap:Header><soap:Body>";
}
public static String getDiscoverySoapBody() {
return "<tns:Probe/>";
}
// 上面都为源码本身自带的构建参数
// 创建构建停止onvif请求的请求体 配置文件token 是否立即停止所有移动 是否立即停止焦距缩放
public static String getStopBody(String profileToken, Boolean panTilt, Boolean zoom) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
sb.append("<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" ")
.append("xmlns:tptz=\"http://www.onvif.org/ver20/ptz/wsdl\">")
.append("<s:Body>")
.append("<tptz:Stop>")
.append("<tptz:ProfileToken>").append(profileToken).append("</tptz:ProfileToken>");
// 添加可选参数
if (panTilt != null) {
sb.append("<tptz:PanTilt>").append(panTilt).append("</tptz:PanTilt>");
}
if (zoom != null) {
sb.append("<tptz:Zoom>").append(zoom).append("</tptz:Zoom>");
}
sb.append("</tptz:Stop>")
.append("</s:Body>")
.append("</s:Envelope>");
return sb.toString();
}
}
在源码中实际调用构建的OnvifExecutor,因此需要也同步修改
修改OnvifExecutor类
// 代码过长,这里就不全部贴出来了,就是在OnvifExecutor下增加以下方法
void sendStopRequest(OnvifDevice device, OnvifRequest request,String profileToken, Boolean panTilt, Boolean zoom) {
this.credentials.setUserName(device.getUsername());
this.credentials.setPassword(device.getPassword());
this.reqBody = RequestBody.create(this.reqBodyType, OnvifXMLBuilder.getStopBody(profileToken, panTilt,zoom));
this.performXmlRequest(device, request, this.buildOnvifRequest(device, request));
}
最后为了实际调用方便,还需要在OnvifManager中增加方法
修改OnvifManager类
// 代码过长,在OnvifManager类中添加一下方法即可
public void sendStopRequest(OnvifDevice device, OnvifRequest request,String profileToken, Boolean panTilt, Boolean zoom) {
this.executor.sendStopRequest(device, request,profileToken,panTilt,zoom);
}
自定义PTZ停止实际使用
// 代码比较长,这里截全停止功能的那一部分,这里的dto是前端传参,主要就是方向 速度 这两个参数
// 获取速度
float speed = dto.getSpeed();
//原0.01
float zSpeed = dto.getSpeed() / 10;
// 设置响应监听器,用于对响应进行处理,此处为了避免内存开销过大,使用了单例的OnvifManger对象,至于监听器为什么不单例,因为这是个弱引用对象GC会自动回收
ONVIF_MANGER.setOnvifResponseListener(new OnvifResponseListener() {
// 请求成功处理
@Override
public void onResponse(be.teletask.onvif.models.OnvifDevice onvifDevice, OnvifResponse onvifResponse) {
System.out.println("onvifResponse.getXml() = " + onvifResponse.getXml());
log.info("成功");
}
//请求失败处理
@Override
public void onError(be.teletask.onvif.models.OnvifDevice onvifDevice, int i, String s) {
System.out.println("error = " + s);
log.info("失败");
}
});
// 创建ONVIF设备对象,传入设备账号密码
be.teletask.onvif.models.OnvifDevice device = new be.teletask.onvif.models.OnvifDevice(dto.getIp(), USER_NAME, PASSWORD);
// 获取配置文件用于后续操作
ONVIF_MANGER.getMediaProfiles(device, new OnvifMediaProfilesListener() {
// 当成功获取到配置文件时即可开始PTZ操作
@Override
public void onMediaProfilesReceived(be.teletask.onvif.models.OnvifDevice onvifDevice, List<OnvifMediaProfile> list) {
// 取第一个profile就行,一般来说第一个就是摄像头本身,如果摄像头还配有多个云台则需要再处理
String token = list.get(0).getToken();
Integer type = dto.getType();
switch (type) {
case 0:
// 停止 PTZStopRequest是自定义请求类
ONVIF_MANGER.sendStopRequest(device, new PTZStopRequest(), token, true, true);
return;
补充:PTZStopRequest自定义请求类
import be.teletask.onvif.models.OnvifType;
import be.teletask.onvif.requests.OnvifRequest;
public class PTZStopRequest implements OnvifRequest {
@Override
public String getXml() {
// 构造请求的XML内容,此处可以访问ONVIF协议官网根据官方文档提供的api信息进行填写
return "<StopResponse xmlns=\"http://www.onvif.org/ver20/ptz/wsdl/Stop\"></StopResponse>";
}
@Override
public OnvifType getType() {
// 高版本协议貌似取消了命名空间的作用,因此使用OnvifType.CUSTOM也可以,低版本必须指定正确的命名空间
return OnvifType.GET_PTZ_URI;
}
}
三.ONVIF协议测试工具:
最后推荐几个很好用的onvif工具
ONVIF Device Manager :简单连接onvif设备的可视化工具,能够直接看视频操作云台之类的,功能偏少胜在方便
ONVIF Device Test Tool :主要用于调试的onvif设备可视化工具,基本上涵盖了所有的onvif请求的调试
四、后续
当初自己在找成功案例(抄作业==)的时候有些艰难,国内针对此业务的资料不多,主要都是以C/C#为主,所以接下来计划将以ONVIF-Java这个库作为基础对它进行了功能的追加和一些改进(主要是自己在工作中遇到的),空闲时间将代码整理好后续会将该项目开源并放链接在此贴下供各位参考,主要改动如下:
1.原项目是gradle项目,改为符合国内的web项目主流的Maven项目
2.增加整套云台操作api
3.增加事件订阅api,意在能在Java中简便的来监听网络视频设备的事件(如人脸识别、移动监测、入侵报警等)
4.将一些功能进行更改,比如将某些不需要异步操作的api改为同步
5.增加单元测试代码
自己基于ONVIF-Java修改后的源码如下,供各位参考
GitHub - Rchion/onvif-java-plus: 基于ONVIF-Java开源项目的拓展