首页 / 资讯中心 / 文章详情

BCB6环境下网络高清摄像机OSD字符叠加例程实战详解

BCB6环境下网络高清摄像机OSD字符叠加例程实战详解 ★ FEATURED ARTICLE
简介这是一份针对海康威视网络高清摄像机的字符叠加OSD开发例程面向具备C Builder基础、从事视频监控二次开发的工程师基于BCB6.0环境调用海康SDK实现视频画面上的时间戳、文字注记等信息叠加解决监控场景中的自定义信息显示需求。资源包共56个文件、约11.61MB含动态库、静态库、头文件、C源码及可执行测试程序涵盖SDK调用与OSD参数设置等关键模块目前已有7562人学习下载适合直接编译学习。其中叠加测试工程完整展示了从设备连接、句柄获取到OSD叠加API调用的流程并提供可运行的exe与界面源码。开发者可深入学习SDK初始化、字体颜色位置设定、错误处理等环节也可基于现成工程快速实现项目中的字符叠加功能显著缩短开发调试周期。整体代码结构紧凑、配套文件完整是视频监控领域实用的参考例程。1. 一场“把字烙进视频流”的实战某安防厂商网络高清摄像机OSD叠加例程接手一个老项目时对方丢过来一个 BCB6.0 工程说要给网络高清摄像机做字符叠加。屏幕上十几路视频同时预览每一路都要实时显示点位编号和时间字不能抖、不能拖影还要能随时改内容。这套例程拆开一看核心就是通过厂商网络 SDK 的路径把 OSD 区域坐标、字符内容、字体颜色一次性下发到设备由设备视频处理芯片把字符直接“烙”进视频流。它适合两类人一类是还在用 C Builder 老环境维护监控上位机的开发者另一类是给工厂、园区做视频点位改造需要给实时画面加标识的集成商。这篇笔记把例程从工程边界讲到像素级实现最后落在怎么改成自己的区域模板。2. 环境与依赖BCB6.0 怎么和厂商 SDK 握手2.1 例程的工程边界这套例程不是一套完整的上位机软件它是一个可单独编译运行的“OSD 叠加演示工程”。工程里只有一个主窗体、一个预览显示控件、一组厂商 SDK 封装接口以及一个专门处理字符叠加的类。边界很清晰凡是和登录、拉流、设参数相关的操作都走厂商 SDK凡是和 OSD 文本内容、字体、位置、颜色有关的逻辑都放在OsdOverlay类里。我用 BCB6 打开工程后第一反应是找“代码量最多的文件”。结果发现最多的不是业务逻辑而是 SDK 封装的兼容层。原因是某厂商这套网络 SDK 本身是为 VC6 设计的接口导出用的是__stdcall头文件里大量使用BOOL、DWORD这类 Windows 类型。BCB6 的 VCL 控件和编译器对__stdcall是支持的但头文件里的某些宏展开方式不同直接 include 会报一堆undefined。所以例程作者在工程里放了一个SdkCompat.h把 SDK 头文件里依赖的 Windows 基础类型预先 typedef 了一遍再extern C包一层 C 函数接口。2.2 最小可用的依赖清单整个工程能跑起来依赖的东西不多。编译侧需要 Borland C Builder 6.0安装时勾选IEEE浮点优化和旧版VCL兼容选项运行侧需要厂商网络 SDK 的 DLL 文件其中核心的是PlayCtrl.dll播放库和HCCore.dll设备通信库这两个 DLL 必须放在 exe 同目录。例程里没有用第三方 UI 库OSD 设置面板是 BCB6 原生组件搭的。SDK 初始化是整套流程的起点。常见做法是在主窗体OnCreate里调用一次NET_DVR_Init()然后在程序退出时调用NET_DVR_Cleanup()。这两个函数一个分配全局句柄表一个释放句柄漏掉的话会出现“第二次打开设备失败”的诡异情况。// 主窗体创建事件里做 SDK 初始化 void __fastcall TMainForm::FormCreate(TObject *Sender) { // 1. 全局初始化内部创建句柄表必须最先调用 NET_DVR_Init(); // 2. 设置连接超时网络摄像机偶尔不在线卡住主线程就是事故 NET_DVR_SetConnectTime(2000, 1); // 3. 设置重连尝试次数为 3超过后回调会触发断线事件 NET_DVR_SetReconnect(3000, 3); }参数说明NET_DVR_SetConnectTime的第一个参数单位是毫秒2000 表示两秒连不上就返回超时第二个参数是连接方式1 表示主动连接。NET_DVR_SetReconnect的第一个参数是重连间隔毫秒数第二个是最大次数。这两个参数决定了工程刚启动时的体验如果监控点离线程序不会卡在白屏上等十几秒。2.3 登录接口的人格分裂时刻登录设备是 OSD 下发的前提例程里用的是NET_DVR_Login_V40登录成功后得到一个用户 IDlUserID后面所有操作都靠这个 ID 定位设备通道。这里有个特别容易翻车的点BCB6 的char是有符号类型而 SDK 结构体里的char字节数组是按无符号处理的。调用NET_DVR_Login_V40时设备 IP 和用户名密码拷贝进结构体后如果设备返回“密码错误”但明明密码是对的基本可以断定是结构体字节对齐出了问题。例程的解决方案是在工程设置里把“Structure member alignment”从默认的 8 字节改成 1 字节并且在includeSDK 头文件之前强制#pragma pack(push, 1)用完再恢复。很多移植到 BCB6 的工程都会在 SDK 数据结构上栽跟头就是因为没对齐。这套例程的目录里专门有一段注释提醒所有NET_DVR_*结构体声明和调用处必须处于同一套#pragma pack作用域内。// 登录设备得到操作句柄 NET_DVR_USER_LOGIN_INFO loginInfo {0}; NET_DVR_DEVICEINFO_V40 deviceInfo {0}; // 注意在 BCB6 下结构体需要 1 字节对齐否则字段错位 strcpy(loginInfo.sDeviceAddress, 192.168.1.64); loginInfo.wPort 8000; strcpy(loginInfo.sUserName, admin); strcpy(loginInfo.sPassword, password123); int lUserID NET_DVR_Login_V40(loginInfo, deviceInfo); if (lUserID 0) { // 取错误码300 表示用户名密码错201 表示设备无响应 int errCode NET_DVR_GetLastError(); ShowMessage(登录失败错误码: String(errCode)); return; }这段代码里wPort是设备默认 SDK 服务端口某厂商网络高清摄像机一般是 8000如果设备改过端口要同步修改。NET_DVR_Login_V40的第二个参数不仅仅是接收设备能力集还会返回通道数量OSD 叠加时要从deviceInfo.byStartChan判断从哪个通道号开始。之前有同事没看这个字段直接写死通道 1结果设备拨码到了通道 5OSD 设置一直报参数错误。3. 核心模块拆解一个字符从下发到上屏的完整链路3.1 设备端 OSD 和显示端 OSD 的分野很多第一次接触这个例程的人会问OSD 字符叠加不是图像处理的事吗为什么直接调用厂商 SDK 就能把字写到画面上这里要分清两条路线。设备端 OSD 是指字符由摄像机的视频处理芯片在编码前叠加到码流里保存的录像和实时预览都能看到字显示端 OSD 是指在 PC 端播放器拿到解码后的 YUV 数据后由软件把文字画在窗口表面。本例程用的是设备端 OSD好处是录像文件里也有字坏处是字符一旦下发进设备只能通过 SDK 修改不能像软件层那样随时弹开关。设备端 OSD 和显示端 OSD 的叠加链路完全不同我把两种方式的区别整理成了下面这张表方便开发时选型。对比项设备端OSD显示端OSD叠加位置摄像机编码前本地解码后录像文件带有字符不带动字符刷新速度受设备能力限制随窗口刷新掉电保存存在设备Flash内存中消失适用场景固定点位标识动态弹窗本地显示端叠加的典型做法是响应窗口的WM_PAINT在视频窗口上覆盖一个透明 Label。但例程里如果你把透明 Label 放在视频控件上会发现文字被视频画面“吃掉”。这是因为预览控件使用了硬件加速绘制普通窗口控件无法和视频图层叠加。设备端 OSD 绕过了这个坑字符直接进入视频帧没有本地图层竞争的问题。3.2 OSD 叠加参数组装设备端 OSD 的接口设计比较直接向设备写入一个“字符叠加配置块”里面包含叠加行数、每行文本内容、字体大小、颜色、位置。例程里把这部分封装成了一个函数输入是lUserID和通道号输出是设备返回的写入结果。// 组装并下发 OSD 文本叠加配置 void SetOsdText(int lUserID, int iChannel, String text, int posX, int posY) { // 1. 取设备当前 OSD 配置不先取直接写会把原有配置清空 NET_DVR_OSDCFG_V50 cfg {0}; cfg.dwSize sizeof(NET_DVR_OSDCFG_V50); NET_DVR_GetDVRConfig(lUserID, NET_DVR_GET_OSDCFG_V50, iChannel, cfg, sizeof(cfg)); // 2. 把文本按 UTF-8 转成设备需要的编码 // 某厂商华东区设备默认 GBK字符串直接拷贝会花屏 WideString wsText text; char szBuf[64] {0}; wsText.WideCharBuf((unsigned short*)szBuf, 64); strcpy((char*)cfg.stOSDCharLine[0].sOSDChar, szBuf); // 3. 叠加位置使用归一化坐标0~1000不是像素坐标 cfg.stOSDCharLine[0].wCharPosX posX; cfg.stOSDCharLine[0].wCharPosY posY; cfg.stOSDCharLine[0].byCharFormat 1; // 字体格式宋体 // 4. 写回设备 NET_DVR_SetDVRConfig(lUserID, NET_DVR_SET_OSDCFG_V50, iChannel, cfg, sizeof(cfg)); }这段代码里有几个参数需要注意。NET_DVR_GET_OSDCFG_V50是配置类型宏V50 版本兼容网络高清摄像机的多码流特性如果不先GetDVRConfig就直接SetDVRConfig设备会把没处理的字段当默认值覆盖导致之前设置过的叠加行丢失这是经验教训。wCharPosX、wCharPosY的取值范围是 0 到 1000对应画面宽高的千分比。例程注释里写得很清楚X100、Y50表示字符左上角在画面横向 10%、纵向 5% 的位置。比像素坐标直观而且录像分辨率变化后不需要重新算像素。3.3 多行叠加的格式控制单行 OSD 一般够用但生产环境里往往要同时显示点位编号、运维电话、时间日期。例程的 OsdOverlay 类里维护了一个vectorLineItem每行独立存文本、坐标、格式。下发时循环调用刚才的SetOsdText并把行号作为stOSDCharLine[lineIndex]的索引。这里有个特别隐蔽的坑设备 OSD 配置里的每一个 line 都绑定一个显示区域而行和行之间不能重叠。如果第 2 行 Y 坐标比第 1 行小设备不会报错但显示时字会互相覆盖。例程的做法是行距按字符高度的 1.4 倍递增比如第 1 行 Y50第 2 行 Y95第 3 行 Y140。这个经验值来自真实项目某工厂摄像头画面高度只有 720p行距太小直接把两行字叠成黑块调低字体高度也没用。字体格式字段byCharFormat在不同设备固件上含义不同。例程适配的是某厂商 5.x 版本固件1 表示宋体、2 表示黑体。如果写入后看到字是方块可以先读设备能力集里的byOSDNumber和字体数量再决定格式值。我把字符叠加的字段对照整理了一下方便改造时快速定位。3.4 预览控件里验证 OSD 是否生效配置下发成功不等于显示正确。原因是设备端 OSD 在编码器里生效但预览窗口拉流有缓存。例程里加了一个“手动刷新预览”按钮点击后做一次NET_DVR_StopRealPlay再重新NET_DVR_RealPlay_V40。// 重启预览让 OSD 配置强制生效 NET_DVR_StopRealPlay(lRealHandle); NET_DVR_CLIENTINFO_V40 clientInfo {0}; clientInfo.lChannel iChannel; clientInfo.hPlayWnd PanelVideo-Handle; // 视频渲染窗口句柄 lRealHandle NET_DVR_RealPlay_V40(lUserID, clientInfo, NULL, NULL); if (lRealHandle 0) { int err NET_DVR_GetLastError(); ShowMessage(预览失败错误码: String(err)); }NET_DVR_CLIENTINFO_V40结构体里hPlayWnd要传的是真正能接收视频的窗口句柄BCB6 里一般用Panel的Handle而不是Form的Handle。用后者会导致视频画面黑屏但 OSD 字符可能还显示在屏幕上——因为 OSD 是设备端烙进去的只是显示窗口不对。这个结构体还有两个可选字段byPreviewMode设为 0 表示实时预览设为 2 表示数字通道预览对网络高清摄像机一定要设为 0否则会提示“通道号不合法”。4. 避坑与排查BCB6 下叠加例程的四条血泪经验4.1 现象OSD 下发提示“设备不支持”某次在项目现场部署同样的 exe在仓库摄像头上一按“下发 OSD”就提示设备不支持但在厂区摄像头却正常。排查过程一度怀疑是 exe 文件损坏。反复对比后发现仓库那批摄像机固件版本是 4.x老固件不支持 V50 级配置接口NET_DVR_GET_OSDCFG_V50返回错误码 12命令字不支持。 原因V50 配置接口是在某厂商 5.0 固件之后才支持的老设备要用旧的NET_DVR_GET_OSDCFG结构体。 解决写一个能力探测函数先调用NET_DVR_GetDVRConfig的旧版接口返回NET_DVR_NOERROR就用旧结构体返回不支持错误码再切到 V50。例程后来在架构上把旧版结构体也封装了一层两条路径共用同一个 OsdOverlay 类。4.2 现象中文字符在画面上变成乱码第一次在 BCB6 里跑通例程时中文点位名显示成“锟斤拷”。原因很明确C Builder 的String默认是 UTF-16 编码而某个网络高清摄像机设备的 OSD 配置期望的是 GBK 编码字节流。直接把String转成char*后UTF-16 的高字节被丢弃低字节按 GBK 解码就形成了乱码。 解决方法是例程里的编码转换链。先用WideString接收 BCB6 的String然后用WideCharBuf把 UTF-16 转成 UTF-16LE 字节序最后调用厂商 SDK 的编码转换接口把 UTF-16LE 转成 GBK。转换后的字节数组再拷贝进sOSDChar字段。从那以后我每次写这个项目的叠加逻辑都强制走一遍转换链不敢直接strcpy。// UTF-16 转 GBK 的可复用函数 void Utf16ToGbk(String input, char* outBuf, int outLen) { // 1. 得到 UTF-16 的字节指针 wchar_t* pWide input.c_str(); // 2. 调用厂商 SDK 的编码转换接口 NET_DVR_CharConv(1, pWide, input.Length(), outBuf, outLen, 0); }NET_DVR_CharConv的参数含义分别是转换方向1 表示 UTF-16 到 ANSI源字符串源长度目标缓冲目标长度保留值。outLen不能小于源字符串字节数的两倍否则返回错误码NET_DVR_PARAMETER_ERROR。这个函数在例程里被封装成了OsdOverlay::ToDeviceEncoding任何外部文本进来必须先过这一步。4.3 现象OSD 位置在 4K 分辨率和 1080P 之间跳动现场有两台不同分辨率的摄像机同一套配置下发后一台的字符在左上角另一台跑到中间去了。排查发现虽然归一化坐标是按百分比映射的但某厂商设备对 Y 轴坐标的解析在 4K 分辨率下从顶部像素对齐在 1080P 下从底部像素对齐。 原因设备固件里对 OSD 区域的基准点定义不一致老固件按左下角为原点新固件按左上角为原点。 解决判断设备能力集里的字符叠加上限如果 Y 坐标超过上限就用1000 - byOsdMaxLine - y反推一次。例程里写了一个归一化坐标校正函数在设备端分辨率变化后自动重算下发值。4.4 现象下发 OSD 后预览画面闪烁O S D 字符下发成功后预览画面每隔几秒闪一下黑边。这个现象在例程的测试机上稳定复现但不是所有设备都有。最后定位到是NET_DVR_SetDVRConfig在 BCB6 工程里被主线程调用而预览线程正在从设备拉流。SDK 的配置通道和实时流通道共享同一个 FIFO配置写入时流数据被阻塞。 解决把所有SetDVRConfig调用放到一个单独的工作线程里用QueueUserAPC或者简单的生产者-消费者队列。例程里实现了一个POsDPoster线程事件触发后在线程里执行SetDvrConfig主线程只负责更新 UI。从那以后每次下发都强制走线程界面也不卡了画面也不闪了。5. 把例程跑起来从源码到屏幕出字的实际步骤5.1 目录结构与编译顺序打开例程包之后不要急着进 BCB6 双击工程文件先把目录结构看清楚。例程根目录下的组织方式是按“设备接入层 / 叠加业务层 / 界面表现层”三层划分的我在拆包时把它理成了金字塔结构。OsdDemo/ ├── Config │ └── device.ini // 设备 IP、端口、通道号、OSD 初始文本 ├── Device │ ├── SdkCompat.h // BCB6 下 SDK 头文件的类型兼容层 │ ├── DeviceManager.cpp // 登录、拉流、重连等设备生命周期管理 │ └── DeviceManager.h ├── Overlay │ ├── OsdOverlay.cpp // OSD 配置的组装、下发、编码转换 │ └── OsdOverlay.h ├── UI │ ├── MainForm.cpp // BCB6 主窗体预览面板和配置面板 │ └── MainForm.h └── OsdDemo.bpr // BCB6 工程文件编译顺序不是从.bpr直接按 F9而是先编译SdkCompat.h和DeviceManager.cpp确认兼容层通过后再编译OsdOverlay.cpp最后打开MainForm.cpp构造界面。实际编译中遇到最多的问题是把SdkCompat.h和 SDK 原始头文件同时 include 到了同一个单元编译器报出重复定义。例程里有个约定凡是引过SdkCompat.h的模块就不要直接 include 厂商 SDK 原生头文件二者互斥。编译通过后把厂商 SDK 的 DLL 拷贝到 Debug 目录。这一步容易被忽略因为 BCB6 的 IDE 环境会把 DLL 加载路径自动指向工程目录但 exe 在 Debug 目录运行时找不到。例程包自带的“部署说明.txt”里写得很细HCCore.dll、PlayCtrl.dll、HCCoreTest.dll缺一不可前两个缺了程序启动报“初始化失败”最后一个缺了预览窗格右下角没有码流信息。5.2 配置文件与首次运行运行前设置Config/device.ini里的参数。例程这套代码把配置读取封装在DeviceManager里每次启动时读一次。我在测试时改了几个字段对比效果差异明显。[Device] Ip192.168.1.64 Port8000 Usernameadmin Password12345 Channel1 [Osd] Enable1 Text1仓库A区-01号机位 Text1X50 Text1Y50 TextFormat1Enable1表示启动后自动下发一次 OSD如果设为 0程序只做预览不下发。Text1X、Text1Y就是第 3 章讲的归一化坐标值范围 0 到 1000。第一次跑通时建议把Text1仓库A区-01号机位改成纯英文测试比如WH-A-01排除编码问题确认英文显示正常后再改回中文验证Utf16ToGbk转换是否生效。启动程序后主界面显示预览画面左侧是 OSD 配置面板。点击“应用”按钮后程序按“读配置 - 调取旧配置 - 改单行文本和坐标 - 写回设备 - 重启预览”的顺序执行。如果一切正常屏幕上应在一个呼吸周期内出现字符。5.3 失败时看什么运行中如果 OSD 没有出现不要盲目反复点击应用先按顺序检查三处。第一处是登录状态主窗体标题栏显示的用户 ID 如果是数组下标而不是正整数说明登录失败此时下发 OSD 必然报错。第二处是窗口句柄预览面板的PanelVideo-Handle如果是在OnShow事件里取的某些显卡驱动下会拿到 0导致视频画面黑屏把所有取 Handle 的动作放到OnResize之后执行即可。第三处是设备日志某厂商摄像机可以打开 Web 管理页在“系统日志”里看OSD cfg set success或failed记录比在 PC 端猜错误码快得多。这套检查顺序能覆盖九成的叠加不生效场景。最后一招是把 PC 上的 SDK 例程自带的诊断工具打开它会列出设备支持的配置命令字列表如果列表里压根没有NET_DVR_SET_OSDCFG_V50那就是设备型号真的不支持设备端 OSD。6. 二次开发把固定叠加改成区域模板例程默认只有一行文本叠加实际项目里常见的是“多行信息 半透明背景 不同点位不同字体”。改造方向不是继续堆叠单行配置而是把 OSD 内容抽象成模板结构按点位批量替换参数。第一步定义模板数据结构。我在 OsdOverlay 里加了一个RegionTemplate包含区域名、行数组、每行坐标、字体格式、是否启用背景框。一个区域对应一个通道不同的通道加载不同的模板文件。// 模板结构体把一个点位的所有叠加行打包 struct RegionTemplate { String regionName; // 区域名如 A区-01 struct Line { String content; // 行内容 int x; // 归一化 X 坐标 int y; // 归一化 Y 坐标 int font; // 字体格式 } lines[4]; BOOL bEnableBg; // 是否显示半透明背景 };第二步从 ini 或数据库加载模板。例程原封不动地读device.ini二次开发时可以加一个LoadTemplateFromDB函数从 SQLite 查询点位表按regionName返回模板。下发逻辑不变只是把strcpy那一行换成从RegionTemplate::Line取值。第三步验证模板效果。改完模板后批量修改多个点位的 OSD 内容然后连续预览多路视频。我一般会在每个点位编号上加一个随机数尾缀通过尾缀随机性确认是实时生效而不是缓存画面。这套例程拆到这里能体会到 BCB6 工程的生存之道老环境遇到新设备靠的不是重写而是把 SDK 的复杂度隔离在兼容层后面。每次往工程里加新功能我都强制走一遍“读设备能力集 - 编解码转换 - 下发 - 重启预览”的完整链路。希望这次拆解能帮你把自己的点位字符叠加需求快速落地。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站