资讯动态

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

发布时间:2026/10/23 1:08:09 来源:尧图企业网站定制
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_serverHTTP 服务注册那部分直接链接失败。很多人拿到组件源码直接丢进去编译报错八成是依赖没声明全。2.2 组件依赖与 IDF 版本选择onvif-c依赖的核心组件包括依赖组件作用缺失后果esp_http_server承载 SOAP 请求的 HTTP 服务无法注册 ONVIF 端点lwip网络栈UDP 组播用于发现设备无法被 NVR 搜到mbedtlsSHA1/Base64用于 WS-Security鉴权失败NVR 报 401nvs_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_IGMPy CONFIG_LWIP_MULTICASTy或者在代码里调用esp_netif相关接口加入组播组。另外如果设备连的是 5G 频段 WiFi部分路由器默认关闭组播转发这时候要么切 2.4G要么在路由器里打开 IGMP Snooping。这个坑我在三个不同的路由器上都遇到过表现完全一致手动加 IP 能通自动发现不行。3. ONVIF 交互全链路拆解NVR 添加相机时到底发生了什么3.1 发现阶段WS-Discovery 的 Probe 与 ProbeMatchNVR 添加设备的第一步是找。它会在局域网里发一个 UDP 组播包叫Probe内容是一段 SOAP XML问谁是 ONVIF 设备。ESP32 上的onvif-c收到后要回一个ProbeMatch里面带上自己的设备服务地址通常是http://ip/onvif/device_service。这个 ProbeMatch 的 XML 结构必须严格符合规范尤其是Types字段要包含dn:NetworkVideoTransmitterScopes字段要带上设备名称和位置信息。如果 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 与 GetCapabilitiesNVR 拿到设备地址后第一件事是调GetDeviceInformation问设备的厂商、型号、固件版本、序列号。这些信息会显示在 NVR 的设备列表里。onvif-c里这些字段通常是可配置的建议填成有意义的值别用默认的 Unknown否则在多设备场景下根本分不清谁是谁。紧接着是GetCapabilitiesNVR 问你支持哪些能力。返回的 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 有一个 tokenNVR 后续用这个 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初始化顺序很讲究。正确的顺序是初始化 NVSnvs_flash_init因为后面要存配置。初始化网络WiFi 或以太网拿到 IP。同步时间SNTP这是鉴权的前提。启动 HTTP 服务器注册 ONVIF 端点。启动 WS-Discovery 的 UDP 监听。启动视频流服务。如果顺序错了比如先启动 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要有价值得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑