Onvif协议NVR开发方案指南
Onvif协议NVR开发方案指南
一、架构设计
1. 双重角色功能
角色 | 功能描述 | 协议交互对象 |
---|---|---|
服务端 | 提供多通道视频流、事件订阅、PTZ控制,向上级平台暴露ONVIF接口 | 上级监控平台、第三方客户端 |
客户端 | 发现并接入IPC设备,拉取视频流、订阅事件,将数据整合到本地服务 | 下级IPC设备 |
2. 数据流逻辑
二、核心模块实现
模块1:设备发现与动态通道管理
实现思路
-
服务端发现
- NVR作为设备响应WS-Discovery Probe请求,返回服务地址和Profile信息。
- 使用UDP多播(端口3702)实现,兼容ONVIF 2.0/2018版本。
-
客户端发现
- NVR主动探测局域网IPC,通过
GetCapabilities
协商能力。 - 动态注册IPC到通道表,支持通道增删改查。
- NVR主动探测局域网IPC,通过
关键代码
// 服务端:响应Probe请求
int onvif_probe_handler(...) {
struct wsdd__ProbeMatchesType resp;
resp.ProbeMatch->XAddrs = "http://nvr_ip:8080/onvif/device_service";
resp.ProbeMatch->Scopes = "onvif://www.onvif.org/Profile/S";
return soap_wsdd_ProbeMatches(soap, ...);
}
// 客户端:发现并注册IPC
void discover_ipc() {
soap_wsdd_Probe(soap, SOAP_WSDD_ADHOC, "dn:NetworkVideoTransmitter", nullptr);
while (soap_wsdd_listen(soap, 1000) == SOAP_OK) {
for (auto &match : matches.ProbeMatch) {
DeviceBindingProxy proxy(match.XAddrs);
_tds__GetCapabilitiesResponse caps;
if (proxy.GetCapabilities(nullptr, caps) == SOAP_OK) {
add_channel({
id: channels.size() + 1,
name: "IPC_" + std::to_string(channel_id),
rtsp_url: get_ipc_stream_url(proxy), // 调用IPC的GetStreamUri
profile_token: "Profile_IPC_" + std::to_string(channel_id),
is_local: false,
is_online: true
});
}
}
}
}
模块2:鉴权与安全机制
实现思路
-
服务端鉴权
- 实现WS-Security Digest认证,验证
UsernameToken
中的Nonce和PasswordDigest。 - 兼容Basic Auth(旧设备)和TLS 1.3(2021版)。
- 实现WS-Security Digest认证,验证
-
客户端鉴权
- 动态添加鉴权头,适配不同IPC设备的认证方式。
关键代码
// 服务端:密码校验
int validate_password(...) {
std::string stored_pwd = query_db(username);
return (sha1(nonce + timestamp + stored_pwd) == password) ? SOAP_OK : SOAP_FAULT;
}
// 客户端:添加动态鉴权头
void add_auth_header(soap* ctx, const IPCConfig &ipc) {
if (ipc.version >= 2018) {
soap_wsse_add_UsernameTokenDigest(ctx, "user", ipc.user.c_str(), ipc.pwd.c_str());
} else {
// Basic Auth
soap->auth = soap_wsse_add_BasicAuth(ctx, ipc.user.c_str(), ipc.pwd.c_str());
}
}
模块3:多通道视频流管理
实现思路
-
本地通道
- NVR直接管理物理摄像头,通过FFmpeg/Live555发布RTSP流。
-
IPC通道
- 拉取IPC的RTSP流,转封装为NVR的RTSP服务。
- 支持负载均衡:限制最大并发流数量(如16路)。
关键代码
// 统一视频流入口
void start_all_streams() {
thread_pool pool(MAX_STREAMS); // 限制最大并发数
for (auto &[id, channel] : channels) {
pool.enqueue([&channel] {
if (channel.is_local) {
// 发布本地摄像头流
system(fmt::format("ffmpeg -i /dev/video{} -c h264 -f rtsp rtsp://nvr_ip:554/local_ch{}",
id, id).c_str());
} else {
// 拉取IPC流并转发
system(fmt::format("ffmpeg -i {} -c copy -f rtsp rtsp://nvr_ip:554/ipc_ch{}",
channel.rtsp_url, id).c_str());
}
});
}
}
// 服务端:GetStreamUri接口
int MediaService::GetStreamUri(...) {
auto ch = find_channel_by_token(req->ProfileToken);
resp->MediaUri->Uri = ch.is_local ?
fmt::format("rtsp://nvr_ip:554/local_ch{}", ch.id) :
fmt::format("rtsp://nvr_ip:554/ipc_ch{}", ch.id);
return SOAP_OK;
}
模块4:事件双向传递
实现思路
-
客户端事件订阅
- 订阅所有IPC的
MotionDetector
事件,解析事件中的通道ID。
- 订阅所有IPC的
-
服务端事件转发
- 将IPC事件转换为NVR事件格式,添加通道标识后推送给上级平台。
-
控制指令传递
- 解析上级平台的PTZ指令,转发到对应IPC。
关键代码
// 客户端:订阅IPC事件
void subscribe_ipc_events(const IPCConfig &ipc) {
EventBindingProxy proxy(ipc.xaddr);
_tev__CreatePullPointSubscriptionResponse sub;
if (proxy.CreatePullPointSubscription(nullptr, sub) == SOAP_OK) {
event_threads.emplace_back([sub, ipc] {
while (true) {
_tev__PullMessagesResponse msgs;
if (proxy.PullMessages(..., msgs) == SOAP_OK) {
for (auto &msg : msgs.NotificationMessage) {
// 添加通道ID标识
std::string enriched_msg = fmt::format(
"<nvr:ChannelID>{}</nvr:ChannelID>{}", ipc.channel_id, msg.Message);
event_queue.push(enriched_msg); // 进入全局事件队列
}
}
}
});
}
}
// 服务端:事件推送线程
void event_push_thread() {
EventBindingProxy upper_proxy(upper_platform_url);
while (true) {
auto msg = event_queue.pop();
_tev__Notify notify;
notify.NotificationMessage.push_back(build_tev_message(msg));
upper_proxy.Notify(notify); // 推送至上级平台
}
}
// PTZ指令转发
int PTZService::ContinuousMove(...) {
int ch_id = extract_channel_id(req->ProfileToken);
auto &ipc = get_ipc_by_channel(ch_id);
PTZBindingProxy proxy(ipc.xaddr);
add_auth_header(proxy.soap, ipc); // 添加IPC鉴权
return proxy.ContinuousMove(req, resp); // 转发指令
}
模块5:协议兼容性处理
实现思路
-
多版本代码隔离
- 使用不同命名空间生成2.0/2018/2021版代码。
-
动态接口适配
- 根据
GetCapabilities
返回的命名空间选择接口版本。
- 根据
关键代码
// 接口工厂
template<typename T20, typename T18>
auto create_service_proxy(const std::string &xaddr) {
if (xaddr.find("ver20") != string::npos)
return std::make_unique<T18>(xaddr);
else
return std::make_unique<T20>(xaddr);
}
// 动态调用示例
void get_device_info(const std::string &xaddr) {
auto proxy = create_service_proxy<DeviceBindingProxy_2_0, DeviceBindingProxy_2018>(xaddr);
_tds__GetDeviceInformationResponse resp;
proxy->GetDeviceInformation(nullptr, resp);
}
三、验证与测试
1. 测试用例设计
测试场景 | 验证方法 | 预期结果 |
---|---|---|
设备发现 | 使用ONVIF Device Manager搜索NVR和IPC | NVR和IPC均可见 |
多通道视频流 | VLC同时播放NVR的local_ch1和ipc_ch2 | 两路流均流畅无卡顿 |
事件级联 | 触发IPC移动侦测,查看上级平台日志 | 10秒内收到带通道ID的事件 |
级联PTZ控制 | 在上级平台控制NVR通道,观察IPC摄像头转动 | IPC云台按指令运动 |
断线重连 | 拔掉IPC网线,5分钟后恢复 | NVR日志显示通道状态变化 |
2. 性能压测
# 模拟16路1080P@25fps流
for i in {1..16}; do
ffmpeg -re -i test.mp4 -c copy -f rtsp rtsp://nvr_ip:554/stress_ch$i &
done
# 监控资源占用
top -b | grep nvr_server