ARTICLE DETAIL

资讯详情

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

ESP32摄像头接入NVR:纯C实现ONVIF协议全指南

ESP32摄像头接入NVR:纯C实现ONVIF协议全指南 把一台 ESP32 摄像头接到局域网里客户的 NVR 能自动发现、一键添加、稳定出图——听起来是件小事但做过 IPC 的人都知道这背后是一整套 ONVIF 协议握手。很多人一听到 ONVIF 就头疼官方 SDK 是 gSOAP 生成的 C 代码依赖一大堆在 ESP32 这种只有几百 KB RAM 的芯片上根本跑不动。于是就有了自己用 plain C 在 ESP-IDF 里重写一个 onvif-c 组件的想法。这篇文章就是整套过程的完整参考手册从建工程、搭组件目录到实现 WS-Discovery 设备发现、Device/Media 服务、Digest 认证再到让 RTSP 流被 NVR 真正拉走。适合谁读手上正好有 ESP32-CAM 或者 ESP32-S3 摄像头模组想做低成本 IPC 方案却还没被 NVR 兼容性折腾过的开发者也适合已经被“添加失败”“无视频流”折磨到想砸 NVR 的同行。我在实测阶段用海康、大华两家 NVR 各加了一遍也跑过 ONVIF Device Test Tool下面写的都是实际验证过的路径。1. ONVIF 对嵌入式相机到底意味着什么1.1 一次 NVR 添加流程背后的七次握手如果只看协议文档ONVIF 是一个巨大的标准设备管理、媒体、成像、PTZ、事件、录像、门禁几十个服务。但对一台嵌入式相机来说NVR 添加你时实际只会走一条固定链路。我把它拆成七步NVR 向239.255.255.250:3702发送 WS-Discovery 的 Probe 多播报文设备回 ProbeMatch报文里携带 Types、Scopes、XAddrsNVR 根据 XAddrs 请求 GetSystemDateAndTime很多厂商会先做校时NVR 请求 GetCapabilities拿到 Device 服务、Media 服务的具体地址NVR 请求 GetDeviceInformation确认厂商、型号、序列号NVR 请求 GetProfiles拿到视频编码配置 ProfileTokenNVR 请求 GetStreamUri获得 RTSP 拉流地址然后走 RTSP 的 DESCRIBE / SETUP / PLAY。这条链路里任何一个环节的格式不对、字段为空、地址不可达NVR 就会在界面上报“网络不可达”“设备离线”或者“添加失败”。所以做 ONVIF 不是“实现一个协议”而是“对 NVR 的期待做精准应答”。理解了这一点后面写代码时就不会想着把 ONVIF 全标准实现一遍而是把这条主链路先打通。1.2 为什么偏要用 plain C 在 ESP-IDF 里重写我一开始也想过直接用官方 SDK 或者现成的开源库但在 ESP32 上试过之后基本都放弃了。原因很现实gSOAP 生成的代码是 C 的XML 序列化/反序列化本身就吃掉大量内存。ESP32 在跑 WiFi 驱动、camera 驱动之后空闲堆内存常常只剩一百多 KB完整 SOAP 栈很容易就把余量吃光。很多 ONVIF 开源实现依赖 Boost、OpenSSL、libxml2在 PC 上编译没问题交叉编译到 ESP32 就是灾难。官方 SDK 是面向通用 IPC 的接口极多什么 Event、PTZ、Imaging 都有可嵌入式相机往往只需要其中四五个接口。用 plain C 重写最大的优势是可控。ESP-IDF 本身是 CMake 驱动组件机制对纯 C 库非常友好mbedtls 组件自带 SHA1 和 Base64刚好满足 ONVIF 认证计算XML 解析用 expat 的 SAX 接口就够了不需要拉进一整套 DOM。我自己实现下来ONVIF 相关代码的静态内存占用压在 25KB 以内这在嵌入式场景里是很有意义的。方案内存占用特征依赖情况嵌入式适配gSOAP 官方 SDK高常超过 80KBC、XML 库、TLS 栈差交叉编译体验差开源 C ONVIF 库中高Boost、OpenSSL 等差组件化困难自写 plain C 组件低实测 25KBmbedtls / expat / lwip好ESP-IDF 原生融入那什么时候别自己写如果商业项目需要过 ONVIF Profile S 认证要支持完整的事件推送、PTZ 云台、HTTPS或者面对的是必须通过严格认证测试的海外客户直接买商业 SDK 更划算。自己写这套的定位就是“能被主流 NVR 添加、能稳定出图”这能覆盖掉一线工程里九成以上的需求。1.3 组件目标定位先跑到“能添加、能出图”我在动工之前给自己列了一个明确范围避免掉进 ONVIF 文档的海洋里传输层先用 HTTP不做 HTTPS。需要加密再单独开 TLS但那个内存开销不小优先级往后放。认证只做 WS-Security UsernameToken支持 Digest 和明文两种模式。服务端点只提供 Device 和 Media 两个服务Event、PTZ 全部占位返回“未实现”。RTSP 推流和 ONVIF 逻辑解耦onvif-c 只负责告诉 NVR“流在哪个 URL”真正推流由 camera pipeline 负责。把这个边界画清楚之后整个工程从立项到跑通速度会快很多。NVR 真的关心的是你能不能被发现、能不能通过认证、能不能给出正确拉流地址至于其他花哨能力只是加分项。2. 工程落地把 onvif-c 放进 ESP-IDF 组件体系2.1 建工程与目录规划先创建一个 ESP-IDF 工程。我用的是idf.py create-project --target esp32 onvif_cam也可以手动建目录。关键是组件目录结构一个 ESP-IDF 组件本质上就是components/下的一个目录带自己的 CMakeLists.txt。我的 onvif-c 组件目录长这样components/onvif_c/ ├── CMakeLists.txt ├── include/ │ └── onvif_c/ │ ├── onvif_server.h │ ├── onvif_discovery.h │ ├── onvif_device.h │ └── onvif_media.h └── src/ ├── onvif_server.c ├── onvif_discovery.c ├── onvif_device.c ├── onvif_media.c ├── onvif_xml.c └── onvif_auth.convif_xml.c负责用 expat 解析请求中的方法名和参数onvif_auth.c负责解析 UsernameToken 并计算密码摘要onvif_server.c负责 HTTP 和 UDP 的 socket 生命周期。CMakeLists.txt 里的依赖声明是idf_component_register( SRCS src/onvif_server.c src/onvif_discovery.c src/onvif_device.c src/onvif_media.c src/onvif_xml.c src/onvif_auth.c INCLUDE_DIRS include REQUIRES mbedtls esp_wifi lwip expat json )REQUIRES 里的每一项都有用途mbedtls 用来算 SHA1 和 Base64不是用来做 TLS 的expat 用它的 SAX 解析 XMLjson 是给后面可能加的配置解析留的esp_wifi 和 lwip 是为了拿当前网卡 IP、处理 UDP 多播。这些依赖都是 ESP-IDF 内置组件不需要额外拉第三方包。2.2 让 sdkconfig 少踩几个坑很多人建完工程直接编结果 ONVIF 死活不通回头一看是 sdkconfig 里没开多播支持。我整理了一份关键配置项照着核对就行配置项作用说明CONFIG_LWIP_IGMPy开启 IGMP 协议栈收不到 WS-Discovery 多播 Probe 的根因之一CONFIG_LWIP_SO_REUSEADDRy允许 3702 端口绑定复用多播 socket 和单播 socket 可能需要同时监听CONFIG_MBEDTLS_SHA1_Cy启用 SHA1PasswordDigest 计算必须有CONFIG_MBEDTLS_BASE64_Cy启用 Base64同样用于认证计算另外主板 main task 的栈如果只有默认的 3584 字节跑 XML 解析和拼响应可能会出现 stack overflow。我给 onvif_task 单独开了 6144 字节的栈实测压力测试下稳定。环境搭建方面国内下载 ESP-IDF 和工具链时记得配置镜像源尤其是第一次安装官方源很容易卡在下载阶段。VS Code 配 ESP-IDF 插件可以直接选镜像地址能节省大量时间。2.3 线程模型三块互不阻塞的 socket我给 onvif-c 设计的线程模型很简单一个任务里创建三个监听端点但处理逻辑上彼此独立。UDP socket 监听 3702负责 WS-Discovery收到 Probe 后立刻回 ProbeMatch。这个处理必须快否则 NVR 的发现超时是 3 秒左右一旦排队就发现不了。TCP socket 监听 80负责 SOAP/HTTP 请求。每次 accept 之后我采用短任务方式处理避免一个慢请求阻塞后面的请求。RTSP 服务监听 554和 ONVIF 不在同一个线程里但会话管理要单独做。一开始我图省事把 UDP 收到 Probe 后的应答逻辑直接放在 WiFi 事件回调里结果发现 NVR 扫描时响应剧烈丢包。后来改成独立任务加队列UDP 收到报文后立刻在任务上下文里回包问题才消失。教训就是协议栈回包这件事永远不要放在别人驱动的回调里做长操作。3. WS-DiscoveryNVR 在局域网里找到你的三条报文3.1 收 Probe、回 ProbeMatch 的最小实现WS-Discovery 是 NVR 发现设备的方式。设备端要做的事很单纯加入多播组监听 UDP 3702收到 Probe 后回一个 ProbeMatch。加入多播组的代码关键部分就这几行struct ip_mreq mreq {0}; mreq.imr_multiaddr.s_addr inet_addr(239.255.255.250); mreq.imr_interface.s_addr sta_ip_addr; // 绑定到 STA 网卡 IP setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, mreq, sizeof(mreq));这里有个细节imr_interface最好填具体网卡的 IP尤其是 ESP32 同时开了 AP 和 STA 的时候。如果填 0.0.0.0内核可能把多播报文路由到了错误接口上NVR 发 Probe 你收不到。收到 Probe 后要识别请求的 Body 里是否包含Probe元素命名空间是http://schemas.xmlsoap.org/ws/2005/04/discovery。NVR 发来的报文大致长这样?xml version1.0 encodingutf-8? soap:Envelope xmlns:soaphttp://www.w3.org/2003/05/soap-envelope xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:dnhttp://www.onvif.org/ver10/network/wsdl soap:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /soap:Body /soap:Envelope有些 NVR 发 Probe 时不带 Types意思是“所有设备都回”我们直接回带了 Types 时只要包含NetworkVideoTransmitter或者NetworkVideoDisplay就要回。真实环境里也见过少数 NVR 的 Probe 里带了一长串 Types这时就做简单的子串匹配即可。ProbeMatch 的响应格式里有几个字段是 NVR 后续动作的关键d:ProbeMatch d:Typesdn:NetworkVideoTransmitter/d:Types d:Scopesonvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/name/ESP32-CAM onvif://www.onvif.org/hardware/ESP32CAM-0001/d:Scopes d:XAddrshttp://192.168.1.100/onvif/device_service/d:XAddrs /d:ProbeMatch注意 XAddrs 里的 IP 必须是当前设备实际使用的 IP后面我会专门讲这个坑。3.2 Scope 和 XAddrs为什么不能随便填NVR 拿到 ProbeMatch 之后下一步就是拿着 XAddrs 字符串去发 HTTP 请求。我见过一个很典型的错误XAddrs 在代码里写死成192.168.1.100结果客户现场网段是192.168.50.xNVR 能扫描到设备但添加的时候永远超时。因为 NVR 拿到的地址根本不可达。XAddrs 一定要在运行时拼接。ESP-IDF 里可以用esp_netif_get_ip_info()拿到当前 STA 接口的 IP再通过snprintf拼出完整的服务地址char xaddr[128]; esp_netif_ip_info_t ip_info; esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey(STA_DEF), ip_info); snprintf(xaddr, sizeof(xaddr), http:// IPSTR /onvif/device_service, IP2STR(ip_info.ip));Scope 字段同样不建议乱写。它的作用不只是给人看部分 NVR 会拿 Scope 里的onvif://www.onvif.org/type/video_encoder来判断这台设备是不是视频编码器、要不要显示在 IPC 列表里。少了video_encoder这一条有些 NVR 会直接把设备归到其他分类界面里不显示客户就以为你没做成功。3.3 Hello、Bye 以及在双网卡下的多播细节除了被动的 Probe/ProbeMatchONVIF 还定义了设备主动发 Hello 和 Bye。设备上线时向239.255.255.250:3702发一个 Hello 多播NVR 会主动感知到新设备设备下线前发 ByeNVR 就知道设备离线了。这不是必须的但做了之后体验会好很多。我的实现里设备启动 WiFi 连接成功、拿到 IP 后发一次 Hello然后每隔 30 秒周期补发一次。实测海康 NVR 对周期性 Hello 比较敏感能减少设备被误判离线的概率。双网卡是个隐藏雷区。ESP32 同时开 AP 和 STA 时多播路由可能只生效其中一个接口。解决方式就是我前面提到的加入多播组时把imr_interface绑定到 STA 网卡 IP同时确保 UDP socket 只用 STA 对应的本地地址来发送应答。如果实在调不通最后的手段是让客户在 NVR 里手动添加 IP虽然绕过了 Discovery但总比设备完全不可用强。4. Device 与 Media 服务从“被发现”到“被拉流”的协议路径4.1 用裸 socket 搭一个最小 SOAP 服务端点NVR 发现设备之后会按 XAddrs 发一串 SOAP 请求。我们不需要上完整的 HTTP 服务器框架直接基于 lwip 裸 socket 就能撑起一个最小服务端点。处理流程是listen(80)accept 客户端连接读取 HTTP 请求头和 Body用Content-Length确定 Body 长度从 Body 的 XML 里提取 SOAP Action 方法名根据路径/onvif/device_service和/onvif/media_service分发给不同 handler拼好 XML 响应带上Content-Type: application/soapxml写回。实现层面我用 expat 的 SAX 回调来解析 XML。不引入 DOM 解析器是因为嵌入式环境里 DOM 的内存开销太大而 SOAP Body 的结构非常固定SAX 足够用。具体做法是在StartElement回调里记录当前元素名等到了 Body 下第一个子元素时那个就是方法名。再往下找我们需要的一两个参数比如ProfileToken找到后把 CDATA 或文本内容存下来即可。整个解析器不到两百行 C 代码。有个细节SOAPAction 头在 ONVIF 里其实可以忽略直接用 Body 里的元素名路由更可靠。因为有的 NVR 厂商在 SOAPAction 里填的内容五花八门跟着它走反而容易踩坑。路由表我固定成两张/onvif/device_service处理 GetSystemDateAndTime、GetCapabilities、GetDeviceInformation/onvif/media_service处理 GetProfiles、GetStreamUri、GetVideoEncoderConfiguration。如果收到不认识的路径或方法就回一个 SOAP Fault,内容是Action not supported但 HTTP 状态码还是 200这样 NVR 不会因为异常终止后续流程。4.2 GetCapabilities 与 GetDeviceInformation 的字段清单GetCapabilities 的响应里最重要的两个字段是Device.XAddr和Media.XAddr。NVR 拿到这两个地址之后后面的所有请求都会往这两个地址发。如果这里返回空值NVR 可能直接跳过媒体服务导致后面拉流失败。我的响应节选是这样的Capabilities Device XAddrhttp://192.168.1.100/onvif/device_service/XAddr /Device Media XAddrhttp://192.168.1.100/onvif/media_service/XAddr StreamingCapabilities RTPMulticasttrue/RTPMulticast RTP_TCPtrue/RTP_TCP RTP_RTSP_TCPtrue/RTP_RTSP_TCP /StreamingCapabilities /Media /CapabilitiesGetDeviceInformation 的响应字段会直接显示在 NVR 的设备页面比如“厂商”“型号”“固件版本”。我建议写清楚方便现场排查问题。字段有 Manufacturing、Model、FirmwareVersion、SerialNumber、HardwareId。SerialNumber 可以做成由 MAC 地址派生保证每台设备唯一。4.3 GetProfiles 与 GetStreamUriProfileToken 就是 NVR 的“坐标”NVR 拿到 GetProfiles 的响应后会先记住 Profile 里的 Token比如MainStream然后拿这个 Token 去调用 GetStreamUri。如果设备在 GetStreamUri 里不按传入的 Token 返回对应流NVR 就会在界面上报“获取码流地址失败”。我踩过的坑是GetProfiles 里声明编码为 JPEG但 GetStreamUri 返回的 RTSP URL 却指向了另一路 H.264 流。这种自相矛盾在界面上很难看出来但有的 NVR 会在拉流后黑屏。所以 Profile 里的编码类型、分辨率、帧率必须和 RTSP 实际推的内容严格一致。GetStreamUri 的响应比较简单核心就是一个Uri节点GetStreamUriResponse MediaUri Urirtsp://192.168.1.100:554/main/Uri InvalidAfterConnectfalse/InvalidAfterConnect InvalidAfterRebootfalse/InvalidAfterReboot TimeoutPT10S/Timeout /MediaUri /GetStreamUriResponse注意 Uri 里的 IP 同样要拼接真实 IP绝对不能写成0.0.0.0。设备自己访问0.0.0.0没问题但 NVR 拿到这个地址后不会去“本地回环”找你的流只会认为这是非法地址。5. Digest 认证与 RTSP 联动最后一个门槛5.1 WS-UsernameToken 的两种认证方式ONVIF 要求设备支持 WS-Security 的 UsernameToken。大多数 NVR 发请求时会直接在 SOAP Head 里带一个 Security 块长这样Security UsernameToken Usernameadmin/Username Password Typehttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest MmUwN2E... /Password NonceZGF0YQ/Nonce Created2025-01-01T00:00:00Z/Created /UsernameToken /SecurityPassword 的 Type 有两种PasswordDigest和PasswordText。实现时两种都要接住因为有些老 NVR 或第三方平台默认发明文。PasswordDigest 的计算公式是digest Base64(SHA1(Base64Decode(Nonce) Created Password))Nonce 是 Base64 编码的随机数计算时要先解码再拼接 Created 字符串和设备密码做 SHA1最后 Base64。在 ESP32 上用 mbedtls 实现参考代码unsigned char nonce_buf[16]; size_t nonce_len 0; unsigned char sha1_out[20]; char digest_buf[64]; size_t digest_len 0; mbedtls_base64_decode(nonce_buf, sizeof(nonce_buf), nonce_len, (unsigned char*)nonce_b64, strlen(nonce_b64)); memcpy(buf, nonce_buf, nonce_len); strcpy((char*)buf nonce_len, created); // created 格式如 2025-01-01T00:00:00Z strcat((char*)buf, password); mbedtls_sha1(buf, strlen((char*)buf), sha1_out); mbedtls_base64_encode((unsigned char*)digest_buf, sizeof(digest_buf), digest_len, sha1_out, 20);这里最容易出错的是字符串拼接的缓冲区长度Nonce、Created、Password 三个加起来最坏情况可能有几十字节给缓冲区留 256 字节以上比较保险。5.2 时间、Nonce 和 401 循环折磨人的三连问很多人把认证代码写对了还是反复弹“用户名或密码错误”。排到最后十有八九是时间问题。ESP32 默认没有 RTC 电池上电后系统时间是 1970 年。NVR 发来的请求里带着Created字段部分厂商会校验这个时间与设备本地时间的差值发现设备时间不对就直接拒绝认证。解决办法有三个第一启动后尽快做 SNTP 校时这个最正规第二在 onvif_auth.c 里做一个“宽认证”——只校验 Created 的格式不校验新旧程度只要格式合法就继续算 Digest第三真的没校时成功时用收到请求里的 Created 反向校准内部时钟至少让设备端日志和 NVR 侧看起来是一致的。我实际用了第二种方案配合 SNTP 双保险兼容性最好。Nonce 的问题也常见。ONVIF 里 nonce 的作用是防重放设备端收到同一个 nonce 应该返回相同的 digest。如果设备每次都随机生成新的 nonce或者对同一 nonce 返回不同结果某些 NVR 会认为请求被篡改直接拒绝。实现上我维护了一个“最近一次 nonce”的缓存收到请求时先比对如果相同就复用上次计算的 digest不同才重新计算。实测这对部分海外 NVR 的兼容性是有效果的。5.3 RTSP 拉流SDP 和编码格式必须对得上ONVIF 解决的是“NVR 知道去哪拉流”真正出画面还得靠 RTSP。ESP32-CAM 最常见的输出是 JPEG最简单稳定的方案就是封装成 MJPEG over RTP对应 RFC 2435。SDP 描述要写清楚负载类型mvideo 0 RTP/AVP 26 artpmap:26 JPEG/90000 acliprect:0,0,480,640 aframerate:15payload 26 是 JPEG 的静态 RTP 负载类型。cliprect表示画面的裁剪区域不写的话有些 NVR 在界面上显示异常比例。framerate建议和实际编码输出保持一致虚标帧率会导致 NVR 计算录像时长不准。如果客户现场要求 H.264事情会复杂一倍。SDP 里必须带上 SPS/PPS也就是sprop-parameter-sets参数否则 NVR 收到的 H.264 流无法解码界面上显示黑屏或花屏。SPS/PPS 需要 Base64 编码后拼进 SDPmvideo 0 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1; sprop-parameter-setsZ0LAHpWoKANF..., aM4xHw注意这两个参数的 Base64 串必须和实际编码器输出完全一致不能拿其他设备的 SPS/PPS 来凑。另一个经验是 RTP 发送时的 MTU/MSS 设置如果包太大某些交换机会丢包画面就会出现马赛克。我通常把 RTP 负载控制在 1200 字节以内。5.4 维护两个 Profile 提升兼容性ONVIF Profile 不是只能有一个。我建议无论如何都声明两个MainStream和SubStream。一些 NVR 的逻辑是预览界面拉 SubStream 码流录像和回放拉 MainStream 码流。如果设备只提供一个 Profile有些 NVR 在界面切换清晰度时就会卡住。如果摄像头只有一个编码通道两个 Profile 可以指向同一路流但 Profile 里的分辨率、帧率必须写真实值不能为了“看起来高级”把 720P 的传感器标成 1080P。NVR 的设备管理页面会显示分辨率客户对比一番发现不对比不添加更麻烦。我实测中把 Main 设为最高分辨率 30 帧Sub 设为 640x360 15 帧海康和大华都能正常识别并出图。6. 调试链路与实测排坑从“添加失败”到定位根因6.1 用 ONVIF Device Test Tool 先把水搅浑调试 ONVIF 不能只依赖 NVR。NVR 是闭源黑盒报错信息往往只有一行很难判断是设备端问题还是 NVR 侧问题。我推荐先装一个 ONVIF Device Test Tool官方免费工具能逐项跑 Profile S 的测试用例。我的调试顺序是固定的先用工具自动探测设备看 Device Service 是否响应再跑 Media 的 GetProfiles、GetStreamUri看返回字段是否完整如果工具提示字段缺失或格式错误就用 Wireshark 抓包对照。ODT 比很多 NVR 严格有些 NVR 能“睁一只眼闭一只眼”通过的报文ODT 会标红。反过来ODT 全绿的设备上 NVR 的成功率就很高。6.2 Wireshark 怎么抓 ONVIF 包抓包是 ONVIF 调试里最重要的技能没有之一。过滤器很简单udp.port 3702先确认 NVR 有没有发 Probe、设备有没有回 ProbeMatch。如果 NVR 发了 Probe 但设备没回应就检查多播组绑定如果设备回了但 NVR 没后续请求就检查 ProbeMatch 里的 XAddrs 是否可达。等设备能扫描到了再过滤http contains GetCapabilities看具体的 SOAP 请求和响应字段格式一目了然。最后在 NVR 界面上点一下“播放”抓 RTSP 的包rtsp || rtp看 RTSP 握手是否正常、RTP 包有没有持续发送。这三个过滤器串起来就是 ONVIF RTSP 的全链路排查流程。6.3 常见失败模式对照表把实测中遇到较多的失败模式整理成一张表方便直接对照现象大概率根因处理办法NVR 扫描不到设备3702 端口未监听、多播组没加入、绑定错网卡开启 LWIP IGMP绑定 STA 网卡 IPWireshark 确认收到 Probe能扫到但添加超时XAddrs 返回 localhost 或旧 IPProbeMatch 和 Capabilities 都用运行时拼接的真实 IP反复弹用户名密码错误Created 时间偏差、Nonce 处理不当SNTP 校时缓存最近一次 Nonce添加成功但无画面RTSP URL 不可达或 SDP 与编码声明不一致核对 Profile 编码声明和 SDP确保 URL 使用真实 IP画面花屏、黑屏SPS/PPS 丢失或 RTP 分片过大H.264 时把 SPS/PPS 写进 SDP控制 RTP 包长这五类覆盖了我项目里遇到的九成问题。其中第一类和第四类出现频率最高前者是环境配置问题后者是自相矛盾声明问题都是可以通过抓包快速定位的。6.4 让 NVR 长时间在线跑的稳定性清单添加成功不代表项目结束NVR 通常要求设备 7x24 小时在线。我在长期运行中总结了三条经验第一给设备固定 IP。NVR 会缓存设备地址DHCP 重新分配后NVR 会显示设备离线。做法可以是路由器 DHCP 绑定也可以直接在 ESP32 里配静态 IP后者更省事。第二定期发送 WS-Discovery Hello。部分 NVR 靠 Hello 报文的接收情况判断设备在线状态只靠 RTSP 连接是不够的。我每 30 秒补发一次 Hello实测在线状态明显稳定。第三关注内存泄漏。RTSP 会话断开后socket 和 RTP 缓冲必须及时释放。我调试时在日志里每秒打印一次heap_caps_get_free_size()跑 24 小时观察内存有没有持续下降。如果内存曲线是平缓的设备基本就稳了如果一直在降赶紧查 RTSP 会话的释放逻辑。另外一个工程建议把 onvif-c 做得足够“哑”它只负责回答 XML 问题、给出 RTSP URL不掺和摄像头采集和推流。我当时把推流管线独立成camera_pipeline组件后来换编码方式、调整分辨率时ONVIF 层一行代码都没动大大降低了回归测试的压力。最后说一个我自己的体会做完这一整套东西之后最大的感受是 ONVIF 实现到“能被添加、能出图、能稳定在线”这个程度就够用了剩下的 PTZ、事件推送、HTTPS 都属于增量。如果你也准备踩这个坑建议先装好 Wireshark 和 ONVIF Device Test Tool再借一台海康或大华的 NVR 做真机验证不要只靠模拟器因为模拟器不会告诉你 NVR 的耐心有多短。后面有机会我再写一篇关于 ESP32 上 H.264 推流和 ONVIF Event 推送的扩展实践这套组件已经在为那部分内容铺路了。
返回列表