资讯详情

资讯详情

ESP32纯C实现ONVIF协议栈:从零打造可被NVR识别的IP相机

1. 为什么要在 ESP32 上折腾 ONVIF 相机1.1 这个项目到底在做什么先把话说清楚这个项目的目标是用一块 ESP32 系列模组配合 ESP-IDF 的原生 C 环境从零搭出一个能被标准 NVR网络录像机识别并添加的 IP 相机。核心不在于能出图而在于能被别人认出来。很多人第一次做网络摄像头思路是写个 HTTP 服务把 JPEG 往浏览器一丢看着有画面就以为成了。结果拿到 NVR 上一接要么搜不到设备要么添加时报不支持的设备要么加进去了却一直显示离线。问题几乎都出在 ONVIF 这一层——NVR 不是靠你能出图来认你的它是靠一套标准化的发现、鉴权、能力协商流程来判断你是不是一台合规的相机。ONVIF 全称是开放网络视频接口论坛制定的一套标准它规定了 IP 摄像机、NVR、视频管理软件之间怎么互相发现、怎么鉴权、怎么查询能力、怎么拉流。对 NVR 来说一台相机能不能被添加取决于它能不能正确响应几个关键的 SOAP 请求。这个项目要做的就是在 ESP32 这种资源受限的芯片上把这套协议栈用纯 C 实现出来并且跑通到能被 NVR 添加的程度。适合谁来参考有 ESP-IDF 基础、玩过 ESP32 网络编程、想深入理解 ONVIF 协议交互流程的嵌入式开发者也适合做安防类产品原型、需要低成本 IP 相机方案的团队。如果你只是想快速出个能看的图传这个项目可能偏重但如果你要的是能被专业软件认出来的设备那这条路绕不开。1.2 为什么不用现成的库市面上确实有一些 ONVIF 相关的开源实现但大多跑在 Linux 上依赖 gSOAP 这类重型框架代码体积和内存占用对 ESP32 来说不现实。ESP32 的 RAM 通常只有几百 KB 可用PSRAM 也不是每块板子都有gSOAP 生成的那套代码光数据结构就能把内存吃干净。所以这个项目选择用 plain C 手写 ONVIF 的核心交互逻辑只实现 NVR 添加设备真正需要的那几个接口其余全部砍掉。这是一种够用就好的取舍不追求协议全覆盖只追求能被主流 NVR 顺利添加并稳定拉流。这个思路贯穿整个项目后面每个技术选择基本都围绕它展开。1.3 整体架构长什么样在动手之前先把整个系统的骨架理清楚不然后面写代码容易迷路。一个能被 NVR 添加的 ESP32 相机大致分成这么几层网络层ESP32 通过 Wi-Fi 或以太网接入局域网拿到 IP这是所有通信的基础。发现层响应 WS-Discovery 的组播探测让 NVR 能在网络里看见你。协议层一个 HTTP 服务器处理 ONVIF 的 SOAP 请求包括设备信息查询、能力查询、媒体配置查询、流地址获取等。鉴权层处理 WS-Security 的 UsernameToken 摘要认证这是 NVR 添加设备时的必过关卡。媒体层真正提供视频流通常是 RTSP 服务把摄像头采集的 H.264 或 MJPEG 数据推出去。这五层里发现层、协议层、鉴权层是 ONVIF 的核心也是这个项目最花心思的地方。媒体层虽然重要但它和 ONVIF 是相对独立的——ONVIF 负责告诉 NVR 流在哪RTSP 负责把流送出去。理解这个分工后面排查问题会轻松很多。2. 工程搭建与 ESP-IDF 环境准备2.1 组件化思路把 ONVIF 做成独立组件这个项目标题里特意强调了ESP-IDF plain C 组件这不是随便说的。ESP-IDF 的组件机制允许你把功能模块做成可复用的独立单元有自己的 CMakeLists.txt、include 目录和源文件。把 ONVIF 做成组件好处是它和主程序解耦换个项目直接拷过去就能用。一个典型的组件目录结构大概是这样components/ onvif/ CMakeLists.txt include/ onvif.h onvif_discovery.h onvif_soap.h onvif_auth.h onvif_discovery.c onvif_soap.c onvif_auth.c onvif_device.c onvif_media.c组件的 CMakeLists.txt 里用idf_component_register注册源文件和头文件路径依赖项里要声明esp_http_server、lwip、mbedtls这些。这里有个容易踩的坑如果你用了esp_http_server一定要在依赖里显式写上否则编译时头文件找不到报错信息还特别隐晦。提示组件命名尽量用纯小写加下划线避免大小写混用。ESP-IDF 在部分平台上对路径大小写敏感混用容易在换机器编译时出问题。2.2 从零建工程的完整步骤假设你已经装好了 ESP-IDF建议 5.0 以上版本下面是从零建工程的流程。我按实际操作顺序写你照着敲就行。第一步用 idf.py 创建工程骨架idf.py create-project onvif_cam cd onvif_cam第二步创建组件目录把 ONVIF 相关代码放进去mkdir -p components/onvif/include第三步配置目标芯片。这个项目在 ESP32-S3 上验证比较充分因为它有 PSRAM处理视频缓冲更从容idf.py set-target esp32s3第四步打开配置菜单把该开的开关打开idf.py menuconfig在 menuconfig 里重点确认这几项Wi-Fi 相关配置、LWIP 的组播支持WS-Discovery 依赖组播、HTTP 服务器最大连接数、以及 PSRAM 是否启用。组播这一项特别关键如果 LWIP 没开组播WS-Discovery 的探测包根本收不到NVR 自然发现不了你。2.3 关键配置项与参数计算配置里有几个参数值得单独说因为它们直接决定设备能不能被稳定添加。HTTP 服务器最大连接数NVR 添加设备时会连续发好几个 SOAP 请求如果连接数设太小后面的请求会被拒。建议至少设成 7。这个数字不是拍脑袋来的——一次完整的设备添加流程NVR 通常会依次请求设备信息、能力、媒体配置、流地址等加上重试7 个并发连接是比较稳妥的余量。SOAP 响应缓冲区大小ONVIF 的 SOAP 响应里带大量 XML 命名空间声明一个设备信息响应轻松超过 2KB。缓冲区给小了响应会被截断NVR 解析失败。建议单次响应缓冲区不低于 4KB如果设备信息里要带多个网卡、多个媒体配置最好给到 8KB。WS-Discovery 的组播地址和端口标准规定是239.255.255.250的3702端口。这个不能改改了 NVR 就发现不了。ESP32 加入这个组播组后才能收到 NVR 发来的 Probe 探测包。鉴权摘要算法WS-Security 的 UsernameToken 用的是 SHA-1 摘要配合 Nonce 和 Created 时间戳。这个算法是协议规定的不能换成 SHA-256否则 NVR 算出来的摘要和你算的对不上直接鉴权失败。把这些参数理清楚后面写代码时心里就有底了。3. ONVIF 核心交互流程拆解3.1 WS-Discovery让 NVR 先看见你NVR 添加设备的第一步是在局域网里发一个组播 Probe 包问谁是 ONVIF 设备。所有 ONVIF 设备收到后要回一个 ProbeMatch告诉 NVR我在这我的服务地址是 xxx。这一步是整个流程的入口也是最容易被忽略的一步。很多人的设备在 NVR 里搜不到八成是 WS-Discovery 没做对。实现要点有这么几个ESP32 要先加入组播组239.255.255.250然后在这个组播地址的 3702 端口上监听 UDP 包。收到 Probe 后解析里面的 MessageID构造一个 ProbeMatch 响应通过 UDP 单播回给发送方。响应里必须包含设备的 EndpointReference一个唯一的设备标识通常是 urn:uuid: 开头的字符串和 XAddrs设备 ONVIF 服务的 HTTP 地址。这里有个细节ProbeMatch 里的 XAddrs 必须是 NVR 能访问到的地址。如果你 ESP32 拿的是动态 IP这个地址要实时填当前 IP不能写死。我见过有人把地址写死在代码里换了个网络环境设备就再也加不上了。注意WS-Discovery 的响应要快。NVR 发完 Probe 后等待响应的时间窗口通常只有几秒如果 ESP32 处理太慢NVR 已经放弃等待了。建议把 Probe 处理逻辑放在高优先级任务里别被视频编码任务阻塞。3.2 SOAP 请求处理设备信息与能力协商NVR 发现设备后会通过 HTTP POST 发来一系列 SOAP 请求。这些请求的 body 是 XML头部带 SOAPAction 标识请求类型。ESP32 这边要做的是根据 SOAPAction 分发到不同的处理函数构造对应的 XML 响应。最常被请求的几个接口SOAPAction作用响应关键内容GetDeviceInformation获取厂商、型号、固件版本Manufacturer、Model、FirmwareVersionGetCapabilities查询设备支持哪些能力Media、Device、Events 等能力声明GetProfiles获取媒体配置文件列表Profile token 和名称GetStreamUri获取视频流地址RTSP 地址GetSystemDateAndTime获取设备时间UTC 时间和时区GetCapabilities 这个接口特别重要它相当于设备的能力清单。NVR 会根据这个响应判断你支持什么、不支持什么。如果你在能力里声明了支持 Events但实际没实现事件推送NVR 可能会一直尝试订阅事件然后超时。所以原则是只声明你真正实现了的能力别贪多。GetProfiles 返回的 Profile token 是后续所有媒体操作的钥匙。NVR 拿到 token 后会用这个 token 去请求流地址。token 可以是任意字符串但要保证在设备内唯一且稳定别每次请求都变。3.3 WS-Security 鉴权最容易卡住的一关如果前面几步都顺到了鉴权这一步往往才是真正的拦路虎。NVR 添加设备时会带上用户名密码用 WS-Security 的 UsernameToken 方式做摘要认证。请求头里会有这么几个关键字段Username明文用户名Nonce一个随机数Base64 编码Created时间戳ISO 8601 格式PasswordDigest摘要值摘要的计算方式是Base64(SHA1(Nonce Created Password))。注意这里是二进制拼接后再做 SHA-1不是字符串拼接。Nonce 要先 Base64 解码成二进制Created 是 ASCII 字符串Password 也是 ASCII 字符串三者按顺序拼起来做 SHA-1最后 Base64 编码。ESP32 这边收到请求后要用同样的方式算出摘要和请求里的 PasswordDigest 比对。一致就放行不一致返回 401。这里有几个高频坑第一Nonce 的 Base64 解码。有些实现直接把 Base64 字符串当二进制用结果算出来的摘要永远对不上。必须老老实实解码。第二Created 时间戳的格式。标准要求是2024-01-01T00:00:00Z这种格式带 Z 表示 UTC。如果你的设备时间和 NVR 差太多有些 NVR 会直接拒绝。所以设备启动后要尽量同步时间哪怕只是从 NVR 的请求里取时间戳来校准。第三SHA-1 的实现。ESP-IDF 自带的 mbedtls 里有 SHA-1直接用就行别自己手写容易出错。提示调试鉴权问题时把 NVR 发来的 Nonce、Created、PasswordDigest 和你自己算出来的值都打印出来对比。十有八九是 Nonce 解码或者时间格式的问题。4. 媒体流与 RTSP 服务实现4.1 为什么选 RTSP 而不是别的ONVIF 的 GetStreamUri 返回的流地址主流是 RTSP。虽然也有用 HTTP 的但 NVR 生态里 RTSP 是绝对主流。所以这个项目的媒体层核心就是实现一个 RTSP 服务器。RTSP 本身不传数据它是个控制协议负责协商和指挥。真正的视频数据通过 RTP 传输。一次典型的拉流流程是NVR 发 DESCRIBE 请求拿 SDP 描述发 SETUP 请求建立传输通道发 PLAY 请求开始播放之后 ESP32 就通过 RTP 持续推流直到 NVR 发 TEARDOWN。在 ESP32 上实现 RTSP难点不在协议本身而在资源调度。视频编码、RTP 打包、网络发送这几件事要协调好不能互相阻塞。4.2 SDP 描述怎么写才被 NVR 接受SDP会话描述协议是 DESCRIBE 请求的响应内容它告诉 NVR 这个流里有什么、怎么解码。一个能被 NVR 接受的 SDP至少要包含这几行v0 o- 0 0 IN IP4 192.168.1.100 sESP32 Camera t0 0 mvideo 0 RTP/AVP 96 artpmap:96 H264/90000 acontrol:trackID0这里mvideo行声明了媒体类型和 payload typeartpmap声明了编码格式和时钟频率。H.264 的时钟频率固定是 90000这个不能错。acontrol是 track 标识SETUP 请求会用到。如果 NVR 支持 MJPEG也可以声明artpmap:26 JPEG/90000。但 H.264 兼容性更好码率也更低推荐优先用 H.264。有个细节SDP 里的 IP 地址要填设备实际 IP和 WS-Discovery 里一样不能写死。4.3 RTP 打包与发送节奏RTP 打包是媒体层最考验功力的地方。H.264 的 NAL 单元有大有小小的可以一个 RTP 包发完大的要分片。标准做法是小于 MTU 的 NAL 直接打包大于 MTU 的用 FU-A 分片。MTU 一般是 1500减去 IP 头、UDP 头、RTP 头实际可用的 payload 大概 1400 字节左右。分片时每个 RTP 包的 marker 位和序列号要处理好最后一个分片要把 marker 位置 1表示一帧结束。发送节奏也很关键。如果一股脑把帧全推出去网络缓冲区会爆丢包严重。合理的做法是根据帧率和码率控制发送间隔。比如 25 帧每秒每帧间隔 40ms用定时器或者任务延时来控制。注意ESP32 的 Wi-Fi 在高负载下容易抖动建议给 RTP 发送任务较高的优先级并且用独立的 socket别和 HTTP 服务器抢资源。5. 常见问题与排查实录5.1 NVR 搜不到设备怎么办这是最高频的问题。排查顺序建议这样走先确认 ESP32 和 NVR 在同一个网段能互相 ping 通。然后抓包看 NVR 有没有发 Probe 组播包。如果 NVR 发了但 ESP32 没回检查 ESP32 有没有加入组播组、有没有在 3702 端口监听。如果 ESP32 回了但 NVR 还是搜不到检查 ProbeMatch 里的 XAddrs 地址对不对、格式是不是标准的http://ip:port/onvif/device_service。还有一种情况是防火墙拦了组播。有些企业网络默认禁组播这种环境下 WS-Discovery 直接失效只能手动输入设备地址添加。5.2 添加时提示鉴权失败按前面说的重点查 Nonce 解码、Created 格式、摘要算法。另外确认 NVR 里填的用户名密码和 ESP32 里配置的一致。有些 NVR 对密码长度有要求太短可能被拒。5.3 添加成功但一直离线这种情况通常是 RTSP 拉流失败。用 VLC 之类的工具直接拉一下 RTSP 地址看能不能出图。如果 VLC 也拉不到问题在 RTSP 服务端如果 VLC 能拉但 NVR 不行可能是 SDP 格式或者 RTP 打包的问题。常见原因还有ESP32 的 IP 变了但 SDP 里还是旧地址RTP 端口没开对H.264 的 SPS/PPS 没有正确发送。SPS/PPS 是解码关键信息必须在每个 IDR 帧前发送否则 NVR 解不出画面。5.4 问题速查表现象可能原因排查方向搜不到设备组播未加入/未监听检查 LWIP 组播配置和 3702 端口搜到但加不上鉴权失败检查 Nonce 解码和摘要算法加上后离线RTSP 拉流失败用 VLC 验证流地址有画面但花屏SPS/PPS 缺失检查 IDR 帧前是否发 SPS/PPS画面卡顿发送节奏过快调整 RTP 发送间隔时间相关报错设备时间偏差大同步时间或从请求取时间6. 实操中的经验与避坑心得6.1 内存管理是重中之重ESP32 的内存很紧张ONVIF 的 XML 处理又是内存大户。我的经验是所有 XML 响应都用静态缓冲区或者预分配的内存池别在请求处理过程中动态 malloc 大块内存。SOAP 响应缓冲区可以做成全局的多个请求串行复用省内存。如果用了 PSRAM把视频缓冲和大的 XML 缓冲放 PSRAM内部 RAM 留给协议栈和任务栈。这个分配策略能明显提升稳定性。6.2 日志分级要合理调试 ONVIF 时日志会非常多全开的话串口刷屏还拖慢系统。建议把日志分级WS-Discovery 和鉴权相关的用 INFO 级SOAP 请求内容用 DEBUG 级RTP 发送这种高频的用 VERBOSE 级或者干脆关掉。这样既能定位问题又不至于被日志淹没。6.3 兼容性测试要覆盖多品牌不同品牌的 NVR 在 ONVIF 实现上有细微差异。有的对 XML 命名空间要求严格有的对时间戳格式挑剔有的会发一些非标准的请求。建议至少准备两三个不同品牌的 NVR 或者 ONVIF 客户端工具做交叉测试。我实测下来有些设备对 ProbeMatch 的响应顺序都有要求先回哪个后回哪个会影响识别。6.4 一个容易被忽略的小技巧在 GetSystemDateAndTime 的响应里把时间返回得准一点很多 NVR 会用这个时间来校准后续鉴权的时间戳比对。如果设备时间偏差太大鉴权会莫名其妙失败。可以在设备启动连上网络后从 NVR 发来的第一个请求里提取时间戳反向校准自己的时间这样能省掉一个 NTP 客户端的开销。这个项目从工程搭建到能被 NVR 添加中间涉及的环节比想象中多但每一步拆开看都不复杂。关键是把 ONVIF 的交互流程理解透知道 NVR 在每个阶段期待什么响应然后针对性地实现。资源受限不是问题问题是想清楚哪些必须做、哪些可以砍。把发现、鉴权、能力协商、流地址这几件事做扎实一台能被专业 NVR 认出来的 ESP32 相机就跑起来了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →