ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ESP32 ONVIF 实战:从 MJPEG 出图到 NVR 稳定接入

ESP32 ONVIF 实战:从 MJPEG 出图到 NVR 稳定接入

1. 为什么要在 ESP32 上折腾 ONVIF:从"能出图"到"能被 NVR 认"的鸿沟

很多人第一次用 ESP32 做网络摄像头,流程都差不多:拿一块带摄像头的开发板,跑通官方例程,浏览器里打开一个地址,看到 MJPEG 画面在动,心里就觉得"成了"。结果把设备往 NVR(网络录像机)里一加,要么搜不到,要么加进去显示离线,要么能连上但一直转圈不出画面。这个落差,就是本文要解决的核心问题。

ONVIF 不是一个"协议",而是一整套围绕网络视频设备的互操作规范集合。它规定了设备怎么被发现(WS-Discovery)、怎么描述自己的能力(Device Service)、怎么提供媒体流地址(Media Service)、怎么被鉴权(WS-Security UsernameToken)等等。NVR 添加一台相机,背后其实是一连串标准化的 SOAP 请求和响应在跑。ESP32 上跑通 MJPEG 只是把视频流做出来了,但 NVR 根本不知道你是谁、你能提供什么、流地址在哪、用什么编码——这些信息全靠 ONVIF 接口告诉它。

onvif-c这个组件,就是把这个"告诉 NVR"的过程用纯 C 在 ESP-IDF 里实现出来。它不依赖 C++、不依赖大型框架,适合资源受限的 ESP32 系列芯片。本文会从建工程开始,一步步讲到怎么让一台 ESP32 相机真正被 NVR 添加并稳定出流。适合两类人:一是已经能让 ESP32 出图、但卡在 NVR 接入这一步的开发者;二是想理解 ONVIF 交互全流程、准备自己实现或改造组件的进阶玩家。

需要先说明一个预期:ONVIF 的完整规范非常庞大,onvif-c这类轻量组件通常只实现 NVR 接入所必需的最小子集——设备发现、设备信息、能力集、媒体配置、流地址获取、鉴权。它不会实现 PTZ 云台控制、录像回放、事件订阅这些高级功能。搞清楚这个边界,后面的调试才不会跑偏。

2. 动手之前:ESP-IDF 工程骨架与 onvif-c 的依赖关系

2.1 工程目录怎么摆,组件放哪里

ESP-IDF 的组件机制是理解整个搭建过程的基础。一个标准工程里,main目录是你的应用代码,而第三方组件放在工程根目录下的components文件夹里,或者通过idf_component.yml从组件仓库拉取。onvif-c作为 plain C 组件,通常以源码形式放进components/onvif-c,这样编译时 IDF 会自动把它纳入构建系统。

我建议的目录结构是这样的:

my_esp32_cam/ ├── CMakeLists.txt ├── sdkconfig ├── main/ │ ├── CMakeLists.txt │ └── main.c └── components/ └── onvif-c/ ├── CMakeLists.txt ├── include/ │ └── onvif.h └── src/ ├── onvif_device.c ├── onvif_media.c ├── onvif_discovery.c └── onvif_http.c

这里有个容易踩的坑:onvif-c的CMakeLists.txt里必须正确声明REQUIRES,把esp_http_server、lwip、mbedtls这些依赖写进去。如果漏了mbedtls,编译到鉴权部分会报找不到 SHA1 相关符号;漏了esp_http_server,HTTP 服务注册那部分直接链接失败。很多人拿到组件源码直接丢进去编译报错,八成是依赖没声明全。

2.2 组件依赖与 IDF 版本选择

onvif-c依赖的核心组件包括:

依赖组件作用缺失后果
esp_http_server承载 SOAP 请求的 HTTP 服务无法注册 ONVIF 端点
lwip网络栈,UDP 组播用于发现设备无法被 NVR 搜到
mbedtlsSHA1/Base64,用于 WS-Security鉴权失败,NVR 报 401
nvs_flash存储设备配置与凭据重启后配置丢失

IDF 版本方面,建议用 4.4 及以上。4.4 之前esp_http_server对某些 HTTP 头处理有差异,会导致 SOAP 解析异常。我实测在 5.0 和 5.1 上跑得比较稳,5.2 的改动主要在构建系统,组件本身兼容性没问题。

2.3 网络配置:别让组播成为隐形杀手

ONVIF 发现依赖 WS-Discovery,它走的是 UDP 组播,目标地址是239.255.255.250,端口3702。ESP32 默认的 lwip 配置里,组播接收是关闭的。如果你发现 NVR 死活搜不到设备,但手动输入 IP 又能加上,那基本就是组播没开。

需要在sdkconfig里确认这几项:

CONFIG_LWIP_IGMP=y CONFIG_LWIP_MULTICAST=y

或者在代码里调用esp_netif相关接口加入组播组。另外,如果设备连的是 5G 频段 WiFi,部分路由器默认关闭组播转发,这时候要么切 2.4G,要么在路由器里打开 IGMP Snooping。这个坑我在三个不同的路由器上都遇到过,表现完全一致:手动加 IP 能通,自动发现不行。

3. ONVIF 交互全链路拆解:NVR 添加相机时到底发生了什么

3.1 发现阶段:WS-Discovery 的 Probe 与 ProbeMatch

NVR 添加设备的第一步是"找"。它会在局域网里发一个 UDP 组播包,叫Probe,内容是一段 SOAP XML,问"谁是 ONVIF 设备"。ESP32 上的onvif-c收到后,要回一个ProbeMatch,里面带上自己的设备服务地址(通常是http://<ip>/onvif/device_service)。

这个 ProbeMatch 的 XML 结构必须严格符合规范,尤其是Types字段要包含dn:NetworkVideoTransmitter,Scopes字段要带上设备名称和位置信息。如果 Types 写错,NVR 会认为这不是一台摄像机,直接忽略。我见过有人把 Types 写成dn:NetworkVideoDisplay,结果 NVR 把它当成了显示器,怎么都加不进去。

ProbeMatch 里还有一个关键字段是XAddrs,这是 NVR 后续发 SOAP 请求的目标地址。这里必须填设备实际的 IP,不能填0.0.0.0或127.0.0.1。如果设备有多个网卡或 IP 会变,建议在运行时动态获取当前 IP 再拼进 XML。

3.2 设备服务:GetDeviceInformation 与 GetCapabilities

NVR 拿到设备地址后,第一件事是调GetDeviceInformation,问设备的厂商、型号、固件版本、序列号。这些信息会显示在 NVR 的设备列表里。onvif-c里这些字段通常是可配置的,建议填成有意义的值,别用默认的 "Unknown",否则在多设备场景下根本分不清谁是谁。

紧接着是GetCapabilities,NVR 问"你支持哪些能力"。返回的 XML 里要明确声明 Media 服务的地址、是否支持 PTZ、是否支持事件等。对于只做视频流的 ESP32 相机,Media 服务地址必须给对,PTZ 和 Events 可以声明为不支持。这里有个细节:如果声明了 Media 服务但地址填错,NVR 会在下一步调 Media 接口时报连接失败,错误信息往往很模糊,排查起来费劲。

3.3 媒体服务:GetProfiles 与 GetStreamUri

这是整个流程里最关键的一步。NVR 调GetProfiles,问设备有哪些"媒体配置文件"。一个 Profile 描述了一路流的编码格式、分辨率、帧率、码率等。ESP32 相机通常只提供一个 Profile,编码是 MJPEG 或 H.264。

GetProfiles返回的 XML 里,每个 Profile 有一个 token,NVR 后续用这个 token 去调GetStreamUri,问"这路流的实际地址是什么"。返回的 URI 就是 NVR 真正去拉流的地方,比如http://<ip>:81/stream或rtsp://<ip>:554/stream。

这里最容易出问题的是 URI 的格式和可达性。URI 必须是 NVR 能直接访问的完整地址,不能是相对路径。如果 ESP32 的流服务跑在非标准端口上,URI 里要带上端口。另外,如果流服务需要鉴权,URI 里可以内嵌用户名密码,但更规范的做法是通过 ONVIF 的鉴权机制统一处理。

3.4 鉴权:WS-Security UsernameToken 的坑

ONVIF 的鉴权用的是 WS-Security 的 UsernameToken 方案。NVR 在每个 SOAP 请求的 HTTP 头里带一个Security元素,里面有用户名、随机数(Nonce)、创建时间(Created)和密码摘要(PasswordDigest)。摘要的算法是:

PasswordDigest = Base64( SHA1( Nonce + Created + Password ) )

设备端收到后,用自己存储的密码做同样的计算,比对摘要是否一致。这里有几个高频坑:

第一,Nonce 是二进制随机数,参与 SHA1 计算时用的是原始字节,但传输时是 Base64 编码的。解析时必须先 Base64 解码再参与计算,直接用 Base64 字符串算会得到错误结果。

第二,Created 是 ISO8601 格式的时间戳,设备端如果时间不对(比如没同步 NTP),计算出的摘要必然不匹配。ESP32 上电后默认时间是 1970 年,必须先用 SNTP 同步时间,否则鉴权永远失败。这个坑极其隐蔽,因为 NVR 只会报 401,不会告诉你时间不对。

第三,密码摘要用的是 SHA1,不是 SHA256。有些实现图省事用了 SHA256,结果和 NVR 对不上。规范里 UsernameToken 默认就是 SHA1,别自作主张换算法。

4. 从零跑通:建工程、配参数、写第一个能被搜到的设备

4.1 创建工程与集成组件

先用 IDF 的命令行工具建一个空工程:

idf.py create-project esp32_onvif_cam cd esp32_onvif_cam

然后把onvif-c组件源码放进components/onvif-c。如果你是从组件仓库拉取,可以在main/idf_component.yml里加依赖,但源码方式更可控,方便改。

main/CMakeLists.txt里要声明依赖:

idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES onvif-c nvs_flash esp_wifi)

onvif-c自己的CMakeLists.txt里:

idf_component_register(SRCS "src/onvif_device.c" "src/onvif_media.c" "src/onvif_discovery.c" "src/onvif_http.c" INCLUDE_DIRS "include" REQUIRES esp_http_server lwip mbedtls nvs_flash)

4.2 初始化顺序:一步错步步错

ESP32 上跑 ONVIF,初始化顺序很讲究。正确的顺序是:

  1. 初始化 NVS(nvs_flash_init),因为后面要存配置。
  2. 初始化网络(WiFi 或以太网),拿到 IP。
  3. 同步时间(SNTP),这是鉴权的前提。
  4. 启动 HTTP 服务器,注册 ONVIF 端点。
  5. 启动 WS-Discovery 的 UDP 监听。
  6. 启动视频流服务。

如果顺序错了,比如先启动 HTTP 再同步时间,那么在时间同步完成前的所有鉴权请求都会失败。NVR 如果在设备刚上电时就尝试添加,可能连续失败几次后就放弃了。建议在时间同步完成后再对外宣告设备就绪。

4.3 配置设备信息与凭据

onvif-c通常提供一个配置结构体,让你填设备信息:

onvif_config_t config = { .manufacturer = "MyCam", .model = "ESP32-CAM-01", .firmware_version = "1.0.0", .serial_number = "SN12345678", .hardware_id = "HW-ESP32", .username = "admin", .password = "admin123", .stream_uri = "http://192.168.1.100:81/stream", };

用户名密码建议不要用默认的admin/admin,很多 NVR 对默认凭据有安全策略,可能直接拒绝。密码长度也别太短,某些 NVR 要求至少 8 位。流地址里的 IP 如果是动态的,建议在运行时用esp_netif_get_ip_info获取后动态拼接,而不是写死。

4.4 验证设备是否被搜到

在写 NVR 之前,可以先用工具验证设备是否正常响应发现请求。在电脑上跑一个 ONVIF 设备发现工具,或者直接用命令行发组播包。如果能看到设备返回的 ProbeMatch,说明发现阶段通了。

然后手动访问http://<设备IP>/onvif/device_service,应该能看到 SOAP 的响应(可能会返回 400 或 401,但至少说明 HTTP 服务在跑)。如果连这个都访问不了,检查 HTTP 服务器是否启动、端口是否被占用。

5. 调试实战:NVR 添加失败时的分层排查法

5.1 第一层:网络可达性与发现

NVR 搜不到设备,先确认三件事:设备和 NVR 是否在同一网段、组播是否通、防火墙是否拦了 UDP 3702。可以在设备端加日志,打印收到的每个 UDP 包。如果完全没日志,说明包没到设备,问题在网络层。

如果手动输入 IP 能加上但自动搜不到,那 100% 是组播问题。检查sdkconfig里的 IGMP 配置,检查路由器是否开了 IGMP Snooping,检查设备是否真的加入了组播组。

5.2 第二层:SOAP 请求与响应

设备被搜到后加不上,通常是 SOAP 交互出了问题。这时候最有效的办法是抓包。在设备端把收到的每个 HTTP 请求体和返回的响应体都打印出来,对比 NVR 发了什么、你回了什么。

常见的响应错误包括:XML 格式不合法(少闭合标签、命名空间前缀不对)、字段值不符合规范(比如 token 里有非法字符)、返回了 NVR 不认识的元素。ONVIF 的 XML 对命名空间极其敏感,tds:、trt:、tt:这些前缀必须和规范一致,不能自己乱起。

5.3 第三层:鉴权失败

如果 NVR 报 401 或"认证失败",按这个顺序查:

排查项检查方法常见问题
时间同步打印设备当前时间未同步 NTP,时间偏差大
Nonce 解码打印解码后的字节直接用 Base64 字符串计算
摘要算法确认用 SHA1误用 SHA256
密码一致性对比配置与 NVR 输入密码有空格或特殊字符
Created 格式检查 ISO8601 格式时区或格式不对

我遇到最多的是时间同步问题。ESP32 上电后如果 WiFi 连接慢,SNTP 还没同步完,NVR 就来添加了,这时候鉴权必失败。解决办法是在时间同步完成前,先不响应发现请求,或者延迟启动 ONVIF 服务。

5.4 第四层:流地址与拉流

鉴权过了,NVR 也拿到了流地址,但画面出不来,问题就在流服务本身。检查流地址是否能在电脑上用播放器直接打开。如果电脑能打开、NVR 打不开,可能是 NVR 对流的编码格式有要求(比如只支持 H.264 不支持 MJPEG),或者 NVR 拉流时带的 HTTP 头和你的服务不兼容。

ESP32 的 MJPEG 流通常是multipart/x-mixed-replace格式,部分 NVR 对这个格式支持不好。如果条件允许,用 H.264 编码的流兼容性会好很多,但对 ESP32 的算力要求也更高。

6. 让设备更稳:性能、并发与长期运行的注意事项

6.1 内存与并发连接

ESP32 的内存有限,ONVIF 的 SOAP 解析、HTTP 服务、视频流服务同时跑,内存很容易紧张。建议给 HTTP 服务器设置合理的最大连接数,别让 NVR 的频繁轮询把连接占满。onvif-c里通常有max_connections之类的配置,根据实际 NVR 数量调整。

另外,SOAP 请求的 XML 解析建议用流式解析或固定缓冲区,别动态分配大块内存。ESP32 上频繁 malloc/free 容易产生碎片,跑久了会出问题。

6.2 心跳与保活

NVR 通常会定期调GetSystemDateAndTime或类似接口做保活。设备端要能稳定响应这些高频请求,不能因为处理慢导致 NVR 判定离线。如果发现 NVR 隔一段时间就掉线重连,检查是不是某个接口响应超时了。

6.3 时间同步的持续性

SNTP 同步一次不够,时间会漂移。建议定期重新同步,或者在检测到时间偏差过大时主动同步。有些 NVR 对时间偏差的容忍度很低,偏差超过几分钟就拒绝鉴权。

6.4 日志与远程诊断

量产设备上不方便接串口,建议把关键日志通过 HTTP 接口暴露出来,或者用 syslog 发到局域网内的日志服务器。排查问题时,能看到设备端的 SOAP 交互记录,比在 NVR 端猜要高效得多。

7. 几个我踩过的坑和对应的解法

第一个坑是组播。前面提过,但值得再强调:路由器不开 IGMP Snooping,自动发现就是不通。我当时的解法是在设备端同时支持手动添加的路径,并且在文档里明确写"如果自动搜不到,请手动输入 IP"。这样至少不会卡死在第一步。

第二个坑是 XML 命名空间。ONVIF 的响应里,GetCapabilities返回的 Media 服务地址,命名空间前缀必须是trt:,我一开始写成了tds:,NVR 解析时直接忽略了这个字段,导致后续调 Media 接口时找不到地址。这种错误不会报错,只会表现为"功能不工作",非常难查。

第三个坑是密码里的特殊字符。有一次密码里带了&,在 XML 里没转义,导致 SOAP 解析失败。后来统一在拼接 XML 时对特殊字符做转义,问题解决。ONVIF 的 XML 里,&、<、>、"、'都必须转义。

第四个坑是流地址的端口。ESP32 的 HTTP 流服务默认跑在 80 端口,但 ONVIF 的 HTTP 服务也在 80,冲突了。后来把流服务挪到 81 端口,ONVIF 服务留在 80,两个服务各跑各的,互不干扰。这个端口规划在工程初期就要想好,别等冲突了再改。

8. 后续可以怎么扩展

onvif-c跑通基础接入后,如果想做更多,有几个方向。一是支持多 Profile,让 NVR 能选择不同分辨率或码率的流,这在多路复用场景下有用。二是实现 PTZ 接口,虽然 ESP32 相机通常不能动,但可以对接外部的云台控制。三是支持事件订阅,把移动侦测等事件推给 NVR,这样 NVR 的录像策略可以更智能。

不过要提醒一句:每加一个功能,都要确认 NVR 是否真的会用到。ONVIF 规范里很多接口,消费级 NVR 根本不调。与其把规范实现全,不如把 NVR 实际会调的那几个接口做稳。我在实际项目里的体会是,稳定响应 5 个接口,比勉强实现 20 个接口但每个都有 bug,要有价值得多。

返回列表