免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

ONVIF V2.4 源码包实战:从设备发现到 PTZ 控制,一份能直接编译的协议栈底稿

ONVIF V2.4 源码包实战:从设备发现到 PTZ 控制,一份能直接编译的协议栈底稿 简介这份ONVIF源码V2.4面向从事IP视频监控、物联网设备开发的工程师与协议学习者提供ONVIF V2.4版本的客户端与服务器端实现重点覆盖设备发现功能可用于构建能自动发现并接入新设备的网络监控系统也适合作为研究ONVIF协议的实践材料。压缩包共18个文件约1.54MB以9个C源文件和6个头文件为核心另含Makefile构建脚本、readme说明及tcpdump抓包文件代码结构围绕设备管理、媒体服务、PTZ控制、事件订阅、SSDP/Bonjour发现、认证安全、SOAP/XML解析与网络通信等模块展开并附带onvif_test测试模块用于验证协议实现的正确性与性能。目前已有1509人学习下载读者可据此理解ONVIF通信流程、调试设备互操作问题并在此基础上开发符合标准的监控产品。1. ONVIF V2.4 源码包从设备发现到 PTZ 控制一份能直接编译的协议栈底稿如果你手头有一台天地伟业、海康或者大华的网络摄像机想把它接进自己的平台而不是用厂商那套黑匣子 SDK那你迟早会撞上 ONVIF。ONVIF 是网络视频接口论坛定的互操作规范说白了就是让不同品牌的 IPC、NVR、门禁能互相说话。这次拆的是一份 ONVIF V2.4 版本的源码包它不是某个厂商的私有实现而是协议栈层面的参考代码覆盖设备发现、能力协商、媒体配置、PTZ 控制这几条主线。适合谁适合正在做视频管理平台、需要对接多品牌设备、又不想被厂商 SDK 绑死的后端和嵌入式工程师。V2.4 这个版本号不是随便标的它对应的是 ONVIF Core 2.4 规范WS-Discovery、SOAP 1.2、gSOAP 这套东西都在里面。你拿到手第一件事不是急着编译而是先搞清楚它到底把哪些 Profile 做进去了这决定了你后面能不能直接抄作业。2. 拆开源码包先看什么目录结构、依赖和 Profile 覆盖范围2.1 目录结构里藏着这份源码的真实边界拿到一个 ONVIF 源码包我一般不会先看 README而是直接tree -L 2看目录。这份 V2.4 的包典型结构大致是这样顶层有src/、include/、wsdl/、examples/、build/几个目录。wsdl/里放的是 ONVIF 官方 WSDL 文件这是整个协议栈的契约所有 SOAP 消息的格式都由它定义。src/下面通常按功能模块切分比如discovery/、device/、media/、ptz/、events/。include/里是 gSOAP 生成的头文件命名一般是soapH.h、soapStub.h这种。这里有个血泪经验很多人拿到源码直接make结果报一堆undefined reference原因就是 gSOAP 的 stub 文件没重新生成。WSDL 改了或者版本对不上必须用wsdl2h和soapcpp2重新跑一遍。常见做法是# 用 wsdl2h 把多个 WSDL 合并成一个头文件 wsdl2h -o onvif.h -t typemap.dat \ wsdl/devicemgmt.wsdl \ wsdl/media.wsdl \ wsdl/ptz.wsdl \ wsdl/discovery.wsdl # 用 soapcpp2 生成 C stub 和骨架 soapcpp2 -2 -C -j -I import onvif.h-2表示生成 SOAP 1.2 绑定ONVIF 强制要求 SOAP 1.2用 1.1 会直接被设备拒绝。-C只生成客户端代码-j生成 C 而非 C。-I import指定 import 目录因为 ONVIF 的 WSDL 之间有相互引用。跑完这两步你会得到soapStub.h、soapH.h、soapClient.cpp等文件把它们放进src/再编译链接错误基本就消了。2.2 依赖清单和版本坑这份源码的依赖不算多但版本敏感。核心依赖是 gSOAP 2.8.xopenssl 用于 WS-Security 的 digest 认证还有 libxml2 在某些实现里用来解析配置。gSOAP 版本低于 2.8.30 的话SOAP 1.2 的application/soapxmlcontent-type 处理有 bug会导致设备返回 415。openssl 建议 1.1.1 以上因为 ONVIF 的 UsernameToken 用到了 SHA-1 摘要老版本 openssl 在 FIPS 模式下会直接拒绝。编译前先确认# 检查 gSOAP 版本 soapcpp2 -v # 检查 openssl openssl version # 检查 libxml2 xml2-config --version如果 gSOAP 版本不对别硬扛直接去官网下 2.8.114 或更高。我见过有人用 apt 装的 gSOAP 2.8.20 编译折腾两天最后发现是版本问题这种坑完全没必要踩。2.3 Profile 覆盖范围决定你能做什么ONVIF 不是铁板一块它分 Profile S、Profile G、Profile C、Profile A、Profile Q 等。这份 V2.4 源码主要覆盖 Profile S也就是流媒体和 PTZ 控制。具体来说它实现了 Device Management设备信息、网络配置、系统重启、Media获取流 URI、编码配置、PTZ连续移动、绝对移动、预置位、Events基础事件订阅。Profile G 的录像回放和 Profile C 的访问控制这份源码里基本没有别指望拿它直接做 NVR 回放。怎么验证编译完之后跑examples/里的 discovery 示例如果能发现局域网里的 IPC再跑 media 示例拿到 RTSP URI说明 Profile S 的核心链路是通的。如果拿不到 URI先别怀疑代码用 ONVIF Device Manager 这个工具连一下同一台设备确认设备本身开了 ONVIF 且认证信息正确。天地伟业的摄像机有个特点ONVIF 默认可能是关的要在 Web 后台手动开启而且用户名密码和你 Web 登录的可能是两套。3. 从发现到取流把 SOAP 请求真正发出去3.1 WS-Discovery为什么你的 Probe 没人回ONVIF 的设备发现走的是 WS-Discovery基于 UDP 多播。源码里discovery/目录下一般有个DiscoveryClient.cpp核心逻辑是往239.255.255.250:3702发一个 Probe 消息然后等 ProbeMatch 回应。听起来简单但翻车率极高。第一个坑多播地址绑错网卡。如果你的机器有多张网卡默认路由可能走错Probe 发出去设备收不到。解决方法是显式指定 outgoing interface// 设置多播发送接口eth0 换成你实际的网卡名 struct ip_mreq mreq; mreq.imr_multiaddr.s_addr inet_addr(239.255.255.250); mreq.imr_interface.s_addr inet_addr(192.168.1.100); // 本机对应网卡 IP setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, mreq.imr_interface, sizeof(mreq));第二个坑Probe 消息里的Types字段写错。ONVIF 要求dn:NetworkVideoTransmitter少一个字母设备就不回。源码里一般有常量定义别自己手写字符串。第三个坑防火墙。Windows 上默认会拦 UDP 3702 的入站回应Linux 上 iptables 也可能挡。调试阶段先把防火墙关了通了再按需放行。3.2 SOAP 认证UsernameToken 的 digest 怎么算ONVIF 的认证不是 HTTP Basic而是 WS-Security 的 UsernameToken带 Nonce 和 Created 时间戳密码用 SHA-1 digest。源码里一般有个soap_wsse_add_UsernameTokenDigest的封装但你要理解它干了什么否则认证失败时无从下手。核心计算逻辑// 伪代码展示 digest 计算过程 std::string nonce generateRandomNonce(); // 16 字节随机数 std::string created getCurrentUTCTime(); // 格式2024-01-01T00:00:00Z std::string raw nonce created password; std::string digest base64(sha1(raw)); // 然后把 nonce、created、digest 塞进 SOAP Header注意 Nonce 必须是原始字节的 base64不是 hex。Created 必须是 UTC 时间格式精确到秒带 Z 后缀。设备端会校验时间戳偏差超过 5 分钟直接拒绝。我遇到过一台设备因为 NTP 没同步时间差了 10 分钟认证一直失败查了半天以为是 digest 算错了结果是设备时间不对。从那以后我每次对接新设备第一件事就是date -u对一下时间。3.3 拿 RTSP URIGetStreamUri 的参数怎么填Media 服务的GetStreamUri是取流的关键。请求里要指定ProfileToken这个 token 从GetProfiles拿。源码里一般有封装好的函数但参数填错照样拿不到。// 获取 Profile 列表 auto profiles mediaClient.GetProfiles(); std::string token profiles[0].token; // 取第一个 Profile // 构造 StreamSetup StreamSetup setup; setup.Stream StreamType::RTP_Unicast; setup.Transport.Protocol TransportProtocol::RTSP; // 调用 GetStreamUri auto uri mediaClient.GetStreamUri(setup, token); std::cout RTSP URI: uri.Uri std::endl;StreamType选RTP_Unicast还是RTP_Multicast取决于你的场景。单播适合点对点拉流多播适合多个客户端看同一路流。TransportProtocol一般选 RTSP也有设备支持 HTTP 隧道但兼容性差。拿到 URI 之后用 ffplay 或 VLC 验证一下ffplay -rtsp_transport tcp rtsp://admin:password192.168.1.64:554/Streaming/Channels/101如果 ffplay 能播但你的代码播不了问题多半在认证或者 URI 里的特殊字符转义。密码里有或:的话必须 URL 编码。4. PTZ 控制和事件订阅源码里最容易翻车的两块4.1 PTZ 连续移动为什么设备不动PTZ 的ContinuousMove接口参数看着简单但有个隐藏坑Velocity的取值是 -1 到 1 的浮点数不是角度也不是速度值。很多设备对 0.5 以下的数值不响应你填 0.1 它觉得太慢直接忽略。// PTZ 连续移动示例 PTZSpeed speed; speed.PanTilt.x 0.5; // 水平方向-1 左1 右 speed.PanTilt.y 0.0; // 垂直方向-1 下1 上 speed.Zoom.x 0.0; // 变焦-1 拉远1 拉近 ptzClient.ContinuousMove(profileToken, speed, ); // 移动后必须发 Stop否则设备一直转 sleep(2); ptzClient.Stop(profileToken, true, true); // PanTilt 和 Zoom 都停Stop的第二个和第三个参数分别是PanTilt和Zoom的布尔值传true表示停止对应轴。忘了发 Stop 的话设备会一直转到限位有些设备还会报错。我见过有人测试时忘了 Stop摄像机转了一整夜第二天发现电机过热保护了。另一个坑ProfileToken必须和 Media 的 Profile 对应。PTZ 服务的 ProfileToken 和 Media 服务的 ProfileToken 是同一个东西但有些设备要求你先调GetConfigurationOptions确认 PTZ 配置存在否则ContinuousMove返回InvalidArg。4.2 事件订阅PullPoint 还是 BasicONVIF 事件有两种模式Basic Notification 和 PullPoint。Basic 是设备主动推需要你开一个 HTTP 服务端接收PullPoint 是你主动拉实现简单但实时性差。源码里一般两种都有示例我建议先用 PullPoint 跑通再换 Basic。PullPoint 的核心是CreatePullPointSubscription拿到订阅地址后循环PullMessages// 创建 PullPoint 订阅 auto sub eventClient.CreatePullPointSubscription(); std::string subUrl sub.SubscriptionReference.Address; // 循环拉取消息 while (running) { auto messages eventClient.PullMessages(subUrl, 5, 100); // 超时 5 秒最多 100 条 for (auto msg : messages) { // 解析 msg里面是 NotificationMessage handleEvent(msg); } }PullMessages的超时参数别设太大设 5 到 10 秒比较合适。设太大程序退出时卡住设太小频繁空轮询浪费 CPU。事件里的Topic是 XPath 格式比如tns1:RuleEngine/CellMotionDetector/Motion解析的时候注意命名空间。4.3 事件丢失和重复的排查事件丢失最常见的原因是订阅没续期。ONVIF 的订阅有TerminationTime默认可能是 10 分钟到期不续就断了。源码里如果有Renew接口记得定时调用。重复事件一般是设备端的问题有些设备在运动检测触发时会连发多条你的处理逻辑要做去重比如按Topic 时间戳窗口去重。排查的时候先用 ONVIF Device Manager 看事件流是否正常。如果 ODM 能收到你的代码收不到对比两者的订阅参数重点看InitialTerminationTime和MessageLimit。5. 避坑与常见问题对接多品牌设备时的真实翻车记录5.1 现象设备发现得到但 GetDeviceInformation 超时原因设备支持 WS-Discovery 但 Device Management 服务端口没开或者认证方式不匹配。有些设备 discovery 走 3702 端口但 SOAP 走 80 或 8000源码里如果写死了端口就会连错。解决从 ProbeMatch 的XAddrs字段里取实际的服务地址不要自己拼 IP 和端口。XAddrs可能是http://192.168.1.64/onvif/device_service直接拿这个 URL 去发 SOAP 请求。5.2 现象GetProfiles 返回空列表原因设备没有配置媒体 Profile或者当前用户没有权限。天地伟业的设备默认可能只有一个主码流 Profile如果被删了就返回空。解决先用设备 Web 后台确认码流配置存在再用管理员账号测试。如果管理员能拿到而普通用户拿不到检查 ONVIF 用户权限设置。5.3 现象PTZ 控制返回 401 Unauthorized原因PTZ 服务的认证和 Device 服务是独立的有些设备要求 PTZ 请求也带 UsernameToken但源码里可能只在 Device 服务加了认证头。解决确保每个 SOAP 请求都走同一个认证封装。gSOAP 里可以用soap_wsse_add_UsernameTokenDigest在每次请求前调用不要只在初始化时调一次。5.4 现象事件订阅成功但收不到任何消息原因设备的事件触发条件没配或者订阅的 Topic 不对。有些设备默认不推运动检测事件需要在设备端开启。解决先用GetEventProperties拿到设备支持的所有 Topic再按实际 Topic 订阅。不要凭猜写 Topic。5.5 现象编译通过但运行时段错误原因gSOAP 的soap结构体没有初始化或者多线程下共用了同一个soap对象。gSOAP 不是线程安全的每个线程要有自己的soap实例。解决用soap_new创建实例或者每个线程soap_copy一份。别图省事全局共用一个。6. 进阶技巧用 WSDL 反推设备能力少写一半兼容代码这份源码最大的价值不是它已经实现的那些接口而是wsdl/目录里的 WSDL 文件。WSDL 是设备的契约你可以用它反推设备到底支持哪些操作、哪些参数是可选的、哪些枚举值合法。我一般会写个小脚本从 WSDL 里提取所有operation和element生成一份能力清单。# 用 xmllint 提取 WSDL 里的所有 operation 名称 xmllint --xpath //*[local-name()operation]/name wsdl/devicemgmt.wsdl | \ grep -o name[^]* | cut -d -f2 | sort -u跑出来你会看到GetDeviceInformation、GetNetworkInterfaces、SetSystemDateAndTime这些操作。然后对照源码里的实现看哪些没做。没做的部分你可以用 gSOAP 生成的 stub 直接调不用自己从头写 SOAP 消息。另一个技巧用GetCapabilities的返回结果做运行时能力探测。设备返回的Capabilities里会标明支持哪些服务、哪些 Profile。你的代码可以根据这个动态决定走哪条路径而不是硬编码假设设备支持所有功能。// 获取设备能力 auto caps deviceClient.GetCapabilities({CapabilityCategory::All}); if (caps.Media) { // 设备支持 Media 服务 } if (caps.PTZ) { // 设备支持 PTZ 服务 }这样写出来的对接层面对不同品牌设备时鲁棒性高很多。我现在的习惯是每接一个新品牌先跑一遍GetCapabilities把返回的 XML 存下来当基线后面出问题先对比基线看是设备行为变了还是我的代码有 bug。这个习惯帮我省了无数个加班的夜晚。希望帮到你。本文还有配套的精品资源点击获取
返回列表