☰
ESP32 上跑 ONVIF:从零实现能被 NVR 添加的 IP 相机
2026/10/26 1:08:35 网站建设 项目流程

1. 为什么要在 ESP32 上跑 ONVIF:从需求到方案选型

1.1 一个很现实的需求场景

做安防或者做智能家居集成的朋友大概率都遇到过这个场景:手头有一块 ESP32 加摄像头模组,想把它做成一个能接入现有 NVR(网络录像机)的 IP 相机。硬件成本压得很低,几十块钱就能搞定,但问题卡在软件协议这一层——NVR 不认识你的设备,手动添加也加不进去,因为 NVR 添加设备走的是 ONVIF 协议,你的 ESP32 根本不会说这套“方言”。

ONVIF(Open Network Video Interface Forum)是安防行业里设备互联互通的事实标准。市面上主流的 NVR、监控客户端、录像软件,添加设备时基本都依赖 ONVIF 的发现和描述流程。你的设备只要能被 NVR 通过 ONVIF 发现、拿到设备信息、拿到 RTSP 流地址,就能像一台正规 IP 相机一样被添加进来。

onvif-c这个组件就是干这件事的:它是一个用纯 C 写的、基于 ESP-IDF 的 ONVIF 服务端组件,专门给 ESP32 系列芯片用。它不依赖任何 C++ 运行时,不依赖大型框架,直接跑在esp_http_server之上,配合 WS-Discovery 做设备发现,配合 RTSP 服务端提供视频流地址。说白了,它就是把 ONVIF 那套 SOAP/XML 的交互逻辑,用最轻量的方式在 ESP32 上实现出来。

这篇文章适合谁看?如果你正在用 ESP-IDF 开发 ESP32 相机类项目,想让设备被 NVR 或 ONVIF 客户端识别,或者你单纯想搞清楚 ONVIF 设备发现和描述的完整流程在嵌入式端怎么落地,那这篇内容就是给你准备的。我会从建工程开始,一路讲到设备能被 NVR 成功添加,把中间的关键细节、踩过的坑、参数怎么算都讲清楚。

1.2 为什么选 onvif-c 而不是自己从零写

有人可能会想,ONVIF 不就是几个 HTTP 接口返回 XML 吗,我自己写不行吗?行,但你会很快发现几个绕不过去的坎。

第一是WS-Discovery。NVR 添加设备的第一步不是直接访问你的 IP,而是往组播地址239.255.255.250:3702发一个 Probe 探测包,你的设备必须在这个组播组里监听,并且用 SOAP 格式回一个 ProbeMatch。这个组播收发在 ESP-IDF 里要用 lwIP 的 socket 选项配合 IGMP,自己写容易漏掉SO_REUSEADDR、组播加入时机、多网卡绑定这些细节,导致 NVR 根本发现不了你。

第二是SOAP 动作的完整覆盖。NVR 添加设备时会依次调用GetDeviceInformation、GetCapabilities、GetServices、GetProfiles、GetStreamUri等一串动作,每个动作的请求 XML 命名空间、响应结构都有讲究。少一个字段、命名空间前缀写错,NVR 就可能直接判定设备不兼容。

第三是和 RTSP 的衔接。ONVIF 本身不传视频,它只负责告诉 NVR“视频流在 rtsp://xxx 这个地址”。所以你的 ESP32 上还得有一个能用的 RTSP 服务端,onvif-c负责把GetStreamUri的响应指向它。

onvif-c把这些都封装好了,你只需要配置设备信息、媒体配置、RTSP 地址,剩下的协议交互它来处理。这就是选它的核心理由:把协议复杂度收敛到一个组件里,让你专注在摄像头和编码上。

1.3 整体架构长什么样

在动手之前,先把整个数据流在脑子里过一遍,后面配置的时候就不会迷路。

整个系统分四层。最底层是网络层,ESP32 通过 Wi-Fi 或以太网拿到 IP,加入 WS-Discovery 的组播组。往上是HTTP 服务层,用esp_http_server起一个 HTTP 服务,监听 ONVIF 的 SOAP 请求端口(默认 80 或你自定义的端口)。再往上是ONVIF 逻辑层,也就是onvif-c组件本身,它注册 HTTP 处理函数,解析 SOAP 请求,生成 SOAP 响应。最上层是媒体层,包括摄像头采集、H.264/MJPEG 编码、RTSP 推流。

NVR 的交互顺序是这样的:先组播 Probe 发现你,然后 HTTP POST 发 SOAP 请求拿设备信息和能力,再拿 Profile 和 StreamUri,最后用 RTSP 去拉流。onvif-c覆盖的是前三步,第四步靠你的 RTSP 服务端。

2. 环境搭建与工程创建:把地基打牢

2.1 ESP-IDF 版本选择和安装要点

onvif-c是纯 C 组件,对 ESP-IDF 版本有一定要求。实测下来,ESP-IDF v5.0 及以上比较稳,v5.1、v5.2 都验证过。v4.4 也能跑,但esp_http_server的一些 API 行为有差异,建议直接上 v5.x。

安装 ESP-IDF 我推荐用官方安装器或者 git 方式,不要用某些第三方打包的离线包,版本混乱容易出问题。安装完之后,第一件事是确认工具链能用:

idf.py --version

能正常打印版本号就说明环境 OK。如果你在 Windows 上编译速度慢,这是正常现象,ESP-IDF 的编译涉及大量小文件,Windows 文件系统开销大。我的做法是把工程放在 SSD 上,并且关掉杀毒软件对工程目录的实时扫描,编译时间能从十几分钟降到三四分钟。

提示:不要用中文路径,不要用带空格的路径。ESP-IDF 的构建系统对路径里的空格和中文处理不好,会报一些莫名其妙的错。

2.2 创建工程骨架

新建工程有两种方式。一种是从idf.py create-project开始,另一种是直接复制官方 example 改。我建议用后者,因为onvif-c通常自带 example,直接拿来改最快。

假设你已经拿到了onvif-c组件源码,目录结构大概是这样:

my_onvif_cam/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── main.c ├── components/ │ └── onvif-c/ │ ├── CMakeLists.txt │ ├── include/ │ └── src/ └── sdkconfig

顶层CMakeLists.txt里要设置项目名和引入组件目录:

cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(my_onvif_cam)

main/CMakeLists.txt里声明依赖:

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

这里有个细节:REQUIRES里必须显式写上onvif-c,否则组件不会被链接进来。很多人第一次用组件时忘了这步,编译能过但运行时找不到符号。

2.3 组件依赖和 menuconfig 关键项

onvif-c内部依赖esp_http_server,如果你还要跑 RTSP,那 RTSP 服务端是另一个组件。在idf.py menuconfig里,有几个配置项必须确认。

第一是lwIP 的组播支持。路径在Component config -> LWIP -> Enable multicast,必须打开,否则 WS-Discovery 的组播收不到。

第二是HTTP 服务端的最大连接数。NVR 可能会并发发多个 SOAP 请求,默认的HTTPD_MAX_OPEN_SOCKETS如果太小会拒绝连接。建议设到 7 以上。

第三是TCP 发送缓冲区。SOAP 响应虽然不大,但 RTSP 流是持续的,缓冲区太小会导致丢包。LWIP_TCP_SND_BUF_DEFAULT建议调到 5744 以上。

第四是Wi-Fi 的省电模式。做视频流一定要关掉 Wi-Fi 省电,路径在Component config -> Wi-Fi -> WiFi sleep,选None。省电模式会让 Wi-Fi 周期性休眠,视频流会卡顿甚至断流。

注意:这些配置改完要保存到sdkconfig.defaults里,不然下次idf.py fullclean之后又得重新配一遍。

3. ONVIF 核心机制拆解:搞懂原理才能配得对

3.1 WS-Discovery 是怎么被 NVR 发现的

WS-Discovery 是 ONVIF 设备发现的基石。NVR 不知道局域网里有哪些相机,它只能往组播地址发探测包,谁回应就说明谁是相机。

具体流程是这样的:NVR 往239.255.255.250的3702端口发一个 UDP 包,内容是一个 SOAP 的Probe消息,里面通常带一个Types过滤条件,比如dn:NetworkVideoTransmitter,意思是“我要找网络视频发送设备”。你的 ESP32 必须加入这个组播组,收到 Probe 后,回一个ProbeMatch,里面带上你的设备 UUID、设备类型、服务地址(XAddrs)。

这里有几个关键点。组播加入的时机很重要,必须在网络拿到 IP 之后再加入,否则组播组绑定到错误的网卡上。UUID 必须稳定,每次重启都变的话,NVR 会认为是新设备,反复添加。UUID 建议存在 NVS 里,第一次生成后固定下来。XAddrs 里的 IP 必须是设备实际可达的 IP,如果你有多个网卡,要选对那个。

onvif-c把这些逻辑封装好了,你只需要在初始化时传入设备 UUID 和 HTTP 服务端口,它会自动处理组播监听和 ProbeMatch 的构造。但你要确保 lwIP 的组播配置打开了,这是前提。

3.2 SOAP 请求的解析和响应构造

ONVIF 的所有交互都是 HTTP POST,body 是 SOAP XML。一个典型的GetDeviceInformation请求长这样:

<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> <soap:Body> <tds:GetDeviceInformation/> </soap:Body> </soap:Envelope>

响应要带上制造商、型号、固件版本、序列号、硬件 ID 这些字段。NVR 拿到这些信息后,会在界面上显示设备名称。

onvif-c内部用一个轻量的 XML 解析器来提取动作名和参数。它不依赖完整的 XML 库,而是针对 ONVIF 的固定结构做字符串匹配和提取,这样代码体积小、内存占用低。这也是它适合 ESP32 的原因——ESP32 的 RAM 有限,跑一个完整的 XML 解析库太奢侈。

响应构造时,命名空间前缀必须和请求对应。ONVIF 的命名空间有好几个:tds是设备服务,trt是媒体服务,tt是通用类型。onvif-c会根据动作自动选择正确的命名空间,你不需要手动拼 XML。

3.3 Profile 和 StreamUri 的关系

NVR 添加设备后,要拿视频流地址,走的是GetProfiles然后GetStreamUri。

GetProfiles返回设备支持的媒体配置,每个 Profile 有一个 token,里面包含视频编码格式、分辨率、帧率等信息。NVR 会选一个 Profile,然后用它的 token 去调GetStreamUri,拿到 RTSP 地址。

这里的关键是Profile 的 token 必须和 StreamUri 请求里的 token 对应。onvif-c允许你配置一个或多个 Profile,每个 Profile 绑定一个 RTSP 地址。配置的时候,Profile 里的编码格式要和你的 RTSP 流实际编码一致,否则 NVR 可能按 H.264 去解,结果你推的是 MJPEG,画面就出不来。

我的建议是,ESP32 上如果做 H.264 软编,分辨率和帧率都别设太高,640x480@15fps是比较稳的起点。Profile 里如实填写,NVR 会按这个去协商。

4. 从零写出能被 NVR 添加的相机:完整实操

4.1 初始化流程和代码骨架

先把main.c的骨架搭起来。整个初始化顺序不能乱,网络必须在 ONVIF 服务之前起来,ONVIF 服务必须在 RTSP 之前起来(因为 StreamUri 要指向 RTSP 地址)。

#include "onvif_c.h" #include "esp_http_server.h" #include "esp_wifi.h" #include "nvs_flash.h" void app_main(void) { // 1. NVS 初始化,UUID 要存这里 esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { nvs_flash_erase(); nvs_flash_init(); } // 2. 网络初始化(Wi-Fi 或以太网) wifi_init_sta(); // 3. 等待拿到 IP wait_for_ip(); // 4. 启动 RTSP 服务端(先起,拿到端口) rtsp_server_start(); // 5. 启动 ONVIF 服务 onvif_config_t cfg = { .device_uuid = get_or_create_uuid(), .http_port = 80, .manufacturer = "MyCompany", .model = "ESP32-CAM-01", .firmware_version = "1.0.0", .serial_number = "SN12345678", .hardware_id = "HW-ESP32S3", }; onvif_start(&cfg); // 6. 注册媒体 Profile onvif_add_profile("profile_1", "rtsp://192.168.1.100:554/stream"); }

这段代码里,get_or_create_uuid()是从 NVS 读 UUID,没有就生成一个存进去。UUID 的格式是urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,必须符合这个格式,NVR 才认。

4.2 设备信息配置的每个字段怎么填

设备信息看起来简单,但填错了 NVR 可能不认。逐个说。

Manufacturer:制造商名称,随便填但要非空。有些 NVR 会按制造商做兼容性判断,填主流品牌名可能触发特殊处理,建议填你自己的标识。

Model:型号,非空即可。NVR 界面上显示的就是这个。

FirmwareVersion:固件版本,建议用语义化版本号,比如1.0.0。

SerialNumber:序列号,必须唯一。如果你批量生产,这个要从芯片的 MAC 或者 efuse 里派生,保证每台不一样。

HardwareId:硬件标识,非空即可。

这些字段在GetDeviceInformation响应里返回。NVR 拿到后可能还会调GetCapabilities,onvif-c会自动返回支持的能力列表,包括设备、媒体、事件等。

实操心得:设备信息里的字符串不要带特殊字符,尤其是 XML 里的<、>、&,会导致响应 XML 解析失败。onvif-c内部会做转义,但你自己传进去的时候也注意一下。

4.3 媒体 Profile 和 RTSP 地址的绑定

Profile 的配置是让 NVR 能拉到流的关键。一个 Profile 至少包含这些信息:token、名称、视频编码、分辨率、帧率、RTSP 地址。

onvif-c的onvif_add_profile接口通常接受 token 和 RTSP URL,编码和分辨率信息在组件内部有默认值,也可以扩展配置。如果你的 RTSP 流是 H.264,Profile 里就声明 H.264;如果是 MJPEG,就声明 MJPEG。

RTSP 地址的 IP 部分必须是设备实际 IP。如果你在代码里写死了192.168.1.100,但设备实际拿到的是192.168.1.101,NVR 拿到的 StreamUri 就是错的,拉流会失败。正确做法是动态获取本机 IP 拼进去:

char rtsp_url[128]; esp_netif_ip_info_t ip_info; esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"), &ip_info); snprintf(rtsp_url, sizeof(rtsp_url), "rtsp://" IPSTR ":554/stream", IP2STR(&ip_info.ip)); onvif_add_profile("profile_1", rtsp_url);

这样无论设备拿到什么 IP,StreamUri 都是对的。

4.4 让 NVR 成功添加的完整验证流程

代码烧进去之后,怎么验证?分三步。

第一步,验证 WS-Discovery。在同一个局域网里用一台电脑,跑一个 ONVIF 设备发现工具(很多开源工具都支持),看能不能发现你的设备。如果发现不了,先查组播配置和防火墙。

第二步,验证 SOAP 接口。用 curl 手动发一个GetDeviceInformation请求:

curl -X POST http://192.168.1.100/onvif/device_service \ -H "Content-Type: application/soap+xml" \ -d '<?xml version="1.0"?><soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><soap:Body><tds:GetDeviceInformation/></soap:Body></soap:Envelope>'

能返回正确的 XML 就说明 HTTP 和 SOAP 层通了。

第三步,在 NVR 里添加。NVR 添加设备时,输入设备 IP、ONVIF 端口(默认 80)、用户名密码(如果你的设备没做鉴权,有些 NVR 会拒绝,需要配置允许匿名)。添加成功后,NVR 界面上应该能看到设备信息,并且能预览到画面。

如果 NVR 添加失败,看 NVR 的日志或者抓包。常见问题是鉴权没配、StreamUri 的 IP 不对、RTSP 流拉不到。

5. 常见问题排查与避坑经验

5.1 NVR 发现不了设备怎么办

这是最高频的问题。排查顺序如下。

先确认 ESP32 和 NVR 在同一个子网,跨网段组播是不通的。然后确认 lwIP 组播配置打开了。再确认防火墙没拦 3702 端口。如果都正常,抓包看 ESP32 有没有收到 Probe 包,收到了有没有回 ProbeMatch。

我遇到过一种情况:ESP32 加入了组播组,但绑定的是 AP 网卡而不是 STA 网卡,导致收不到。解决办法是在加入组播前明确指定网卡接口。

还有一种情况是 UUID 格式不对。NVR 对 UUID 格式有要求,必须是标准的urn:uuid:加 36 位 UUID,少一位都不行。

5.2 添加成功但预览黑屏

添加成功说明 ONVIF 层通了,黑屏说明 RTSP 流有问题。先确认 RTSP 地址在电脑上用播放器能拉到流。如果电脑能拉、NVR 拉不到,可能是 NVR 用的传输协议(TCP/UDP)和你的 RTSP 服务端不匹配,试试在 NVR 里切换传输模式。

如果电脑也拉不到,检查 RTSP 服务端有没有正常推流,编码格式和 Profile 里声明的是否一致。H.264 的 SPS/PPS 如果没正确发送,解码器解不出来,也是黑屏。

5.3 内存不够导致服务崩溃

ESP32 的 RAM 有限,ONVIF 服务、HTTP 服务、RTSP 服务、摄像头驱动都要占内存。如果启动时报malloc failed,要精简。

onvif-c本身内存占用不大,大头在 RTSP 和视频缓冲。可以降低 RTSP 的缓冲帧数,或者用外部 PSRAM。ESP32-S3 带 PSRAM 的型号会宽裕很多,建议做视频项目直接上带 PSRAM 的模组。

下面这张表是我整理的高频问题速查:

现象可能原因排查方向
NVR 发现不了组播未加入/网卡绑定错查 lwIP 组播配置、抓 3702 包
添加时提示不兼容设备信息字段缺失用 curl 验证 SOAP 响应
添加成功无画面StreamUri 错误检查 RTSP 地址 IP 和端口
预览卡顿Wi-Fi 省电未关关闭 WiFi sleep
服务崩溃内存不足降缓冲、上 PSRAM
UUID 重复未持久化UUID 存 NVS

5.4 几个容易被忽略的细节

第一个是HTTP 服务的 URI 路径。ONVIF 的请求路径通常是/onvif/device_service和/onvif/media_service,onvif-c会注册这些路径。如果你自己又注册了同名路径,会冲突。

第二个是Content-Type。ONVIF 用的是application/soap+xml,不是text/xml。有些 NVR 严格检查这个头,返回错了会被拒。

第三个是响应里的时间戳。某些 ONVIF 动作会带UTCDateTime,如果设备时间不对(比如没同步 NTP),返回的时间戳会很离谱,个别 NVR 会因此判定设备异常。建议启动后同步一次 NTP。

第四个是并发请求。NVR 添加设备时会连续发多个请求,如果你的 HTTP 服务端是单连接的,后面的请求会被拒。确保HTTPD_MAX_OPEN_SOCKETS够大。

6. 性能调优与扩展思路

6.1 让响应更快的几个调整

ONVIF 的 SOAP 响应本身不大,但构造 XML 时的字符串拼接如果效率低,会拖慢响应。onvif-c内部用的是预分配缓冲加snprintf,比动态拼接快。你可以把 HTTP 任务的优先级适当调高,避免被视频任务抢占。

另外,把 ONVIF 服务和 RTSP 服务放在不同的核心上(ESP32 是双核),能减少相互干扰。用xTaskCreatePinnedToCore把 ONVIF 的 HTTP 任务绑到 core 0,视频编码绑到 core 1。

6.2 后续可以怎么扩展

基础的设备发现和流地址已经能用了,往下可以做的方向不少。比如加事件服务,让设备能上报移动侦测事件,NVR 就能做联动录像。再比如加PTZ 控制,如果你的云台是步进电机驱动的,可以通过 ONVIF 的 PTZ 服务接收控制指令。还有多 Profile,同时提供高清和低清两路流,NVR 可以按需选择。

这些扩展onvif-c有的已经预留了接口,有的需要你自己在组件基础上加。核心思路是一样的:解析 SOAP 动作,返回对应响应,把实际动作转给底层驱动。

我个人在实际操作中的体会是,ONVIF 这套协议看着复杂,但真正跑通一次之后,后面加功能就是照葫芦画瓢。关键是把发现、描述、取流这三步的链路先打通,剩下的都是增量。踩过的坑主要集中在组播和 StreamUri 这两块,把这两个地方盯紧了,NVR 添加基本不会出问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询