一、网络拓扑与连接
1.1 网络环境示例
主机 (192.168.1.100)
│
│ 网线
▼
NVR LAN口 (192.168.1.10) ← 同网段 192.168.1.x
│
│ NVR内部隔离
▼
NVR POE口 (内部网络)
│
├── 通道1 (POE口1)
├── 通道2 (POE口2) → 相机 (192.168.2.10) ← POE内部网段 192.168.2.x
├── 通道3 (POE口3)
└── ...
1.2 NVR 网络结构说明
NVR 有两类网口,物理隔离:
| 网口类型 | 用途 | 网段示例 | 特点 |
|---|---|---|---|
| LAN口 | 连接外部网络(主机、交换机) | 192.168.1.10 | 普通网口,对外通信 |
| POE口 | 连接POE相机,供电+数据 | 192.168.2.x(默认) | 专用口,供电48V,NVR内部管理 |
关键点: LAN口和POE口是两个独立的物理网络,NVR在中间做隔离转发。
二、常见疑问解答
疑问1:主机和相机不在同一网段,为什么还能采集图像?
答: 主机不需要直接访问相机。所有通信都通过 NVR 中转:
- 主机 → NVR LAN口(如 192.168.1.10)→ NVR内部转发 → POE口上的相机
- 主机只跟 NVR 的 LAN口通信,通过通道号区分不同相机
- 相机在哪个网段无所谓,NVR负责内部路由
类比: NVR 就是一个视频代理网关,对外一个IP,对内管理所有相机。
疑问2:相机IP能改成其他网段吗?比如改成 192.168.1.20?
答: 可以改,但没有意义。
- POE口相机的网段是 NVR 内部管理的,可在 NVR Web 管理界面修改:
配置 → 网络配置 → 基本配置 → POE网段设置 - 即使改成与主机同网段,主机也无法直接访问
- 因为 LAN口和POE口是 Layer 2 物理隔离,NVR 不会把 LAN口的数据转发到 POE口
- 程序配置中
ip字段永远写 NVR 的 LAN口 IP,与相机实际IP无关
疑问3:主机能不能接在 NVR 的 POE口上直接访问相机?
答: 不能,且有硬件损坏风险。
- POE口带 48V 供电 ,直接接主机网卡可能烧坏网口
- POE口之间没有路由转发,接上去设备之间也不能互访
- POE口是给POE相机专用的,不是普通网口
疑问4:如果相机接在交换机上(不接NVR),能直连吗?
答: 可以。接入方式对比:
| 接入方式 | 主机能直连相机? | 程序配置 devType | 配置 IP |
|---|---|---|---|
| 相机 → NVR POE口 | ❌ 必须走NVR | "nvr" |
NVR的IP |
| 相机 → 同一交换机 | ✅ 可以直连 | "camera" |
相机的IP |
# 直连方式拓扑
主机 (192.168.1.100) → 交换机 ← 相机 (192.168.1.20)
│
NVR LAN口 (192.168.1.10)
三、通过 C++ 海康 SDK 采集图像
3.1 采集原理
┌─────────┐ SDK初始化 ┌─────────┐ 内部转发 ┌─────────┐
│ 主机 │ ──────────────→ │ NVR │ ────────────→ │ 相机 │
│ (C++程序)│ ←────────────── │ (代理) │ ←──────────── │ (图像源) │
└─────────┘ 返回图像数据 └─────────┘ 取回图像 └─────────┘
(端口8000)
核心流程:初始化 → 登录 → 预览 → 抓图
3.2 核心代码流程
步骤1:SDK初始化
cpp
NET_DVR_Init(); // 初始化海康SDK,只需调用一次
步骤2:登录设备
cpp
NET_DVR_USER_LOGIN_INFO loginInfo = {0};
loginInfo.bUseAsynLogin = 0;
strcpy(loginInfo.sDeviceAddress, "192.168.1.10"); // NVR的IP
loginInfo.wPort = 8000; // SDK端口
strcpy(loginInfo.sUserName, "admin");
strcpy(loginInfo.sPassword, "123456");
NET_DVR_DEVICEINFO_V40 deviceInfo = {0};
LONG userID = NET_DVR_Login_V40(&loginInfo, &deviceInfo);
// userID >= 0 表示登录成功
步骤3(NVR专用):获取数字通道起始号
cpp
// 仅 devType == "nvr" 时需要
NET_DVR_IPPARACFG_V40 ipcfg;
DWORD bytesReturned = 0;
NET_DVR_GetDVRConfig(userID, NET_DVR_GET_IPPARACFG_V40, 0,
&ipcfg, sizeof(ipcfg), &bytesReturned);
// ipcfg.dwStartDChan 即数字通道起始号(通常为33)
步骤4:实时预览
cpp
NET_DVR_PREVIEWINFO previewInfo;
previewInfo.hPlayWnd = (HWND)widget->winId(); // Qt窗口句柄,SDK直接渲染
previewInfo.lChannel = startDChan + channel - 1; // 实际通道号
// 例:startDChan=33, channel=2 → lChannel=34
previewInfo.dwStreamType = 0; // 0=主码流, 1=子码流
previewInfo.dwLinkMode = 0; // 0=TCP, 1=UDP
previewInfo.bBlocked = 1;
LONG playHandle = NET_DVR_RealPlay_V40(userID, &previewInfo, callback, this);
// playHandle >= 0 表示预览成功,画面自动渲染到 hWnd
通道号计算:
| 场景 | 通道号计算 | 示例 |
|---|---|---|
| 直连相机(devType=camera) | m_nStartChan + channel - 1 |
1+1-1 = 1 |
| NVR连接(devType=nvr) | m_dwStartDChan + channel - 1 |
33+2-1 = 34 |
步骤5:抓图(三种方式,自动降级)
cpp
NET_DVR_JPEGPARA jpegPara = {0};
jpegPara.wPicSize = 0xFF; // 当前分辨率
jpegPara.wPicQuality = 0; // 最高质量
// 方式1:设备端抓图到文件(原图分辨率,推荐)
BOOL ret = NET_DVR_CaptureJPEGPicture(userID, channel, &jpegPara, "save.jpg");
// 方式2:设备端抓图到内存(原图分辨率)
char buf[10*1024*1024];
DWORD picSize = 0;
BOOL ret = NET_DVR_CaptureJPEGPicture_NEW(userID, channel, &jpegPara,
buf, sizeof(buf), &picSize);
// 将 buf 前 picSize 字节写入文件即为JPEG图片
// 方式3:从预览流抓图(子码流分辨率,兜底方案)
char buf[10*1024*1024];
DWORD picSize = 0;
BOOL ret = NET_DVR_CapturePictureBlock_New(playHandle, buf, sizeof(buf), &picSize);
抓图方式优先级:
| 优先级 | 方式 | 分辨率 | 依赖 |
|---|---|---|---|
| 1 | NET_DVR_CaptureJPEGPicture | 原图 | 需登录,不需要预览 |
| 2 | NET_DVR_CaptureJPEGPicture_NEW | 原图 | 需登录,不需要预览 |
| 3 | NET_DVR_CapturePictureBlock_New | 子码流 | 需登录+预览 |
3.3 配置文件说明
json
{
"viewtype": "fill",
"cameras": [
{
"id": "nvr_ch2",
"name": "NVR通道2",
"ip": "192.168.1.10",
"port": 8000,
"user": "admin",
"password": "123456",
"channel": 2,
"devType": "nvr"
}
]
}
| 字段 | 说明 | NVR模式 | 直连相机模式 |
|---|---|---|---|
ip |
设备IP | NVR的LAN口IP | 相机IP |
port |
SDK端口 | 8000 | 8000 |
channel |
通道号(整数) | NVR通道号(1,2,3...) | 固定填1 |
devType |
设备类型 | "nvr" |
"camera" |
四、通过 RTSP 采集图像
4.1 RTSP 采集原理
┌─────────┐ RTSP请求(端口554) ┌─────────┐ 内部转发 ┌─────────┐
│ 主机 │ ──────────────────→ │ NVR │ ──────────→ │ 相机 │
│ (客户端) │ ←────────────────── │ (代理) │ ←────────── │ (图像源) │
└─────────┘ 返回视频流数据 └─────────┘ 取回图像 └─────────┘
(RTSP/RTP协议)
与 SDK 方式的区别:
- SDK 走 8000端口 ,RTSP 走 554端口
- SDK 需要海康专用 DLL,RTSP 只需要标准播放器/FFmpeg
- 都是通过 NVR LAN口通信,NVR 内部转发到对应通道
4.2 RTSP 地址格式
rtsp://用户名:密码@NVR_IP:554/Streaming/Channels/通道码
通道码规则:通道号 × 10 + 码流号
| 通道号 | 主码流(码流号1) | 子码流(码流号2) |
|---|---|---|
| 1 | 101 |
102 |
| 2 | 201 |
202 |
| 12 | 1201 |
1202 |
4.3 RTSP 地址示例
# 通过 NVR 访问通道2
主码流: rtsp://admin:123456@192.168.1.10:554/Streaming/Channels/201
子码流: rtsp://admin:123456@192.168.1.10:554/Streaming/Channels/202
# 通过 NVR 访问通道12
主码流: rtsp://admin:123456@192.168.1.10:554/Streaming/Channels/1201
子码流: rtsp://admin:123456@192.168.1.10:554/Streaming/Channels/1202
# 直连相机(相机接交换机时)
主码流: rtsp://admin:123456@192.168.1.20:554/Streaming/Channels/101
子码流: rtsp://admin:123456@192.168.1.20:554/Streaming/Channels/102
4.4 RTSP 采集图像方式
方式1:用 FFmpeg 命令行抓图
bash
# 抓取一帧保存为图片
ffmpeg -i "rtsp://admin:123456@192.168.1.10:554/Streaming/Channels/201" \
-frames:v 1 output.jpg
# 连续抓图(每秒1帧)
ffmpeg -i "rtsp://admin:123456@192.168.1.10:554/Streaming/Channels/201" \
-r 1 output_%04d.jpg
方式2:用 VLC 验证
打开 VLC → 媒体 → 打开网络串流 → 粘贴 RTSP 地址
方式3:C++ 代码集成 FFmpeg(需要开发)
cpp
// 伪代码:通过 FFmpeg C API 从 RTSP 取流并保存图像
AVFormatContext* fmtCtx = nullptr;
avformat_open_input(&fmtCtx,
"rtsp://admin:123456@192.168.1.10:554/Streaming/Channels/201",
nullptr, nullptr);
avformat_find_stream_info(fmtCtx, nullptr);
// 找到视频流
int videoIdx = av_find_best_stream(fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
// 读取帧
AVPacket pkt;
while (av_read_frame(fmtCtx, &pkt) >= 0) {
if (pkt.stream_index == videoIdx) {
// 解码并保存为JPEG
// ...
}
av_packet_unref(&pkt);
}
4.5 SDK 方式 vs RTSP 方式对比
| 对比项 | 海康 SDK | RTSP |
|---|---|---|
| 依赖库 | HCNetSDK.dll/.so 等30+个库文件 | FFmpeg(或OpenCV等) |
| 跨平台 | Windows + Linux(需对应平台SDK) | 全平台(Windows/Linux/Mac) |
| 设备兼容性 | 仅海康设备 | 几乎所有网络相机 |
| 预览渲染 | SDK直接渲染到HWND | 需自行解码渲染 |
| 抓图方式 | SDK专用接口(原图) | 解码视频帧(或触发RTSP快照) |
| 抓图延迟 | 几乎无延迟 | 有明显延迟(见4.6节) |
| 端口 | 8000 | 554 |
4.6 RTSP 延迟问题分析
实际测试结论
在同一网络环境下,分别用 SDK 和 RTSP 对同一 NVR 通道进行抓图:
| 采集方式 | 抓图一帧的耗时 | 延迟感受 |
|---|---|---|
| SDK(NET_DVR_CaptureJPEGPicture) | 约 0.2~0.5 秒 | 几乎即时 |
| RTSP(FFmpeg 取流解码) | 约 1~3 秒 | 明显卡顿/滞后 |
延迟原因分析
RTSP 取流存在多层延迟叠加:
RTSP 延迟构成
─────────────────────────────────────────────
① 连接建立 RTSP DESCRIBE → SETUP → PLAY 握手 ~200ms
② 缓冲等待 等待下一个关键帧(I帧)才能解码 ~0.5~2s
③ 解码延迟 FFmpeg 解码 H.264/H.265 ~100ms
④ 缓冲策略 FFmpeg 默认缓冲队列 ~200ms
─────────────────────────────────────────────
总延迟 ~1~3s
关键瓶颈在第②步: RTSP 传输的是 H.264/H.265 视频流,必须等到下一个 I 帧(关键帧)才能开始解码。相机的关键帧间隔通常为 1~2 秒,这就是 RTSP 抓图慢的主要原因。
为什么 SDK 没有延迟
SDK 抓图走的是完全不同的路径:
SDK 抓图路径:
主机 ──SDK命令──→ NVR ──触发相机传感器直接拍照──→ 返回JPEG原图
│ │
│ 不经过视频编码/解码流程 │
│ 直接获取相机ISP输出的原始图像 │
←──────── JPEG 二进制数据 ──────────────←
| 对比 | SDK 抓图 | RTSP 抓图 |
|---|---|---|
| 触发方式 | 发送命令,相机即时拍照 | 从已有视频流中取帧 |
| 图像来源 | 相机传感器直出 | 视频编码流解码 |
| 是否依赖关键帧 | ❌ 不依赖 | ✅ 必须等 I 帧 |
| 是否需要解码 | ❌ 直接返回 JPEG | ✅ 需要 H.264/H.265 解码 |
| 输出质量 | 原图分辨率,最高画质 | 取决于码流分辨率和画质 |
缓解 RTSP 延迟的方法(效果有限)
bash
# 降低 FFmpeg 缓冲
ffmpeg -fflags nobuffer -flags low_delay -i "rtsp://..." -frames:v 1 output.jpg
# 使用 UDP 代替 TCP(减少握手开销)
ffmpeg -rtsp_transport udp -i "rtsp://..." -frames:v 1 output.jpg
# 设置超短超时
ffmpeg -stimeout 2000000 -i "rtsp://..." -frames:v 1 output.jpg
即使做了以上优化,RTSP 延迟仍然在 0.5~1.5 秒左右,无法达到 SDK 的毫秒级响应。
结论
- 对实时性要求高的场景(如工业采集):必须使用 SDK 方式
- 对延迟不敏感的场景(如远程查看):RTSP 可用,且跨平台更方便
- Linux 下需要低延迟采集:使用 Linux 版 SDK 封装 Python 接口(参见《C++封装Python采集接口技术文档》)
五、总结
推荐方案
| 场景 | 推荐方式 |
|---|---|
| 仅用海康设备、Windows平台 | SDK方式(当前已实现) |
| Linux平台、需要低延迟采集 | SDK方式(Linux版 + Python封装) |
| 需要跨平台或兼容多品牌相机 | RTSP方式(需集成FFmpeg,但有延迟) |
| 远程查看、对延迟不敏感 | RTSP方式即可 |
关键要点
- 主机通过 NVR 访问相机,不需要和相机同网段
- NVR LAN口和POE口物理隔离,不要把主机接到POE口上
- 通道号必须是整数,不能写 "D2" 这样的字符串
- devType 决定通道计算方式 :
"nvr"用数字通道起始号,"camera"用模拟通道 - RTSP 地址通道码 = 通道号 × 10 + 码流号
- RTSP 存在明显延迟(1~3秒),SDK 抓图几乎即时,对实时性有要求的场景必须用 SDK