☰
ESP32-CAM接入NVR:用ESP-IDF实现ONVIF协议全流程
2026/10/23 5:32:51 网站建设 项目流程

上个月我把一块ESP32-CAM模组点亮,WiFi连上,摄像头能出JPEG画面,本地用浏览器看一切正常。然后我开始膨胀,把这块板子推到局域网,准备用某品牌NVR添加相机。结果添加界面转了好几圈,最后甩给我一句"设备不在线"。当时我第一反应是板子电源不稳,抓包一看才发现问题根本不是电源——NVR一直在局域网里发多播搜索包,而我的板子压根一声不吭。NVR要找的不是"一个能出视频的IP地址",而是一台提供ONVIF服务的设备。

这篇文章要讲的就是怎么在ESP-IDF这种纯C环境里,用onvif-c组件给ESP32相机补齐这套服务,让它能通过标准流程被NVR发现、识别、添加、拉流。阅读对象是手上正好有ESP32-CAM模组、想把视频接进现有NVR系统的开发者,以及那些想把ONVIF协议真正吃透而不只是抄代码的嵌入式工程师。内容会从协议原理讲到工程落地,再到联调排错,尽量少废话。

1. 拆开ONVIF这层壳:发现、管理、媒体、流四条链路

1.1 一个"能被添加"的相机必须打通的四段链路

很多人第一次接触ONVIF,脑子里全是SOAP XML那堆啰嗦的东西,天然产生抗拒。其实你换个角度看就清楚了:NVR添加一台相机,本质就是按顺序干四件事。

第一件,设备发现。NVR在局域网里喊一嗓子"有没有摄像头",你的相机得回答"我在"。这个喊话用的是WS-Discovery协议,UDP多播,发到239.255.255.250的3702端口。没有这一步,后面的全是空谈。

第二件,设备管理。发现了你之后,NVR会问一堆基础信息:厂商是谁、型号是什么、固件版本、序列号,还有系统时间。这就是Device Service干的事。

第三件,媒体配置。NVR问你有哪些视频流、分辨率编码是什么、RTSP的拉流地址是什么。这是Media Service干的事。

第四件,拉流。NVR拿着媒体配置里返回的RTSP地址,用RTSP/RTP协议把视频帧取走。

打个比方,你住酒店:发现环节是你站在大堂里让服务员知道来了个客人;设备管理是前台登记身份证,告诉对方你姓什么叫什么;媒体配置是前台给你房卡;RTSP才是真正坐电梯进房间。这四段链路缺一环,NVR的添加界面就会报错。

1.2 调用顺序背后隐藏的细节

这四段的调用顺序并不是随意的。NVR先发多播Probe,收到ProbeMatches拿到HTTP地址,然后才发设备管理的HTTP请求。这里有一个关键点:ProbeMatches里返回的XAddrs字段写的是HTTP服务地址,不是RTSP地址。NVR后续所有的ONVIF调用都基于这个地址,所以你的HTTP服务必须先跑起来,UDP发现才有意义。

设备管理过了之后,NVR会调用GetCapabilities拿到能力列表,从中解析出Media Service的地址。我以前图省事,直接在能力列表里只写了几个字段,结果某个NVR死活不肯往下走。后面仔细一看,它需要从Capabilities里找到Media的XAddr,才能去调GetProfiles。这个字段必须给全。

1.3 先定死支持范围再动手写代码

ONVIF规范厚得能砸死人,你要是照着全实现,一年都出不了活。我强烈建议先把支持范围定死,我的选择是:Profile S基础配置、单Profile、JPEG编码、WS-Discovery发现、UsernameToken认证。这套组合足以应对绝大多数NVR的添加流程。

Profile S是ONVIF里最基础的配置档,涵盖了实时流传输相关的所有接口。一个ESP32相机从实用角度讲,不需要搞多Profile,给一个MainStream就够。Profile Token我定为"MainStream",所有相关配置都以这个token来关联。先把范围钉死,后面每个Handler写起来脑子里就不会乱。

2. 纯C组件在ESP-IDF工程里的落地方式

2.1 为什么用纯C而不是C++

esp_camera、esp_http_server这些ESP-IDF的核心组件,底层全是C接口。onvif-c定位成plain C组件,最大的好处是编译依赖干净,不引入C++运行时,整个固件的体积和内存占用都更可控。加上组件注册和依赖管理也简单,你在主程序的CMakeLists里把它挂上就完事,不用额外操心ABI兼容的问题。

另外,纯C还有一个实际好处——调试方便。ESP32出问题的时候,我习惯开一个GDB或者直接把日志堆到串口上。C代码的调用栈比C++那层对象关系直白得多,崩了能一眼看到是哪个函数出了事。对于这种协议栈类型的代码,直白比花哨重要。

2.2 组件目录与CMake组织方式

我建议在工程根目录下建一个components/onvif-c目录,目录结构是这样的:

components/onvif-c/ ├── include/ │ └── onvif_c.h ├── src/ │ ├── onvif_core.c │ ├── onvif_device_service.c │ ├── onvif_media_service.c │ ├── onvif_discovery.c │ └── onvif_security.c └── CMakeLists.txt

CMakeLists里把需要的系统组件声明清楚:

idf_component_register( SRCS "src/onvif_core.c" "src/onvif_device_service.c" "src/onvif_media_service.c" "src/onvif_discovery.c" "src/onvif_security.c" INCLUDE_DIRS "include" REQUIRES esp_http_server mbedtls lwip nvs_flash )

REQUIRES不要贪多,够用就行。esp_camera、WiFi这些是主程序自己的依赖,onvif-c组件本身不需要直接引用它们。这样组件可以独立测试,也方便以后移植到别的平台。

2.3 模块划分与内存预算

onvif_core负责SOAP envelope的解析和公共XML构建;device_service和media_service分别对应两个WSDL的端点;discovery管UDP多播那摊事;security管UsernameToken。模块之间用最简单的方式交流:每个handler接收一个解析后的action名称和body缓冲区,返回一个构建好的XML字符串。

内存上要提前算账。ESP32虽然内存比很多MCU宽裕,但省着点用总没错。我的方案是预分配一块6KB的静态缓冲区,串行处理每个SOAP请求的body和响应XML,配合一个专用的HTTP处理函数。实测下来,设备管理类的最大响应也就3KB左右,6KB完全够用。RTSP那边才需要真正小心,每路播放会话的收发缓冲和RTP打包队列才是吃内存的大头。

HTTP服务直接用ESP-IDF自带的httpd。启动方式很简单,注册两个URI就够:

static void onvif_http_start(void) { httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); cfg.server_port = 80; cfg.uri_match_fn = httpd_uri_match_wildcard; httpd_handle_t server = NULL; httpd_start(&server, &cfg); httpd_uri_t dev_uri = { .uri = "/onvif/device_service", .method = HTTP_POST, .handler = device_service_handler, .user_ctx = NULL, }; httpd_register_uri_handler(server, &dev_uri); httpd_uri_t media_uri = { .uri = "/onvif/media_service", .method = HTTP_POST, .handler = media_service_handler, .user_ctx = NULL, }; httpd_register_uri_handler(server, &media_uri); }

这里有个经验:ONVIF请求全是HTTP POST,body里是SOAP XML。不要试图去解析HTTP header里的SOAPAction,不同NVR发过来的SOAPAction格式五花八门,老老实实从body里提取操作名最稳。

3. 设备发现:让NVR在局域网里找到你的那一次握手

3.1 组播监听与UDP socket准备

WS-Discovery基于UDP多播,所以第一件事是把socket绑定到3702端口,并且加入多播组。ESP-IDF基于lwIP,接口跟BSD socket基本一致:

#include "lwip/sockets.h" #include "lwip/netdb.h" static void discovery_task(void *arg) { int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); struct sockaddr_in addr = {0}; 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 = {0}; inet_aton("239.255.255.250", &mreq.imr_multiaddr); mreq.imr_interface.s_addr = htonl(INADDR_ANY); setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)); while (1) { char buf[2048]; struct sockaddr_in from; socklen_t from_len = sizeof(from); int len = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&from, &from_len); if (len < 0) continue; handle_probe(buf, len, &from); } }

需要注意,有些AP会开"多播隔离",尤其是办公网络或酒店网络。如果ESP32和NVR在同一WiFi下,NVR却搜不到设备,大概率就是AP把多播吞了。这种情况换到开放网络或者单独拿个家用路由器测试先。

3.2 Probe报文解析与ProbeMatches响应构造

收到Probe请求后,先判断一下Types里有没有NetworkVideoTransmitter。不同NVR发的Types格式不完全一样,有的带前缀dn:,有的不带。稳妥的做法是找子串"NetworkVideoTransmitter",别做死匹配。

响应要构造一个SOAP envelope,核心字段是Scope和XAddrs。XAddrs一定要填你的HTTP服务地址,格式是http://你的IP/onvif/device_service。我在这里踩过坑:有次我把XAddr写成了RTSP端口,NVR直接懵了,拿这个地址去发GetDeviceInformation,连不上。

ProbeMatches的标准响应长这样:

<Envelope xmlns="http://www.w3.org/2003/05/soap-envelope"> <Header> <RelatesTo xmlns="http://www.w3.org/2005/08/addressing">uuid:从Probe里提取的MessageId</RelatesTo> </Header> <Body> <ProbeMatches xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery"> <ProbeMatch> <Types>dn:NetworkVideoTransmitter</Types> <Scopes>onvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/name/esp32cam onvif://www.onvif.org/hardware/esp32</Scopes> <XAddrs>http://192.168.1.100/onvif/device_service</XAddrs> <MetadataVersion>1</MetadataVersion> </ProbeMatch> </ProbeMatches> </Body> </Envelope>

注意Header里的RelatesTo字段,要原样回填Probe请求里的MessageID。我早期经常忽略这个细节,直接填个固定uuid,结果部分NVR的发现流程能跑通,但添加设备时出现莫名其妙超时。后来加上RelatesTo才稳定。

3.3 Scope的格式没那么难,但也别乱写

ONVIF Scope的构成方式是"onvif://www.onvif.org/类别/值",类别包括type、name、hardware、location等。type是NVR判断你是不是视频设备的主要依据,必须带上video_encoder。name会显示在NVR添加界面的设备名里,你可以改成自己的产品型号名。hardware可以填模组型号。

有一个技巧:原来几台NVR同时扫描时,有些NVR会按Scope里的name字段去重。如果两台相机的name完全一样,可能只显示一台。所以最好把name设置成带唯一性的值,比如加上mac后几位。

4. 设备管理与媒体配置:NVR逐个调用的接口清单

4.1 GetSystemDateAndTime与GetDeviceInformation两个必回接口

NVR一旦从发现拿到你的HTTP地址,第一个调用的一般是GetSystemDateAndTime,第二个是GetDeviceInformation。这两个接口相当于是设备的"身份证和当前时间",一定不能出问题。

GetSystemDateAndTime的响应要给出UTC时间和本地时间。ESP32用SNTP同步过之后,直接从系统时间取就行,格式不能错,Year/Month/Day/Hour/Minute/Second每个字段单独标签。另外TimeZone字段要带上时区偏移,比如"+08:00",有些NVR解析这个字段时不够宽容,写得不标准它会按默认UTC处理,导致认证时时间校验失败。

GetDeviceInformation的响应相对简单,照着下面这个格式填就行:

<GetDeviceInformationResponse xmlns="http://www.onvif.org/ver10/device/wsdl"> <Manufacturer>Generic</Manufacturer> <Model>ESP32CAM</Model> <FirmwareVersion>1.0.0</FirmwareVersion> <SerialNumber>ESP32CAM0001</SerialNumber> <HardwareId>ESP32-OV2640</HardwareId> </GetDeviceInformationResponse>

Manufacturer和Model会显示在NVR的设备信息面板里。FirmwareVersion建议用一个语义化版本号,别填乱七八糟的长字符串,有NVR校验字段长度,超过255个字符会直接拒绝。

4.2 GetCapabilities的回复策略:Media XAddr必须给全

GetCapabilities返回的是能力块列表,里面会分Analytics、Device、Events、Imaging、Media等多个块。对于NVR添加流程来说,最重要的就是Media块里的XAddr。NVR要拿到它才知道去哪儿请求媒体配置。

我的响应策略是:Media能力块必填,给出/onvif/media_service的完整HTTP地址;Device能力块给一个基础版本号;其他块可以写空结构。实测发现,绝大多数NVR只关心Media的XAddr,其它块空着也能通过。但有一个坑:如果你把Media块整个省略,部分NVR会认为这台设备不支持视频,直接放弃添加。所以Media块宁简勿缺。

4.3 GetProfiles与GetStreamUri的标准响应

NVR拿到Media地址后,下一步就是GetProfiles。这个接口把设备的视频能力描述清楚。对ESP32-CAM这种MJPEG相机,Profile里面关键是VideoEncoderConfiguration,编码是JPEG,分辨率和帧率按实际配置填。我用的响应结构:

<GetProfilesResponse xmlns="http://www.onvif.org/ver10/media/wsdl"> <Profiles token="MainStream"> <Name>MainStream</Name> <VideoSourceConfiguration token="VSC_Main"> <Name>VideoSource</Name> <SourceToken>VSC_Main</SourceToken> <Bounds x="0" y="0" width="640" height="480"/> </VideoSourceConfiguration> <VideoEncoderConfiguration token="VEC_Main"> <Name>VideoEncoder</Name> <Encoding>JPEG</Encoding> <Resolution><Width>640</Width><Height>480</Height></Resolution> <Quality>60</Quality> <FrameRateLimit><Current>15</Current></FrameRateLimit> </VideoEncoderConfiguration> </Profiles> </GetProfilesResponse>

注意Profile的token属性,它相当于主键,后续GetStreamUri请求会用这个token来指定要哪一路流。有的NVR在添加时会要求GetVideoEncoderConfigurationOptions,这是要返回编码参数的取值范围。你可以简单写死:编码JPEG,分辨率支持列表给三档,帧率给1到30。反正ESP32-CAM实际跑不了多高,但取值范围写够宽,NVR更愿意接受。

GetStreamUri的响应里有一个Uri字段,必须跟你的RTSP服务器实际监听保持一致。我的RTSP监听8554端口,所以返回:

<GetStreamUriResponse xmlns="http://www.onvif.org/ver10/media/wsdl"> <MediaUri> <Uri>rtsp://192.168.1.100:8554/MainStream</Uri> <InvalidAfterConnect>false</InvalidAfterConnect> <InvalidAfterReboot>false</InvalidAfterReboot> <Timeout>PT0S</Timeout> </MediaUri> </GetStreamUriResponse>

这里有一个容易被忽略的点:很多NVR拿到这个URI后,不会再去解析DNS,直接按IP连。你返回的IP地址必须跟NVR实际能访问到的地址一致。如果ESP32既有WiFi IP又有Eth IP,尽量返回NVR所在网段那个IP,否则NVR拿着一个不路由的地址拉流,自然黑屏。我在测试中发现,一部分NVR还会要求GetSnapshotUri接口,用来在添加界面显示预览缩略图。顺手把这个接口也实现了,返回一个HTTP快照地址,体验会好很多。

5. 认证及时钟偏差:两个让NVR直接拒你于门外的暗坑

5.1 UsernameToken摘要的计算细节

NVR和摄像头之间通信时,认证方式最常见的就是WS-UsernameToken。NVR发过来的请求里带一个Security头,里面有Username、PasswordDigest、Nonce、Created四个字段。你要在设备端校验这个PasswordDigest,确认这个请求确实知道密码。

PasswordDigest的算法是:

digest_raw = SHA1( nonce_raw + created_str + password_str ) digest_b64 = Base64(digest_raw)

这里的坑在于Nonce的处理。网络传输时Nonce是Base64编码过的字符串,但你参与SHA1计算时,要用Base64解码后的原始字节,而不是直接用Base64字符串。我最初就是栽在这里,直接用收到的Nonce字符串去算摘要,结果怎么比对都不对。

ESP-IDF里用mbedtls实现:

#include "mbedtls/sha1.h" #include "mbedtls/base64.h" static int verify_digest(const char *digest_b64, const unsigned char *nonce_raw, size_t nonce_len, const char *created, const char *password) { unsigned char sha1_out[20]; mbedtls_sha1_context ctx; mbedtls_sha1_init(&ctx); mbedtls_sha1_starts(&ctx); mbedtls_sha1_update(&ctx, nonce_raw, nonce_len); mbedtls_sha1_update(&ctx, (const unsigned char *)created, strlen(created)); mbedtls_sha1_update(&ctx, (const unsigned char *)password, strlen(password)); mbedtls_sha1_finish(&ctx, sha1_out); mbedtls_sha1_free(&ctx); unsigned char calc_b64[64] = {0}; size_t calc_len = 0; mbedtls_base64_encode(calc_b64, sizeof(calc_b64), &calc_len, sha1_out, 20); return strlen(digest_b64) == calc_len && memcmp(digest_b64, calc_b64, calc_len) == 0; }

另外,NVR在初始化添加流程时不一定带认证头,而是先发一个匿名请求试试看。你的设备如果直接拒绝,它就会在界面上提示"认证失败";如果你允许匿名访问,有些NVR就不会再带认证了。我的建议是,读操作类的接口放宽,允许匿名,写操作或者拉流地址这种敏感接口再做认证。

5.2 SNTP同步与时间偏差问题

ONVIF从设计上就对时间很敏感。NVR收到的Created字段是设备的UTC时间,如果设备和NVR的时钟偏差太大,NVR会认为摘要已经失效,直接拒绝添加。实测下来,偏差在几十秒内问题不大,要是差了一两分钟,就会看到NVR反复转圈,然后报"设备认证失败"或者"时间不同步"。

解决方法是上电后立刻启动SNTP同步:

#include "esp_sntp.h" static void init_sntp(void) { esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); esp_sntp_setservername(0, "pool.ntp.org"); esp_sntp_setservername(1, "ntp.aliyun.com"); esp_sntp_init(); }

同步完成后,在GetSystemDateAndTime里返回的UTCDateTime必须基于同步后的系统时间。ESP-IDF在SNTP同步完成后会自动更新系统时间,你只需要确保在联网后等一两秒再让ONVIF服务对外响应,否则时间还停留在编译时期的默认值,认证必挂。

5.3 401应答与交互边界

如果你的设备要求认证,而NVR发来的请求没带认证头,你应该返回HTTP 401,同时在响应头里带一个WWW-Authenticate字段。NVR看到401后,会重新带上UsernameToken再请求。

这里有个细节:ONVIF要求401响应不能直接返回空body,而是要在body里放一个SOAP Fault,或者至少给一个合法的XML结构。有些NVR对空body的401很敏感,会直接判定设备协议不兼容。我在初期踩过这个坑,NVR添加界面显示"设备无响应",抓包才发现401返回体是空的。补上SOAP Fault之后问题消失。

6. 把JPEG变成RTSP流:与ONVIF媒体服务对接的关键细节

6.1 RTSP Server选型:自己写还是用现成组件

ONVIF媒体服务只负责告诉你"去哪个地址拉流",真正把视频流送出去的还是RTSP服务器。有两种做法:自己写一个极简RTSP server,或者直接用ESP-IDF组件注册表里的现成组件。

我建议直接用现成的rtsp_server组件,省下大量调试时间。它的接口已经适配ESP32的RTP发送逻辑,你只需要注册一个回调,把视频帧数据喂进去。如果是学习目的想自己写,至少要实现OPTIONS、DESCRIBE、SETUP、PLAY、TEARDOWN这五个方法。说白了RTSP就是一个文本协议,控制信道上命令交互,媒体数据走RTP/UDP,但细节多,一个字段拼错就黑屏。

选型的原则是:把RTSP当独立模块,跟ONVIF的SOAP服务分开。这样做的好处是你可以先用VLC或ffplay单独验证RTSP流,确认没问题再接上ONVIF,排错边界一下子清晰了。

6.2 SDP描述与RTP/JPEG封装

RTSP的DESCRIBE响应返回SDP,NVR靠它知道流格式。对MJPEG,标准RTP Payload Type是26。SDP大致的格式:

v=0 o=- 0 0 IN IP4 192.168.1.100 s=JPEG Stream t=0 0 m=video 8554 RTP/AVP 26 a=rtpmap:26 JPEG/90000 a=control:track1

SDP里的IP地址要和RTSP监听地址一致,不要写0.0.0.0或localhost,有NVR解析不了。m=video那行的端口只影响SETUP响应里的默认传输端口,最终以RTSP交互时协商的端口为准。

JPEG over RTP有自己的分片头,不是简单地把JPEG塞进RTP payload就行。每帧JPEG数据要拆成MTU大小的RTP包,在RTP头后面加一个JPEG传输头,里面要填类型、质量、宽度、高度、量化表等信息。这块如果你用现成组件,人家已经处理好了;如果是手写,建议先拿ffplay做兼容性测试,再用NVR实测。

6.3 帧率、分辨率与WiFi带宽的调参经验

ESP32-CAM在WiFi下推MJPEG,瓶颈通常在无线带宽。我实测的参考值:640x480、15帧、JPEG质量60,单帧大小约25KB,算下来码率3Mbps左右,大多数路由器扛得住。要是上到800x600或者30帧,就会经常出现丢包和花屏,NVR画面一卡一卡的,看起来就像协议不稳定。

我建议在代码里加一个节流机制:从esp_camera取帧后,跟上一帧的时间戳比较,如果距离开放间隔还没到就不发。这比硬编码延时更高效,也能平滑处理帧率波动。另外一个心得:JPEG质量别用拉满,质量85以上对MJPEG的码率控制完全是负优化,画质提升肉眼不可见,带宽却多出一倍。调到60-70之间收益最高。

7. 实测联调:从ODT测试到NVR屏幕的排查经验

7.1 先用ONVIF官方测试工具过一遍功能

接NVR之前,强烈建议先用ONVIF Device Test Tool(ODT)做一轮功能验证。这个工具会模拟NVR的完整添加流程,按标准顺序调用发现、设备管理、媒体配置、拉流,并且会展示每个请求和响应的XML。

ODT怎么用?填上设备IP和端口,选择Profile S测试集,它会自动跑。如果某个测试用例红色失败,直接点进去看响应内容。我那次ProbeMatches的RelatesTo错误,就是ODT里第一个用例就标红了,一眼看穿。ODT跑通了再上NVR,能省至少一半的排错时间。

7.2 用Wireshark定位三类问题

ODT只能告诉你"哪一步失败",但说不清"为什么失败"。这时候用Wireshark抓包。过滤条件建议分三层:

  • 发现层:udp.port == 3702
  • ONVIF层:http && ip.addr == 你的摄像头IP
  • RTSP层:tcp.port == 8554

发现层看不到ProbeMatches,基本就是多播组没加入或者AP隔离了;ONVIF层能看到请求和响应,重点看HTTP状态码、SOAP Fault内容、XAddr地址是否正确;RTSP层主要是看DESCRIBE的SDP响应和PLAY请求返回码。

7.3 常见失败模式与对应解法

整理了一张对照表,开发时可以直接按表查:

现象大概率原因定位手段
NVR搜索不到摄像头多播未通、AP隔离、ProbeMatches无响应抓3702端口,确认设备收到Probe
能发现但添加失败设备信息服务XML结构错误、GetCapabilities缺Media块ODT跑Device测试,检查响应XML
提示认证失败PasswordDigest算法错、时间偏差大核对Nonce是否先解码再SHA1,检查SNTP同步
添加成功但预览黑屏GetStreamUri返回地址与RTSP不符、RTP/JPEG封装错误先用VLC单独拉流,确认RTSP独立可用
画面卡顿、马赛克WiFi带宽不够、JPEG单帧过大降分辨率或帧率,JPEG质量调到60-70

7.4 最后一点工程上的话

如果你让我给一条最省心的开发顺序,我会说:先把RTSP流跑通,用VLC能拉流,再去做ONVIF,最后再接NVR。原因很简单,RTSP是视频的基础通道,通道不通就别谈协议。反过来说,ONVIF只是告诉NVR"去哪拿流",你把这个接口调对了,NVR添加就是水到渠成。

我这次在ESP32-CAM上完整走了一遍之后,最大的感受是:ONVIF并没有多高深,它就是一系列HTTP请求和响应的组合。难的不是协议本身,而是你需要在资源有限的MCU上,把SOAP解析、内存管理、多线程、网络时延这些因素捏合在一起。onvif-c这套纯C组件的好处就是把每个环节都拆成独立的小模块,调试的时候拿着ODT和Wireshark一步步过,思路非常清晰。

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

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

立即咨询