简介:C#调用大华摄像机的可执行示例工程,面向需要对接大华网络摄像机做桌面端二次开发的C#开发者,解决实时预览、云台控制、参数获取、报警事件接收等常见需求。压缩包内共76个文件,以30余个动态链接库、13个C#源代码文件、3个可直接运行的可执行程序为主,另含配置文件、接口描述文档、调试符号等,整体体积约7.14MB,便于下载后直接运行或者阅读源码。工程中设计了添加设备、视频显示、主控制等多个窗体模块,覆盖网络通信、接口调用、数据解析、多线程处理及身份认证等核心知识点。已有1342人学习此示例,适合希望快速掌握大华摄像机二次开发流程与C#网络编程实战的中高级开发者,既可提炼出可复用的对接思路,也能基于现成模块继续扩展功能。 做上位机开发的这几年,我发现自己跟大华摄像机打交道的频率一点不比海康少。C#调用大华摄像机这事,网上搜出来的东西大多是一段一段复制粘贴的片段,能一次跑通的不多。主要原因在于,大华对外提供了好几套接入方式,很多人一开始就选错了方向;方向错了,后面所有代码都会写得非常别扭。所以在贴代码之前,我会先把“路线选择”讲清楚,这是我觉得做到“100%可用”的前提。
大华网络摄像机,也就是大家常说的IPC,通常有两条主流接入路线:一条是走NetSDK直连,另一条是走RTSP拉流。这两条路我都正经在项目里用过,各有各的适用场景,不是哪一条绝对更好。这篇文章会把两条路的细节都拆开,再把我反复踩过的坑逐条列出来,不管你是刚入职的新手,还是写了几年上位机的老手,应该都能找到对自己有用的东西。
1. 先选路线:NetSDK直连与RTSP拉流,别急着写代码
1.1 两种方案的对比
第一条,走NetSDK直连。这是大华自己封装的私有协议SDK,通过设备默认的37777端口进行通信。它能做的事情非常多:实时预览、抓图、云台控制、报警订阅、设备参数配置、双向语音、远程回放,几乎是设备页面上能点出来的功能它都有对应接口。缺点是C接口的结构体多、参数杂、生命周期需要严格管理,新手面对C#里一大堆P/Invoke封装很容易崩溃。
第二条,走RTSP拉流。绝大多数大华摄像机都内置RTSP服务端,端口554,常用的地址格式大概是这样的:
rtsp://用户名:密码@IP地址:554/cam/realmonitor?channel=1&subtype=0这条路最大的优点是开发量极小,C#里只要引入OpenCvSharp或者FFmpeg封装,几行代码就能把画面拉出来。缺点是它只能拿到视频流,云台控制、报警、设备管理这些能力统统没有,而且能不能稳定取流,很大程度上取决于你用的视频库对H.264/H.265的支持情况。
我把两条路线的差异整理成了表格,方便你对照选择:
| 对比项 | NetSDK直连 | RTSP + OpenCV |
|---|---|---|
| 功能覆盖 | 预览、云台、报警、配置全支持 | 只有视频流 |
| 稳定性 | 私有协议,不依赖外部解码库 | 依赖FFmpeg及视频库兼容性 |
| 开发量 | 较大,需要P/Invoke和结构体封装 | 很小,一个VideoCapture |
| 画面延迟 | 可控,可做低延迟 | 取决于缓冲配置 |
| 适合场景 | 正式交付项目、需要联动控制 | 原型验证、算法测试、临时工具 |
1.2 我自己的选型结论
我自己的选型原则是这样的:如果这个项目是长期交付给客户使用的上位机系统,画面只是其中一部分,后面大概率要接云台预置位、报警联动、设备热重启之类的需求,那就老老实实走NetSDK。如果只是为了在公司里做一个视觉算法验证工具,把画面喂给算法看效果,RTSP最省事,半小时就能跑通。
还要提醒一句:大华还有一类工业面阵相机和线阵相机,它们的SDK和这条路完全不同。工业相机走的是GenICam、GigE Vision或USB3 Vision标准,接入方式是另一套体系。本文针对的是监控类网络摄像机,也就是大多数人说的“大华摄像机”。
2. 环境准备:SDK从哪里拿、DLL怎么摆、位数怎么选
选好NetSDK之后,第一个坑就来了:SDK到底从哪下,下载下来之后哪些文件放哪个目录。这一步如果心里没谱,后面每写一段代码都会报一些莫名其妙的错。
2.1 SDK从哪里拿
大华官方的支持中心或开发者社区,搜索你设备型号对应的“NetSDK”就能找到“通用NetSDK客户端”。下载解压之后,目录结构大概是这样:
bin目录:放着运行所需DLL和可执行示例程序demo或ClientDemo目录:C/C++、C#、Java、Python等多语言的示例工程doc目录:接口说明文档和结构体说明文档,每个SDK版本对应一份PDF
我建议下载最新版SDK,因为新设备型号的兼容性往往只在新版里才做全。拿到之后,先打开文档目录里的“SDK使用说明”,花20分钟把目录结构和命名规则过一遍,比直接上手写代码省时间得多。
2.2 需要哪些DLL、32位还是64位
需要用到的关键DLL一般是这几个:
DHNetSDK.dll:最核心的动态库,登录、预览、控制全靠它dhconfigsdk.dll:配置读写相关,即使你不主动调用,很多版本初始化时也会间接依赖PlayCtrl.dll:播放解码库,用于把码流解码显示,或者从中拿YUV原始数据- 配套的一堆小DLL,比如
dhlog.dll、dhnetsdk.dll等,建议整个目录一起拷进项目输出目录
这里有一个特别容易翻车的点:SDK的DLL是按位数分开的。大华发布的SDK里通常有lib/win32和lib/x64两份,你必须在C#项目属性里把“目标平台”明确设为x86或x64,然后把对应位数的DLL放到生成的bin目录下。很多人的程序在开发机上跑得好好的,换一台电脑就启动即崩溃,大概率就是没有把DLL位数和平台目标匹配起来。Debug和Release都要注意,别只拷一份。
C#工程的P/Invoke声明,入口点就是DHNetSDK.dll。我见过有人为了省事,把DLL改名或者放进系统目录,这完全没有必要,DLL和程序exe放在同一目录就是最稳的做法。
2.3 初始化顺序与退出清理
初始化流程不是简单调一个NET_DVR_Init就完事。我推荐按下面这个顺序来:
// 1. 初始化SDK,整个进程只调用一次 NET_DVR_Init(); // 2. 设置连接超时,单位毫秒,我习惯设3000,重试1次 NET_DVR_SetConnectTime(3000, 1); // 3. 设置断线自动重连,间隔10秒 NET_DVR_SetReconnect(10000, true); // 4. 程序退出前一定记得清理 NET_DVR_Cleanup();NET_DVR_Init和NET_DVR_Cleanup必须成对出现,而且建议只在进程开始和结束时各调一次。反复Init、Cleanup会造成句柄泄漏,媒体库状态紊乱,症状就是程序运行几个小时之后,摄像机怎么都连不上。我在现场遇到过几次这种“越用越卡”的问题,最后定位下来都是SDK生命周期管理不当。
3. 登录那一步:结构体封装是九成问题的源头
初始化做完之后,下一步就是登录设备。很多C#程序员折在这一步,并不是因为网络不通或者密码不对,而是登录结构体封装错了。大华的C头文件里全是char[],C#里如果你手写StructLayout,字段顺序和大小稍微偏差一个字节,整个结构体就会错位,SDK往里面填内容的时候就会写乱内存,表现就是登录返回莫名其妙的错误码,甚至直接崩溃。
3.1 登录结构体的正确封装方式
以NET_DVR_USER_LOGIN_INFO为例,核心字段顺序大致是这样的:
sDeviceAddress[129]:设备IP或域名byUseTransport:传输方式,一般填0wPort:端口,默认37777sUserName[64]:用户名sPassword[40]:密码cbLoginResult:登录结果标志pLoginResult:登录结果指针- 若干保留字节
C#里对应的是这样:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct NET_DVR_USER_LOGIN_INFO { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] public string sDeviceAddress; public byte byUseTransport; public ushort wPort; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string sUserName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 40)] public string sPassword; public byte bLoginResult; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] public byte[] byReserved1; public IntPtr pLoginResult; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] public byte[] byReserved2; }老实说,不同版本的SDK保留字段长度会略有调整,所以我建议直接打开你下载的SDK包里C# demo工程,把它的完整结构体定义拷贝过来用,不要全凭记忆手写。你自己封装的重点是理解里面每一段的含义,而不是连保留字段都重新发明一遍。
3.2 登录调用与错误码排查
登录调用本身很简单。先用NET_DVR_Login_V40传入用户名信息,第二个参数NET_DVR_DEVICEINFO_V40是用来接收设备信息的,里面包含设备序列号、通道数量、设备类型等。登录成功返回一个大于0的userId,失败返回-1,然后调用NET_DVR_GetLastError()获取错误码。
NET_DVR_USER_LOGIN_INFO loginInfo = new NET_DVR_USER_LOGIN_INFO(); loginInfo.sDeviceAddress = ipTextBox.Text.Trim(); loginInfo.wPort = 37777; loginInfo.sUserName = "admin"; loginInfo.sPassword = "你的密码"; loginInfo.byUseTransport = 0; NET_DVR_DEVICEINFO_V40 deviceInfo = new NET_DVR_DEVICEINFO_V40(); int userId = NET_DVR_Login_V40(ref loginInfo, ref deviceInfo); if (userId == -1) { uint err = NET_DVR_GetLastError(); // 根据err排查 }登录失败的常见错误码,大致有下面几个,具体以SDK头文件注释为准:
| 错误码 | 常见含义 | 排查方向 |
|---|---|---|
| 7 | 网络连接不上 | 确认设备IP可达、端口37777是否通 |
| 10 | 网络超时 | 跨网段、防火墙阻挡、设备负载过高 |
| 17 | 用户名或密码错误 | 去设备Web管理页确认账号密码 |
| 33 | 登录失败 | 检查是否被锁定,或SDK版本过旧 |
| 71 | 用户被锁定 | 错误次数过多,等锁定时间结束 |
我调试时最常用的排查手段是:先用设备Web管理页确认一下,浏览器能打开说明网络和HTTP端口没问题;再用telnet IP 37777验证私有协议端口通不通。如果Web能开但37777不通,多半是防火墙挡了,或者设备端口被改过。还要注意一点,有些摄像机出厂默认IP和你的电脑不在同一网段,比如设备是192.168.1.108,你电脑是192.168.2.50,这时候直接访问肯定失败,先把网段调通再谈代码。
4. 拉流显示和抓图:黑屏和花屏基本都是操作顺序问题
登录成功只代表你拿到了设备的“管理权”,要真正把画面显示出来,还需要走另一套流程。这套流程比登录复杂,也是很多人做了几天依然黑屏的地方。
核心思路是这样:先申请一个预览句柄realHandle,SDK在后台把码流数据通过回调函数送过来;我们在回调里拿到的是经过压缩的视频流,不是可以直接显示的RGB,所以还要把它喂给PlayCtrl.dll解码库,解码后才能显示,或者拿去做算法处理。
4.1 预览结构体与预览句柄
预览结构体NET_DVR_PREVIEWINFO里有几个关键字段:
lChannel:通道号。单台网络摄像机大多从1开始,NVR后面的通道会更多,要看登录返回的设备信息dwStreamType:0主码流,1子码流。想要清晰画面选主码流,想低延迟流畅预览选子码流dwLinkMode:0表示TCP,1表示UDP。我建议局域网里默认TCP,可靠性和实时性比较均衡
调用预览:
NET_DVR_PREVIEWINFO previewInfo = new NET_DVR_PREVIEWINFO(); previewInfo.lChannel = 1; previewInfo.dwStreamType = 0; // 主码流 previewInfo.dwLinkMode = 0; // TCP previewInfo.hPlayWnd = IntPtr.Zero; // 先不绑定窗口,用回调方式 int realHandle = NET_DVR_RealPlay_V40(userId, ref previewInfo, RealDataCallback, IntPtr.Zero); if (realHandle == -1) { uint err = NET_DVR_GetLastError(); }4.2 回调解码与PlayCtrl的配合
回调函数大概长这样。注意第一个dwDataType == 0时是系统头,需要交给解码库建立解码上下文;dwDataType == 1时是真正的码流数据,喂给解码库输入。
private void RealDataCallback(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser) { if (dwDataType == 0) { byte[] head = new byte[dwBufSize]; Marshal.Copy(pBuffer, head, 0, (int)dwBufSize); PlayM4_GetPort(out playPort); PlayM4_SetStreamOpenMode(playPort, 0); PlayM4_OpenStream(playPort, head, head.Length, 4 * 1024 * 1024); PlayM4_Play(playPort, panel1.Handle); } else if (dwDataType == 1) { byte[] data = new byte[dwBufSize]; Marshal.Copy(pBuffer, data, 0, (int)dwBufSize); PlayM4_InputData(playPort, data, data.Length); } }这段代码里最关键的逻辑是:系统头必须最先处理。如果你一上来就处理流数据,解码库没有拿到编码参数,后面不管喂多少数据都是黑屏。我见过有人在回调里用日志输出每一帧的长度,结果刷屏刷得程序卡死,就是因为没有判断dwDataType,把系统头当普通码流打日志了。
4.3 抓图的两种主流方式
预览建立起来之后,抓图有两种方式。
第一种,基于预览句柄直接抓图:
NET_DVR_CapturePictureBlock(realHandle, "D:\\capture.bmp", 0);第二个参数是保存路径,第三个参数是图片类型,0表示BMP,2表示JPG。这种方式最简单,前提是预览已经成功建立。
第二种,通过设备登录句柄抓图:
NET_DVR_CapturePicture(userId, 1, "D:\\capture.jpg");这种方式不需要建立预览,适合只做定时抓图的场景。但有的老设备和NVR对这种方式支持不是很好,如果返回失败,先确认设备通道号是否正确。
如果你走的是回调拿码流这条路,还有更高级的玩法:通过PlayM4_SetDecCallBack设置解码回调,把H.264/H.265码流解码成YUV原始帧,再由C#转成Bitmap或者OpenCV的Mat,直接做视觉检测。工业现场经常用这种方式替代RTSP,因为它不依赖第三方视频库,而且解码延迟通常更低。
5. 不想写P/Invoke?OpenCvSharp拉RTSP的速通方案
讲完NetSDK,我再聊聊很多人实际在用的RTSP方案。这个方案最大的价值在于:它能让你在10分钟内看到画面,适合快速验证或者做视觉算法Demo。
5.1 大华RTSP地址格式
大华的RTSP地址有固定的套路,主码流和子码流的区别就在最后两个参数:
rtsp://admin:密码@192.168.1.64:554/cam/realmonitor?channel=1&subtype=0 rtsp://admin:密码@192.168.1.64:554/cam/realmonitor?channel=1&subtype=1channel=1:通道号,1通常表示第一个通道subtype=0:主码流,清晰度高、码流大subtype=1:子码流,分辨率低、码流小、延迟相对可控
5.2 用VideoCapture打开并取帧
C#里用OpenCvSharp打开非常直接。先通过NuGet安装OpenCvSharp4和OpenCvSharp4.runtime.win,然后:
using OpenCvSharp; string url = "rtsp://admin:密码@192.168.1.64:554/cam/realmonitor?channel=1&subtype=0"; using (VideoCapture capture = new VideoCapture(url)) { if (!capture.IsOpened()) { Console.WriteLine("打开失败,检查IP、账号密码、防火墙"); return; } using Mat frame = new Mat(); while (true) { if (capture.Read(frame)) { // 这里既可以显示,也可以直接交给算法 Cv2.ImShow("preview", frame); if (Cv2.WaitKey(1) == 27) // ESC退出 break; } } }是的,就这么简单。VideoCapture底层走的是FFmpeg,所以需要OpenCvSharp自带的FFmpeg相关DLL一起在输出目录里,正常情况下NuGet会把它带全。
5.3 这条路有哪些坑
但这条路也不是完全没有坑:
- 密码里有特殊字符要URL编码。比如密码是
a@b:c,里面的@和:必须转成%40和%3A,否则RTSP地址解析会断掉。 - 主码流如果是H.265,旧版本FFmpeg可能解不了。遇到黑色画面但程序没有报错,先换子码流试试,如果子码流正常,基本就是解码能力问题。
- 跨网段的稳定性没有NetSDK好。我在产线上遇到过设备在192.168.1.x网段,上位机在192.168.2.x网段,RTSP偶尔花屏,换用NetSDK的TCP模式后明显稳定。
- 延迟问题。OpenCvSharp默认可能有较大的缓冲,画面会比实际慢1到3秒。做纯监控预览可以接受,做人机交互对延迟敏感的场景,建议用NetSDK。
综合下来,RTSP方案的适用范围是:自己写工具、算法验证、临时抓帧、快速原型。如果项目是要交付给客户用的,我还是建议你在NetSDK上投入时间。
6. 最让我怀疑人生的那些坑,逐个帮你排掉
最后这部分,算是我实际项目里反复踩过的泥潭。前面几章把主流程讲通了,但真正让可用性从“90%”提到“100%”的,往往就是下面这些细节。
6.1 平台位数不匹配,程序启动就崩
我最早做大华项目时,C#工程默认是AnyCPU,配的却是32位SDK,程序一调用NET_DVR_Init就报“尝试读取或写入受保护的内存”。后来才知道大华SDK的DLL必须和进程位数一致。解决办法就是把C#项目强制指定为x86或x64,并且把对应位数的SDK DLL放进输出目录。这是新手最常见的崩溃源,没有之一。
6.2 回调线程里直接操作界面控件
大华的回调函数跑在SDK的内部线程里,不是UI线程。如果你在回调里直接写textBox1.Text = ...或者panel1.BackColor = ...,程序会随机性崩溃。正确做法是回调里只做数据搬运,UI更新用Invoke或BeginInvoke丢给WinForms/WPF的UI线程。处理视频帧也一样,不要在回调里做耗时的图像分析,否则会阻塞SDK的码流处理,长时间运行后出现马赛克和卡顿。
6.3 跨网段、防火墙、端口被改
这是最让人苦笑不得的一种“连不上”:设备Web页面能打开,但程序NET_DVR_Login_V40就报超时或网络错误。排查步骤按顺序来:先在设备Web管理页确认端口设置,大华的私有协议端口默认是37777;再用telnet 设备IP 37777看端口是否通;如果Web通而37777不通,优先检查Windows防火墙和路由器端口映射。有些设备装在办公网里,网管开了MAC白名单,那就要找网络管理员放行。
6.4 主码流是H.265,解码库不支持导致黑屏
现在很多新设备默认主码流是H.265,老版本的PlayCtrl.dll只支持H.264,结果就是预览句柄建立成功、回调也有数据,但屏幕上永远是黑的。解决办法有两个:一是去SDK官网下载最新版本的DLL;二是预览时把码流类型指定为子码流,很多设备子码流还是H.264。如果你要的是高清画质,那就必须升级解码库,这个没有捷径。
6.5 没开自动重连,设备断电重启后程序就废了
现场设备经常因为停电或者检修离线。如果你没有调用NET_DVR_SetReconnect,SDK不会自动帮你重连,预览会停在最后一帧,程序里其他逻辑也会进入异常状态。我建议初始化时就把断线重连打开,并且在回调里监听异常事件。即使这样,重连后的预览句柄也可能失效,稳妥的设计是在业务层做“检测到断开就重新登录、重新启动预览”的状态机。
6.6 通道号从0还是从1开始
不同型号和NVR联动时的通道号规则不完全一样。一般单台IPC是1通道,但有些设备预览时通道号从0开始,这就是为什么有人登录成功但预览返回失败。判断依据是NET_DVR_DEVICEINFO_V40返回的byChanNum和byStartChan,byStartChan代表第一个可预览通道号。把这个值拿来做基准,比写死1稳妥得多。
最后分享一个我自己的小习惯:每次拿到一台崭新的大华摄像机,我不会直接写代码,而是先花五分钟做三件事:一,确认固件版本和SDK版本,到官网把最新的NetSDK下下来;二,用设备自带的网页管理界面把所有端口、编码、码流类型记下来;三,在有线环境下先手动登录一次,抓一张图,确认网络没有乱七八糟的干扰。做完这三步,再回来写C#代码,后面踩坑的概率会低很多。C#调用大华摄像机这件事,说到底就是把“初始化、登录、预览、清理”四件事的调用顺序和生命周期理顺。把这个骨架搭稳,不管是预览、抓图、云台控制还是视觉算法接入,后面都只是在这个骨架上添砖加瓦。
本文还有配套的精品资源,点击获取