简介:面向海康网络摄像机二次开发者的OSD字符叠加例程,基于官方SDK在视频画面中叠加文字水印与Logo图标,解决监控画面自定义信息标注需求,适合在BCB6.0环境下进行安防客户端功能扩展的工程师参考。压缩包共56个文件,整体约11.61MB,包含dll动态链接库、lib导入库、C++源码、头文件、可执行demo及设备配置数据等,SDK运行组件与示例工程齐全,便于对照编译和运行。当前已有4012人学习/下载。资源内提供了叠加RGB565、RGB555、RGB24、RGB32等多种图像格式的像素级修改方法,支持多OSD区域叠加;通过查看源码和工程结构,读者可快速掌握海康SDK的播放、预览与字符叠加调用流程,并在此基础上扩展自己的监控客户端功能。 做监控项目的时间久了,对“海康网络摄像机”这几个字的感情是又爱又恨。爱的是SDK功能确实全,恨的是文档和结构体太绕,不踩几个坑根本拿不到想要的效果。前阵子接了个仓储监控的活,要求给几十台摄像头的实时画面叠加OSD文字水印,显示库位编号、责任人和时间。现场同事第一反应是让我去Web后台一台台配置,但那等于把自己绑死在运维上,业务系统一旦改了库位,还得派人去摄像头后台改。所以最后我还是决定用海康API接口SDK做统一管理。这篇文章就围绕这个项目中OSD叠加文字字符水印的完整实现来写,从SDK接入、结构体理解到下发配置和排障经验,全部是我实测过、可以直接套用的内容。如果你正准备做海康设备二次开发,这一篇应该能帮你少走不少弯路。
1. 先想清楚:这个demo解决的是哪一类问题
1.1 实际应用场景
OSD是On-Screen Display的缩写,在视频监控里最常见的用法就是把字符、时间、通道名直接烧录到视频流中,和画面融为一体。这种叠加发生在摄像头编码固件内部,不是播放器后期贴上去的,所以无论是实时预览、本地存储还是平台转发,水印一直都在。
我这次项目里最典型的场景有三类。第一是库位标识:仓库有十几个区,每个区对应若干摄像头,画面上需要固定显示“A-03-02”这样的库位号,技防人员调回放时一看就知道是哪个位置。第二是责任人信息:值班人员交接后,负责人姓名要能跟随业务数据自动变更,而不是每次手动去摄像头后台改。第三是告警联动提示:当红外告警触发时,业务系统希望画面上出现一行“ALARM: DOOR-OPEN”的提示文字,方便事后核对录像和告警时间。这三个需求有一个共同点:文字内容不是写死的,要么来自业务数据库,要么来自现场事件,所以必须有程序化手段去动态控制。
1.2 为什么不用页面配置和播放端叠加
容易想到的替代方案有三个。第一是摄像头Web管理页手工配置OSD,这个方案几乎不写代码,单台设备调试也快,缺点是完全没法批量管理,几十台设备逐个改配置会让人崩溃,而且业务系统无法动态变更文字。第二是在客户端播放器上叠加文字,比如用Web插件、自绘控件在显示层画字,实现简单、效果好看,但水印只存在于播放窗口,录像文件里根本看不到,对安防溯源没有意义。第三就是本文要说的设备SDK方案,在摄像头内部把OSD配置下发给设备,让它在视频编码时直接加上字符,预览和录像双生效,还能循环下发控制多台设备。
从工程角度讲,第三种才是正规做法。前两种适合演示和临时调试,不能作为正式项目交付方案。尤其需要注意,很多刚接触海康二次开发的同学会把“播放端叠加”和“设备端OSD”混为一谈,到了验收阶段发现录像里没有水印才回头整改,工期就耽误了。
2. 环境准备与SDK接入
2.1 SDK下载与目录结构
海康的设备网络SDK从官网服务支持板块能找到,类型选“设备网络SDK”,版本很多,建议优先拿最新的稳定版。下载后是一个压缩包,解压出来一般会有几个关键目录:inc目录放的是头文件,lib目录下面是运行库,doc目录里有《设备网络SDK使用手册》,demo目录则是官方示例代码。我自己习惯在工程里单独建一个HikSDK目录,把inc和lib拷贝进去,后续升级SDK时只替换这一个目录就行。
这里要额外提醒一句:网上下到的所谓“精简版SDK”、“绿色版SDK”不要乱用。海康的接口依赖很多基础组件,少了某个DLL运行时才报错最折腾人。老老实实用官方包,缺什么组件都能在doc目录里查到说明。
2.2 开发工程配置
以Windows加Visual Studio为例,项目属性里做三步:
- C/C++,常规,附加包含目录,填SDK的inc路径;
- 链接器,常规,附加库目录,填SDK的lib路径;
- 链接器,输入,附加依赖项,加上HCNetSDK.lib和PlayCtrl.lib。
如果只是做OSD下发,不涉及预览播放,PlayCtrl.lib其实可以不加,但实际项目中大多数情况还是要看到画面,所以建议直接都引进来。Debug和Release模式都要检查一遍配置,很多朋友在Debug下编译通过,切Release就报一堆链接错误,基本都是库目录配置遗漏。
运行阶段还需要把HCNetSDK.dll、PlayCtrl.dll、hlog.dll、hpr.dll这一堆动态库放到程序输出目录。最简单的方式是把lib目录下的DLL全部复制过去,避免缺文件。部署到客户机器时也照做,我见过太多现场报“找不到HCNetSDK.dll”的情况,提前把依赖收拾干净能省一半售后。
2.3 初始化和登录
写代码之前先把SDK的初始化、登录、注销这套标准流程理清楚。SDK使用前一定要调用NET_DVR_Init做初始化,进程退出时调用NET_DVR_Cleanup释放资源。登录推荐用NET_DVR_Login_V40,它同时返回设备信息和通道数等基础属性,省掉很多额外的获取接口调用。
#include "HCNetSDK.h" #include <cstdio> int main() { // 1. 初始化SDK NET_DVR_Init(); NET_DVR_SetConnectTime(3000, 2); NET_DVR_SetReconnect(10000, 1); // 2. 填写设备登录信息 NET_DVR_USER_LOGIN_INFO struLogin = { 0 }; struLogin.bUseAsynLogin = 0; strcpy(struLogin.sDeviceAddress, "192.168.1.64"); struLogin.wPort = 8000; strcpy(struLogin.sUserName, "admin"); strcpy(struLogin.sPassword, "your_password"); NET_DVR_DEVICEINFO_V40 struDevInfo = { 0 }; LONG lUserID = NET_DVR_Login_V40(&struLogin, &struDevInfo); if (lUserID < 0) { printf("login failed, error=%d\n", NET_DVR_GetLastError()); NET_DVR_Cleanup(); return -1; } printf("login ok, serial: %s\n", struDevInfo.struDeviceV30.sSerialNumber); // 此处放后面章节的业务代码 // 3. 注销并清理 NET_DVR_Logout(lUserID); NET_DVR_Cleanup(); return 0; }注意struLogin.sPassword字段是ANSI字符数组,如果工程是UNICODE编码,记得先转成ANSI再拷贝。SDK默认走ANSI,这一点在后面处理中文OSD的时候还会再次踩到。
3. 核心实现:OSD叠加文字水印
3.1 先把实时预览建立起来
进入正题。要让OSD效果直观可见,首先得把实时预览跑起来。用NET_DVR_CreateRealPlay_V40创建播放句柄,hPlayWnd是窗口句柄,如果程序是纯后台服务没有界面,也可以传NULL,预览照样能建立,只是看不到画面。常见错误是把lChannel写成0,实际上通道号从1开始,通道0在某些设备上是虚拟通道,不容易踩对。
NET_DVR_PREVIEWINFO struPlay = { 0 }; struPlay.hPlayWnd = NULL; // 没有UI就传NULL struPlay.lChannel = 1; // 通道从1开始 struPlay.dwStreamType = 0; // 主码流 struPlay.dwLinkMode = 0; // TCP方式 struPlay.bBlocked = 1; // 阻塞式取流 LONG lRealHandle = NET_DVR_CreateRealPlay_V40(lUserID, &struPlay, NULL, NULL); if (lRealHandle < 0) { printf("create realplay failed, error=%d\n", NET_DVR_GetLastError()); NET_DVR_Logout(lUserID); NET_DVR_Cleanup(); return -1; }预览句柄建立之后,设备端才会真正开始推流。OSD下发布置和取流通道是同一套会话体系,把预览开起来再下发,配置生效的成功率会高很多,尤其是对部分老型号固件。
3.2 OSD叠加核心结构体详解
OSD叠加字符的核心是一个结构体:NET_DVR_SHOWSTRING_V40。网上很多旧代码用的是老结构NET_DVR_SHOWSTRING,所有字符串共享一套字号、颜色、对齐方式,定制性弱。V40版本把每个字符串的信息拆到了NET_DVR_STRING_INFO结构里,可以给每行单独设置字体大小、颜色和位置。
| 字段 | 说明 |
|---|---|
| wYear/byMonth/byDay/byHour/byMinute/bySecond/byMillisecond | 时间相关字段,普通文字留0即可,系统时间叠加由设备自己处理 |
| sString | 显示的字符串内容,中文字符按GBK编码传入 |
| byStringSize | 字符大小,0大字,1小字 |
| byAlignment | 对齐位置,0左上、1右上、2左下、3右下、4中央 |
| byFontColor | 字体颜色,0白、1红、2黄、3蓝、4绿、5橙、6粉、7深蓝 |
| byStringFont | 字体类型,0宋体、1黑体、2楷体、3仿宋 |
| byRes | 保留字段,务必清零 |
外层NET_DVR_SHOWSTRING_V40里,dwStringNum表示本次要下发几个字符串,dwStringInfoNum在V40里通常与dwStringNum保持一致,struStringInfo数组最多8个元素。需要注意dwSize字段必须等于sizeof(NET_DVR_SHOWSTRING_V40),否则接口会返回参数错误。很多第一次用的人栽在这里,明明逻辑都对,就是返回失败,其实就是结构体大小没填对。
3.3 下发到设备并生效
看代码。这个示例假设通道1,叠加两行OSD,第一行是库位号,第二行是责任人。
NET_DVR_SHOWSTRING_V40 struShow = { 0 }; struShow.dwSize = sizeof(NET_DVR_SHOWSTRING_V40); struShow.dwStringNum = 2; struShow.dwStringInfoNum = 2; // 第一行:库位号,红色、大字、左上角 strcpy(struShow.struStringInfo[0].sString, "A-03-02"); struShow.struStringInfo[0].byStringSize = 0; struShow.struStringInfo[0].byAlignment = 0; struShow.struStringInfo[0].byFontColor = 1; struShow.struStringInfo[0].byStringFont = 0; // 第二行:责任人,黄色、小字、左上角 strcpy(struShow.struStringInfo[1].sString, "luru: zhangsan"); struShow.struStringInfo[1].byStringSize = 1; struShow.struStringInfo[1].byAlignment = 0; struShow.struStringInfo[1].byFontColor = 2; struShow.struStringInfo[1].byStringFont = 1; // 通道为1,命令码用NET_DVR_SET_SHOWSTRING_V40 if (!NET_DVR_SetDVRConfig(lUserID, NET_DVR_SET_SHOWSTRING_V40, 1, &struShow, sizeof(struShow))) { printf("set OSD failed, error=%d\n", NET_DVR_GetLastError()); }下发完成后不要马上关程序,把预览窗口开在那里观察几秒,确认OSD真的显示、位置没有压到关键监视区域再收工。这个配置是持久化到设备的,注销登录后依然保留,除非有人改回去。所以现场验收时要特别注意,别把A区域的文字带到B区域的摄像头。
3.4 动态刷新与批量管理思路
这个项目真正的价值在于动态和批量。比如告警联动,当上位机收到门磁信号后,需要把告警文字推送到对应画面,可以在回调线程里直接调NET_DVR_SetDVRConfig,把sString改成新内容重新下发即可。但要注意控制频率,建议不要低于5秒刷新一次。有些设备对OSD配置的写操作比较敏感,频繁下发可能导致画面短暂花屏,严重时卡死预览流。别问我怎么知道的,项目上线时一位同事写了个2秒一次的定时器,结果现场画面每隔几分钟就闪一下,排查了很久。
批量管理就更简单了。把登录和设置封装成一个函数,循环遍历设备IP列表,逐个登录下发,最后统一退出。真正要花时间的是设备在线状态探测、失败重试和日志记录,这些才是工程落地里容易被忽略的部分。另外建议把OSD内容做成配置项,不要硬编码在程序里,尤其是责任人名字、库位号这类会变的业务数据,放到数据库或配置文件里,系统重启后自动加载。
4. 常见问题与排查实录
4.1 登录和网络类问题
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| 登录返回error 7 | 网络不通、IP或端口不对 | ping设备,确认8000端口;检查网线和VLAN隔离 |
| 登录返回error 23 | 用户名或密码错误;设备启用非法登录锁定 | 核对账号权限,确认密码,必要时登录Web后台解锁 |
| 设置OSD返回error 17 | 参数错误,通常是结构体大小或命令码问题 | 检查dwSize、命令码、通道号,逐项打印核对 |
| 设置返回error 29 | 设备不支持该功能或固件版本过旧 | 升级设备固件,或改用兼容性更好的旧版结构体 |
| 中文显示乱码 | 传入字符串不是GBK编码 | 把UTF-8字符串转成GBK后再下发 |
登录失败是最常见的拦路虎,我一般按“先网络、再密码、后安全策略”的顺序排查。先ping通,再telnet测8000端口,确认TCP能连上,然后才去核对密码。海康设备有个恶心人的设置叫“非法登录锁定”,密码试错几次后会把IP锁一段时间,SDK报错和Web登录报错还不完全一样,容易被误导。遇到这种情况,先去Web后台把锁定解除,再回头跑SDK。
4.2 设置成功但不生效
如果NET_DVR_SetDVRConfig返回成功,画面却没有变化,优先怀疑通道号配错。多通道设备尤其容易出问题,每通道OSD是独立配置的,你在通道1下发了两行字,看的却是通道2的画面,自然认为没生效。这不能用“看起来差不多”来判断,要把通道号、设备型号、当前预览通道都打出来对比。
另一个原因是设备固件的叠加总开关被占用了。比如有人用4200客户端或者海康vm软件管理过这台设备,Web端手工配过文字,SDK再下发时被Web端的配置覆盖或者互踩。实际项目里如果多个系统同时管理同一批设备,必须约定配置来源以谁为准,否则今天SDK覆盖Web,明天Web覆盖SDK,排查会非常痛苦。
4.3 中文乱码与编码处理
中文乱码是OSD开发里绕不开的坎。海康老版本SDK的ANSI接口,对字符串的编码约定是GBK。你在Visual Studio的源文件里直接写中文,如果源文件保存成UTF-8,strcpy过去的就是UTF-8字节流,设备端按GBK解析,自然乱码。
我建议统一走编码转换,别依赖运气。Windows下可以用MultiByteToWideChar和WideCharToMultiByte做转换,或者干脆把OSD内容统一从UTF-8配置转成GBK再调用SDK。新版SDK也提供了NET_DVR_SetSDKInitCfg,可以设置全局编码类型,但老设备兼容性有限,最稳的做法还是程序里显式转成GBK。测试时多测几个含生僻字的名称,比只测“张三”要靠谱。
4.4 设备型号和固件差异
海康的设备线很长,不同型号、不同固件对OSD叠加的支持程度差异不小。同一套代码,在A型号上完美运行,换到B型号可能就返回不支持。遇到这种情况,先查设备型号和固件版本,再去官网确认该型号是否支持字符叠加功能。部分摄像机固件开启移动侦测或人脸抓拍后,会把OSD区域强制挪动位置,导致你配置的对齐方式看起来变了,这是设备自身的策略,SDK层面控制不了。
另外,现在有些项目会搭配海康vm软件做视觉定位,那是另一套视觉平台,和本文说的设备网络SDK不是一回事,不要混用。如果视觉工位也要出图带水印,通常是在vm流程里配置文字绘制模块,而不是调摄像头的OSD接口。两边的实现路径完全不同,选择前要先想清楚你到底需要的是设备端永久的OSD,还是临时显示的文字标签。
5. 复盘:这套OSD方案值得注意的几个细节
最后聊点个人体会。
我在交付这个项目之后复盘过几次,最深的感受是“设备端OSD虽然功能简单,但它属于编码器层面的能力,改错一次影响面不亚于动一次业务配置”。所以现在我做OSD下发前,一定会先调用NET_DVR_GetDVRConfig把当前配置读出来,在程序里比对后再覆盖下发。这样既能确认通道号对不对,也能避免把现场已有配置无意中清掉。很多问题不是代码写不出来,而是没做好提前校验。
另一个经验是预留日志。每次设置OSD之后,把设备IP、通道、下发内容、错误码和时间戳记到本地日志里,线上回溯会轻松很多。尤其是批量管理几十台设备时,光靠控制台打印根本看不出哪台设备失败了。
如果你是在做一个长期维护的监控平台,建议把OSD内容的来源从业务数据库读取,而不是把值写在代码里。这样今天改库位、明天换责任人,都只改数据不动程序。能做到这一步,这个demo就不再是演示玩具,而是一个可以被业务系统持续调用的标准能力。
后面我还在计划把OSD内容做成可视化配置界面,让现场实施人员不用改代码就能调整文字位置和颜色,到时候再写一篇具体实现分享给大家。
本文还有配套的精品资源,点击获取