ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32纯C实现ONVIF协议:让NVR自动发现并添加网络摄像机

ESP32纯C实现ONVIF协议:让NVR自动发现并添加网络摄像机 1. 从零理解 onvif-c为什么要在 ESP32 上跑 ONVIF搞安防监控的朋友大概率都碰过这样的场景手头有一块 ESP32 模组接了个摄像头传感器想把它做成一个能被海康、大华、TP-LINK 这些 NVR 直接识别的网络摄像机。最省事的路径当然是走 RTSP 推流但问题在于——NVR 添加设备时第一步不是拉流而是通过 ONVIF 协议去“发现”设备、获取设备能力、拿到 RTSP 地址。如果设备不响应 ONVIF 的 WS-Discovery 广播也不提供 Device Service 的 SOAP 接口那 NVR 的“自动发现”列表里根本不会出现你的设备只能手动填 URL体验差一大截。onvif-c这个组件就是来解决这个问题的。它是一个用纯 C 写的、跑在 ESP-IDF 上的 ONVIF 协议实现核心目标很明确让一块 ESP32 相机在局域网里被标准 NVR 当成一台“正经”的 ONVIF 设备添加进去。它不依赖任何 C 运行时不依赖 gSOAP 这种重量级框架而是基于esp_http_server手写 SOAP 解析与响应配合 UDP 组播实现 WS-Discovery。整个组件体积小、依赖少非常适合资源受限的嵌入式场景。这篇文章适合三类人看第一类是做 ESP32 摄像头项目、卡在 NVR 对接环节的嵌入式工程师第二类是想了解 ONVIF 协议最小实现集合、不想啃几百页规范文档的开发者第三类是想拿一个真实可跑的 plain C 组件来学习 ESP-IDF 组件化开发的人。我会从工程搭建一路讲到 NVR 成功添加设备把中间踩过的坑、参数怎么算、代码为什么这么写都摊开说。需要先明确一个认知ONVIF 不是一个单一协议而是一整套基于 SOAP over HTTP 的服务集合。一台设备要被 NVR 接受最少要实现三块内容——WS-Discovery让 NVR 发现你、Device Service让 NVR 查询你的能力、Media Service让 NVR 拿到你的流地址。onvif-c组件围绕这三块做了最小可用实现其余如 PTZ、事件、录像回放等一概不碰这也是它能在 ESP32 上跑起来的关键取舍。2. 工程搭建与组件集成把 onvif-c 塞进 ESP-IDF 项目2.1 环境准备与 ESP-IDF 版本选择先把地基打牢。onvif-c依赖esp_http_server这个组件在 ESP-IDF v4.4 之后趋于稳定建议用ESP-IDF v5.0 或 v5.1。我实测过 v4.4 也能跑但 v5.x 的httpd在并发连接和 header 处理上更省心。安装方式用官方 installer 最稳Windows 下编译慢是老问题后面会讲加速办法。装完之后先验证环境idf.py --version能打印出版本号就说明工具链没问题。接着创建一个空白工程idf.py create-project esp32_onvif_cam cd esp32_onvif_cam2.2 组件目录结构与 CMakeLists 配置onvif-c作为一个 ESP-IDF 组件标准做法是放在工程的components/目录下。目录结构大概长这样esp32_onvif_cam/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── main.c └── components/ └── onvif-c/ ├── CMakeLists.txt ├── include/ │ └── onvif_c.h └── src/ ├── onvif_discovery.c ├── onvif_device.c ├── onvif_media.c └── onvif_soap.c组件自己的CMakeLists.txt关键内容是注册源文件和依赖idf_component_register( SRCS src/onvif_discovery.c src/onvif_device.c src/onvif_media.c src/onvif_soap.c INCLUDE_DIRS include REQUIRES esp_http_server nvs_flash lwip )这里有个容易翻车的点REQUIRES里必须显式写上esp_http_server否则链接阶段会报httpd_start未定义。很多人第一次集成时只写了lwip结果编译过了链接挂掉排查半天。主工程的main/CMakeLists.txt则要引用这个组件idf_component_register( SRCS main.c INCLUDE_DIRS . REQUIRES onvif-c nvs_flash esp_wifi )2.3 网络初始化与组件启动顺序ONVIF 依赖网络所以启动顺序不能乱。正确顺序是NVS 初始化 → 网络栈初始化 → Wi-Fi 连接并获得 IP → 启动 HTTP 服务器 → 启动 WS-Discovery 组播监听。如果顺序颠倒比如在拿到 IP 之前就启动 Discovery组播 socket 绑定会失败或者绑到错误的网卡上。void app_main(void) { ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); wifi_init_sta(); // 连接路由器阻塞直到拿到 IP onvif_http_server_start(); // 启动 SOAP over HTTP 服务 onvif_discovery_start(); // 启动 WS-Discovery 组播 }注意onvif_discovery_start()内部会创建 UDP socket 并加入组播组239.255.255.250:3702。如果你的设备同时开了 AP 和 STA 两个网口务必指定绑定到 STA 网口的 IP否则组播包可能从 AP 口发出去NVR 永远收不到。2.4 编译加速与国内源配置Windows 下 ESP-IDF 编译慢是公认的痛点尤其是第一次全量编译。几个实测有效的加速手段把工程放在 SSD 上、关闭杀毒软件对 build 目录的实时扫描、使用ccache。在idf.py menuconfig里打开Compiler options - Enable ccache二次编译能快 40% 以上。另外组件依赖下载慢的话可以配置国内镜像源。在~/.gitconfig里把 github 的 URL 替换规则配好或者直接用乐鑫的组件镜像。这一步不是必须但能省不少等待时间。3. WS-Discovery 实现细节让 NVR 在列表里看到你3.1 WS-Discovery 的工作原理WS-Discovery 是 ONVIF 设备被发现的入口。它的机制其实很朴素NVR 往组播地址239.255.255.250的3702端口发一个Probe消息所有在线的 ONVIF 设备收到后单播回一个ProbeMatch消息里面带上自己的设备服务地址XAddr。NVR 拿到这个地址后才会去请求 Device Service 的详细信息。用生活化的类比NVR 在小区里拿喇叭喊“谁是摄像头”每个摄像头听到后单独打电话回复“我是我的联系方式是 xxx”。onvif-c要做的就是监听这个喇叭、然后回电话。3.2 组播 socket 的创建与绑定关键代码在于 socket 的创建和组播组加入int sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); struct sockaddr_in addr { .sin_family AF_INET, .sin_port htons(3702), .sin_addr.s_addr htonl(INADDR_ANY), }; bind(sock, (struct sockaddr *)addr, sizeof(addr)); struct ip_mreq mreq; mreq.imr_multiaddr.s_addr inet_addr(239.255.255.250); mreq.imr_interface.s_addr htonl(INADDR_ANY); setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, mreq, sizeof(mreq));这里imr_interface填INADDR_ANY在单网口设备上没问题但双网口设备必须填具体网口的 IP。我踩过一次坑设备同时开了 STA 和 AP结果组播包从 AP 口出去NVR 在 STA 网段里死活发现不了设备查了两小时才定位到。3.3 Probe 消息解析与 ProbeMatch 构造收到 Probe 后需要解析出消息里的MessageID因为ProbeMatch必须引用同一个 IDNVR 才能把回复和请求对应上。onvif-c用简单的字符串查找来提取 UUID而不是上完整的 XML 解析器——这是资源受限场景下的合理取舍。ProbeMatch的响应体核心结构如下soap:Envelope soap:Header wsa:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches/wsa:Action wsa:RelatesTourn:uuid:请求里的ID/wsa:RelatesTo /soap:Header soap:Body ProbeMatches ProbeMatch EndpointReferenceAddressurn:uuid:设备UUID/Address/EndpointReference Typesdn:NetworkVideoTransmitter/Types XAddrshttp://设备IP/onvif/device_service/XAddrs /ProbeMatch /ProbeMatches /soap:Body /soap:EnvelopeTypes必须是dn:NetworkVideoTransmitter这是 ONVIF 对网络摄像机的标准类型标识。写错了 NVR 会认为你不是摄像头直接忽略。XAddrs里的地址必须和 NVR 能访问到的 IP 一致如果设备有多个 IP这里要填对。3.4 设备 UUID 的生成与持久化EndpointReference里的 UUID 是设备的唯一标识NVR 靠它区分不同设备。这个 UUID 必须持久化不能每次重启都变否则 NVR 会认为是新设备反复添加。onvif-c的做法是首次启动时生成一个 UUID 存进 NVS之后每次读取。char uuid[37]; if (nvs_get_str(handle, dev_uuid, uuid, len) ! ESP_OK) { generate_uuid(uuid); // 基于 MAC 地址生成 nvs_set_str(handle, dev_uuid, uuid); nvs_commit(handle); }实操心得UUID 生成建议基于芯片 MAC 地址做哈希这样即使 NVS 被擦除重新生成的 UUID 也一致避免 NVR 里出现重复设备条目。4. Device Service 与 Media ServiceNVR 真正要的数据4.1 SOAP 请求的分发机制esp_http_server收到 POST 请求后onvif-c根据 SOAP Body 里的第一个元素名来判断是哪个服务请求。比如 Body 里是GetDeviceInformation就分发到设备信息服务是GetProfiles就分发到媒体服务。这种基于字符串匹配的分发方式虽然土但在只有十来个接口的场景下足够用而且比引入 XML 解析库省几百 KB 内存。分发逻辑大致是if (strstr(body, GetDeviceInformation)) { handle_get_device_info(req); } else if (strstr(body, GetCapabilities)) { handle_get_capabilities(req); } else if (strstr(body, GetProfiles)) { handle_get_profiles(req); } else if (strstr(body, GetStreamUri)) { handle_get_stream_uri(req); }4.2 GetDeviceInformation 与 GetCapabilitiesGetDeviceInformation返回厂商、型号、固件版本、序列号这些信息。NVR 添加设备时会显示这些内容填得规范一点体验更好。GetCapabilities则告诉 NVR 这台设备支持哪些能力比如是否支持媒体、是否支持 PTZ、是否支持事件。tds:GetDeviceInformationResponse tds:ManufacturerMyVendor/tds:Manufacturer tds:ModelESP32-CAM-ONVIF/tds:Model tds:FirmwareVersion1.0.0/tds:FirmwareVersion tds:SerialNumberESP32A1B2C3/tds:SerialNumber tds:HardwareId1.0/tds:HardwareId /tds:GetDeviceInformationResponseGetCapabilities的响应里Media节点的XAddr必须指向媒体服务地址通常是http://设备IP/onvif/media_service。如果这个地址填错NVR 后续的GetProfiles请求会打到错误的路径上直接 404。4.3 GetProfiles 与 GetStreamUri 的配合这是整个对接流程里最关键的一环。NVR 通过GetProfiles获取设备的媒体配置档Profile每个 Profile 里包含视频编码器配置、分辨率、帧率等信息。然后 NVR 拿 Profile 的 token 去调GetStreamUri换取真正的 RTSP 地址。GetProfiles响应示例trt:GetProfilesResponse trt:Profiles tokenprofile_1 fixedtrue tt:NamemainStream/tt:Name tt:VideoEncoderConfiguration tt:EncodingH264/tt:Encoding tt:Resolutiontt:Width1280/tt:Widthtt:Height720/tt:Height/tt:Resolution tt:RateControltt:FrameRateLimit25/tt:FrameRateLimit/tt:RateControl /tt:VideoEncoderConfiguration /trt:Profiles /trt:GetProfilesResponseGetStreamUri响应里返回的就是 RTSP 地址trt:GetStreamUriResponse trt:MediaUri tt:Urirtsp://192.168.1.100:554/stream1/tt:Uri tt:InvalidAfterConnectfalse/tt:InvalidAfterConnect tt:InvalidAfterRebootfalse/tt:InvalidAfterReboot /trt:MediaUri /trt:GetStreamUriResponse注意Uri里的 IP 必须是 NVR 能访问到的地址不能填127.0.0.1或0.0.0.0。我见过有人图省事写死0.0.0.0结果 NVR 拉流一直失败排查半天才发现是地址问题。正确做法是运行时动态获取本机 IP 拼进去。4.4 编码配置与 NVR 兼容性不同 NVR 对 Profile 的解析严格程度不一样。海康的 NVR 相对宽容大华的会校验VideoEncoderConfiguration里的必填字段。实测下来Encoding、Resolution、RateControl这三块必须完整缺一个字段某些 NVR 就会报“不支持的媒体配置”。分辨率建议填设备实际能推流的分辨率。ESP32 用 OV2640 的话720p 是上限填 1080p 会导致 NVR 按 1080p 协商但实际流是 720p画面可能被拉伸或黑边。老老实实填 1280x720 最稳。5. 常见问题与排查技巧实录5.1 NVR 发现不了设备这是最高频的问题。排查顺序建议这样走先确认设备是否真的发出了 ProbeMatch。用 Wireshark 抓包过滤udp.port 3702看设备有没有回复。如果没有回复检查组播 socket 是否绑定成功、是否加入了正确的组播组。如果有回复但 NVR 还是发现不了检查XAddrs里的 IP 是否可达以及Types是否为dn:NetworkVideoTransmitter。现象可能原因排查方法抓包无 ProbeMatch组播 socket 未绑定成功检查 bind 返回值有 ProbeMatch 但 NVR 无反应XAddrs IP 不可达ping 该 IPNVR 显示设备但添加失败Device Service 返回异常用 ONVIF Device Manager 测试添加成功但无画面GetStreamUri 地址错误手动用 VLC 拉流验证5.2 SOAP 响应格式错误导致解析失败ONVIF 对 XML 命名空间很敏感。tds、trt、tt这些前缀必须和 NVR 期望的命名空间 URI 对应。如果命名空间写错NVR 的 XML 解析器会直接报错表现为“设备不支持该操作”。建议直接参考 ONVIF 官方 WSDL 里的命名空间定义不要自己臆造。另一个坑是 SOAP Envelope 的encodingStyle属性。有些 NVR 要求这个属性存在且为空字符串有些则要求不存在。onvif-c默认不写这个属性实测对主流 NVR 兼容性最好。5.3 内存不足与任务栈溢出ESP32 的内存有限esp_http_server默认给每个连接分配的任务栈是 4KB 左右。如果 SOAP 响应体比较大比如 GetProfiles 返回多个 Profile拼接字符串时可能栈溢出。解决办法是把响应缓冲区放到堆上或者调大httpd_config_t里的stack_size。httpd_config_t config HTTPD_DEFAULT_CONFIG(); config.stack_size 8192; // 默认 4096调大一倍 config.max_open_sockets 4; // 按需调整实操心得调试阶段把max_open_sockets设小一点比如 2能更快暴露连接泄漏问题。如果 NVR 反复请求后设备卡死大概率是 socket 没释放。5.4 设备重启后 NVR 重复添加前面提过UUID 必须持久化。如果每次重启 UUID 都变NVR 会认为是新设备。除了 UUIDGetDeviceInformation里的SerialNumber也建议持久化有些 NVR 用序列号做去重。两个都存 NVS双保险。5.5 编译报错与依赖缺失最常见的编译错误是undefined reference to httpd_start原因是组件CMakeLists.txt里没写REQUIRES esp_http_server。另一个是lwip/sockets.h找不到需要确认REQUIRES lwip已添加。如果用了nvs_flash但没在REQUIRES里声明链接阶段同样会报错。6. 从能发现到能出画面完整联调流程6.1 用 ONVIF Device Manager 做中间验证在把设备交给 NVR 之前强烈建议先用 ONVIF Device ManagerODM这类工具验证。ODM 能自动发现设备、展示 Device Service 返回的所有信息、列出 Profiles、甚至能直接拉流预览。如果 ODM 能正常识别和预览说明 ONVIF 实现基本没问题剩下的就是 NVR 兼容性微调。ODM 里重点看三个地方设备信息页是否正常显示厂商型号、媒体页是否能列出 Profile、预览页是否能出画面。三个都通过NVR 对接成功率在 90% 以上。6.2 NVR 添加设备的实际操作以海康 NVR 为例进入“配置 - 系统 - 设备管理 - 在线设备”正常情况下应该能看到你的 ESP32 设备。勾选后点“添加”NVR 会自动填充协议为 ONVIF、端口为 80、用户名密码为空如果设备没做鉴权。添加成功后状态变成“在线”点“预览”就能看到画面。如果在线设备列表里没有点“刷新”再等几秒。WS-Discovery 是周期性的NVR 可能每隔几秒才发一次 Probe。如果一直不出现回到 5.1 节排查。6.3 RTSP 流地址的对接GetStreamUri返回的 RTSP 地址必须和实际推流服务一致。onvif-c本身不负责推流它只负责告诉 NVR 流地址在哪。推流部分需要另外实现可以用 ESP32 的 RTSP 服务器组件或者用其他方案。关键是地址要匹配端口要一致。如果 NVR 添加成功但预览黑屏先用 VLC 手动打开rtsp://设备IP:554/stream1确认流本身是通的。VLC 能播但 NVR 不能播通常是编码格式问题——NVR 可能只支持 H.264 Baseline Profile而推流用的是 Main Profile。把编码配置改成 Baseline 再试。6.4 鉴权与安全配置生产环境建议开启 ONVIF 鉴权。onvif-c支持 HTTP Digest 鉴权在GetDeviceInformation等接口上校验用户名密码。NVR 添加设备时填上对应的凭据即可。不开鉴权的话局域网内任何人都能访问设备接口存在风险。鉴权实现上esp_http_server本身不直接支持 Digest需要在 handler 里手动解析Authorization头并计算摘要。onvif-c把这部分封装成了独立函数调用时传入期望的用户名密码即可。7. 性能优化与扩展方向7.1 减少内存占用onvif-c的响应字符串拼接如果全用snprintf到固定缓冲区会占用不少栈空间。优化做法是预估最大响应长度用malloc分配堆缓冲区拼完发送后立即free。实测这样能把峰值栈占用从 6KB 降到 2KB 左右。另一个优化点是复用缓冲区。多个请求处理函数可以共用一个全局缓冲区加锁保护即可。但要注意esp_http_server是多任务并发处理请求的共享缓冲区必须加互斥锁否则会出现响应内容错乱。7.2 支持多 Profile 与子码流单 Profile 够用但有些 NVR 会请求子码流用于多画面预览。扩展方式是GetProfiles返回两个 ProfileGetStreamUri根据传入的 token 返回不同的 RTSP 地址。主码流 720p子码流 480p 或更低能显著降低 NVR 多画面时的解码压力。7.3 事件与 PTZ 的扩展思路onvif-c目前不实现事件和 PTZ但架构上留了扩展点。事件服务需要实现CreatePullPointSubscription和PullMessagesPTZ 需要实现ContinuousMove和Stop。如果只是做固定摄像头这两块可以完全不碰。如果要做云台控制建议单独开一个组件不要塞进onvif-c里保持职责单一。7.4 固件 OTA 与配置持久化设备部署后难免要升级固件。ESP-IDF 自带 OTA 机制配合onvif-c的配置持久化可以做到升级后 UUID、序列号、网络配置都不丢。关键是把这些配置存在 NVS 的独立命名空间里OTA 不会擦除 NVS 数据。配置持久化建议存这几项设备 UUID、序列号、ONVIF 用户名密码、RTSP 端口、分辨率配置。这样即使换固件版本NVR 那边也不需要重新添加设备。8. 我踩过的几个真实坑第一个坑是组播绑定。前面提过双网口设备必须指定网口 IP这个坑我花了两个小时。后来养成习惯任何涉及组播的代码第一件事就是确认绑定网口。第二个坑是 SOAP 命名空间。我一开始自己编了个前缀onvif结果 ODM 能识别但海康 NVR 不认。后来老老实实按 WSDL 里的tds、trt、tt来写问题消失。ONVIF 的命名空间是硬约定不要自作聪明。第三个坑是响应里的 IP 地址。我图省事在代码里写死了192.168.1.100测试环境没问题换到客户现场网段变了NVR 拉流全失败。后来改成运行时通过esp_netif_get_ip_info动态获取再拼进响应里彻底解决。第四个坑是httpd的 socket 数量。默认配置下并发连接数有限NVR 在添加设备时会连续发好几个请求如果 socket 不够用后面的请求会被拒绝表现为“添加设备超时”。把max_open_sockets调到 4 或 6 就稳了。第五个坑是 UUID 持久化。早期版本没存 NVS每次重启 NVR 里就多一台设备列表越来越长。加上 NVS 存储后这个问题再没出现过。这几个坑的共同点是都不是代码逻辑错误而是对协议约定和运行环境的理解不到位。ONVIF 对接这件事代码只占一半另一半是对 NVR 行为的理解和适配。多抓包、多用 ODM 验证、多换几台 NVR 测试比闷头改代码有效得多。
返回列表