news 2026/10/29 10:13:49

ESP32-CAM通过ONVIF接入NVR并推送RTSP/MJPEG流

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32-CAM通过ONVIF接入NVR并推送RTSP/MJPEG流

去年冬天我把一块 ESP32-CAM 从抽屉里翻出来,旁边是那台等了大半年的硬盘录像机。NVR 页面里的“添加设备”按钮我点了无数次,结果永远是失败、超时、连接不上。摄像头本身是好的,网页预览一开就能出画面,问题出在 NVR 根本不认识我,也不肯跟一个只会吐 HTTP 网页的“野生摄像头”对话。

后来我才彻底搞明白:NVR 添加一台网络摄像机,靠的并不是 IP 通不通,而是你这台摄像机能不能“自报家门”、能不能按标准协议回答一串问题。这套标准协议,就是 ONVIF(Open Network Video Interface Forum)。只要 ESP32 实现了 ONVIF 的发现、设备管理和媒体流描述接口,NVR 就会把你看作一台正经 IP 摄像头,然后照着它给你的 RTSP 地址去拉流。

这篇文章我会把整个过程摊开讲一遍,从建立 ESP-IDF 工程开始,接入 onvif-c 这个纯 C 组件,配置摄像头驱动,到把 OV2640 采集到的 JPEG 帧封装成 RTSP/MJPEG 流,最后让主流 NVR 成功添加并录像。整条链路我会按我实际踩过的顺序来写:先讲清楚为什么 NVR 不认你,再把工程一步步搭起来,中间穿插 ONVIF 协议的关键消息和排查方法,尽量做到每个环节你都能照着操作复现。

1. 为什么 NVR 不认你的 ESP32:ONVIF 是入场券

1.1 NVR 添加摄像头的真实流程

先搞清楚 NVR 是怎么“找朋友”的。绝大多数家用和商用 NVR 的添加界面里都有一个“自动搜索”或“在线设备”按钮,点下去以后,NVR 会向局域网发一组 UDP 组播请求。组播地址通常是239.255.255.250,端口是3702。这组请求属于 WS-Discovery 协议,它是在问网段内所有设备:你们谁是摄像头?支持哪些 ONVIF 服务?

如果你手里是一台普通 ESP32-CAM,里面只跑了一个 HTTP Web Server,它也收到了这个 UDP 包,但它的代码里没有任何 ONVIF 相关处理,于是直接忽略。NVR 等半天没回应,自然就把你划到“离线/不可用”一类。就算你手动在 NVR 里填了 IP 地址,NVR 接下来还是会去访问摄像头的 ONVIF 端口(默认是 80 或 8899),请求GetDeviceInformation、GetProfiles这类 SOAP 接口。你的 ESP32 没有这些接口,返回 404 或者干脆拒绝连接,添加必然失败。

所以整个问题的核心不是你画质差、帧率低,而是你没有以 NVR 听得懂的语言进行自我介绍。

1.2 ONVIF 协议到底做了什么

ONVIF 不是单个协议,它是好几个协议凑在一起的一个“语言簇”,对摄像头上跑的那部分来说,主要包含三块:

  • WS-Discovery:设备发现。摄像头开机以后加入组播组,应答 NVR 的 Probe 消息,回一个 ProbeMatch,把自己的物理地址、服务地址、类型告诉 NVR。
  • ONVIF Device Service:设备管理。提供GetDeviceInformation(厂商、型号、固件版本)、GetSystemDateAndTime(设备时间)、SetUser/Grant之类的操作。NVR 靠它确认你的身份和基本能力。
  • ONVIF Media Service:媒体配置。最核心的是GetProfiles、GetStreamUri、GetSnapshotUri。NVR 通过这组调用拿到一个 RTSP 地址,比如rtsp://192.168.1.100:8554/stream1,然后去这个地址拉视频流。

在协议传输层面,ONVIF 的发现走的是 UDP 组播,而设备管理和媒体服务走的是 HTTP + SOAP。SOAP 就是用 XML 封装请求和响应的那套东西,NVR 发送一个 XML 报文,你返回一个 XML 报文。好消息是这些报文格式非常固定,只要你把必要的 XML 模板写好,动态填充序列号和地址就行,不需要引入任何重量级 SOAP 框架。

1.3 为什么在 ESP32 上要选 plain C 实现

这一点值得单说。ESP-IDF 本身是 C 语言的框架,虽然它也能编译 C++ 代码,但用onvif-c这类纯 C 组件有几个实际好处:

  • 内存占用可控。一个 SOAP 请求解析器用纯手写 C 实现,ROM 和 RAM 开销通常只有几 KB;如果套用 gSOAP 之类的 C++ 工具,代码量动辄上百 KB,ESP32 的 320KB RAM 会非常紧张。
  • 不依赖 C++ 运行时。ESP-IDF 默认新libc++ 支持完善,但 C++ 异常、STL 容器会带来额外的栈和堆开销,在单片机场景里并不划算。
  • 层次清楚。ONVIF 协议本身就是一个“字符串模板 + 少量状态机”的组合,用 C 的snprintf生成 XML、用简单的字符串查找解析响应,反而直观得多。

如果你用过 ESP8266/ESP32 上比较流行的 HTTP 服务器库,你会知道纯 C 实现一个 Web 服务并不复杂。onvif-c 组件做的就是把这套思路扩展到 SOAP/WS-Discovery/RTSP 上。

2. 搭环境:建立 ESP-IDF 工程并接入 onvif-c 组件

2.1 ESP-IDF 开发环境准备

如果你还没装 ESP-IDF,先把这个搞定。官网文档给的安装器比较直观,下载 offline installer 之后会自动帮你把工具链、Python 环境、CMake、Ninja 都装到指定目录。装完以后命令行里执行:

idf.py --version

能正常输出版本号就说明环境 OK。

需要注意一个细节:ESP-IDF 版本不同,内部 API 差异不小。我当前用的是 v5.1.3 分支,配 onvif-c 组件比较顺利。如果你用的是 v4.x,摄像头驱动和网络接口方面可能要小改。建议新项目直接用 v5.x,旧项目就别硬迁移了。

2.2 创建一个空项目

创建一个新的 ESP-IDF 项目很简单:

idf.py create-project esp32_onvif_cam

这个命令会生成一个最简单的 hello_world 工程,里面有main文件夹、CMakeLists.txt、sdkconfig.defaults。

接着把摄像头驱动和 onvif-c 组件加进来。ESP32-CAM 最常见的摄像头驱动是esp32-camera组件,你可以直接用 IDF 组件管理器的语法:

cd esp32_onvif_cam idf.py add-dependency espressif/esp32-camera

onvif-c 组件在 IDF Component Registry 里不一定有正式版本,通常需要手动指定 Git 地址。加载方式有两种:一是直接在工程根目录的CMakeLists.txt里使用idf_component.yml添加依赖,二是我更常用的方式:把组件仓库克隆到项目的components/目录下:

mkdir -p components cd components git clone https://github.com/example/onvif-c.git onvif-c

提示:无论哪种方式,建议锁定到一个你测试过的 commit,别直接跟着 master 走,因为这类小众组件作者偶尔会大改接口,今天能用明天就编译不过的情况不少见。

2.3 onvif-c 组件的目录结构

我手里的 onvif-c 组件目录大致长这样:

components/onvif-c/ ├── CMakeLists.txt ├── include/ │ ├── onvif_server.h │ ├── onvif_discovery.h │ ├── onvif_media.h │ └── onvif_rtsp_server.h ├── src/ │ ├── onvif_discovery.c │ ├── onvif_soap.c │ ├── onvif_media.c │ ├── onvif_server.c │ └── rtsp/ │ ├── rtsp_server.c │ └── rtsp_session.c └── examples/ └── main.c

这个组件的设计思路很简单:它不是一个完整成品固件,而是把你的 ESP32 设备变成一台“可对话的摄像头”所需的所有协议骨架。你要做的只是实现几个回调函数,把摄像头采集到的数据交给它。

在主工程的main/CMakeLists.txt里,需要把包含路径和链接库加上,常见写法是:

idf_component_register( SRCS "main.c" INCLUDE_DIRS "." REQUIRES nvs_flash esp_wifi esp_event esp_netif esp32-camera onvif-c )

如果你的cmake报错说找不到esp32-camera,先确认你用idf.py add-dependency时选了正确的组件版本。

2.4 menuconfig 里的关键配置项

工程能编译之后,打开配置菜单:

idf.py menuconfig

这里我重点说三个必改项:

  • WiFi 配置。SDK 里没有默认 WiFi SSID,你需要把自己家网络的 SSID 和密码写进 menuconfig 的Example Connection Configuration项,或者直接在代码里用esp_wifi_set_config硬编码。
  • 摄像头型号选择。esp32-camera驱动会探测摄像头型号。用的是 AI-Thinker ESP32-CAM 板子,需要在 menuconfig 里确认 PSRAM 已启用,因为 OV2640 的 JPEG 模式必须把帧缓冲放到 PSRAM。
  • RTSP 端口。onvif-c 组件默认监听 8554。这里建议改成 554,因为很多 NVR 对 RTSP 端口有倾向性,非标准端口虽然能配置,但第一次联调时会平白多一个变量。

3. ONVIF 协议基础:NVR 要看的 SOAP/WS 消息模板

3.1 WS-Discovery 的 Probe 和 ProbeMatch

先看设备发现。NVR 发送的 Probe 报文长这样:

<?xml version="1.0" encoding="UTF-8"?> <e:Envelope xmlns:e="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dn="http://www.onvif.org/ver10/network/wsdl"> <e:Header> <w:MessageID>uuid:84ed9e18-79ed-4d61-b33c-9f0d1a9a5d01</w:MessageID> <w:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To> <w:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action> </e:Header> <e:Body> <d:Probe> <d:Types>dn:NetworkVideoTransmitter</d:Types> <d:Scopes>onvif://www.onvif.org/type/video_encoder</d:Scopes> </d:Probe> </e:Body> </e:Envelope>

NVR 在问:谁是一台网络视频发射器?你的 ESP32 必须在 UDP3702端口收到这个报文,解析出 Action 是Probe,然后回一个 ProbeMatch。ProbeMatch 里最关键的内容是:

<d:ProbeMatch> <w:EndpointReference> <w:Address>uuid:6a1b2c3d-4e5f-4098-a1b2-c3d4e5f60718</w:Address> </w:EndpointReference> <d:Types>dn:NetworkVideoTransmitter</d:Types> <d:Scopes>onvif://www.onvif.org/name/ESP32Cam onvif://www.onvif.org/hardware/ESP32 onvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/location/Test</d:Scopes> <d:XAddrs>http://192.168.1.100:80/onvif/device_service</d:XAddrs> <d:MetadataVersion>1024</d:MetadataVersion> </d:ProbeMatch>

XAddrs 是 NVR 后续访问你的入口地址,必须是你设备实际的局域网 IP 和监听端口。很多第一次做 ONVIF 的人在设备管理接口上调试了好几个小时,结果问题出在 ProbeMatch 里的 XAddrs 写成了localhost或者0.0.0.0,NVR 自然找不到你。

3.2 设备服务里的核心 SOAP 接口

设备一旦被发现,NVR 会立刻访问http://<ip>/onvif/device_service,依次请求这些操作:

  • GetDeviceInformation:返回厂商、型号、固件版本、序列号。
  • GetCapabilities:返回你支持哪些服务,Media 服务地址、Device 服务地址等。
  • GetSystemDateAndTime:NVR 会拿这个值跟你设备的实际时间做比对,对不上可能后面录像时间错乱。

GET 请求和 POST 请求都要能处理,NVR 的实现差异很大,有的先发 GET,有的直接发 POST。SOAP 请求是放在 HTTP POST 的 body 里,Content-Type通常是application/soap+xml或text/xml。onvif-c 组件里会有一个统一的入口函数解析 HTTP body,把它交给对应的 handler。

3.3 媒体服务与 Profile 结构

NVR 最关心的是你能出什么流。它调用GetProfiles后,你需要返回一个或多个 Profile,每个 Profile 里定义了视频编码、分辨率、帧率和码率。

对 ESP32 摄像头来说,我的 Profile 通常写成这样:编码格式标记为JPEG,分辨率 640x480,帧率 15fps。虽然 H.264 更省带宽,但 ESP32 没有硬件 H.264 编码器,软件编码会占掉太多 CPU。ONVIF Profile S 协议是支持 JPEG/MJPEG 的,所以不丢脸,NVR 也能接。

接着 NVR 会调GetStreamUri,并传入一个StreamSetup参数,里面指定传输协议是 RTSP,流类型是 RTP-Unicast。我在响应里返回一个 RTSP 地址:

<tt:MediaUri> <tt:Uri>rtsp://192.168.1.100:554/stream1</tt:Uri> <tt:InvalidAfterConnect>false</tt:InvalidAfterConnect> <tt:InvalidAfterReboot>false</tt:InvalidAfterReboot> <tt:Timeout>PT30S</tt:Timeout> </tt:MediaUri>

到这里,NVR 的逻辑就清晰了:先靠 WS-Discovery 找到你,再靠 Device Service 确认你的身份,再靠 Media Service 拿到你的 RTSP 地址,最后用 RTSP 你自己的流。onvif-c 组件要负责的就是前三个环节的协议反馈,以及第四个环节的 RTSP 服务。

3.4 SOAP 解析的轻量实现思路

不要一上来就想着引入 expat 或者 libxml2,ESP32 项目犯不着为这几个固定报文背上几十 KB 的解析器。我自己实现 SOAP 处理只用了几百行 C 代码:

  • 用strstr找<Action>标签,确定这次调用的方法名。
  • 用strstr找关键参数标签,比如<tt:ProfileToken>。
  • 用snprintf拼返回报文,注意转义 XML 特殊字符。

只要你严格按 ONVIF 文档里的字段名和命名空间来写,这样朴素的实现就够用。NVR 的 SOAP 客户端通常不会故意出刁钻的请求,它只按文档的约定来。

4. 摄像头采集链路:从 OV2640 到内存里的 JPEG 帧

4.1 esp32-camera 的配置细节

摄像头驱动配置我在app_main里做,核心代码如下:

#include "esp_camera.h" static camera_config_t camera_config = { .pin_pwdn = -1, .pin_reset = -1, .pin_xclk = 4, .pin_sccb_sda = 5, .pin_sccb_scl = 18, .pin_d7 = 19, .pin_d6 = 36, .pin_d5 = 39, .pin_d4 = 40, .pin_d3 = 41, .pin_d2 = 42, .pin_d1 = 12, .pin_d0 = 13, .pin_vsync = 44, .pin_href = 47, .pin_pclk = 48, .xclk_freq_hz = 20000000, .pixel_format = PIXFORMAT_JPEG, .frame_size = FRAMESIZE_VGA, .jpeg_quality = 12, .fb_count = 2, .grab_mode = CAMERA_GRAB_WHEN_EMPTY, };

这里几个参数我解释一下为什么这么设:

  • pixel_format = PIXFORMAT_JPEG:让摄像头硬件尽量直接输出 JPEG 帧,避免在 MCU 上做图像格式转换,省掉大量 CPU 计算。
  • jpeg_quality = 12:范围是 0-63,数字越小质量越高。对网络传输而言,12-15 是比较平衡的点,文件大小和水印观感都尚可。
  • fb_count = 2:双帧缓冲。采集线程和发送线程各持一帧,避免发送慢的时候把采集线程堵死。如果板子 PSRAM 充足,设成 3 更稳。
  • CAMERA_GRAB_WHEN_EMPTY:当没有消费者读帧时才抓新帧,发送线程读取的时候如果缓冲为空,驱动会自动抓一帧。这种模式适合流媒体场景。

注意 GPIO 引脚必须跟你实际板子匹配。AI-Thinker ESP32-CAM 的引脚图和普通 ESP32-DevKitC 完全不一样,照着 demo 抄之前先看板子丝印,不然配置错一根引脚就会导致 SCCB 总线超时,摄像头一整块黑屏。

4.2 从帧缓冲到发送线程

摄像头初始化成功之后,采集线程和 RTSP 发送线程之间通过一个简单的环形队列递指针。每次抓帧:

camera_fb_t *fb = esp_camera_fb_get(); // 将 fb->buf(JPEG 数据)交给 RTSP 发送线程 // 注意:不要在这里做 JPEG 解码或缩放,只在需要时用 fb->len esp_camera_fb_return(fb);

camera_fb_t里的buf和len就是完整的 JPEG 编码结果,直接可以作为 RTP 包负载发送。

我踩过的一个大坑:不要在同一个任务里既抓帧又立刻发送,更不要为了“高帧率”让抓帧线程无限循环。ESP32 的 PSRAM 读写有延迟,WiFi 发包又会卡在 TCP 拥塞控制上,单线程流水线很容易把内存耗尽然后死机。正确做法是抓帧线程只管抓,发送线程只管从队列取帧并阻塞发送,两个线程之间用队列深度做背压。

4.3 为什么我选择 MJPEG 而不是硬解 H.264

很多人一听到 NVR 就想到 H.264。但 ESP32 上做 H.264 会遇到三个麻烦:一是没有硬件编码器,软件编码的帧大小和延迟都很难看;二是 H.264 的 SPS/PPS 信息要在 RTSP DESCRIBE 阶段正确发送,流程上多一个状态;三是 NVR 对 H.264 流的容错比较严格,丢一个关键帧可能画面就花掉几十秒。

MJPEG 则完全不同:每一帧都是独立的 JPEG,天然适合丢帧恢复,延迟极低,NVR 拉流的时候就算有几帧丢了,下一帧马上恢复。在室内监控、门铃这类不需要长期录像保存的场景,MJPEG 完全够用。ONVIF Media Service 的VideoEncoderConfiguration允许Encoding=JPEG,所以合规性也没问题。

5. RTSP 握手与流封装:NVR 拉流的最后一公里

5.1 四个经典的 RTSP 交互

NVR 拿到rtsp://192.168.1.100:554/stream1之后,会发起标准 RTSP 握手。四种关键方法:

  • OPTIONS:询问服务器支持哪些方法。
  • DESCRIBE:请求 SDP 描述,服务器要返回视频流的编码参数和 RTP 负载类型。
  • SETUP:协商传输通道,包括 RTP/AVP、RTP/AVP/TCP 等。
  • PLAY:通知服务器开始推流。

onvif-c 组件的 RTSP 会话状态机要处理这些方法。关键顺序不能乱:大多数 NVR 会先发 OPTIONS,再发 DESCRIBE,然后 SETUP,最后 PLAY。如果客户端跳过 OPTIONS 直接 DESCRIBE,也要能应对。

5.2 返回 SDP 描述文件

DESCRIBE 响应里的 body 是 SDP 文本,类似这样:

v=0 o=- 1681200000000000 1 IN IP4 192.168.1.100 s=ESP32 Camera c=IN IP4 0.0.0.0 t=0 0 m=video 0 RTP/AVP 26 a=rtpmap:26 JPEG/90000 a=control:rtsp://192.168.1.100:554/stream1

26是 RTP 静态负载类型,对应 JPEG。注意rtpmap里面写的是JPEG/90000,这是 RTP JPEG 封装规范要求的采样率固定值。NVR 拿到这个 SDP 就知道接下来要收的是 RTP 包里的 JPEG。

5.3 把 JPEG 帧塞进 RTP 包

RTP 封装 JPEG 有一个固定的头结构,起始 8 字节分别是:

  • Type-specific(1 字节)
  • Fragment offset(3 字节)
  • Type(1 字节)
  • Quality(1 字节)
  • Width(2 字节,高字节先),高度类似。

对于不超过 MTU 的 VGA JPEG 单帧,通常不需要分片。每个 RTP 包的 payload 前面加 8 字节 JPEG 头,再接 JPEG 原始数据。如果你发现 ESP32 的 WiFi 吞吐不够导致丢包,可以把分辨率继续降到 QVGA,或者把jpeg_quality调大到 30,单帧体积可以压到 20KB 以下。

5.4 RTSP 端口和 TCP/UDP 的选择

大多数 NVR 默认会让摄像头走 RTP over UDP,然后 NAT 环境下的家用路由器如果没开端口映射就容易失败。我实测下来,把 onvif-c 的 RTSP 传输模式默认设为RTP/AVP/TCP更稳,也就是 RTP 数据直接内嵌在 RTSP 的 TCP 连接里传输。这样少一套 UDP 端口映射,NVR 只需要访问 TCP 554 端口就能完成全部拉流。

做法上,ONVIF 的GetStreamUri响应里可以直接写rtsp://...,而 RTSP SETUP 响应的 Transport 头里返回RTP/AVP/TCP;unicast;interleaved=0-1。即使你在GetStreamUri里没声明传输方式,RTSP 协商阶段也能通过这种方式告诉 NVR 走 TCP。

6. 实战联调:让 NVR 真的添加成功,并拆解几个高频报错

6.1 添加设备的完整操作路径

我以自己用过的 NVR 添加流程为参考,步骤如下:

  1. 电脑和 ESP32 摄像头确保在同一网段,先ping通。
  2. 在 NVR 的“添加设备”页面点“在线搜索”,等 3-5 秒,应该能看到一个包含ESP32Cam字样的设备。
  3. 输入你在 onvif-c 里配置的用户名和密码(通常是admin),协议选 ONVIF。
  4. 添加成功之后,NVR 会拉取 Media Profile,并且开始播放预览画面。

如果搜索不到设备,优先检查 UDP 3702 端口有没有监听。很多路由器会隔离组播,AP 隔离选项打开的情况下,手机和摄像头、NVR 之间互相看不见是很正常的。

6.2 报错一:Authorization 失败

NVR 会先拿GetDeviceInformation,如果返回 401 或者 403,就认为密码错误。onvif-c 里默认用 HTTP Basic Auth,但你必须在收到请求时检查 Authorization 头,并且跟你配置的密码做比对。很多第一次实现的人只做了 SOAP 回复,忘了做认证,NVR 会因此认为服务不可用。

实现方法很简单:解析请求头里的Authorization: Basic base64(admin:password),解码后做一次字符串比较。注意是明文 base64,不是 HTTPS,所以别截图暴露摄像头口令。

6.3 报错二:RTSP DESCRIBE 返回错误

常见错误有三种:

  • 返回404 Not Found:RTSP URL 的路径写错了。你GetStreamUri返回的是/stream1,你自己 RTSP 服务器注册的路径也得是/stream1。
  • 返回461 Unsupported Transport:SETUP 用的 Transport 头,你服务器端无法解析。常见原因是对方要求 TCP interleaved,你只实现了 UDP。
  • 返回454 Session Not Found:SETUP 之后隔了很久才 PLAY,或者 RTSP 会话超时被回收。这类情况同 NVR 的实现粒度有关,把 RTSP 会话超时时间设到 60 秒以上,能消掉一大半报错。

6.4 用 Wireshark 和 VLC 快速定位问题

联调时我抓包工具主要看三类流量:

  • UDP 3702:看 Probe 是否发出、ProbeMatch 是否响应、XAddrs 地址是否可达。
  • HTTP/80 或 8899:看 SOAP 请求和响应是否成对出现,XML 是否格式错误。
  • RTSP/TCP 554:看 DESCRIBE 请求是否收到正确 SDP。

桌面端可以把 VLC 当作轻量 NVR 来测。直接在里面打开:

rtsp://admin:password@192.168.1.100:554/stream1

如果 VLC 能出画面,说明 RTSP 链路没问题,问题大概率在 ONVIF 发现或 SOAP 上,NVR 不认是你没做对。

6.5 我印象最深的三个问题

  • 第一个是jpeg_quality设成 5,单帧 JPEG 超过 120KB,而 ESP32 的 WiFi 实际吞吐只有约 2-3MB/s,结果 15fps 的流直接撕裂。把质量调到 12 之后,单帧 40KB 左右,问题消失。
  • 第二个是fb_count只设 1,NVR 一拉流就出现画面卡顿和花屏。双缓冲之后发送线程不再阻塞采集线程,画质稳定下来。
  • 第三个是 XAddrs 里写的是http://0.0.0.0:80/onvif/device_service,NVR 拿到这个地址之后去连 0.0.0.0,自然失败。必须在启动时想办法取得真实局域网 IP,然后用snprintf动态拼装 XAddrs。

7. 部署后的稳定性经验:几处值得再收拾的设置

7.1 WiFi 吞吐和连接保持

ESP32 在 2.4G WiFi 下做实时视频流,最怕信道拥塞和丢包。WiFi 设置上注意两点:

  • 不要开老式兼容模式,让路由器优先用 802.11n,信道带宽选 40MHz,吞吐才会够。
  • 把 TCP 发送缓冲区设大一点,在esp_netif或者lwip配置里调一下TCP_SND_BUF,默认值很可能偏小,TCP 窗口小会导致 NVR 拉流卡顿。

另外要监听 WiFi 断开事件。我用esp_event_handler注册了WIFI_EVENT_STA_DISCONNECTED,一旦断开就自动重连,最多重试 5 次,失败则软复位。摄像头放在室外或者隔墙场景,这个保活逻辑必不可少。

7.2 内存泄漏排查

ESP32 一共也没多少 RAM,ONVIF 报文和 RTP 包都在堆里进进出出,稍不留神就泄漏。我在main里挂了空闲任务监控:

static void memory_monitor_task(void *arg) { while (1) { vTaskDelay(pdMS_TO_TICKS(10000)); printf("free heap: %d, min: %d\n", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT)); } }

如果每次添加/断开设备都掉几 KB 堆,多半是 SOAP 响应循环里某个malloc忘了free。我排查时最常发现的是esp_camera_fb_get拿到帧后没及时esp_camera_fb_return,这会造成 PSRAM 缓冲泄漏,跑到 40 分钟以后画面彻底不动。

7.3 进阶优化方向

如果你觉得 MJPEG 流量太大,可以尝试降分辨率到 320x240,实测画质还能接受,带宽占用直接降到 1MB/s 以内。如果想要真正的 H.264,可以外接一个带内置编码的摄像头模组,例如某些 ESP32-S3 + OV5640 的开发板支持硬件缩放,但编码端仍然不做 H.264。另一个思路是用 ESP32-S3 / ESP32-C6 搭配外部 H.264 编码芯片,但这套方案成本高,工程复杂度也上去了。

以我目前的经验和测试结果,MJPEG 加 ONVIF 这套组合,足以覆盖家用 NVR 的预览画面需求,而且代码量小、不依赖编译工具链的额外组件,后续要加云台控制、抓拍存档这些功能,也只是往 onvif-c 的 Profile 里继续填接口的事。你可以先把这套跑通,然后根据自己的场景,在 Profile 里增加对应的 ONVIF 操作就好。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 10:13:49

帝国CMS站SEO收录卡在两位数?一套插件包解决伪静态、canonical与sitemap

简介&#xff1a;面向帝国CMS站长与内容运营人员的免费SEO插件合集&#xff0c;围绕采集、伪原创、自动内链、关键词插入和自动配图等环节&#xff0c;提供一套可落地的站内优化工具组合&#xff0c;适合需要批量维护内容、提升搜索引擎收录与排名的初中级网站管理员。压缩包约…

作者头像 李华
网站建设 2026/10/29 10:12:51

Android反编译工具包:四层拆解与工程化实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/29 10:12:45

工业电源路径主动防护:TPS259483与PIC18F97J60协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/29 10:10:18

全连接网络在具身智能中的优化与应用

1. 全连接网络在具身智能中的核心定位全连接网络&#xff08;Fully Connected Network&#xff09;作为深度学习的基础架构&#xff0c;在具身智能系统中扮演着神经中枢的角色。不同于传统视觉任务中的卷积网络&#xff0c;具身智能场景下的全连接网络需要处理多模态传感器数据…

作者头像 李华
网站建设 2026/10/29 10:07:54

AI小说创作助手Zip包解析:从Agent架构到长篇一致性

简介&#xff1a;这是一套基于大语言模型的智能小说创作助手&#xff0c;面向长篇故事创作者、编剧与AI应用开发者&#xff0c;重点解决世界观设定不连贯、角色发展缺失、剧情前后矛盾等创作痛点。压缩包内共一百六十三个文件&#xff0c;其中Python后端脚本与TypeScript/React…

作者头像 李华
网站建设 2026/10/29 9:55:36

从技能树到刻意练习:构建可持续迭代的个人技能系统

最近两年我越来越意识到一个扎心的事实&#xff1a;在职场和项目里&#xff0c;真正拉开人与人差距的&#xff0c;往往不是谁更聪明、谁加班更狠&#xff0c;而是谁拥有一套属于自己的、可持续迭代的“技能系统”。这里说的“skills”&#xff0c;不是指简历上那一串“熟练使用…

作者头像 李华