简介:这是一个面向Java开发者的Onvif协议封装SDK,基于Spring Boot与SOAP通信实现,适合需要快速对接网络摄像头、构建视频监控系统的工程师。压缩包共48个文件,主要包括3个Java源码(含TestController.java调用示例)、38个XML配置、2个properties配置、1个jar依赖包,以及少量class、pb等辅助文件,整体仅10.52MB,轻量易用。SDK已实现获取Authorization与token列表、获取截图URL与视频流地址、预置位获取与跳转、云台启动/停止控制以及设备自动发现等完整功能,覆盖了监控设备对接的常用场景。目前已吸引299人学习,适合希望免去Onvif协议底层对接成本、快速验证功能的开发者;如需深入二次开发,也可通过配套源码直接查阅与扩展。
1. ONVIF SDK:安防摄像头接入为什么这么费劲
接过安防项目的工程师都懂,新项目里最花时间的往往不是业务逻辑,而是把一路或几路IPC接进来。各厂商的设备SDK五花八门,协议私有,文档随缘,联调时经常被迫在几套SDK之间来回切。ONVIF协议出现的初衷就是把这些私有接口统一掉。基于标准WSDL生成一套客户端,再按设备能力集做一次封装,对外暴露发现、取流、云台、OSD这几个常用接口,这就是"自己封装的onvif-sdk"在做的事。标题里的TestController.java是可执行的Demo入口,通过它你可以快速确认通道、取到RTSP地址、验证PTZ是否可用,而不用先啃一遍协议文档。本文把这套封装方案拆开讲清楚:WSDL怎么转stub、设备发现怎么做、RTSP地址怎么拿、联调时哪些坑最常见,适合刚接触ONVIF开发、或正被厂商SDK折腾的Java后端工程师。
2. ONVIF SDK的核心架构:三条链路合并成一个门面
2.1 从WSDL生成stub开始:ONVIF协议不是REST接口
第一次接触ONVIF的人容易拿它当REST来用,实际上ONVIF是基于SOAP的WebService协议,走HTTP传输,但消息体是XML格式的SOAP Envelope。SDK封装的第一步,是把ONVIF官方的WSDL文件转换成Java客户端类。常见做法是用Apache Axis2或CXF的wsdl2java工具。我一般选用Axis2,它对ONVIF设备兼容性较好,生成的stub类直接对应设备上的服务。
java -cp axis2-1.7.9/lib/org.apache.ws.commons.axiom.jar:axis2-1.7.9/lib/axis2-kernel.jar \ org.apache.axis2.wsdl.WSDL2Java \ -uri devicemgmt.wsdl \ -p com.example.onvif.gen \ -o ./src/main/java生成后的目录结构会按命名空间拆开,ver10、ver20分别对应不同版本的ONVIF规范。封装层做的事情就是把这一堆stub类收口成几个Manager类,比如DeviceManager、MediaManager、PtzManager。外面调用时只需要传入IP和凭据,不用关心内部是哪个服务、哪个端口。这里的关键参数是-uri指向的WSDL文件版本。ONVIF有2.0、2.2、2.4、2.6等多个版本,不同厂商支持程度不同,尽量用2.4以上的WSDL生成。-p指定包名,注意不要和业务代码混在同一个包下,否则后续升级WSDL时容易误删业务类。
2.2 三条核心链路:发现、媒体、控制
ONVIF设备上跑的是一组WebService服务,最常见的是这三个:Device Management(设备信息、系统重启、网络配置)、Media(视频源、 profiles、RTSP地址获取)、PTZ(云台控制)。SDK封装时要为这三条链路各建一个门面类,后面再通过一个总入口暴露出去。
发现链路上,ONVIF用的是WS-Discovery协议,本质上是一个UDP多播。设备启动后会加入239.255.255.250:3702这个多播组,客户端同样发多播报文,收到设备的ProbeMatch响应后就能拿到设备IP和XAddr地址。媒体链路的核心是两层结构:先列出Media服务下的所有Profile,再从指定Profile里拿StreamUri。这个StreamUri就是RTSP地址,拿到后可以用VLC、FFmpeg或自己写的播放器去拉流。
控制链路相对简单,PTZ服务的ContinuousMove、Stop、GotoHomePosition等操作都是标准的SOAP调用,但不同厂商云台速度曲线差别很大,封装时要把速度参数归一化,避免直接暴露0到1的浮点值让上层业务去调。
public class OnvifSdk { private final DeviceManager deviceManager; private final MediaManager mediaManager; private final PtzManager ptzManager; public OnvifSdk(OnvifConfig config) { this.deviceManager = new DeviceManager(config); this.mediaManager = new MediaManager(config); this.ptzManager = new PtzManager(config); } public List<OnvifDevice> discover(int timeoutSeconds) { return deviceManager.discover(timeoutSeconds); } public String getRtspUrl(OnvifDevice device, String profileToken) { return mediaManager.getStreamUri(device, profileToken); } }这个门面类的设计核心是:上层业务只依赖OnvifSdk这个类,不感知SOAP细节。OnvifConfig里保存连接超时、请求超时、重试次数等参数,不同设备差异很大,后面第4章会细说。discover返回的OnvifDevice对象里,除了IP和端口,还保留了XAddr、DeviceId、固件版本等字段,这些在后续排查问题时非常有用。
2.3 按设备能力做动态降级:不是每台IPC都有PTZ
ONVIF协议虽然统一了接口,但设备能力差异巨大。一台几百块的固定枪机可能只实现了Device Management和Media,PTZ服务完全没有。一台高速球机则有完整的PTZ,还带辅助对焦和预置位功能。封装SDK时不能假设所有设备都支持全部接口,必须在运行时探测设备能力。
常见做法是启动时拉一次GetCapabilities,把返回的Capability信息缓存到内存里。比如capabilities.getPtz() == null就说明这台设备不支持云台控制,此时前端按钮应置灰而不是点击后报错。媒体服务里的GetProfiles返回的每个Profile包含视频编码、分辨率、帧率信息,有的设备Profile下还有多个VideoSourceConfiguration,需要按需选择。
public class MediaManager { public List<Profile> getProfiles(OnvifDevice device) { MediaBindingStub stub = new MediaBindingStub(); stub._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, device.getMediaXAddr()); GetProfilesResponse response = stub.getProfiles(new GetProfiles()); return Arrays.asList(response.getProfiles()); } }代码里的ENDPOINT_ADDRESS_PROPERTY是用来设置SOAP请求的目标地址的,ONVIF的XAddr就是设备暴露的服务端点,一般是http://ip:port/onvif/device_service这类地址。GetProfilesResponse解析出来的Profile对象里,比较关键的字段是token、Name、EncoderConfiguration。其中的token在后续调用GetStreamUri时要用,所以要缓存下来。
判断能力时还要注意一点:GetCapabilities返回的Category参数可以控制返回范围,建议分两次调用,一次拿All,一次拿Media和PTZ。有的设备在All模式下会超时,分开调用反而稳定。
3. 用TestController.java跑通IPC发现:最小调用路径
3.1 TestController是什么:一个被HTTP包住的ONVIF客户端
标题里的TestController.java是一个Spring MVC或Spring Boot风格的Controller类,它的意义在于把ONVIF SDK的调用能力暴露成HTTP端点,这样你在浏览器里就能触发发现、获取RTSP地址、转动云台,而不需要写独立的命令行程序。对没有UI的调试环境来说,这个Controller就是整个项目的中控台。
@RestController @RequestMapping("/api/onvif") public class TestController { private final OnvifSdk onvifSdk; public TestController(OnvifSdk onvifSdk) { this.onvifSdk = onvifSdk; } @GetMapping("/discover") public List<OnvifDevice> discover(@RequestParam(defaultValue = "5") int timeout) { return onvifSdk.discover(timeout); } }这段代码的逻辑非常直白:调用SDK的discover方法,通过UDP多播在局域网内找设备,超时时间默认为5秒,找到的设备列表直接以JSON形式返回。用浏览器访问http://localhost:8080/api/onvif/discover就能看到当前网段下有哪些IPC在线。如果项目还没有引入Spring Boot,也可以写成Servlet版本,但Controller的好处在于Spring自动处理JSON序列化,不会因为手写XML序列化而踩坑。
3.2 跑通设备发现:UDP组播的玄学时刻
ONVIF的设备发现走的是WS-Discovery,用UDP多播实现。这里有个常见的误区:认为发现不到设备是代码写错了,实际上多半是网络环境问题。你的开发机和IPC必须处于同一个二层网络,也就是同一台交换机下面。如果中间隔了路由器和三层交换机,广播包和组播包不会穿透过去,这是协议本身的设计限制。
# 验证本机是否能正常收发多播 ping -c 3 239.255.255.250ping通这个组播地址只说明本机路由表里有多播路由,并不代表设备就能收到Probe报文。更直接的办法是用Wireshark抓包,在调试机网卡上抓UDP端口3702的报文,如果能抓到设备回包但程序收不到,那就是防火墙或socket绑定问题。
Java里发送WS-Discovery报文时,有个参数特别容易忽略:socket的setReuseAddress。如果上次程序异常退出,端口没有释放干净,新socket绑定相同地址会失败。而且响应监听通常绑定在随机端口上,不能直接用发送socket接收响应,必须单独监听。我封装SDK时是这么处理的:
public List<OnvifDevice> discover(int timeoutSeconds) { DatagramSocket socket = new DatagramSocket(); socket.setSoTimeout(timeoutSeconds * 1000); socket.setReuseAddress(true); String probeMessage = buildProbeMessage(); byte[] requestBytes = probeMessage.getBytes(StandardCharsets.UTF_8); InetAddress multicastGroup = InetAddress.getByName("239.255.255.250"); DatagramPacket requestPacket = new DatagramPacket(requestBytes, requestBytes.length, multicastGroup, 3702); socket.send(requestPacket); List<OnvifDevice> devices = new ArrayList<>(); long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L; while (System.currentTimeMillis() < deadline) { byte[] buffer = new byte[8192]; DatagramPacket responsePacket = new DatagramPacket(buffer, buffer.length); try { socket.receive(responsePacket); OnvifDevice device = parseProbeMatch(responsePacket); devices.add(device); } catch (SocketTimeoutException e) { break; } } socket.close(); return devices; }buildProbeMessage构造的SOAP报文里,wsa:Action必须是http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe,d:Types要写成tds:NetworkVideoTransmitter或留空探测所有设备。有的设备对Types字段很敏感,传了不匹配的类型就直接不响应。这里留空是最稳妥的,走的是兼容性优先的路线。
3.3 获取RTSP地址:GetStreamUri的前置条件
设备发现只是起点,真正接入播放链路还要拿到RTSP地址。这一环在TestController里对应的是/streamUri端点。第一次调GetStreamUri时需要带上前面提到的ProfileToken,所以要先调GetProfiles拿到profile列表,再取第一个profile的token。很多新手直接把profileToken写死,结果换一台设备后就拿不到流。
@GetMapping("/streamUri") public String streamUri(@RequestParam String ip, @RequestParam String username, @RequestParam String password) throws Exception { OnvifDevice device = OnvifDeviceBuilder.fromIp(ip) .withCredentials(username, password) .build(); String profileToken = onvifSdk.getFirstProfileToken(device); return onvifSdk.getRtspUrl(device, profileToken); }这里的OnvifDeviceBuilder是SDK内部的一个构造器类,负责探测设备的服务地址和鉴权信息。它的build方法内部会调用GetCapabilities和GetProfiles,加载设备的能力信息和媒体配置。拿到RTSP地址后,建议直接用FFmpeg或者VLC拉流验证是否可播放,避免上游地址不可用还继续往下游调试。
RTSP地址的形式一般是rtsp://ip:554/Streaming/Channels/101,但不同品牌差异很大。海康的设备端口不一定在554,大华的地址带编码参数,宇视的路径规则也自成一套。SDK封装时不要把地址格式写死在代码里,直接透传GetStreamUri返回的值即可。
4. 设备参数配置:必调的5个连接参数与认证机制
4.1 五个必调参数:超时、重试、端口、编码、缓冲
ONVIF联调时,默认参数几乎必然要改。SDK里我建议把下面这几个参数暴露成配置文件或构造参数,后面排障会省很多时间:
| 参数名 | 建议值 | 说明 |
|---|---|---|
| connectTimeout | 3000ms | 建立TCP连接的超时,局域网内一般3000ms足够 |
| requestTimeout | 10000ms | SOAP请求的响应超时,部分设备启动慢,可放宽到15秒 |
| retryCount | 2 | 请求失败后的重试次数,不要设太多次,避免设备崩溃 |
| maxContentLength | 2MB | SOAP响应体大小限制,部分设备GetProfiles时返回较大XML |
| multicastSocketBuffer | 1MB | 发现设备时接收UDP报文的socket缓冲区 |
如果connectTimeout设置太短,设备在线但响应慢时就直接超时。设置太长,设备不在线时调用方要等很久。maxContentLength尤其容易被忽略,Axis2默认的SOAP响应体限制是1MB左右,有的设备返回的Profiles信息特别长,超过限制就直接报错。这里的参数必须在stub初始化时设置:
public MediaBindingStub buildMediaStub(OnvifDevice device) { MediaBindingStub stub = new MediaBindingStub(); stub._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, device.getMediaXAddr()); stub._setProperty(Stub.CONNECTION_TIMEOUT_PROPERTY, config.getConnectTimeout()); stub._setProperty(Stub.SO_TIMEOUT_PROPERTY, config.getRequestTimeout()); return stub; }CONNECTION_TIMEOUT_PROPERTY和SO_TIMEOUT_PROPERTY是Axis2定义的标准属性。前者的作用是限制TCP三次握手的时间,后者的作用是限制等待SOAP响应的时间。调试时如果发现设备操作一直卡住,优先检查这两个值是否被设置成了默认值0,0在Axis2里代表无限等待,容易让调用线程一直挂死。
4.2 认证机制:WS-Security UsernameToken的坑
ONVIF的第一个版本只支持HTTP Digest认证,后来的版本开始支持WS-Security UsernameToken。两种方式对密码的传输和处理完全不同,封装时要做兼容。用Axis2生成的stub,默认走的是WS-Security,路径上通过Client对象添加一个处理handler。
public static void applyAuthentication(Stub stub, String username, String password) { Client client = stub._getServiceClient(); client.addInterceptor(new UsernameTokenInterceptor(username, password)); } public static class UsernameTokenInterceptor implements Handler { // 在SOAP头部插入UsernameToken节点 }实际使用中需要注意,有些老型号的设备(特别是改过固件的)只支持HTTP Digest,对WS-Security的处理有bug,收到请求后返回401 Unauthorized。这时需要降级到Digest认证。我在SDK里用了一个简单的策略:先尝试WsSecurity,返回401就切换成Digest,并缓存设备支持的认证方式,避免每次连接都重复试探。
4.3 设备时间偏移:一个隐蔽的认证失败原因
搭好认证后,如果发现能请求服务但总是收到ExpiredToken错误,多半是设备时间和你开发机的时间不同步。WS-Security UsernameToken 里的wsu:Created字段带了时间戳,设备在服务端校验这个时间戳,超出容忍范围就会拒绝,常见的容忍值是5分钟。
# 查看设备时间,多数IPC支持通过NTP同步 curl --digest -u admin:password "http://192.168.1.64/ISAPI/System/time"如果项目环境里没有NTP服务器,临时方案是在SDK里对时间戳做偏移补偿。具体做法是启动时从GetSystemDateAndTime接口读取设备时间,和本机时间算出差值,在生成SOAP消息时对这个差值做补偿。这个做法属于投机取巧,正式环境不推荐,正规做法还是在工程上部署NTP同步。但做SDK封装时留了这个补偿开关,确实能省不少售后沟通的时间。
5. ONVIF设备接入联调避坑:5条必踩记录
5.1 WebService客户端初始化失败:WSDL缓存漂移
现象是SDK在开发环境跑得好好的,部署到服务器后第一次调用直接抛AxisFault异常,定位到是WSDL2Java生成的那批stub类初始化失败。原因是开发环境用的是最新WSDL生成的代码,服务器上引用的依赖版本是旧的,WSDL里定义的元素和生成的Java类对不上。解决方法是不要直接引用依赖里的WSDL生成代码,把生成后的类和WSDL文件一起提交到代码库,版本由我们控制,而不是交给依赖管理工具去碰运气。这一步在CI上很容易翻车,因为新环境默认拉最新依赖,一拉旧代码就炸。
5.2 IPC时间不准导致认证401
现象是设备接入后,先调用GetDeviceInformation正常,再调带凭据的接口时报401。原因排查后发现设备上时间比真实时间慢了一个多小时,我们这边按本机时间生成的SOAP报文在设备端校验时间戳失败。解决有三个方向:一是同步设备时间到NTP服务器;二是调整SDK的时间容忍窗口,这个窗口在部分设备上能配置;三是在SDK初始化时检测设备时间和本机时间差值,超过5分钟就主动告警提示运维介入。第三种做法在已上线的系统里很有用,至少日志里马上能定位到问题,不用背着抓包工具去现场。
5.3 UDP组播发现不到设备:又是二层广播隔离
现象是程序跑起来后设备列表永远为空,但设备确实在线,用VLC手动输RTSP地址又能拉流。用Wireshark抓包发现,本机的Probe报文已经发到组播地址,但设备毫无回包。原因是开发机连接的是有线的办公网络,IPC接在无线AP上,AP默认开启了客户端隔离,局域网内的组播和广播报文不会在无线客户端之间转发。解决方法是把开发机和IPC接到同一个二层交换机上,或者临时在无线AP上关掉客户端隔离选项。这种网络环境问题在项目交付时尤其常见,排查时先确认网络拓扑,再怀疑代码。
5.4 RTSP串流花屏或卡顿:MTU和码流参数作怪
现象是RTSP地址已经能拿出来,播放器也能连上,但画面花屏严重,或者卡顿明显。花屏的第一排查点是MTU,部分摄像机网卡的MTU设置得比交换机小,网络传输时大包被丢弃。排查办法是在开发机上用ping带大包测试链路:
ping -s 1472 -M do 192.168.1.64如果带1472字节的包不通,基本可以判断是MTU问题。不是每一次都要下现场改交换机,先把播放器端的缓存加大,再把请求的码流参数改成低分辨率,很多情况下能绕过。
5.5 GetStreamUri返回错误格式的RTSP地址
现象是GetStreamUri返回的地址不是rtsp://开头,而是一段类似rtsp://ip:554/...?token=xxx的编码格式,直接丢给播放器播放不了。原因是个别厂商在RTSP地址里加了自定义参数,播放器无法解析。解决方法是SDK在返回StreamUri前,对地址做格式规范化,比如去掉多余的query参数或对特殊字符做URL解码。这个功能在对接大华设备时基本必开。
6. 进阶验证:Wireshark抓包核对与封装之后的扩展能力
6.1 用Wireshark核对ONVIF请求:比相信日志更可靠
联调最怕的就是客户端报了错,但不知道报文到底发出去了没有。SDK封装的层次多,日志打了无数行,不如直接抓包看电信号来得直接。抓包时只需要过滤ip.addr == 设备IP,然后看HTTP层的SOAP报文内容。
# tshark 同样能完成过滤 tshark -i eth0 -Y "ip.addr == 192.168.1.64" -w onvif_debug.pcap抓包结果里重点核对三个东西:HTTP头里的Authorization是否带了正确的凭据;SOAP Body里的ProfileToken是否和设备返回的一致;GetStreamUri响应里RTSP地址是否完整。这个习惯比依赖日志定位问题要快得多,尤其当设备返回的错误信息语义含糊时。
6.2 从ONVIF平滑迁移到GB28181平台
ONVIF的封装做完后,再对接GB28181平台就有了一个非常好的参照物。GB28181里大量设计思路和ONVIF相近,比如设备目录都是树状结构,通道编码和ONVIF的ProfileToken间接对应。做平台迁移时,只需维护一个映射表,把ONVIF的ProfileToken对应到GB28181的通道编码,控制信令走GB28181,流媒体仍走RTSP,中间不需要改动播放链路。这套混合方案在多项目交付时非常实用,因为GB28181的平台接入规范各家有出入,而ONVIF的RTSP串流是稳定的下限保证。
6.3 给SDK加一层健康检查:设备在线状态探活
最后给这个SDK补一个实用技巧,在封装层增加一个isDeviceHealthy(OnvifDevice device)方法,内部调用GetDeviceInformation并记录耗时。如果单次耗时超过5秒,就标记为亚健康,上层巡检模块可以据此切换备用链路。这个方法有两个好处:一是接口调用前不需要盲目重试,能节省大量无效请求;二是多设备场景下可以按健康状况做调度,优先把任务分发给响应快的设备。
ONVIF SDK封装这件事,做到能跑通只是起步,真正的价值在于把设备间的差异挡在业务层之外。我自己最深的体会是,不要把GetDeviceInformation返回的信息只当日志打出来,应该落库作为设备台账的一部分。这样以后设备批量升级固件、换IP、调整编码参数时,回溯问题能省一半时间。这套封装方案说起来简单,但真正把发现、取流、控制、探活这几条链路都串起来,踩完上面这些坑之后,你在安防项目里就有了一个随身可用的工具箱。希望帮到你。
本文还有配套的精品资源,点击获取