简介:这份 ONVIF JAVA SDK 面向从事网络摄像头接入、安防平台开发及物联网视频集成的 Java 工程师,帮助开发者快速理解并实现 ONVIF 协议中的设备发现、媒体配置、PTZ 控制等核心能力。压缩包共 1157 个文件,以 1147 个 java 源码为主体,辅以 5 个 jar 依赖库、2 个 gradle 构建脚本及 gradlew、bat、properties 等工程配置文件,整体约 2.32MB,结构完整,可直接导入构建。Demo 仅演示核心功能,复杂场景仍需研读源码,但 ONVIF 本质是 UDP 加 HTTP 加 XML 的组合,难点在于字段繁琐而非技术深度,适合具备一定网络编程基础的开发者深入定制。目前已有 656 人学习下载,读者可借此掌握协议交互流程、字段组织方式与设备对接思路,为二次开发和问题排查提供可复用的代码参考。
1. 从一份 ONVIF JAVA SDK 压缩包说起:网络摄像头接入到底难在哪
手里拿到一个名为ONVIF JAVA SDK.zip的包,第一反应往往不是兴奋,而是犯嘀咕——ONVIF 协议栈在 Java 生态里一直是个尴尬地带,C 语言有 gSOAP 生成的完整框架,Python 有 zeep、onvif-zeep 这类现成轮子,Java 这边要么依赖老旧的 Axis2 生成一堆臃肿 stub,要么自己手写 SOAP 报文拼到怀疑人生。这份 SDK 的价值就在于它把设备发现、能力协商、媒体配置、PTZ 控制、事件订阅这些网络摄像头接入的脏活累活打包好了,让你不用从 WSDL 开始啃。它适合两类人:一是做安防平台、NVR 客户端、视频管理系统的后端开发者,需要在 Java 服务里直接对接海康、大华、宇视等品牌的网络摄像头;二是做设备集成测试、批量配置工具的工程师,想用代码替代网页后台的重复点击。下面我按实际拆包和跑通的顺序,把这份资源从结构到落地讲清楚。
2. 拆开压缩包:ONVIF JAVA SDK 的目录结构与核心模块
2.1 先看目录树,判断它属于哪一类 SDK
拿到包先别急着 import,解压后第一件事是看目录结构。常见的 ONVIF Java SDK 有两种形态:一种是纯协议栈,只提供 SOAP 客户端和 WSDL 生成的类;另一种是带封装层的完整工具包,额外提供设备管理器、媒体管理器、PTZ 管理器。这份包从命名和典型组织方式看,大概率属于后者。解压后一般能看到类似这样的布局:
ONVIF JAVA SDK/ ├── lib/ # 依赖 jar 包 │ ├── onvif-core.jar │ ├── soap-client.jar │ └── ... ├── src/ # 源码或示例 │ ├── main/java/ │ │ └── com/onvif/ │ │ ├── device/ # 设备管理 │ │ ├── media/ # 媒体配置 │ │ ├── ptz/ # 云台控制 │ │ └── event/ # 事件订阅 │ └── test/java/ ├── wsdl/ # 协议描述文件 ├── samples/ # 示例代码 └── README.md判断重点看wsdl/目录是否完整。ONVIF 的核心服务有 Device、Media、PTZ、Event、Imaging、Analytics 等,如果 WSDL 只覆盖了 Device 和 Media,那 PTZ 和事件订阅就得自己补。另一个看lib/里的 SOAP 实现,如果是 Axis2,注意它和 JDK 版本有兼容坑;如果是自己实现的轻量 SOAP 客户端,通常更干净但功能边界要自己确认。
2.2 核心模块的职责划分
这份 SDK 的模块划分基本对应 ONVIF 的服务分类,理解每个模块的职责比死记 API 更重要:
| 模块 | 对应 ONVIF 服务 | 主要职责 |
|---|---|---|
| device | Device Service | 设备发现、能力查询、系统时间、网络配置、用户管理 |
| media | Media Service | 获取媒体配置、视频源、编码器配置、流地址生成 |
| ptz | PTZ Service | 云台方向控制、预置位、巡航 |
| event | Event Service | 事件订阅、拉取通知、属性状态查询 |
| imaging | Imaging Service | 图像参数调节(亮度、对比度、曝光) |
设备发现走的是 WS-Discovery,基于 UDP 组播,不需要知道摄像头 IP 就能扫到局域网内的设备。媒体配置是后续所有操作的基础——你得先拿到 Profile,才能生成 RTSP 流地址,才能控制 PTZ。事件订阅则依赖设备是否支持 PullPoint 或 Basic Notification,很多低端摄像头只支持其中一种,这是后面踩坑的重灾区。
2.3 依赖环境与版本确认
在动手写代码前,先把环境对齐。这份 SDK 通常要求 JDK 8 或以上,如果用了 Axis2 做 SOAP 层,JDK 11 以上可能会遇到 JAXB 模块被移除的问题。我一般会先建一个干净的 Maven 工程,把lib/下的 jar 用mvn install:install-file装进本地仓库,或者直接看有没有提供 pom。依赖里常见的组合是:
<!-- 典型依赖组合,具体版本以包内 lib 为准 --> <dependencies> <dependency> <groupId>org.apache.axis2</groupId> <artifactId>axis2-kernel</artifactId> <version>1.7.9</version> </dependency> <dependency> <groupId>commons-discovery</groupId> <artifactId>commons-discovery</artifactId> <version>0.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> </dependency> </dependencies>如果包内没有 pom,就把lib/下所有 jar 加到 classpath。注意 Axis2 需要axis2.xml配置文件,通常在lib/或resources/下,漏了会在初始化 SOAP 客户端时报ConfigurationException。这一步别偷懒,环境不对后面全是玄学问题。
3. 跑通第一个设备:发现、连接与获取 RTSP 流地址
3.1 WS-Discovery 设备发现
ONVIF 的设备发现不走 HTTP,而是 WS-Discovery,基于 UDP 组播地址239.255.255.250:3702。SDK 里一般会封装一个DeviceDiscovery或DiscoveryManager类。下面是一个典型的发现流程:
// 设备发现示例:发送 Probe 消息并收集响应 import com.onvif.device.DeviceDiscovery; import com.onvif.device.DiscoveredDevice; public class DiscoveryDemo { public static void main(String[] args) throws Exception { // 创建发现实例,默认组播地址和端口 DeviceDiscovery discovery = new DeviceDiscovery(); // 设置超时时间,单位毫秒,局域网一般 3000 足够 discovery.setTimeout(3000); // 开始探测,返回发现的设备列表 List<DiscoveredDevice> devices = discovery.probe(); for (DiscoveredDevice dev : devices) { // 设备服务地址,后续所有 SOAP 请求都发往这个 endpoint System.out.println("Endpoint: " + dev.getEndpoint()); // 设备类型和硬件信息,部分设备可能为空 System.out.println("Type: " + dev.getType()); System.out.println("Scopes: " + dev.getScopes()); } } }逻辑说明:probe()内部构造了一个 WS-Discovery 的 Probe SOAP 消息,通过 UDP 组播发出,然后监听单播回复。参数上,setTimeout太短会漏设备,太长会拖慢启动,局域网 3000ms 是经验值。getEndpoint()返回的是设备服务地址,通常是http://<ip>/onvif/device_service,这个地址是后续所有操作的入口。如果发现列表为空,先确认摄像头和本机在同一网段,且摄像头开启了 ONVIF 功能——很多品牌默认关闭,需要在网页后台手动打开。
3.2 建立设备连接与能力查询
拿到 endpoint 后,下一步是建立设备连接并查询能力。ONVIF 的能力查询(GetCapabilities)决定了这台设备支持哪些服务,是后续选型的关键依据:
// 建立设备连接并查询能力 import com.onvif.device.DeviceClient; import com.onvif.device.Capabilities; public class DeviceConnectDemo { public static void main(String[] args) throws Exception { String endpoint = "http://192.168.1.100/onvif/device_service"; // 创建客户端,传入设备服务地址 DeviceClient client = new DeviceClient(endpoint); // 如果设备开启了认证,需要设置用户名密码 client.setCredentials("admin", "password123"); // 查询设备能力 Capabilities caps = client.getCapabilities(); // 媒体服务地址,后续媒体操作要用 System.out.println("Media: " + caps.getMediaEndpoint()); // PTZ 服务地址,为空说明设备不支持云台 System.out.println("PTZ: " + caps.getPtzEndpoint()); // 事件服务地址 System.out.println("Events: " + caps.getEventEndpoint()); // 设备信息 System.out.println("Manufacturer: " + client.getDeviceInformation().getManufacturer()); System.out.println("Model: " + client.getDeviceInformation().getModel()); } }逻辑说明:setCredentials设置的是 HTTP Digest 认证的用户名密码,ONVIF 设备普遍用 Digest 而非 Basic,SDK 内部会自动处理 401 挑战。getCapabilities()返回的对象里,getMediaEndpoint()和getPtzEndpoint()是后续操作的 base URL,如果 PTZ 地址为空,说明这台设备不支持云台,后面就不用试了。getDeviceInformation()返回厂商、型号、固件版本,调试时先打印出来,方便对照设备手册。注意有些设备的能力查询需要认证,有些不需要,如果报 401 先检查凭据。
3.3 获取 Profile 与生成 RTSP 流地址
媒体服务的核心是 Profile,一个 Profile 代表一路可用的媒体配置,包含视频源、编码器、分辨率等。获取 RTSP 地址的流程是:先列出 Profile,再根据 Profile 生成流地址:
// 获取媒体 Profile 并生成 RTSP 地址 import com.onvif.media.MediaClient; import com.onvif.media.Profile; import com.onvif.media.StreamUri; public class MediaDemo { public static void main(String[] args) throws Exception { String mediaEndpoint = "http://192.168.1.100/onvif/media_service"; MediaClient mediaClient = new MediaClient(mediaEndpoint); mediaClient.setCredentials("admin", "password123"); // 获取所有 Profile List<Profile> profiles = mediaClient.getProfiles(); for (Profile profile : profiles) { System.out.println("Profile Token: " + profile.getToken()); System.out.println("Profile Name: " + profile.getName()); // 获取该 Profile 的流地址 StreamUri uri = mediaClient.getStreamUri(profile.getToken()); System.out.println("RTSP URI: " + uri.getUri()); // 分辨率信息 System.out.println("Resolution: " + profile.getVideoEncoderConfiguration().getResolution()); } } }逻辑说明:getProfiles()返回的列表里,每个 Profile 的 token 是唯一标识,后续 PTZ 控制、流地址获取都要用这个 token。getStreamUri()返回的 RTSP 地址通常带认证信息,格式类似rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101。参数上,有些设备支持多路 Profile,主码流和子码流分开,主码流分辨率高但带宽大,子码流适合预览。如果getStreamUri()返回的地址无法播放,先确认 RTSP 端口是否开放,再用 VLC 手动测试地址是否有效。
4. PTZ 控制与事件订阅:从能看 to 能控、能感知
4.1 PTZ 方向控制与预置位
PTZ 控制是网络摄像头接入里最容易翻车的部分,因为不同厂商对 ONVIF PTZ 的实现差异很大。基本流程是先获取 PTZ 配置,再发控制命令:
// PTZ 控制示例:连续移动与预置位 import com.onvif.ptz.PTZClient; import com.onvif.ptz.PTZConfiguration; public class PTZDemo { public static void main(String[] args) throws Exception { String ptzEndpoint = "http://192.168.1.100/onvif/ptz_service"; PTZClient ptzClient = new PTZClient(ptzEndpoint); ptzClient.setCredentials("admin", "password123"); // 获取 PTZ 配置,拿到 Profile token List<PTZConfiguration> configs = ptzClient.getConfigurations(); String profileToken = configs.get(0).getProfileToken(); // 连续移动:x 为左右,y 为上下,z 为缩放,范围 -1.0 到 1.0 // 向右移动,速度 0.5 ptzClient.continuousMove(profileToken, 0.5f, 0.0f, 0.0f); Thread.sleep(1000); // 停止移动 ptzClient.stop(profileToken); // 设置预置位,token 为预置位编号 ptzClient.setPreset(profileToken, "preset1"); // 调用预置位 ptzClient.gotoPreset(profileToken, "preset1"); } }逻辑说明:continuousMove的三个参数分别是 x、y、z 轴速度,范围 -1.0 到 1.0,0 表示该轴不动。调用后设备会持续移动,必须显式调用stop才会停,这是新手最容易忘的一步——不 stop 摄像头会一直转到底。setPreset和gotoPreset的 token 是预置位标识,有些设备要求先setPreset再gotoPreset,有些支持直接跳转。参数上,速度值不要给太大,0.5 左右比较稳,给 1.0 有些设备会丢步或报错。
4.2 事件订阅与通知拉取
事件订阅让程序能感知摄像头的移动侦测、遮挡报警、数字输入变化等。ONVIF 事件有两种模式:PullPoint 和 Basic Notification,前者由客户端主动拉取,后者由设备推送。PullPoint 更常用,因为不需要公网回调地址:
// 事件订阅示例:创建 PullPoint 订阅并拉取通知 import com.onvif.event.EventClient; import com.onvif.event.PullPointSubscription; import com.onvif.event.NotificationMessage; public class EventDemo { public static void main(String[] args) throws Exception { String eventEndpoint = "http://192.168.1.100/onvif/event_service"; EventClient eventClient = new EventClient(eventEndpoint); eventClient.setCredentials("admin", "password123"); // 创建 PullPoint 订阅,超时时间 600 秒 PullPointSubscription sub = eventClient.createPullPointSubscription(600); System.out.println("Subscription URL: " + sub.getSubscriptionUrl()); // 循环拉取通知 while (true) { // 拉取消息,超时 10 秒 List<NotificationMessage> messages = sub.pullMessages(10); for (NotificationMessage msg : messages) { // 事件主题,如 tns1:RuleEngine/CellMotionDetector/Motion System.out.println("Topic: " + msg.getTopic()); // 事件时间 System.out.println("Time: " + msg.getUtcTime()); // 事件数据 System.out.println("Data: " + msg.getMessage()); } } } }逻辑说明:createPullPointSubscription创建一个订阅,返回的 URL 是后续拉取消息的地址。pullMessages的参数是超时秒数,设备会 hold 住请求直到有消息或超时,所以这个值不要设太小,否则空轮询太频繁。事件主题(Topic)是判断事件类型的关键,移动侦测通常是CellMotionDetector/Motion,遮挡报警是TamperDetector。参数上,订阅有有效期,到期前要续订,否则会静默失效——这是生产环境里最隐蔽的坑之一。
4.3 多设备批量接入的线程模型
实际项目里很少只接一台摄像头,批量接入时线程模型要提前设计。我一般用固定线程池,每台设备一个独立线程负责发现、连接、订阅,主线程只做调度和状态汇总:
// 批量接入的线程池示例 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class BatchConnectDemo { public static void main(String[] args) { // 固定 10 个线程,对应 10 台设备 ExecutorService pool = Executors.newFixedThreadPool(10); List<String> endpoints = getDeviceEndpoints(); for (String endpoint : endpoints) { pool.submit(() -> { try { DeviceClient client = new DeviceClient(endpoint); client.setCredentials("admin", "password123"); // 每台设备独立维护连接和订阅 client.getCapabilities(); // ... 后续操作 } catch (Exception e) { // 单台失败不影响其他设备 System.err.println("Device " + endpoint + " failed: " + e.getMessage()); } }); } pool.shutdown(); } }逻辑说明:每台设备独立线程的好处是单台超时或异常不会阻塞其他设备。参数上,线程池大小根据设备数量调整,一般不超过 20,太多会耗尽文件描述符。注意 ONVIF 的 SOAP 客户端不是线程安全的,每台设备要独立创建客户端实例,不要共享。如果设备数量超过 50,建议加连接池和重试机制,否则网络抖动会导致大量设备掉线。
5. 避坑与排查:ONVIF Java 接入的五个血泪经验
5.1 设备发现不到任何摄像头
现象:probe()返回空列表,但摄像头网页能打开。原因通常是三个:一是摄像头 ONVIF 功能未开启,很多品牌默认关闭;二是本机防火墙拦截了 UDP 3702 端口;三是摄像头和本机不在同一网段,组播跨不了路由。解决:先登录摄像头后台确认 ONVIF 已启用,再检查本机防火墙放行 UDP 3702,最后确认网段。如果还不行,用 Wireshark 抓包看有没有收到 ProbeMatch 回复。
5.2 认证失败报 401 或 500
现象:调用getCapabilities()或getProfiles()时报 401 Unauthorized 或 500 Internal Error。原因:ONVIF 用 HTTP Digest 认证,如果 SDK 的 SOAP 客户端没正确处理挑战,就会反复 401。有些设备还要求密码不能含特殊字符,或者用户名必须是admin。解决:先确认用户名密码正确,再用 curl 手动测试 Digest 认证是否通过。如果 SDK 支持,开启 SOAP 日志看完整的请求响应报文,定位是认证头没带还是密码错误。
5.3 RTSP 地址拿到但无法播放
现象:getStreamUri()返回了地址,但 VLC 或播放器打不开。原因:一是地址里的认证信息没带或格式不对;二是 RTSP 端口不是默认 554;三是设备只允许单路连接,已有其他客户端占用。解决:先用 VLC 手动输入地址测试,确认地址本身有效。如果地址带认证,注意 URL 编码,密码里的@或:要转义。端口问题查设备手册,有些设备 RTSP 端口是 8554。单路连接限制则要断开其他客户端再试。
5.4 PTZ 控制无响应或报错
现象:调用continuousMove后摄像头不动,或报ActionNotSupported。原因:一是设备不支持 PTZ,能力查询里 PTZ 地址为空;二是 Profile token 不对,PTZ 要用媒体服务的 Profile token;三是速度值超出设备支持范围。解决:先确认getCapabilities()里 PTZ 地址非空,再用正确的 Profile token。速度值从 0.1 开始试,逐步加大。如果设备支持 PTZ 但报错,检查是否要先调用getConfigurationOptions获取支持的命令列表。
5.5 事件订阅静默失效
现象:订阅创建成功,但一段时间后收不到任何通知。原因:ONVIF 订阅有有效期,到期后设备自动终止订阅,但不会主动通知客户端。解决:在订阅到期前续订,或者用定时任务定期重建订阅。我一般把订阅有效期设为 600 秒,然后在 500 秒时续订。另外,PullPoint 的pullMessages超时不要设太长,否则续订线程会被阻塞。如果设备支持 Basic Notification,可以考虑改用推送模式,但需要公网回调地址。
6. 进阶技巧:用 ONVIF 做设备能力探测与自动化配置
跑通基本流程后,这份 SDK 真正省事的地方在于批量设备的能力探测和自动化配置。我一般会写一个探测脚本,遍历网段内所有设备,把能力、Profile、流地址、PTZ 支持情况导出成表格,作为后续配置的依据。核心思路是:先 WS-Discovery 发现设备,再逐台查询能力,最后把结果结构化输出。
// 设备能力探测与导出示例 import com.onvif.device.DeviceDiscovery; import com.onvif.device.DeviceClient; import com.onvif.device.Capabilities; import com.onvif.media.MediaClient; import com.onvif.media.Profile; public class CapabilityProbe { public static void main(String[] args) throws Exception { DeviceDiscovery discovery = new DeviceDiscovery(); discovery.setTimeout(5000); List<DiscoveredDevice> devices = discovery.probe(); // 输出表头 System.out.println("IP,Manufacturer,Model,Media,PTZ,Events,Profiles,RTSP"); for (DiscoveredDevice dev : devices) { try { DeviceClient client = new DeviceClient(dev.getEndpoint()); client.setCredentials("admin", "password123"); Capabilities caps = client.getCapabilities(); String manufacturer = client.getDeviceInformation().getManufacturer(); String model = client.getDeviceInformation().getModel(); // 媒体服务 boolean hasMedia = caps.getMediaEndpoint() != null; boolean hasPtz = caps.getPtzEndpoint() != null; boolean hasEvent = caps.getEventEndpoint() != null; // 获取 Profile 和流地址 String rtsp = ""; int profileCount = 0; if (hasMedia) { MediaClient mediaClient = new MediaClient(caps.getMediaEndpoint()); mediaClient.setCredentials("admin", "password123"); List<Profile> profiles = mediaClient.getProfiles(); profileCount = profiles.size(); if (profileCount > 0) { rtsp = mediaClient.getStreamUri(profiles.get(0).getToken()).getUri(); } } // 输出一行 CSV System.out.printf("%s,%s,%s,%s,%s,%s,%d,%s%n", dev.getEndpoint(), manufacturer, model, hasMedia, hasPtz, hasEvent, profileCount, rtsp); } catch (Exception e) { // 单台失败输出错误行 System.out.printf("%s,ERROR,%s%n", dev.getEndpoint(), e.getMessage()); } } } }这段代码的关键在于异常隔离——每台设备独立 try-catch,单台失败不影响整体探测。参数上,setTimeout(5000)给发现留足时间,能力查询的超时用 SDK 默认值即可。输出的 CSV 可以直接导入 Excel 或数据库,作为设备台账。我一般还会加一列固件版本,方便后续批量升级时对照。
另一个实用技巧是预置位批量配置。很多项目需要把摄像头统一转到某个角度再设预置位,手动一台台点太慢。用 SDK 可以批量执行:先continuousMove转到目标角度,stop后setPreset,整个过程用线程池并行,几十台设备几分钟搞定。注意每台设备操作之间加 500ms 间隔,太快有些设备会丢命令。
从那以后我每次拿到新的 ONVIF SDK,都强制先跑一遍能力探测脚本,把设备台账拉出来再写业务代码。这样能提前发现哪些设备不支持 PTZ、哪些事件订阅模式不兼容,避免写到一半才发现设备能力对不上。希望帮到你。
本文还有配套的精品资源,点击获取