恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

ESP32 实现 ONVIF 相机:从设备发现到 RTSP 推流实战

  • 首页
  • 资讯中心
  • /
  • ESP32 实现 ONVIF 相机:从设备发现到 RTSP 推流实战

相关资讯

大模型上下文管理实战:从注意力机制到成本优化的完整指南 2026/10/21 2:07:54
机器人竞赛硬件入门:电源管理与电机驱动实战指南 2026/10/21 2:07:54
上海交通大学Materials Today:14 秒电流活化烧结实现高强耐磨耐腐蚀的均匀纳米晶中熵合金包层 2026/10/21 2:07:54

最新资讯

多虚拟代理如何破解微电网中断周期下的能源优化难题
D78XX系列三端稳压器:工业级高可靠性电源管理
Ubuntu源码编译OpenClaw:老游戏引擎安装全流程解析
D337系列三端可调负压稳压器:工业级高精度电源管理
wifit3 如何给 USB 无线网卡评级?A 到 D 五维评分体系完整指南
STM32驱动OV2640摄像头:DVP接口、SCCB配置与JPEG图像采集实战

今日推荐

SAP 屏幕上的金额未必存在数据库表里,从 WB2R_SV 的 Net Amount 看懂运行时计算
具身智能中‘指哪打哪’的视觉语言动作联合建模
AI时代人工翻译技术博客的价值与实践指南

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

ESP32 实现 ONVIF 相机:从设备发现到 RTSP 推流实战

发布时间:2026/10/21 2:07:54
ESP32 实现 ONVIF 相机:从设备发现到 RTSP 推流实战 1. 为什么要在 ESP32 上折腾 ONVIF 相机把一块 ESP32 做成能被 NVR 直接添加的相机这件事听起来像是用玩具车跑拉力赛但实际做下来你会发现它解决的是一个非常具体的痛点低成本、低功耗、可定制的网络摄像头节点。ONVIF 是网络视频设备之间通信的通用语言NVR、录像机、监控客户端都靠它来发现设备、拉取 RTSP 流、控制云台。只要你的 ESP32 能说这门语言它就能混进任何一个标准监控系统里不需要私有协议不需要额外网关。我最初接触这个方向是因为手头有一批 ESP32-CAM 模组想拿来做几个固定点位的低帧率监控比如门口、阳台、储物间。市面上的成品 IPC 要么太贵要么功能过剩要么固件封闭没法改。ESP32 本身带 Wi-Fi、有摄像头接口、功耗低理论上完全能胜任够用就好的监控场景。但真正动手才发现ONVIF 这套协议栈在嵌入式端的资料极其零散官方文档是给服务器和桌面端写的ESP-IDF 下的 C 语言实现更是少有人系统整理。这篇内容就是把我从建工程到让 NVR 成功添加设备的完整过程拆开讲清楚。适合谁看有 ESP-IDF 基础、懂一点网络协议、想用 ESP32 做标准 ONVIF 相机的开发者。如果你连 ESP-IDF 都没装过建议先把官方 getting started 跑通再回来。全文围绕onvif-c这个纯 C 组件展开不依赖任何高级框架所有代码都能在 ESP-IDF 环境下直接编译。先说结论ESP32 做 ONVIF 相机核心难点不在摄像头驱动而在设备发现WS-Discovery和 RTSP 服务端的正确实现。NVR 添加设备时第一步是发 UDP 组播探测你的设备必须用标准 SOAP 响应第二步是拉 RTSP 流你的服务端必须正确处理 DESCRIBE、SETUP、PLAY 这几个方法。这两步任何一步出问题NVR 都会显示添加失败或离线。下面我按实际开发顺序把每个环节的原理、参数和踩坑点都摊开讲。2. 工程搭建与组件结构设计2.1 从零建一个 ESP-IDF 工程我习惯用idf.py create-project起手但 ONVIF 这种多组件项目更推荐手动组织目录。原因是onvif-c需要和摄像头驱动、Wi-Fi 配网、RTSP 服务端解耦如果全塞在 main 里后期改一处牵动全身。我的目录结构是这样的onvif_cam/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── main.c ├── components/ │ ├── onvif_c/ │ │ ├── CMakeLists.txt │ │ ├── include/ │ │ │ └── onvif_c.h │ │ └── onvif_c.c │ ├── rtsp_server/ │ └── cam_driver/ └── sdkconfig.defaults顶层CMakeLists.txt只写include($ENV{IDF_PATH}/tools/cmake/project.cmake)和project(onvif_cam)。main/CMakeLists.txt里用idf_component_register把onvif_c、rtsp_server、cam_driver都加进REQUIRES。这样做的逻辑是ONVIF 协议层不直接碰硬件它只负责响应 SOAP 请求和返回设备信息摄像头数据由 RTSP 服务端去取。分层之后你换摄像头型号、换 RTSP 实现ONVIF 层都不用动。sdkconfig.defaults里我固定了几个关键配置避免每次menuconfig手点CONFIG_ESP32_SPIRAM_SUPPORTy CONFIG_SPIRAM_USE_MALLOCy CONFIG_LWIP_SO_RCVBUFy CONFIG_LWIP_MAX_SOCKETS16 CONFIG_HTTPD_MAX_REQ_HDR_LEN1024SPIRAM 必须开因为 ONVIF 的 SOAP 响应报文加上 RTSP 的缓冲内部 RAM 很容易吃紧。MAX_SOCKETS调到 16 是因为 NVR 可能同时建立多个连接发现、描述、流媒体默认 10 个在高并发探测时会不够用。2.2 onvif-c 组件的职责边界onvif-c这个组件我给它划的职责非常明确只处理 ONVIF 协议相关的 SOAP 消息解析与构造以及 WS-Discovery 的组播监听与响应。它不负责视频编码不负责 RTSP 会话管理也不负责网络连接状态。这些通过回调函数暴露给上层。组件对外暴露的接口大概长这样typedef struct { const char *manufacturer; const char *model; const char *firmware_version; const char *serial_number; const char *hardware_id; const char *rtsp_url; // 例如 rtsp://192.168.1.100:554/stream1 const char *snapshot_url; // 可选 } onvif_device_info_t; esp_err_t onvif_c_start(const onvif_device_info_t *info); esp_err_t onvif_c_stop(void);onvif_c_start内部会做三件事启动一个 UDP socket 监听 3702 端口WS-Discovery 标准端口启动一个 TCP socket 监听 80 端口ONVIF 设备服务然后进入事件循环。所有设备信息都从info结构体读取这样上层可以在运行时动态改 RTSP URL比如 IP 变了之后重新通告。注意ONVIF 的设备服务端口不一定是 80但绝大多数 NVR 默认按 80 去探测。如果你改成其他端口需要在 Discovery 响应里带上 XAddrs 指明完整 URL否则 NVR 可能找不到。2.3 为什么不用 gSOAP 而手写解析很多人第一反应是用 gSOAP 生成 ONVIF 框架代码。我试过在 ESP32 上编译出来的二进制直接爆了 2MB而且 gSOAP 依赖大量标准库和动态内存在嵌入式环境里跑起来很不稳。ONVIF 的 SOAP 消息虽然看起来复杂但 NVR 实际用到的字段就那么几个GetDeviceInformation、GetCapabilities、GetProfiles、GetStreamUri。手写一个轻量级的 XML 解析器只提取这几个标签代码量不到 500 行RAM 占用可以控制在 20KB 以内。我的解析策略是不建完整 DOM 树用状态机逐字符扫描。遇到tds:GetDeviceInformation就标记当前请求类型遇到tds:Manufacturer就准备读文本。这样不需要递归解析也不需要动态分配节点对 ESP32 这种内存受限的平台非常友好。代价是代码可读性差一点但换来的是稳定性和可控性。3. WS-Discovery 设备发现的核心实现3.1 组播探测的报文格式与响应逻辑NVR 添加设备的第一步是向239.255.255.250:3702发送一个 UDP 组播包内容是一个 SOAP 信封里面包含Probe动作。这个包的结构大致是soap:Envelope xmlns:soaphttp://www.w3.org/2003/05/soap-envelope xmlns:wsahttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:wsdhttp://schemas.xmlsoap.org/ws/2005/04/discovery soap:Header wsa:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/wsa:Action wsa:MessageIDuuid:.../wsa:MessageID wsa:Tourn:schemas-xmlsoap-org:ws:2005:04:discovery/wsa:To /soap:Header soap:Body wsd:Probe wsd:Typesdn:NetworkVideoTransmitter/wsd:Types /wsd:Probe /soap:Body /soap:Envelope你的 ESP32 收到这个包后必须回复一个ProbeMatches消息里面带上设备的 UUID、XAddrs设备服务地址、Types 等信息。关键点在于回复必须单播回发送方的源地址和源端口而不是再组播回去。很多新手在这里翻车用组播回复导致 NVR 收不到。我在onvif_c.c里的处理逻辑是// 收到 UDP 包后 struct sockaddr_in src_addr; socklen_t addr_len sizeof(src_addr); int len recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)src_addr, addr_len); if (is_probe_message(buf, len)) { char response[1024]; build_probe_match(response, sizeof(response), src_addr); sendto(sock, response, strlen(response), 0, (struct sockaddr*)src_addr, addr_len); }build_probe_match里要填的 XAddrs 格式是http://ip:port/onvif/device_service。这里的 IP 必须是 ESP32 当前的实际 IP不能写死。我通常从esp_netif_get_ip_info拿然后动态拼字符串。3.2 UUID 的生成与持久化ONVIF 要求每个设备有唯一的 UUID格式是urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。这个 UUID 不能每次重启都变否则 NVR 会认为是新设备导致重复添加。我的做法是第一次启动时用 ESP32 的 MAC 地址生成一个 UUID存到 NVS 里之后每次启动都从 NVS 读。生成逻辑用 MAC 地址的前 6 字节加上随机数填充保证唯一性。NVS 的 key 用onvif_uuid命名空间用onvif_cfg。这样即使固件升级只要 NVS 不清设备身份就保持不变。实操心得有些 NVR 在设备 UUID 变化后不会自动更新而是保留旧条目变成离线状态。所以 UUID 持久化这件事必须在第一次烧录后就做好不然后期调试会非常痛苦。3.3 组播 socket 的配置细节在 ESP-IDF 里创建 UDP 组播 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)); // 允许端口复用避免重启时 bind 失败 int reuse 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reuse, sizeof(reuse));SO_REUSEADDR这个选项我强烈建议加上。ESP32 重启时如果上一个 socket 还没完全释放bind 会返回EADDRINUSE导致 Discovery 服务起不来。加上之后重启基本不会出现端口占用问题。另外IP_ADD_MEMBERSHIP的imr_interface如果填INADDR_ANY在有些路由器环境下会加入错误的网络接口。更稳妥的做法是等 Wi-Fi 连上、拿到 IP 之后用esp_netif_get_ip_info拿到 netif 的 IP再填进去。我实测下来填具体 IP 比填 ANY 的发现成功率更高。4. ONVIF 设备服务与 RTSP 流地址通告4.1 GetDeviceInformation 与 GetCapabilities 的响应构造NVR 发现设备后会通过 HTTP POST 向设备服务地址发送GetDeviceInformation请求。你的 ESP32 需要解析这个 SOAP 请求然后返回制造商、型号、固件版本、序列号、硬件 ID 这几个字段。响应格式soap:Envelope xmlns:soaphttp://www.w3.org/2003/05/soap-envelope xmlns:tdshttp://www.onvif.org/ver10/device/wsdl soap:Body tds:GetDeviceInformationResponse tds:ManufacturerESP32Cam/tds:Manufacturer tds:ModelONVIF-C Demo/tds:Model tds:FirmwareVersion1.0.0/tds:FirmwareVersion tds:SerialNumberABC123/tds:SerialNumber tds:HardwareIdESP32-S3/tds:HardwareId /tds:GetDeviceInformationResponse /soap:Body /soap:Envelope这里有个坑Manufacturer 和 Model 字段不能随便填。有些 NVR 会根据这两个字段去匹配设备模板如果填了不认识的品牌可能会拒绝添加或者显示为未知设备。我的经验是填一个通用的、不带商标的名称比如Generic或ONVIF兼容性最好。GetCapabilities的响应更复杂一些需要声明设备支持的能力包括 Media、PTZ、Events 等。对于 ESP32 相机通常只需要声明 Media 能力PTZ 和 Events 可以省略。响应里要包含Media的 XAddr指向媒体服务地址通常是http://ip/onvif/media_service。4.2 GetProfiles 与 GetStreamUri 的映射关系ONVIF 的媒体配置模型是Profile 代表一组音视频编码参数的集合StreamUri 是根据 Profile 生成的 RTSP 地址。NVR 添加设备时会先调GetProfiles拿到所有 Profile然后对每个 Profile 调GetStreamUri拿 RTSP 地址。对于 ESP32 这种单流设备我通常只提供一个 Profile命名为MainStream编码固定为 H264ESP32-CAM 的 JPEG 流需要转码或者直接用支持 H264 的模组。GetProfiles响应里要包含 Profile 的 token、名称、编码配置。GetStreamUri响应里返回rtsp://ip:554/stream1。关键点Profile token 必须和 StreamUri 请求里的 token 对应上。NVR 会带着 token 来请求流地址你的代码要根据 token 返回正确的 URL。如果只有一个 Profile直接返回固定 URL 即可但 token 校验不能省否则 NVR 可能认为设备行为异常。4.3 RTSP 服务端的最小实现RTSP 是 ONVIF 相机的实际数据通道。NVR 拿到 StreamUri 后会用 RTSP 协议去拉流。RTSP 的方法不多但顺序很严格OPTIONS查询服务端支持的方法返回Public: DESCRIBE, SETUP, PLAY, TEARDOWNDESCRIBE请求媒体描述返回 SDPSession Description Protocol里面包含编码格式、分辨率、帧率SETUP建立传输会话NVR 会指定 RTP 传输方式通常是 UDP 或 TCP interleavedPLAY开始推流之后 ESP32 持续发送 RTP 包TEARDOWN结束会话我的 RTSP 服务端实现里最核心的是 SDP 的构造。一个典型的 H264 SDP 长这样v0 o- 0 0 IN IP4 192.168.1.100 sESP32 Stream cIN IP4 0.0.0.0 t0 0 mvideo 0 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1;profile-level-id42e01f acontrol:track1profile-level-id要和实际编码器输出的一致否则 NVR 解码会失败。ESP32 如果用的是硬件 H264 编码比如 ESP32-S3 配合特定模组这个值从编码器配置里读如果是软编码需要手动设置成 baseline profile。注意RTSP 的端口默认是 554但 NVR 通常允许自定义。如果你的 ESP32 上 554 被占用可以改成 8554但要在 StreamUri 里带上端口号否则 NVR 会按默认 554 去连。5. 常见问题排查与实战避坑指南5.1 NVR 搜不到设备怎么办这是最常见的问题排查顺序我总结成一张表现象可能原因排查方法NVR 搜索列表为空组播包没收到用 Wireshark 抓包看 3702 端口有没有 Probe收到 Probe 但无响应响应地址错误检查 sendto 的目标地址是否是源地址有响应但 NVR 不显示XAddrs 格式错误确认 URL 是http://ip:port/onvif/device_service显示设备但添加失败设备服务无响应用 curl 手动 POST GetDeviceInformation添加成功但一直离线RTSP 拉流失败用 VLC 测试 StreamUri 能否播放我踩过最深的坑是组播包被路由器过滤。有些企业级路由器默认关闭 IGMP snooping导致组播包无法跨网段。解决办法是把 ESP32 和 NVR 放在同一个子网或者手动在路由器上开启组播转发。如果实在不行可以在 NVR 里手动输入设备 IP 添加绕过 Discovery 步骤。5.2 RTSP 流能播但花屏或卡顿花屏通常是 RTP 包时间戳或序列号不对。ESP32 发送 RTP 包时时间戳增量要按 90000Hz 的时钟计算。比如 25fps 的视频每帧时间戳增量是90000/25 3600。如果增量算错NVR 解码器会认为帧顺序错乱导致花屏。卡顿则多半是网络带宽或缓冲区问题。ESP32 的 Wi-Fi 吞吐有限H264 码率建议控制在 1Mbps 以内分辨率 640x480、帧率 15fps 是比较稳的配置。如果 NVR 支持 TCP interleaved 传输优先用 TCP比 UDP 抗丢包能力强很多。5.3 设备重启后 NVR 显示重复条目这个问题前面提过根源是 UUID 没持久化。但还有一种情况NVS 被擦除了。如果你在开发过程中频繁idf.py erase-flashNVS 里的 UUID 就没了重启后生成新 UUIDNVR 就认为是新设备。解决办法是在开发阶段把 UUID 写死在代码里量产时再改成 NVS 读取。或者每次擦除后手动在 NVR 里删除旧条目再重新添加。5.4 内存不足导致崩溃ONVIF RTSP 摄像头驱动同时跑ESP32 的内部 RAM 很容易耗尽。我的经验是所有大缓冲区SOAP 响应、RTP 包都分配到 SPIRAM用heap_caps_malloc(size, MALLOC_CAP_SPIRAM)替代malloc减少同时打开的 socket 数量RTSP 会话结束后立即关闭把CONFIG_LWIP_TCP_SND_BUF_DEFAULT调小到 4096减少 TCP 缓冲占用如果还是不够可以考虑把 ONVIF 的 HTTP 服务改成按需启动Discovery 阶段只开 UDP等 NVR 真正请求设备信息时再临时开 TCP响应完就关。这样能省下不少内存。5.5 固件升级后配置丢失ONVIF 的设备信息UUID、名称、RTSP URL如果存在 NVS 里固件升级时默认不会擦除 NVS所以配置能保留。但如果你用了 OTA 升级并且改了分区表NVS 分区可能被覆盖。建议在分区表里把 NVS 单独划分并且 OTA 时只写 app 分区不动 NVS。这样升级后设备身份不变NVR 不需要重新添加。6. 性能调优与扩展思路6.1 降低 Discovery 响应延迟NVR 搜索设备时通常只等 3-5 秒。如果你的 ESP32 响应太慢NVR 就认为设备不存在。优化点有两个一是提前把 ProbeMatches 报文模板准备好收到 Probe 后只替换 UUID 和 XAddrs 字段避免每次重新拼 XML二是把 UDP 接收任务优先级调高确保组播包不被其他任务阻塞。我在xTaskCreate里把 Discovery 任务的优先级设成 10比 RTSP 任务高实测响应时间从 200ms 降到 30ms 以内。6.2 多码流与多 Profile 支持如果 ESP32 性能足够比如双核型号可以提供两个 Profile主码流和子码流。主码流高分辨率低帧率子码流低分辨率高帧率。NVR 通常会自动选择子码流做预览主码流做录像。实现上就是在GetProfiles里返回两个 ProfileGetStreamUri根据 token 返回不同的 RTSP URLRTSP 服务端根据 URL 路径区分推流参数。6.3 与 ONVIF 事件服务的对接ONVIF 还定义了事件服务设备可以主动上报移动侦测、遮挡报警等事件。ESP32 如果接了 PIR 传感器可以通过事件服务推送给 NVR。实现方式是 NVR 先订阅事件ESP32 维护一个订阅列表有事件时向订阅地址发送 SOAP 通知。这部分我还没在 ESP32 上完整实现但协议层已经预留了接口后续可以扩展。6.4 低功耗场景的取舍ESP32 做相机功耗是个绕不开的话题。Wi-Fi 持续传输 摄像头工作电流轻松上 200mA。如果要做电池供电的监控节点必须牺牲实时性平时深度睡眠定时唤醒拍照通过 ONVIF 的快照接口提供图片而不是持续 RTSP 流。这种模式下ONVIF 的GetSnapshotUri就比GetStreamUri更实用。我在一个户外场景里试过这种方案用 18650 电池能撑一周左右代价是 NVR 里只能看到定时抓拍的图片没有连续视频。最后分享一个我在调试阶段常用的小技巧用 VLC 直接打开 RTSP 地址测试流比通过 NVR 排查快得多。VLC 的媒体信息里能看到详细的编解码参数和丢包统计如果 VLC 能播但 NVR 不能问题多半在 ONVIF 信令层如果 VLC 也播不了那就是 RTSP 或编码的问题。这个二分法能帮你快速定位故障环节省去大量抓包时间。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号