拓冰建站拓冰建站
首页 / 资讯中心 / 正文

onvif-device-rs:把 Rust 程序变成一台 ONVIF 相机

onvif-device-rscrates.io · MIT · Rust 1.80ONVIF设备端server库SOAP Device/Media/Imaging/PTZ 服务、WS-Discovery 应答器、WS-Security 认证。给你的流媒体程序、采集管线、边缘设备接上一个标准的 ONVIF 门面任何 NVR/VMS 都能发现它、从它取流。crateonvif-device-rscrates.io· 仓库mickeyzzc/onvif-rs当前版本0.2.20.3.0 加固版在途许可证MITHTTP 栈tokio 上手写 HTTP/1.1——不依赖 hyper/axum关键依赖tokio、quick-xml、serde、async-trait、sha1、socket2测试157含钉死响应报文的金串测试仓库https://github.com/mickeyzzc/onvif-rs名字的由来一个 crates.io 的故事仓库名叫 onvif-rscrate 却发布为onvif-device-rs——因为 crates.io 上的onvif-rs这个名字被一个 2018 年的废弃占位 crate 占着其 README 自述DO NOT USE而 crates.io 不支持名字回收。为避免与另一个知名的 WSDL 生成式 ONVIF客户端库lumeohq/onvif-rs混淆README 里有正式的消歧义声明仓库保留原名crate 以onvif-device-rs之名发布。打 tag 自动发布到 crates.io依赖它只需一行版本号。ONVIF 设备端要过哪些坎做一个能被 NVR 认出来的 ONVIF 设备难的不是 SOAP,而是生态里约定俗成的兼容性细节。这个库的设计围绕五个坎展开发现WS-Discovery 的 UDP 组播应答要正确且大量客户端还会走 HTTP POST 的 Probe——两条路都得应答字节稳定大量 NVR 不做完整 SOAP 解析而是对报文做原始 local-name 匹配。GetStreamUriResponse → MediaUri → Uri这条元素链的大小写与嵌套必须与官方 WSDL 逐字一致全部是小写ri错一个字母就是发现了但没有视频认证WS-UsernameToken 的 PasswordText 与 PasswordDigest 两种模式都要验比较必须抗时序侧信道命名空间客户端发来的 SOAP 前缀五花八门s:/soap:/soapenv:业务前缀更是随缘解析必须与前缀无关而响应要用规范的显式前缀序列化多网卡XAddrs 回显给对方的必须是从它那边够得着的地址——按请求来源回显而不是写死一个。一次标准的被消费全过程你的程序 onvif-device-rsONVIF 客户端你的程序 onvif-device-rsONVIF 客户端HTTP POST Probe 同样应答WS-Discovery Probe UDP 组播 37021ProbeMatches Types/Scopes/XAddrs2GetSystemDateAndTime 匿名3200 OK4GetDeviceInformation 未带凭证54016GetDeviceInformation WS-UsernameToken7200 OK 厂商/型号/序列号8GetStreamUri9rtsp 取流地址 按请求来源回显10RTSP DESCRIBE / SETUP / PLAY11上手最小设备端[dependencies] onvif-device-rs 0.2.2 # git 方式: onvif-device-rs { git https://github.com/mickeyzzc/onvif-rs.git, tag v0.2.2 }注册动作、启动服务节选自库自带的自检示例examples/device_demo.rsusestd::sync::Arc;useonvif_device_rs::device::{DeviceHandler,DeviceServiceHandlers};useonvif_device_rs::discovery::DiscoveryServer;useonvif_device_rs::media::{GetProfilesHandler,GetSnapshotUriHandler,GetStreamUriHandler,OnvifMediaConfig,VideoEncoding,};useonvif_device_rs::{DeviceConfig,OnvifConfig,OnvifServer};#[tokio::main]asyncfnmain()-Result(),Boxdynstd::error::Error{letport8080;letdevice_ip192.168.1.50.to_string();// SOAP 服务:端口 WS-Security 凭证。fail-closed:password 留空会让// start() 直接报错;要刻意全开放需显式 allow_no_auth true。letmutsoapOnvifServer::new(OnvifConfig{port,username:admin.to_string(),password:secret.to_string(),..Default::default()});// Device 服务:五个动作共用一个 handlerletdeviceArc::new(DeviceServiceHandlers::new(DeviceConfig{name:My Camera.into(),manufacturer:Example.into(),model:Model X.into(),..Default::default()},port,device_ip.clone(),));foractionin[GetSystemDateAndTime,GetDeviceInformation,GetCapabilities,GetServices,GetScopes]{soap.register_handler(action,Box::new(DeviceHandler(Arc::clone(device))));}// 免认证动作:对齐真机行为——时间/能力/服务清单在出示凭证前就要可读,// 这是很多客户端探测设备的标准姿势foractionin[GetSystemDateAndTime,GetCapabilities,GetServices]{soap.register_anonymous_action(action);}// Media 服务:描述你的真实流与快照端点letmediaArc::new(OnvifMediaConfig{camera_width:1280,camera_height:720,camera_fps:25,camera_bitrate:2_500_000,rtsp_port:8554,device_ip:device_ip.clone(),stream_path:/stream.to_string(),snapshot_port:8080,// 设为 0 则 GetSnapshotUri 如实回 fault,不虚报能力snapshot_path:/snapshot.jpg.to_string(),profile_token:main.to_string(),video_source_token:videoSrc0.to_string(),encoder_token:enc0.to_string(),encoding:VideoEncoding::H264,video_source_name:Video Source.to_string(),});soap.register_handler(GetProfiles,Box::new(GetProfilesHandler::new(Arc::clone(media))));soap.register_handler(GetStreamUri,Box::new(GetStreamUriHandler::new(Arc::clone(media))));soap.register_handler(GetSnapshotUri,Box::new(GetSnapshotUriHandler::new(Arc::clone(media))));// WS-Discovery 应答器(UDP 组播)与 SOAP 服务并行运行DiscoveryServer::new(device_ip,port).start().await?;soap.start().await?;Ok(())}架构上这是一个按动作注册的 handler 模型SOAP body 里第一个子元素的 local name 就是路由键OnvifActionHandler只有一个方法#[async_trait]pubtraitOnvifActionHandler:SendSync{asyncfnhandle(self,body:str,request_info:RequestInfo)-ResultString,OnvifError;}RequestInfo携带client_ip、server_ip与认证结果——需要按请求来源定制 XAddrs 或做访问控制时信息是够的。HTTP/1.1 栈是手写在tokio::net::TcpListener上的没有 hyper/axum依赖树短、编译快、行为完全可控这对要跑在嵌入式/边缘设备上的程序不是小事。成像参数ImagingParams 接缝Imaging 是设备端库里最常见的烂尾项这里被设计成一个极小的宿主接缝——把 ONVIF 的参数读写翻译到你的相机参数管理器上useonvif_device_rs::imaging::{ImagingParamError,ImagingParams,register_imaging_actions};pubtraitImagingParams:SendSync{/// 按 ONVIF 名称(如 Brightness)读当前值fnget_param(self,name:str)-Resultf64,ImagingParamError;/// 写入;value 已归一化到 [0.0, 1.0]fnset_param(self,name:str,value:f64)-Result(),ImagingParamError;}错误类型区分三种情况客户端会收到语义正确的 SOAP faultpubenumImagingParamError{/// 参数名不被识别InvalidName(String),/// 请求值超出有效范围OutOfRange{value:f64,min:f64,max:f64},/// 底层存储/设备 I/O 故障Io(String),}一行注册GetImagingSettings/SetImagingSettings/GetOptions三个动作全部就位参数值统一归一化到[0, 1]区间曝光/白平衡模式默认按 AUTO 上报trait 上带默认实现的exposure_mode/white_balance_mode可覆盖register_imaging_actions(mutsoap,Arc::new(MyParams));虚拟云台PtzState没有电机的设备想被云台控制库内置了一个纯状态机PtzState位置/速度/预置位、tick 驱动的运动模拟连续移动按速度积分、绝对移动指数缓动、IDLE/MOVING状态——PtzHandler把 ContinuousMove/AbsoluteMove/RelativeMove/Stop/预置位增删转跳等 11 个 PTZ 动作全部接进这个状态机。examples/ptz_demo演示了全部动词与模拟运动。字节稳定是怎么保证的响应报文的元素名、命名空间前缀、属性形态被明确当作承重契约仓库的 CONTRIBUTING 写明线格式测试是承重墙金串字符串按契约对待157 个测试里有一批直接断言报文内容。以 ProbeMatches 为例节选自src/discovery.rs的测试letxmlString::from_utf8(resp).expect(valid UTF-8);assert!(xml.starts_with(?xml version\1.0\ encoding\UTF-8\?));assert!(xml.contains(a:RelatesTouuid:test-id/a:RelatesTo));assert!(xml.contains(d:ProbeMatch));assert!(xml.contains(onvif://www.onvif.org/Profile/Streaming));assert!(xml.contains(192.168.1.100:8080/onvif/device_service));assert!(xml.contains(tdn:NetworkVideoTransmitter));assert!(xml.contains(d:MetadataVersion1/d:MetadataVersion));GetProfiles 的响应同样被逐字段钉死profile token、分辨率、帧率上限、码率上限。改任何序列化路径这些测试会先红——这正是设备端库该有的偏执。发现本身是双路的UDP 组播socket2 SO_REUSEADDR 组播加入缺省地址 239.255.255.250:3702之外HTTP POST 的 Probe 也由handle_http_probe应答两条路径的 XAddrs 都按请求来源回显。本机多地址环境还提供detect_local_ip兜底。实战这个库是从我们一款 Rust 摄像头固件的 ONVIF 模块整体抽取的宿主侧一次性删掉了五千五百余行含 PTZ 状态机业务代码现在只是一行pub use onvif_device_rs as onvif;的历史路径兼容加少量配置转发——抽取后对 NVR 客户端跑了全链路验证发现 → GetStreamUri → RTSP DESCRIBE → 录像落盘全绿。它每天作为生产摄像头的 ONVIF 门面运行被 NVR 以最苛刻的方式消费——原始 SOAP local-name 匹配。这也是字节稳定是契约这条设计原则的来源不是规范要求是消费端现实。工程质量157 个测试覆盖发现、认证、各服务应答、PTZ 状态机与配置0.3.0 加固版在途分支待发布鉴权 fail-closed、全量 XML 转义、HTTP 请求上限/超时、优雅停机与监听器注入、log门面以及库卫生守卫——README 示例经#[cfg(doctest)]强制可编译CI 三道闸cargo fmt --check、cargo clippy --all-targets -- -D warnings、cargo test另有 MSRV 1.80 实际构建验证与 Windows 交叉检查作业打 tag 自动发布 crates.io三个示例device_demoSOAP 发现自检匿名动作应 200、未带凭证应 401全自动断言、ptz_demo、imaging_demo含越界值触发 OutOfRange fault 的演示。cargorun--exampledevice_demo ----port8080边界与路线README 如实标注接缝ImagingParams、DeviceConfig、媒体配置已趋稳但未冻结服务面为 Device/Media/Imaging/PTZ WS-Discovery没有事件PullPoint服务——按真实消费端的需求取舍DeviceConfig的默认值忠实保留了源产品线的出身Raspberry Pi/OV5647上生产前记得换成你自己的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门