首页 / 资讯中心 / 文章详情

ESP32-CAM接入NVR实战:onvif-c让摄像头被监控系统自动发现

ESP32-CAM接入NVR实战:onvif-c让摄像头被监控系统自动发现 ★ FEATURED ARTICLE
很多人以为把 ESP32-CAM 拍到的画面推到浏览器里就算做完了一个网络相机但真正要用的时候会发现你在 NVR 后台添加摄像头的列表里永远扫不到这台设备。原因很简单市面上几乎所有商业 NVR 都是通过 ONVIF 协议去发现和接入摄像头的而 ESP32 默认固件根本不带这个协议栈。这篇文章要讲的 onvif-c 就是专门解决这个问题的 —— 它是基于 ESP-IDF、用一个纯 C 组件实现的轻量 ONVIF 协议栈能让你的 ESP32 相机被主流 NVR 当作标准 IP 摄像头直接添加并拉流。我最早接触它的时候资料非常零散几乎是靠啃源码和抓包一个个试出来的所以这篇我按自己的实际踩坑顺序来写适合已经能用 ESP32-CAM 出图、想进一步接入监控生态的开发者。1. ONVIF 到底做了什么先搞清楚 NVR 添加摄像头背后发生了什么在动代码之前我觉得有必要先把 ONVIF 这套东西的底裤扒干净。NVR 添加一个网络摄像头表面上你只是填了个 IP、用户名和密码实际上后台走了一串协议流程。如果你不理解这套流程后面就算把 onvif-c 编译过了也不知道该去调哪个函数、看哪条日志。1.1 设备发现WS-Discovery 与被扫到这件事NVR 添加摄像头通常有两种方式手动填 IP或者在局域网里扫描自动发现。自动发现靠的就是 WS-DiscoveryWeb Services Dynamic DiscoveryONVIF 规范里的 Device Discovery 部分。它的本质很简单NVR 向局域网多播地址239.255.255.250:3702发一个 Probe 请求设备端收到 Probe 后回一个 ProbeMatch 响应告诉对方我在这我是这种设备。这个机制和我最早想象的不太一样 —— 我一度以为摄像头得定期广播自己的存在实际上更多是被动响应 启动时主动宣告的组合。设备上电后onvif-c 会主动发送一个 Hello 多播报文告诉网络里现有设备有个新人加入之后 NVR 发 Probe 来扫描时设备再响应 ProbeMatch。两者配合才能保证 NVR 不管是开机扫描还是运行中刷新都能找到你。1.2 服务与能力协商设备信息、能力、媒体配置的层层调用发现只是第一步。NVR 确认摄像头存在后会按顺序调用一系列 SOAP 请求我画了个主干逻辑GetDeviceInformation拿厂商、型号、固件版本、序列号GetCapabilities问设备支持哪些能力媒体、PTZ、事件等GetProfiles拿设备的媒体配置文件每个 Profile 对应一路视频流GetStreamUri根据 Profile 拿到 RTSP 拉流地址使用 RTSP 协议实际连接并拉取视频流。每一步都是标准 SOAP/XML 报文走 HTTP 传输。onvif-c 的核心工作就是把这些报文按最小必要集实现出来并且保证 XML 格式、命名空间、动作名都和 ONVIF 规范一致。规范全文上千页但真正被 NVR 用的通常就上面几个接口这也是轻量实现能落地的前提。1.3 为什么必须用 C 而不是上现成的 C/Python 方案ESP32 的资源情况摆在那双核跑在 240MHz内部 RAM 通常只有 300-400KB 可用还要留给 WiFi 协议栈和摄像头帧缓冲。ONVIF 官方参考实现很多是拿 C 甚至 Java 写的配合庞大的 SOAP 工具链比如 gSOAP生成一堆代码编译完直接几百 KB放到 ESP32 上不仅 flash 紧张运行时内存也会吃紧。onvif-c 取了个巧不依赖 SOAP 自动生成框架直接用 C 语言手写 XML 构造器和解析器只保留 ONVIF 的设备发现、设备信息、媒体配置、RTSP 这几个少而精的接口。整个组件编译出来很小运行内存可控这正是它适合 ESP-IDF 环境的原因。2. 工程结构搭建把 onvif-c 组件正确植进 ESP-IDF 工程组件再好装不进工程就等于零。我最初在这部分折腾了最久因为 ESP-IDF 的组件系统看着简单但版本匹配、依赖关系、CMakeLists 写法一不对就编不过。2.1 硬件选型与依赖梳理直接用我的配置单我能跑通这套方案的硬件组合如下不一定必须一模一样的型号但关键约束要满足部件推荐方案说明主控ESP32-S3带 PSRAM 的版本内存大一些跑 ONVIF RTSP 摄像头编码压力小很多摄像头OV2640 / OV5640出 RAW 或 JPEG后续接编码器方便存储8MB 以上 Flashonvif-c 本身不大但完整固件加文件系统余量要留足软件依赖主要是两块esp32-camera驱动和 ESP-IDF 自带的lwIPTCP/IP 协议栈。ONVIF 的 SOAP 走 HTTPRTSP 走 TCP这些在 lwIP 里都有。另建议开着mDNS组件后面开发调试时能用esp32-cam.local访问设备和 ONVIF 的 WS-Discovery 并不冲突。2.2 组件目录结构与 CMake 配置我把 onvif-c 放在工程根目录下的components/onvif-c里项目结构大致是my_camera_project/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── app_main.c └── components/ └── onvif-c/ ├── CMakeLists.txt ├── include/ ├── src/ └── idf_component.yml关键点是组件的CMakeLists.txt要声明好依赖关系让它能正确引用esp32-camera和 lwIPidf_component_register( SRCS src/onvif_server.c src/onvif_discovery.c src/onvif_media.c src/onvif_rtsp.c INCLUDE_DIRS include REQUIRES lwip esp_wifi esp_event ) REQUIRES esp32-cameraREQUIRES里如果你用的是 ESP32-S3 的摄像头驱动可以直接把esp32-camera作为一个外部组件链接进来。这里有个坑如果工程里同时有多个组件依赖esp32-camera得保证版本统一不然会出现摄像头回调函数反复定义之类的链接错误。2.3 主程序框架摄像头初始化与 ONVIF 任务的启停主程序逻辑不大核心是三步初始化摄像头、初始化网络、启动 ONVIF 服务。我贴一段自己用的骨架#include esp_camera.h #include onvif_server.h void app_main(void) { // 1. 初始化摄像头 camera_config_t camera_config { .pin_pwdn -1, .pin_reset -1, .pin_xclk 15, .pin_sccb_sda 4, .pin_sccb_scl 5, .pin_d7 13, .pin_d6 12, // ... 其余引脚按你的板子填 .fb_location CAMERA_FB_IN_PSRAM, .jpeg_quality 12, .fb_count 2, }; esp_camera_init(camera_config); // 2. 等 WiFi 连接完成用事件组或简单延时都可以 // 3. 启动 onvif 服务 onvif_server_start(); }注意fb_location CAMERA_FB_IN_PSRAM这一行我吃过没写这行的亏。ONVIF 和 RTSP 同时跑起来后内存非常紧张不把帧缓冲放 PSRAM系统会频繁重启。3. 设备发现与 Hello/Probe 响应让 NVR 的扫描列表里出现你的相机这章单独拎出来讲是因为我发现很多人编译过了、服务也启动了但 NVR 就是扫不到设备最后排查下来全部卡在发现环节。这不怪 onvif-c而是 WS-Discovery 本身有细节容易忽略。3.1 多播监听与报文格式为什么总是收不到 ProbeWS-Discovery 使用 UDP 多播设备要加入239.255.255.250:3702这个多播组。在 ESP-IDF 里这意味着你得用 lwIP 的 UDP 接口绑定端口、加入多播组并且监听来自任意网卡的报文。代码上大致是struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(3702); addr.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));很多人卡在 bind 地址上 —— 如果 bind 了某个具体的本机 IP其他网卡上进来的多播包就收不到直接 bindINADDR_ANY是稳妥的。另外 WiFi 进入省电模式或者 802.11 多播过滤设置不对也会导致丢包调试时可以把CONFIG_POWER_SAVE关掉环境变量CONFIG_LWIP_IP_FORWARD不用管那个不影响。3.2 Probe/ProbeMatch 的 XML 细节一处不对 NVR 就静默忽略收到 Probe 之后设备要回一个标准的 ProbeMatch。ONVIF 规范的 XML 命名空间非常严格少一个xmlns或者Types字段格式不对NVR 会直接丢弃这个包而且不会有任何报错提示。我用的典型响报文体是这样?xml version1.0 encodingUTF-8? e:Envelope xmlns:ehttp://www.w3.org/2003/05/soap-envelope xmlns:wsahttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:tdshttp://www.onvif.org/ver10/device/wsdl e:Header wsa:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches/wsa:Action wsa:MessageIDuuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/wsa:MessageID wsa:Tourn:schemas-xmlsoap-org:ws:2005:04:discovery/wsa:To /e:Header e:Body d:ProbeMatch d:EndpointReferencewsa:Addressuuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/wsa:Address/d:EndpointReference d:Typestds:NetworkVideoTransmitter/d:Types d:Scopesonvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/hardware/ESP32Cam/d:Scopes d:XAddrshttp://192.168.1.100:80/onvif/device_service/d:XAddrs /d:ProbeMatch /e:Body /e:Envelope几个容易翻车的点MessageID每次响应都应不同最好用随机 UUID固定值会导致某些 NVR 判重XAddrs里填的地址必须是 NVR 能直接访问到设备的地址不能是127.0.0.1或 mDNS 域名Types里的tds:NetworkVideoTransmitter是摄像头的标准类型不能省略。3.3 Hello 报文上电广播别忽略Hello 报文是设备启动时主动发往同一个多播地址的作用是告诉网络里的 NVR 和设备管理工具我上线了。它的格式和 ProbeMatch 高度相似主要区别是 Header 里的 Action 变成Hello。实际测试告诉我Hello 报文的作用比你想象的大。某些 NVR 并不会频繁发 Probe它们更依赖设备上线时的 Hello 来刷新列表。如果你的固件启动比 NVR 晚Hello 广播没发出去NVR 可能得等几分钟甚至手动刷新才扫到这台机器。所以 onvif-c 里上电发送 Hello 这步我建议一定保留。4. 媒体能力协商与 RTSP 拉流画面怎么真正送到 NVR 上NVR 能发现设备只是第一步它添加设备后真正关心的是我能不能看到画面。这涉及两套东西一是 SOAP 层面的媒体服务接口二是实际的 RTSP/RTP 媒体传输通道。onvif-c 把这两块都涵盖了。4.1 GetProfiles 与 GetStreamUri把流地址正确交给 NVRGetProfiles返回的设备媒体配置里面包含了 video source 配置、编码格式、分辨率、帧率最关键的是每个 Profile 有一个固定标识比如Profile_0。NVR 会拿这个标识去调GetStreamUri拿到类似这样的返回tt:MediaUri tt:Urirtsp://192.168.1.100:554/stream1/tt:Uri tt:InvalidAfterConnectfalse/tt:InvalidAfterConnect tt:InvalidAfterRebootfalse/tt:InvalidAfterReboot tt:TimeoutPT10S/tt:Timeout /tt:MediaUri这里的 Uri 就是设备端 RTSP 服务地址。你会注意到 Port 是 554这是 RTSP 的标准端口。如果设备端 RTSP 服务在别端口这里要写成一致否则 NVR 拉流会失败。我调试时经常遇到的一种情况是GetStreamUri返回的地址里 IP 写错了。如果你设备是多网卡或者用了桥接最好直接把 IP 写死在配置里别去动态取省得 NVR 拿着一个错误地址去连。4.2 RTSP 握手流程OPTIONS、DESCRIBE、SETUP、PLAYRTSP 本身并不复杂NVR 作为客户端会依次发四个请求OPTIONS探测设备端支持哪些方法DESCRIBE拿 SDP 描述里面含有编码格式、分辨率、帧率等信息SETUP协商 RTP 传输通道通常是RTP/AVP/TCP或RTP/AVP/UDPNVR 很可能会要求 TCP 穿防火墙PLAY开始播放。onvif-c 里实现 RTSP 服务时更多是在处理字符串解析和状态机。因为这个组件是纯 C 写的没有标准库之外的依赖你需要手写一个极简的 RTSP 解析器把请求行、头字段解析出来然后填充 SDP。SDP 的最小结构大致是v0 o- 0 0 IN IP4 192.168.1.100 sSession stream t0 0 mvideo 0 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1这里的96是动态 RTP payload type摄像头编码后的数据要按这个编号封装进 RTP 包里。如果你用的是 M-JPEG 编码则rtpmap会变成JPEG/90000payload type 一般是26。4.3 编码选型H.264 还是 M-JPEG码率怎么控制ESP32 的性能摆在那里GPU 是没有的主流的做法是用芯片内置的硬件 JPEG 编码器输出 M-JPEG 流或者用纯 CPU 跑一个轻量级 H.264 编码库。我的经验是M-JPEG实现最简单、帧率高、CPU 占用低但码率偏大1080p 下可能要 8-12 Mbps对 WiFi 和存储都是压力H.264码率低但纯软编码在 ESP32-S3 上跑 720p 都比较吃力帧率会掉到 10-15fps 左右需要花心思调编码参数。如果你的目标是能被 NVR 识别并流畅预览M-JPEG 是成本最低、最不容易出问题的起点。先把链路跑通日后再考虑 H.264 软编码。码率控制可以通过调整 JPEG 质量参数实现。jpeg_quality从 12 往下降是更清晰、码率更高往上升是更模糊、码率更低。我在实测中把jpeg_quality设为 15、分辨率 720p、帧率 15fps 时RTSP 拉流很稳定带宽占用大约 4-5 Mbps。4.4 设备鉴权Digest 认证才是真正消耗耐心的地方NVR 添加设备时几乎都会填写用户名密码所以设备端必须实现鉴权。ONVIF 的 SOAP 接口支持 WS-Security但 onvif-c 这种轻量实现一般只做了 HTTP Digest 认证。这个认证流程是客户端请求接口服务端返回401 Unauthorized并附带WWW-Authenticate头里面包含 realm、nonce客户端用用户名、密码、nonce、HTTP method、URI 计算 MD5 摘要再重新发起认证请求服务端校验摘要通过则返回真正的业务响应。如果你没实现鉴权NVR 添加设备时会出现用户名或密码错误的提示但逻辑上你可能压根没有校验密码 —— 问题就出在 NVR 根本没走到校验那一步你的服务端不认识它发来的认证头直接回了 401。这个坑在抓包里一眼就能看出来连续两次相同请求第二次多了Authorization头如果你服务端没有对第二次请求做正确处理说明鉴权处理代码少了分支。5. 从接线到 NVR 添加成功的完整走查按这个顺序排查半小时内解决问题我把自己一次从零调通的经验总结成了一条发布前检查链你按这个顺序走能少走很多弯路。5.1 固件烧录后的三项自检上电后不要急着打开 NVR先在电脑上做三件小事能 Ping 通确保 ESP32 已拿到局域网 IP能访问服务地址浏览器打开http://设备IP:80/onvif/device_service能收到一个 SOAP 错误报文也算服务在线比收不到连接强一百倍能看到组播报文用 Wireshark 抓239.255.255.250:3702的包确认设备启动后有 Hello 发出收到电脑发的 Probe 后有 ProbeMatch 响应。第 2 步和第 3 步是最快的诊断方式能帮你把网络没连通服务没起来ONVIF 响应格式错三类问题区别开来。我这里强调一下Wireshark 过滤语法可以直接用udp.port 3702非常直观。5.2 PC 端工具测试ONVIF Device Manager 是你的最佳伙伴NVR 处于家庭环境时可能不好抓包也未必方便立即测试。我会先在电脑上安装 ONVIF Device Manager一个免费的 ONVIF 设备管理工具简称 ODM用它对设备做一遍完整的发现 添加 预览测试。ODM 能干的几件事非常实用自动搜索局域网 ONVIF 设备快速验证 WS-Discovery 是否生效直接修改/查看设备信息、媒体配置验证 SOAP 接口是否正常能显示设备返回的所有 Profile 和 StreamUri方便和 NVR 里的表现对照。在这个环节如果 ODM 能看到设备、能出画面但 NVR 添加失败那问题基本锁定在 NVR 侧的特殊要求上比如它要求 HTTPS 或特定的鉴权方式。5.3 接入 NVR 时的典型失败清单现象可能原因解决方向NVR 扫描不到设备Hello 没发、ProbeMatch 没回、多播过滤抓包确认报文关 WiFi 省电模式能看到设备但添加失败GetDeviceInformation 格式不对或设备类型不匹配检查 Types、Scopes、XAddrs用户名或密码错误Digest 认证实现不完整重点检查 WWW-Authenticate 头的 realm、nonce连接超时RTSP 服务端口没监听或 StreamUri IP 错误检查 RTSP 端口、GetStreamUri 返回地址添加成功但黑屏SDP 编码参数和实际发送数据不符核对 payload type、编码格式、分辨率5.4 添加成功后的最后一公里VLC 二次验证NVR 拉流成功后我还习惯做一次 VLC 验证在电脑上用 VLC 打开rtsp://设备IP:554/stream1输入用户名密码后如果能出画面说明 RTSP 服务和 RTP 封包是标准的后面就算换品牌 NVR 出问题也可以基本撇清设备端责任。6. 内存、时间、兼容性实测中藏得比较深的一批坑能走进这一步说明你已经把主干链路跑通了。接下来这些坑属于低概率但碰到就要命的类型提前知道能省下大量排查时间。6.1 内存不足导致的重启循环ESP32 开发板看着内存还行可一旦 ONVIF RTSP 摄像头同时工作自由堆内存可能只剩几十 KB。最直观的表现一有 NVR 来请求设备立刻重启掉电一样。我当时排查了很久才发现是帧缓冲没有放在 PSRAM。建议fb_count不要设太高2 就够了优先使用CAMERA_FB_IN_PSRAMRTSP 发送线程栈大小建议给 4096 字节甚至更高别省可以周期性打印esp_get_free_heap_size()观察不同工作状态下的内存水位。6.2 NVR 对设备时间的强制校验ONVIF 规范里设备时间是个元数据字段很多 NVR 在添加设备时会要求设备时间与自身时间差在一定范围内超出就报错。我曾遇到一台 NVR 只要时间差超过 5 分钟就拒绝添加设备日志里只显示设备响应异常。解决办法简单粗暴设备启动后向 NTP 服务器同步时间。ESP-IDF 自带esp_sntp组件几行代码就能配好。如果你设备没联网或 NTP 不可用至少也要保证设备能正确响应SystemDateAndTime请求格式要按 ISO 8601 来。6.3 NVR 强制 HTTPS/TLS 的过渡方案一些商业 NVR 出于安全策略只允许 HTTPS 连接设备而 onvif-c 这种轻量组件默认只有 HTTP。遇到这种 NVR能想到的方案很有限查一下当前固件是否支持 TLS 作为传输层手动切到 HTTP-only 模式看 NVR 的兼容模式或允许未加密连接选项是否可用如果实在不行换品牌 NVR 是更实际的出路别在一棵树上吊死。我自己在实测中就遇到过必须开兼容模式才能添加的情况从 NVR 管理界面找到相应开关后就通了。这个属于标准之外的妥协不用太自责。6.4 GetStreamUri 返回的地址不可达最后一个坑比较隐蔽某些 NVR 会在拿到GetStreamUri的响应后用响应里的 Uri 去做 RTSP 连接。如果你的 Uri 里写了一个 NVR 访问不到的内网地址比如设备自认为的网卡地址和 NVR 所在网段不通画面自然拉不起来。最稳妥的策略是GetStreamUri返回的 IP 从当前 WiFi 获取的实际 IP 中提取。如果你不想动态获取就做一个可配置项手动填一个 NVR 能访问到的设备 IP。我踩过一次设备自认为的网卡地址是169.254.x.xNVR 根本连不上这个地址最后在配置里手工指定 IP 才解决。6.5 一件值得打包带走的小事抓包是好习惯不是应急工具调 ONVIF 这类跨设备协议最忌讳闷头改代码。我几乎每一次定位问题都会做三件事先看抓包再对照规范最后才改代码。ONVIF 的报文格式非常死板错一个标签就能静默失败靠眼睛很难看见。所以个人建议是从第一天开始就养成先抓包再动代码的习惯Wireshark 加一条过滤语法就能让你少熬无数个夜。总的来说onvif-c 给 ESP32 生态补上了一个很关键的拼图让一块几十块钱的开发板能被当成正规网络摄像机使用。但协议这个东西光编译过远远不够设备发现、鉴权、媒体协商、时间同步、内存心态每一项都可能是拦路虎。我这篇几乎是把能踩的坑都代你踩了一遍你照着链路走应该能比我当初快很多。如果后面调试中碰到别的问题建议回头看看抓包文件 —— 它往往比任何调试日志都诚实。
阅读完成 · 觉得有帮助?
咨询建站