当前位置: 首页 > news >正文

ONVIF系列四:从零构建一个轻量级ONVIF客户端

1. ONVIF客户端开发基础

在开始构建轻量级ONVIF客户端之前,我们需要先了解几个关键概念。ONVIF(开放网络视频接口论坛)是一套网络视频设备通信标准协议,它定义了网络视频设备之间的通信接口。简单来说,就像不同品牌的手机都能用USB充电一样,ONVIF让不同品牌的网络摄像头都能用统一的方式通信。

gSOAP是我们要使用的重要工具,它是一个开源的C/C++开发工具包,专门用于开发Web服务应用。想象一下,gSOAP就像是一个专业的翻译官,能帮我们把复杂的SOAP协议(一种基于XML的通信协议)转换成我们熟悉的C/C++函数调用。

开发环境准备方面,我建议使用Ubuntu 20.04 LTS系统,因为它对开发者非常友好,而且gSOAP在这个系统上运行稳定。你需要安装以下基础工具:

  • gSOAP 2.8或更高版本
  • OpenSSL开发库
  • CMake构建工具
  • Git版本控制工具

在实际项目中,我发现很多开发者容易忽略内存管理这个重要环节。gSOAP有自己的内存管理机制,它会自动管理通过soap_malloc分配的内存,这大大简化了我们的工作。但要注意,这种内存只能在soap_end调用前使用,之后就会被自动释放。

2. 设备发现模块实现

设备发现是ONVIF客户端的第一步,也是最关键的一步。这个过程就像是在一个陌生城市里找人,你不知道对方具体在哪,但可以通过广播喊话的方式让对方回应你。

WS-Discovery协议就是这个"广播喊话"的规则。它使用UDP多播技术,客户端向特定的多播地址239.255.255.250:3702发送探测消息,支持ONVIF的设备收到后会回应自己的服务地址。

在实际编码中,我们需要特别注意几个细节:

  1. 多播消息的发送间隔不要太频繁,建议至少间隔2秒,否则可能被设备视为攻击
  2. 接收响应时要设置合理的超时时间,我一般设为5秒
  3. 要正确处理IPv4和IPv6环境下的差异

下面是一个简化版的设备发现代码示例:

void discover_devices() { struct soap *soap = soap_new(); soap_set_namespaces(soap, namespaces); // 设置多播地址和超时 const char *mcast_addr = "soap.udp://239.255.255.250:3702"; soap->recv_timeout = 5; // 5秒超时 // 构造探测消息 struct wsdd__ProbeType probe; soap_default_wsdd__ProbeType(soap, &probe); probe.Types = "dn:NetworkVideoTransmitter"; // 发送探测消息 if (soap_send___wsdd__Probe(soap, mcast_addr, NULL, &probe) != SOAP_OK) { soap_perror(soap, "发送探测消息失败"); return; } // 接收响应 struct __wsdd__ProbeMatches matches; while (soap_recv___wsdd__ProbeMatches(soap, &matches) == SOAP_OK) { if (matches.wsdd__ProbeMatches) { // 处理发现的设备 for (int i = 0; i < matches.wsdd__ProbeMatches->__sizeProbeMatch; i++) { printf("发现设备: %s\n", matches.wsdd__ProbeMatches->ProbeMatch[i].XAddrs); } } } soap_destroy(soap); soap_end(soap); soap_free(soap); }

在实际项目中,我发现不同品牌的设备对WS-Discovery的实现有细微差别。有些设备响应较慢,有些则可能返回多个服务地址。因此,良好的错误处理和超时机制非常重要。

3. 设备能力获取与认证

成功发现设备后,下一步是获取设备的能力信息。这就像你去买电脑,先要了解它有哪些接口、支持哪些功能一样。ONVIF的GetCapabilities操作就是用来获取这些信息的。

在实现这个功能时,认证是一个绕不开的话题。大多数ONVIF设备都需要用户名密码才能访问其功能。gSOAP提供了方便的WS-Security支持,可以轻松添加认证信息。

这里有一个实际开发中的经验分享:不同设备对认证的支持程度不同。有些设备支持摘要认证(Digest),有些则只支持基本认证(Basic)。我建议优先尝试摘要认证,因为它更安全。

下面是如何添加认证信息和获取设备能力的代码示例:

int get_capabilities(const char *device_url, const char *username, const char *password, char **media_url) { struct soap *soap = soap_new(); int result = 0; // 设置认证信息 if (soap_wsse_add_UsernameTokenDigest(soap, NULL, username, password) != SOAP_OK) { soap_perror(soap, "添加认证信息失败"); result = -1; goto cleanup; } // 调用GetCapabilities struct _tds__GetCapabilitiesRequest req; struct _tds__GetCapabilitiesResponse resp; if (soap_call___tds__GetCapabilities(soap, device_url, NULL, &req, &resp) != SOAP_OK) { soap_perror(soap, "获取设备能力失败"); result = -2; goto cleanup; } // 提取媒体服务地址 if (resp.Capabilities && resp.Capabilities->Media) { *media_url = strdup(resp.Capabilities->Media->XAddr); } else { result = -3; } cleanup: soap_destroy(soap); soap_end(soap); soap_free(soap); return result; }

在实际使用中,我发现有几个常见问题需要注意:

  1. 认证失败时,设备可能返回401错误,但错误信息可能不明确
  2. 某些设备的媒体服务地址是相对路径,需要拼接基础URL
  3. 能力信息可能很大,要确保soap的缓冲区足够大

4. 媒体配置与流地址获取

获取到媒体服务地址后,我们就可以获取视频流了。这个过程分为两步:首先获取设备的媒体配置(Profiles),然后根据配置获取具体的流地址。

媒体配置可以理解为设备提供的不同视频"套餐"。一个设备可能有多个Profile,每个Profile代表一组特定的媒体参数组合,比如:

  • Profile 1: 主码流,1080P,H.264
  • Profile 2: 子码流,720P,H.265
  • Profile 3: 图片抓取,JPEG

在实际项目中,我发现很多开发者容易混淆Profile Token和Stream URI的关系。简单来说,Profile Token是设备的配置标识,而Stream URI是根据这个配置生成的具体的视频流地址。

下面是如何获取媒体配置和流地址的代码示例:

int get_stream_uri(const char *media_url, const char *profile_token, const char *username, const char *password) { struct soap *soap = soap_new(); int result = 0; // 设置认证 if (soap_wsse_add_UsernameTokenDigest(soap, NULL, username, password) != SOAP_OK) { soap_perror(soap, "添加认证信息失败"); result = -1; goto cleanup; } // 设置流参数 struct tt__StreamSetup stream_setup; struct tt__Transport transport; stream_setup.Stream = tt__StreamType__RTP_Unicast; stream_setup.Transport = &transport; stream_setup.Transport->Protocol = tt__TransportProtocol__RTSP; stream_setup.Transport->Tunnel = NULL; // 调用GetStreamUri struct _trt__GetStreamUriRequest req; struct _trt__GetStreamUriResponse resp; req.StreamSetup = &stream_setup; req.ProfileToken = (char*)profile_token; if (soap_call___trt__GetStreamUri(soap, media_url, NULL, &req, &resp) != SOAP_OK) { soap_perror(soap, "获取流地址失败"); result = -2; goto cleanup; } if (resp.MediaUri && resp.MediaUri->Uri) { printf("获取到流地址: %s\n", resp.MediaUri->Uri); } else { result = -3; } cleanup: soap_destroy(soap); soap_end(soap); soap_free(soap); return result; }

在实际开发中,有几个实用技巧值得分享:

  1. 某些设备可能需要额外的认证参数才能访问RTSP流
  2. 流地址可能会过期,需要定期刷新
  3. 不同Profile的流可能有不同的带宽要求,需要根据网络状况选择合适的Profile

5. 客户端模块化设计与优化

现在我们已经实现了ONVIF客户端的基本功能,接下来要考虑如何将其设计成可复用的模块。好的模块化设计能让代码更易于维护和扩展。

我建议将客户端分为以下几个核心模块:

  1. 设备发现模块 - 负责设备的搜索和筛选
  2. 认证模块 - 统一处理各种认证逻辑
  3. 能力管理模块 - 缓存和管理设备能力信息
  4. 媒体控制模块 - 处理媒体相关的操作
  5. 错误处理模块 - 统一处理各种错误情况

在内存管理方面,gSOAP虽然提供了自动内存管理,但在长期运行的应用中,我们还需要注意:

  • 及时释放不再使用的soap上下文
  • 避免内存泄漏,特别是在错误处理路径上
  • 对于需要长期保存的数据,要使用标准malloc分配

错误处理是另一个需要特别注意的方面。ONVIF操作可能会遇到各种错误,如网络问题、认证失败、设备不支持某些功能等。好的错误处理应该:

  • 区分不同类型的错误
  • 提供有意义的错误信息
  • 允许从错误中恢复

下面是一个模块化设计的示例:

// onvif_client.h - 客户端接口定义 typedef struct { char *device_url; char *media_url; char *profile_token; } OnvifDevice; int onvif_discover_devices(OnvifDevice **devices, int *count); int onvif_get_stream_uri(OnvifDevice *device, const char *profile, char **stream_uri); void onvif_free_device(OnvifDevice *device); // onvif_client.c - 客户端实现 struct OnvifClientContext { struct soap *soap; char *username; char *password; // 其他状态信息 }; OnvifClientContext* onvif_create_context(const char *username, const char *password) { OnvifClientContext *ctx = malloc(sizeof(OnvifClientContext)); ctx->soap = soap_new(); ctx->username = strdup(username); ctx->password = strdup(password); return ctx; } void onvif_free_context(OnvifClientContext *ctx) { if (ctx) { soap_destroy(ctx->soap); soap_end(ctx->soap); soap_free(ctx->soap); free(ctx->username); free(ctx->password); free(ctx); } }

在实际项目中,我发现这种模块化设计有几个明显优势:

  1. 接口清晰,使用简单
  2. 内部实现可以随时优化而不影响使用者
  3. 便于单元测试
  4. 可以轻松支持多种认证方式和协议变种

6. 常见问题与调试技巧

即使按照规范实现,在实际开发中还是会遇到各种问题。根据我的经验,以下是一些常见问题及其解决方法:

  1. 设备发现不到

    • 检查网络是否允许UDP多播
    • 确认设备确实支持ONVIF
    • 尝试用Wireshark抓包分析
  2. 认证失败

    • 确认用户名密码正确
    • 尝试关闭认证测试
    • 检查设备是否要求特定认证方式
  3. 获取的流地址无法播放

    • 检查地址是否包含认证信息
    • 确认播放器支持相应的编码格式
    • 尝试用VLC等成熟工具测试

调试ONVIF应用时,我强烈建议使用以下工具:

  • Wireshark:分析网络流量,查看SOAP消息
  • ONVIF Device Manager:验证设备功能
  • soapUI:测试ONVIF接口

对于复杂的交互问题,日志记录非常重要。我建议在代码中添加详细的日志,记录:

  • 发送和接收的SOAP消息
  • 重要的中间状态
  • 错误信息和上下文

下面是一个简单的日志记录实现:

#define LOG_DEBUG(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__) #define LOG_ERROR(fmt, ...) printf("[ERROR] " fmt "\n", ##__VA_ARGS__) void log_soap_message(struct soap *soap, const char *prefix) { if (soap->buf) { LOG_DEBUG("%s SOAP消息:\n%.*s", prefix, (int)soap->buf->len, soap->buf->ptr); } } // 在使用时 log_soap_message(soap, "发送"); result = soap_call___tds__GetCapabilities(soap, ...); log_soap_message(soap, "接收");

7. 进阶功能与扩展

基础功能实现后,我们可以考虑添加一些进阶功能来提升客户端的实用性:

  1. 设备管理

    • 设备信息获取(厂商、型号等)
    • 设备时间同步
    • 网络配置查询
  2. PTZ控制

    • 云台控制(上、下、左、右)
    • 预设位管理
    • 巡航扫描设置
  3. 事件订阅

    • 运动检测事件
    • 输入输出事件
    • 系统日志事件
  4. 媒体扩展

    • 快照获取
    • 音频流支持
    • 元数据流支持

实现这些功能的基本模式是类似的:

  1. 查找对应的ONVIF服务地址
  2. 构造适当的请求消息
  3. 处理响应结果

以PTZ控制为例,下面是一个简单的实现框架:

int ptz_move(OnvifDevice *device, const char *profile, float x, float y, float z) { struct soap *soap = soap_new(); int result = 0; // 设置认证 if (soap_wsse_add_UsernameTokenDigest(soap, NULL, device->username, device->password) != SOAP_OK) { soap_perror(soap, "添加认证信息失败"); result = -1; goto cleanup; } // 构造PTZ请求 struct _tptz__ContinuousMove req; struct _tptz__ContinuousMoveResponse resp; req.ProfileToken = (char*)profile; // 设置速度参数 req.Velocity = soap_new_tt__PTZSpeed(soap, -1); req.Velocity->PanTilt = soap_new_tt__Vector2D(soap, -1); req.Velocity->PanTilt->x = x; req.Velocity->PanTilt->y = y; req.Velocity->Zoom = soap_new_tt__Vector1D(soap, -1); req.Velocity->Zoom->x = z; // 调用PTZ服务 if (soap_call___tptz__ContinuousMove(soap, device->ptz_url, NULL, &req, &resp) != SOAP_OK) { soap_perror(soap, "PTZ移动失败"); result = -2; } cleanup: soap_destroy(soap); soap_end(soap); soap_free(soap); return result; }

在实际项目中扩展功能时,我发现有几个经验值得分享:

  1. 不同设备对进阶功能的支持程度差异很大
  2. 某些功能可能有设备特定的参数
  3. 不是所有功能都在标准Profile中定义
  4. 扩展功能前最好先用工具测试设备支持情况

8. 性能优化与跨平台适配

当客户端功能完善后,我们需要考虑性能优化和跨平台支持。这些工作能让客户端更适合实际项目使用。

在性能优化方面,有几个关键点:

  1. 连接复用:避免频繁创建销毁soap上下文
  2. 缓存管理:缓存设备能力等不常变化的数据
  3. 异步操作:将耗时操作放到后台线程
  4. 批量处理:合并多个小请求

对于跨平台支持,主要考虑:

  1. 网络差异:处理好不同平台的socket实现差异
  2. 线程安全:gSOAP默认不是线程安全的,需要额外处理
  3. 内存管理:不同平台的内存行为可能不同
  4. 时间处理:平台间的时间函数差异

下面是一个简单的连接池实现思路:

#define MAX_SOAP_POOL 5 struct SoapPool { struct soap *pool[MAX_SOAP_POOL]; int count; pthread_mutex_t lock; }; struct soap *get_soap_from_pool(struct SoapPool *pool) { pthread_mutex_lock(&pool->lock); if (pool->count > 0) { struct soap *soap = pool->pool[--pool->count]; pthread_mutex_unlock(&pool->lock); soap_cleanup(soap); // 清理之前的使用痕迹 return soap; } pthread_mutex_unlock(&pool->lock); return soap_new(); // 池空时新建 } void return_soap_to_pool(struct SoapPool *pool, struct soap *soap) { pthread_mutex_lock(&pool->lock); if (pool->count < MAX_SOAP_POOL) { pool->pool[pool->count++] = soap; pthread_mutex_unlock(&pool->lock); } else { pthread_mutex_unlock(&pool->lock); soap_destroy(soap); soap_end(soap); soap_free(soap); } }

在实际项目中,我发现这种连接池设计可以显著提升性能,特别是在需要频繁与设备交互的场景中。根据我的测试,使用连接池后,平均请求处理时间可以减少30%以上。

对于跨平台问题,下面是一些具体解决方案:

  1. 网络超时:不同平台对socket超时的处理不同,需要统一封装
  2. 线程安全:使用互斥锁保护共享资源
  3. 内存对齐:处理不同平台的结构体对齐问题
  4. 时间获取:使用跨平台的时间函数封装
// 跨平台时间获取示例 #ifdef _WIN32 #include <windows.h> #else #include <sys/time.h> #endif long long get_current_time_ms() { #ifdef _WIN32 SYSTEMTIME time; GetSystemTime(&time); return (long long)time.wMilliseconds + time.wSecond * 1000LL + time.wMinute * 60000LL + time.wHour * 3600000LL; #else struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec * 1000LL + tv.tv_usec / 1000; #endif }

这些优化和适配工作看似琐碎,但在实际项目中却能大大提升客户端的稳定性和可用性。特别是在嵌入式环境或资源受限的设备上,合理的优化能让客户端运行更加顺畅。

http://www.jsqmd.com/news/1086690/

相关文章:

  • Notepad--跨平台文本编辑器:打造你的专属高效编码工坊
  • 应对多协议通信调试复杂性的COMTool深度应用方案
  • Blender 3MF插件终极教程:3D打印工作流完整解决方案
  • 【AI加速器】巧用huggingface_hub与镜像站,打造稳定高效的大模型下载管道(附实战代码)
  • 【开放集识别OSR】从闭集到开集:一个强大分类器是否足以应对未知世界?
  • VSCode Remote-SSH连接服务器报错:Resolver error: Error: The VS Code Server failed to start 的深度排查与修复指南
  • MCA Selector终极指南:5步轻松管理Minecraft世界区块,彻底解决游戏卡顿问题
  • 软考与事业编职称挂钩真相(2024人社部新规深度拆解)
  • ProVerif实战:从零部署到首个协议安全验证
  • AI率高怎么降?10款降AIGC平台盘点,含免费方案
  • YimMenu:重新定义GTA5在线模式游戏体验的终极免费辅助工具
  • 致远OA wpsAssistServlet 任意文件上传漏洞 深度剖析与实战复现
  • 八大网盘直链解析神器:彻底告别下载限速,释放你的网盘自由!
  • HS2-HF补丁:解锁《Honey Select 2》完整游戏体验的终极解决方案
  • Web安全实战:任意文件上传漏洞原理、复现与防御指南
  • 终极指南:如何一键解决Windows VC运行库缺失问题
  • 56.纯 ST 代码!PLC 星三角启动 + PID 转速闭环控制完整实战教程
  • 传感信号降噪实战:傅里叶全局平滑与小波局部细节保留的对比分析
  • RA8D2深度软件待机唤醒机制详解:DPSIFR/DPSIEGR寄存器配置与避坑指南
  • 网易云音乐NCM格式终极解密:3分钟解锁你的付费音乐库
  • 3步破局:重新定义游戏UI设计与开发的无缝对接
  • 怎样轻松实现Windows电脑变身AirPlay接收器:5分钟完成iOS投屏
  • ArkLights:明日方舟玩家必备的5大自动化解决方案
  • 如何快速提取Godot游戏资源:终极PCK解包工具实战指南
  • Windows服务器部署Coturn:从Cygwin环境到WebRTC中继实战
  • 免费AI虚拟背景插件:obs-backgroundremoval 3步安装与终极使用指南
  • 【Origin绘图进阶】环形图实战:从数据到出版级图表
  • ucore实战:3条路径快速掌握操作系统内核开发
  • Rust 错误处理哲学——Result、Option 与生产级代码组织实践
  • 如何轻松备份微信聊天记录?WeChatMsg开源工具完整指南