Java对接海康威视SDK:实时流、抓图与录像下载实战
简介面向Java开发者和安防系统集成人员围绕海康威视网络摄像机与NVR的SDK二次开发覆盖实时流与历史流推流、抓图、录像下载及云台控制等核心能力适用于安防平台对接、视频监控系统定制等场景。压缩包共256个文件、约39.25MB其中49个Java源码文件展示调用逻辑与工程结构131个XML对应配置与界面布局25个DLL与23个SO为Windows/Linux原生运行库另有JAR依赖、启动脚本和Markdown说明便于跨平台部署与扩展已有1163人浏览学习。内容预览可见核心DLL库、启动脚本与基础运行依赖基本还原了完整可运行的海康SDK集成工程读者可据此搭建开发环境理解设备注册、取流、抓图、录像检索下载等关键流程减少踩坑成本。1. Java 对接海康威视 SDK先搞清楚这一堆名词到底在解决什么问题很多团队第一次碰“Java 海康威视 SDK”都会带着一个朴素的想法通过代码把摄像机和 NVR 上的画面拿下来再推到自己的平台里展示或者存储。实际做起来才发现难点不在于 Java 本身而在于海康设备那一整套不太符合互联网开发习惯的接入方式。你会遇到插件下载提示、动态库加载失败、回调线程不触发以及明明预览正常但推流就是黑屏这类让人头疼的问题。这篇文章面向的读者很具体要么在做安防平台的 Java 后端要么在做视频汇聚、巡检抓图、录像归档这类业务要么是把摄像头接入已有的 Web 或流媒体体系。不做浏览器上的插件接入也不聊设备管理后台怎么配置只看 Java 服务端如何通过 HCNetSDK 搞定三种能力取实时流、取历史流、抓图与录像下载并把流推到业务系统真正能消费的地方。整个过程我会按我自己做过的工程路径来讲尽量让你看完就知道下一步该打开哪个文件、调用哪个方法。2. 设备的接入模型和三条路为什么大多数场景应该选 SDK2.1 网络摄像机、NVR 和通道号先把设备模型掰开揉碎海康的设备接入在逻辑上就三层设备、通道、码流。一台网络摄像机是一个设备但你会看到它既可以被 NVR 管理也可以被 SDK 直接登录NVR 本身也是一个设备但它下面可以挂多个网络摄像机你登录 NVR 之后拿到的是逻辑通道而不是每一路相机的独立 IP。这个差别直接决定代码里lChannel参数应该怎么写。NVR 的通道一般从 1 开始对应设备管理界面上显示的通道号但要注意这些是逻辑通道和网络摄像机的物理编号不一定一致有的 NVR 还要做通道映射。另外每一条通道下面还区分主码流和子码流主码流分辨率高、码率大适合录像和推流子码流分辨率低适合预览和移动端。SDK 里这两个东西一般用dwStreamType来区分0 是主码流1 是子码流。还有一个很多人容易忽略的地方NVR 本身有本地硬盘录像录像文件的回放和实时流的取流走的是两套 API。实时流调NET_DVR_RealPlay_V40回放则要NET_DVR_PlayBackByName或者NET_DVR_GetFileByName先定位录像文件。历史流也可以走 RTSP但需要拼带时间参数的 URL后面我会单独说。2.2 Java 接入海康设备的三种路线JNA、ISAPI/GB28181、纯 RTSP网上能看到很多接入海康的方式归类下来其实就三条路选型不对后面全是坑。方案Java 侧做法能干什么开发量典型限制JNA 封装 HCNetSDK用 JNA 加载厂商动态库直接调用NET_DVR_*系列接口实时预览、回放、抓图、录像下载、报警回调、设备配置覆盖能力最全大要理解 JNA 结构体和回调动态库只支持特定操作系统JNA 内存管理要自己小心ISAPI/HTTP 接口直接请求设备或 NVR 的 REST 风格接口抓图、云台控制、配置查询、部分录像检索小没有动态库依赖取流和回放能力受限很多老固件接口不全纯 RTSP/GB28181 接入用 FFmpeg、Zxing 或流媒体网关拉 RTSP 流国标走 GB28181 的 SIP 注册视频播放、转推流小团队容易上手拿不到设备私有事件和录像文件索引对标题里的需求——实时流、历史流推流、抓图、录像下载——这三条路都能覆盖一部分但覆盖度不一样。比如抓图用 ISAPI 最简单GET /ISAPI/Streaming/channels/101/picture就能拿到 JPEG不用碰动态库。录像下载也能通过 ISAPI 的/ISAPI/ContentMgmt/search接口检索到录像文件再下载。但如果你要的是“实时流在 Java 进程里做二次处理再把处理后的数据推出去”那么 ISAPI 就做不到了它只给你一张图或一个文件不给你原始码流。所以常见的从业方案是能和设备交互的都用 HCNetSDK因为它的覆盖面最全从预览到回放、从事件回调到设备校时一套接口都管对外如果需要快速交付或者需要把流直接交给流媒体服务再叠加 RTSP/FFmpeg 的方案。也就是说把 SDK 作为设备能力入口把 FFmpeg 作为流媒体管道。这不是偷懒而是避开在 Java 里从零解析 PS 流再做转封装的大坑。2.3 用 JNA 装载 HCNetSDK 动态库目录、接口和 JAR 包选定 SDK 路线之后第一个门槛就是让 Java 进程能访问海康的动态库。海康官方提供的是 C 语言头文件和对应平台动态库Linux 下是libhcnetsdk.so加一堆关联库Windows 下是一堆 DLLJava 这边没有官方 jar社区通用做法是 JNA 直连。你需要做三件事第一把动态库目录放到 Java 能加载到的路径第二写一个继承Library的接口声明你用到的函数第三用Native.load载入。Maven 依赖只需要 JNA 本身海康那部分不是 Maven 中央仓库能拉到的。dependency groupIdnet.java.dev.jna/groupId artifactIdjna/artifactId version5.13.0/version /dependencyJNA 版本不用追求新5.10 以上的都行。这里要注意海康官方依赖 JNA 但是并没有要求精确版本建议选一个你团队用过、知道不会和 Spring Boot 里其他原生库冲突的版本。然后是接口定义海康官方网上也有提供HCNetSDK.java的封装文件很多人拿来直接用。问题在于这份文件是自动生成的结构体非常多而且有的字段用到了嵌套结构体的内存对齐直接用会踩不少坑。import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.ptr.IntByReference; public interface HCNetSDK extends Library { HCNetSDK INSTANCE Native.load(HCNetSDK, HCNetSDK.class); boolean NET_DVR_Init(); boolean NET_DVR_Cleanup(); int NET_DVR_GetLastError(); int NET_DVR_Login_V40(NET_DVR_USER_LOGIN_INFO loginInfo, NET_DVR_DEVICEINFO_V40 deviceInfo); boolean NET_DVR_Logout(int userId); int NET_DVR_RealPlay_V40(NET_DVR_PREVIEWINFO previewInfo, FRealDataCallBack_V30 callback, Object userData); boolean NET_DVR_StopRealPlay(int playHandle); }这段代码里Native.load(HCNetSDK, ...)用的是动态库文件名Linux 会去找libHCNetSDK.soWindows 会找HCNetSDK.DLL。我们把厂商提供的.so和它依赖的libHCCore.so、libcrypto.so放在服务端的/opt/hik/libs目录。提示不要把这些.so直接丢到系统/usr/lib污染系统目录。用-Djava.library.path/opt/hik/libs指定加载路径同时用LD_LIBRARY_PATH保证动态库之间的依赖能解析。结构体方面JNA 下要用Structure子类并且注意字段顺序要和 C 头文件一致。NET_DVR_USER_LOGIN_INFO里设备地址、用户名、密码都是字节数组不能用String直接替代要getBytes()后拷贝进去。2.4 登录设备并读取通道信息最小可用代码设备初始化后就是登录。早期接口NET_DVR_Login已经不用了现在标准做法是NET_DVR_Login_V40它能一次性拿回设备序列号、通道数量、设备类型这些信息。public int login(String ip, short port, String username, String password) { NET_DVR_Init(); NET_DVR_USER_LOGIN_INFO loginInfo new NET_DVR_USER_LOGIN_INFO(); loginInfo.sDeviceAddress ip.getBytes(); loginInfo.wPort port; loginInfo.sUserName username.getBytes(); loginInfo.sPassword password.getBytes(); loginInfo.bUseTransport 0; NET_DVR_DEVICEINFO_V40 deviceInfo new NET_DVR_DEVICEINFO_V40(); int userId HCNetSDK.INSTANCE.NET_DVR_Login_V40(loginInfo, deviceInfo); if (userId -1) { int err HCNetSDK.INSTANCE.NET_DVR_GetLastError(); throw new RuntimeException(login failed. err err); } // deviceInfo.byStartChan 是起始逻辑通道号byChanNum 是通道总数 int startChan deviceInfo.byStartChan 0xFF; int chanNum deviceInfo.byChanNum 0xFF; log.info(login success, userId{}, channels{}, start{}, userId, chanNum, startChan); return userId; }关键点有三个。一是NET_DVR_Init只需要调用一次整个 JVM 生命周期不要重复调用。二是NET_DVR_USER_LOGIN_INFO里的密码字段是定长字节数组如果密码比数组短剩余位置应该是0不是空格。三是登录句柄userId是后面所有操作的前置条件必须在整个应用生命周期里保存建议用一个静态 Map 按设备 IP 持有这些句柄而不是每次调完就丢。如果你只拿到了通道号还没拿到每个通道的实时流地址也没关系后面取流时可以直接用逻辑通道号去请求。SDK 的方式不需要你自己拼 RTSP URL它内部会按通道号找到对应通道。2.5 错误码与登录失败排查不要只打印负数新手最容易卡在“登录返回 -1”。NET_DVR_GetLastError()拿到的是一个错误码海康的错误码表可以在 SDK 包里找到。常见码我有印象的几个23 表示连接设备失败29 表示用户名或密码错误71 是通道号错误72 是设备未登录73 是内存不足。真实项目里碰到最多的就是 23 和 29。23 不一定是账号问题更多是网络不通。注意海康设备默认端口不是 80而是 8000。很多团队把设备端口写成 80登录直接失败。还有跨网段的问题SDK 底层走私有协议需要保证服务器能访问到设备的 8000 端口不要在还没确认端口连通的情况下反复试。我在项目里习惯把错误码拼进异常信息里方便排障。private String translateError(int code) { switch (code) { case 23: return 23: 连接设备失败检查端口和网络; case 29: return 29: 用户名或密码错误; case 71: return 71: 通道号错误; case 73: return 73: 内存不足检查系统可用内存; default: return code : 未识别的错误码; } }这段翻译逻辑没有玄学就是把最容易遇到的那几个错误码写成可读文案让现场实施的人不用翻几十页的错误码表。3. 实时流、抓图、录像下载的实现细节三个高频功能一次说透3.1 启动实时预览并接收码流回调登录成功后想要拿到实时流调用NET_DVR_RealPlay_V40并传入预览参数和回调对象。很多资料会提到hPlayWnd窗口句柄那是给界面播放用的纯服务端这一项直接传null。NET_DVR_PREVIEWINFO previewInfo new NET_DVR_PREVIEWINFO(); previewInfo.lChannel 1; // 逻辑通道号 previewInfo.dwStreamType 0; // 0 主码流1 子码流 previewInfo.dwLinkMode 0; // 0 TCP1 UDP2 多播 previewInfo.hPlayWnd null; // 服务端不需要窗口 previewInfo.bBlocked 1; // 1 阻塞式取流0 非阻塞 int playHandle HCNetSDK.INSTANCE.NET_DVR_RealPlay_V40( previewInfo, (lRealHandle, dwDataType, pBuffer, dwBufSize, pUser) - { if (dwDataType 0) { // 0 表示原始视频流pBuffer 是码流数据 handleVideoFrame(pBuffer.getByteArray(0, dwBufSize)); } }, null );这里有个容易误解的参数bBlocked 1不是在回调里阻塞而是指NET_DVR_RealPlay_V40这个方法本身是阻塞式还是非阻塞式。服务端取流一般推荐阻塞式因为后续数据会持续在回调里返回如果不设成 1某些设备固件会出现回调不稳的情况。回调里拿到的pBuffer并不直接就是 H.264 裸流。海康默认回传的是 PS 封装流尤其当你用 TCP 方式取流时。你要先做 PS 解复用才能拿到 H.264 的 NAL。这个细节放到推流章节再说在这里记住不要假设回调的第一帧就是 SPS/PPS一定要在解码前判断是否拿到关键帧。3.2 抓图用官方接口直接出 JPEG而不是自己去解码抓图有两种常见做法。一种是在实时流回调里截取视频帧自己转 JPEG另一种是调用 SDK 的NET_DVR_CaptureJPEGPicture直接让设备出图。我的建议很直接能用后者就用后者除非你要抓的是视频里某一瞬间的原始帧并且自己具备解码能力。boolean ok HCNetSDK.INSTANCE.NET_DVR_CaptureJPEGPicture( userId, 1, // 通道号从 1 开始 new NET_DVR_JPEGPARA(),// 默认参数wdnWidth/wdnHeight 可指定分辨率 /data/capture/101_20250101120000.jpg ); if (!ok) { int err HCNetSDK.INSTANCE.NET_DVR_GetLastError(); log.error(capture failed, err{}, err); }NET_DVR_JPEGPARA里面主要设置图片宽高但要注意这个参数并非所有设备都支持有的老设备会忽略宽高直接输出默认分辨率。抓图接口是设备侧完成的不占服务器 CPU也不依赖代码里的解码链路这在多路摄像头场景下是天壤之别。如果你在回调里做软件解码再编码为 JPEG一个 1080P 主码流可能吃掉 30% 的 CPU而NET_DVR_CaptureJPEGPicture基本可以忽略。抓图失败优先排查方向和清晰度无关一般是通道号不对、设备不支持这个接口、或者当前通道处于异常状态。还有一点NVR 的预览通道号后面接 IPC 时抓图通道号依然用 NVR 的逻辑通道号。3.3 按时间段下载录像先检索再下载最后比对文件大小录像下载的需求通常是这样的用户选一个时间段后端去 NVR 上把录像文件拉下来。SDK 提供的接口是NET_DVR_GetFileByName通过文件名下载 NVR 本地录像。但很多人不知道名字要先去检索录像文件列表拿到不是自己随手拼。NET_DVR_TIME startTime new NET_DVR_TIME(); startTime.dwYear 2025; startTime.dwMonth 1; startTime.dwDay 1; startTime.dwHour 0; startTime.dwMinute 0; startTime.dwSecond 0; NET_DVR_TIME endTime new NET_DVR_TIME(); endTime.dwYear 2025; endTime.dwMonth 1; endTime.dwDay 1; endTime.dwHour 23; endTime.dwMinute 59; endTime.dwSecond 59; NET_DVR_FINDDATA_V30 findData new NET_DVR_FINDDATA_V30(); int findHandle HCNetSDK.INSTANCE.NET_DVR_FindFile_V30(userId, 1, startTime, endTime, findData); if (findHandle 0) { // 每次调用 NET_DVR_FindNextFile_V30() 拿一条录像文件 // sFileName 就是设备上的录像文件名 }拿到sFileName之后再调用NET_DVR_GetFileByName把文件名、本地保存路径、起止时间传进去它会在后台慢慢下载。下载过程是异步的要注意几个点。一是不要在主线程死等建议用独立任务线程去轮询下载进度二是多通道并发下载时有些 NVR 固件对单设备并发下载数有限制超过 3 路就会返回失败三是下载完成后建议比对本地文件大小和NET_DVR_GetDownloadPos返回的进度如果大小不对就删除重下避免留下半个文件。3.4 录像下载的进度查询与中断处理录像下载一旦开始SDK 是通过下载句柄来跟踪状态的这个句柄和登录句柄、播放句柄不是一个东西。int downloadHandle HCNetSDK.INSTANCE.NET_DVR_GetFileByName( userId, fileName, savePath, startTime, endTime, 1024 * 1024); // 轮询下载进度 NET_DVR_DOWNLOAD_POS downloadPos new NET_DVR_DOWNLOAD_POS(); while (true) { boolean ok HCNetSDK.INSTANCE.NET_DVR_GetDownloadPos(downloadHandle, downloadPos); if (!ok || downloadPos.dwErrorCode ! 0) { break; } // downloadPos.dwFileSize 是总字节数dwDownloadSize 是已下载字节数 int percent (int) (downloadPos.dwDownloadSize * 100 / downloadPos.dwFileSize); log.info(download progress: {}%, percent); if (downloadPos.dwDownloadSize downloadPos.dwFileSize) { break; } Thread.sleep(1000); } HCNetSDK.INSTANCE.NET_DVR_StopGetFile(downloadHandle);NET_DVR_GetFileByName最后一个参数fileBlockSize表示成块下载时的块大小单位是字节它影响的是内部数据块读取粒度设置成 1MB 比较平衡。如果设得太小比如 4KB大文件下载时会频繁回调导致 CPU 升高设得太大则单块内存占用高。需要注意NET_DVR_DOWNLOAD_POS的dwErrorCode很多情况不是 0常见的一个是因为下载通道被人为停止或设备录像文件被删除。遇到这种情况先把错误码打出来不要只打印一个“下载失败”。4. 实时流和历史流推流把海康码流变成业务系统能消费的内容4.1 推流方案选型SDK 回调直推还是 FFmpeg 转推做推流时团队的第一个分歧点是用 Java 从 SDK 回调里拿原始流自己推还是干脆用 FFmpeg 把 RTSP 拉下来推 RTMP。这两种方案我都见过评价也很极端。维度SDK 回调 Java 直推FFmpeg 转推 RTSP端到端延迟低可到 300ms 以内稍高但 1s 内也能做到开发量要处理 PS 解复用、H.264 分帧、时间戳、FLV 封装启动一个进程写一条命令部署复杂度只依赖 JVM 和 SDK 动态库额外依赖 FFmpeg 二进制故障排查难度高黑盒较多低FFmpeg 日志直观适合场景低延迟内部业务单路或多路定制视频平台、云直播、汇聚转发以我的工程观点除非你对延迟极其敏感否则不要用 Java 自己拆 PS 流做 RTMP 推流。那是一条很深的坑路细节非常多。更推荐的方案是Java 进程做设备接入和信令控制用 FFmpeg 做实际的取流和转推。原因很简单FFmpeg 能把 RTSP 的历史回放地址也直接拉流还能做重连、转码、转封装这些能力自己用 Java 写都不现实。所以下面我会给出两条路。第一用 FFmpeg 命令方式转推这是快速落地路径第二简单介绍 SDK 回调拆帧的思路让你在必须低延迟定制时知道难在哪。4.2 快速落地用 FFmpeg 把海康 RTSP 转推到 SRS如果你已经有 SRS、Nginx-RTMP 或者任何一种流媒体服务器那么推流这件事可以被压缩成一条命令。海康的 RTSP 地址有固定格式。ffmpeg -rtsp_transport tcp \ -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101 \ -c copy \ -f flv rtmp://127.0.0.1:1935/live/cam_101命令里的Streaming/Channels/101是海康设备的标准路径101表示第 1 通道的主码流102是第 1 通道的子码流。如果你要从 NVR 拉第 3 通道子码流那就是301、302的规律。历史流地址稍不同要在这个路径后面拼接起止时间参数。这里-c copy是流拷贝不做转码CPU 占用非常低。但它有一个要求源流和输出封装格式要兼容。RTSP 里通常是 H.264FLV 也支持 H.264所以可以直接 copy。如果你要推给不支持 H.264High Profile 的老播放器就得加-vcodec libx264转码CPU 也会成倍上涨。4.3 历史流的推流两种落地方式对比历史流推流本质上不是“取文件再推送”而是“实时播放录像文件并同时输出流”。常见做法有两种两种我都实践过。第一种方式是利用海康 RTSP 的历史回放能力。海康的 RTSP 地址支持starttime和endtime参数FFmpeg 可以直接拉取一个时间段内的录像流。时间格式是yyyyMMddTHHmmSSZ注意中间有个 T后面的 Z 表示 UTC但我们本地环境一般是东八区需要转换。ffmpeg -rtsp_transport tcp \ -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101?starttime20250101T000000Zendtime20250101T003000Z \ -c copy \ -f flv rtmp://127.0.0.1:1935/live/replay_cam_101_20250101这种方式的优点是实时读取存储介质不会产生本地文件不会占磁盘缺点是依赖设备侧 RTSP 服务的回放能力和网络带宽。NVR 同时支撑多路历史流回放时有些固件会明显变慢原因是硬盘 IO 被占满。第二种方式是先用 SDK 把录像文件下载到本地再用 FFmpeg 推文件也就是说是一条“先取后推”的命令链。优点是长时间历史流的推流更可靠不容易因为设备侧中断而断流缺点是磁盘开销大并且必须等下载完成才能推端到端延迟可能达到分钟级。我觉得它适合文件归档和离线转存不适合实时查看历史。4.4 如果必须用 SDK 回调直推H.264 拆帧和时间戳是关键如果是做低延迟定制播放你才需要 SDK 回调里那层 PS 解复用。SDK 回调数据经过分析能看到典型的 PS 流结构以00 00 01 BA开头是 PS 头后面是 PES 包PES 里才是 H.264 的 NAL。你需要循环解析 PES 头再从 payload 里找到00 00 00 01起始码按起始码切出一个个 NAL。private void handleVideoFrame(byte[] frame) { // 1. 从 frame 中定位起始码 00 00 00 01 // 2. 按 NAL type 判断是否关键帧(0x65)还是非关键帧(0x41) // 3. 把关键帧、PPS、SPS 缓存下来推流前先发 SPS/PPS // 4. 将每个 NAL 打过时间戳后写入发送队列 int offset findStartCode(frame, 0); while (offset frame.length) { int next findStartCode(frame, offset 4); byte nalType (byte) (frame[offset 4] 0x1F); if (nalType 5) { // IDR 关键帧标记为关键帧并等待发送 } else if (nalType 7) { // SPS缓存 } else if (nalType 8) { // PPS缓存 } pushToRtmp(frame, offset, next - offset); offset next; } }这段代码的逻辑就是分帧。真正生产环境里这里有以下几件事必须处理一是 SPS/PPS 的发送时机。RTMP 协议规定必须在流开始阶段发送包含 SPS/PPS 的序列号头如果漏了播放器会黑屏。二是时间戳的处理。RTMP 时间戳是毫秒级增量不能直接用海康回调里的时间需要按收到帧的节奏自己累加。三是关键帧策略如果发送队列积压为了追上播放进度要主动丢弃非关键帧只保留关键帧否则延迟越滚越大。这些细节加起来工程量远比想象中大。所以我个人建议是没有足够理由不碰 SDK 回调直推。你把精力花在 FFmpeg 命令进程管理和 Flv 播放链路上得到的效果会好得多。4.5 SRS 侧的策略与参数调整缓存、GOP、延迟不管用 FFmpeg 还是 SDK 直推最终流到了 SRS 这类服务上之后播放延迟还取决于流媒体服务器的缓存策略。SRS 配置里和低延迟最相关的是gop_cache、queue_length、latency这几个参数。SRS 默认会开启 GOP 缓存也就是会缓存一个关键帧周期内的数据让新进房间的播放器能秒开。但代价是延迟至少增加一个 GOP 长度。如果做低延迟直播可以把gop_cache关掉并限制queue_length。vhost __defaultVhost__ { play { gop_cache off; queue_length 10; LatencyConfig on; } }这些配置的讨论要在业务需求明确后才做得起来而且海康设备的主码流 GOP 默认通常是 50 帧也就是约 2 秒一个关键帧。如果播放器要秒开必须保留gop_cache否则要等下一个关键帧。你会发现“流畅”和“低延迟”在很多视频场景里是一对矛盾需要给出明确取舍而不是两头都要。5. 避坑与排查回调不触发、花屏黑图、通道错位的真实原因5.1 插件的提示与 SDK 混为一谈浏览器提示下插件不代表服务端有问题现象客户用浏览器访问摄像头 IP 时页面提示“海康威视请点击此处下载插件安装时请关闭浏览器”于是认为 Java 也无法取流。原因海康的网页预览依赖 ActiveX 或 NPAPI 插件那是浏览器端的私有播放器和 Java 服务端调 HCNetSDK 完全是两套东西。SDK 走的是私有协议不需要插件也不依赖浏览器。解决先把问题分开。浏览器看不了是插件兼容问题典型出现在 Chrome 49 以后对 NPAPI 插件的封锁。服务端接入直接用NET_DVR_Init和NET_DVR_Login_V40验证只要能登录成功就能取流和浏览器完全无关。5.2 登录成功但回调不触发句柄被 GC 回收线程死掉现象程序明明登录成功NET_DVR_RealPlay_V40也返回了播放句柄但回调一次都没进来。查日志没有报错进程活着就是没有数据。原因这是一个容易被忽略的 JNA 生命周期问题。如果你把FRealDataCallBack_V30的回调对象定义在局部变量里方法执行完就被 GC 回收SDK 侧的回调函数指针变成悬空指针之后再也拿不到数据。另外如果登录句柄userId没有长期保存被 GC 后设备侧连接也会断开。解决把登录句柄和回调对象都提升到长生命周期对象比如放到一个按设备 IP 管理的单例 Map 里并保证应用退出前不释放。还要注意回调线程是 SDK 内部创建的你可以在回调里打印线程名正常情况下它会持续输出。public class DeviceSession { private final int userId; private final int playHandle; private final FRealDataCallBack_V30 callback; public DeviceSession(int userId, int playHandle, FRealDataCallBack_V30 callback) { this.userId userId; this.playHandle playHandle; this.callback callback; } }这类问题用一句话概括就是JNA 回调被回收属于最典型的线上“灵异事件”。排查时先看日志里有没有回调线程的痕迹再看对象是否被强引用。5.3 花屏、马赛克、首屏黑PS 流没解对SPS/PPS 没等到现象实时回调自己解析以后再送播放器画面有花屏或者前几秒黑屏。用 FFmpeg 拉同一个 RTSP 地址却正常。原因SDK 回调给到的是 PS 封装流不是裸的 H.264。如果你直接把 PS 流喂给播放器或者推流播放器解析不到 SPS/PPS画面当然起不来。解决不要直接拿pBuffer的原始字节去推送先做 PS 解复用找到 PES 里的 H.264 负载再按00 00 00 01起始码切帧。而且第一个关键帧之前一定要把 SPS 和 PPS 缓存下来先发出去。有一个小的辅助判断标准解码器只有在收到 IDR 帧后才能真正出图所以推流时如果队列里第一帧不是 IDR丢掉它等下一个 IDR 再开始。5.4 抓图返回成功但图片是黑的现象NET_DVR_CaptureJPEGPicture返回 TRUE但下载下来的 JPEG 全黑或者只有一小块正常画面。原因抓图发生在视频流还没稳定的时候。设备刚上线或通道刚切换码流类型第一帧可能不是关键帧输出的是解码不完整的画面。解决抓图前先确认通道已经处于预览或接入状态。如果是刚启动后的自动抓图给通道 3 到 5 秒预热时间再抓。大量抓图场景下建议把抓图命令放到一个延迟任务里并且在失败后重试一次。还有一点不要在 NVR 正在做录像回放的通道上并发抓图有些设备固件在这个状态下会异常。5.5 NVR 通道号对不上预览的是 1 通道走到图片库里却是另一个摄像头现象前端选了 5 通道摄像头预览但抓图出来的画面是另外一个位置的。原因NVR 的逻辑通道号与前端页面选择的通道号不一致。页面上看到的可能是“物理通道号”而 SDK 里用的是“逻辑通道号”二者在部分型号上相差一个偏移还有一些 NVR 的通道号从 0 开始统计SDK 从 1 开始就会有一路错位。解决登录后读取deviceInfo.byStartChan和byChanNum以这两个字段为准做映射不要写死 1 到 16 的通道号。可以用一个配置接口把摄像头名和逻辑通道号回传给前端避免在界面上凭感觉选。5.6 录像下载任务堆积导致 NVR 假死现象业务高峰期同时对 20 路录像发起下载NVR 响应越来越慢最后卡到预览都断了。原因NVR 的并发下载能力有限尤其老款 7800 系列单台同时下载超过 4 路就会出现资源争抢。下载是磁盘 IO 密集操作会挤占实时录像的写入带宽。解决做一个全局下载调度器同一台 NVR 的下载并发数限制在 2 到 3 路超过的排队等待下载任务间错开开始时间避免同时读盘另外下载尽量安排到业务低峰期并做好失败重试的退避间隔。6. 进阶多通道并发的接入与推流链路性能验证6.1 线程模型每个设备一个线程池通道间用队列隔离并发取流时最忌讳把所有设备的回调任务都扔到同一个线程池因为某一路网络抖动会拖慢其他路。我实践下来比较适合服务端的模型是每个登录设备对应一个小的线程池每个通道对应一个独立的有界阻塞队列回调里只做入队消费线程专门处理抓图和推流。这样做的第一个好处是回调线程永远不会因为业务逻辑慢而阻塞SDK 侧回调线程池的压力小。第二个好处是当某一路推流积压时只丢弃这一路的非关键帧不影响其他通道。public class ChannelBuffer { private final ArrayBlockingQueuebyte[] queue new ArrayBlockingQueue(120); private final ExecutorService executor Executors.newSingleThreadExecutor(); public void offer(byte[] frame) { // 队列满时直接丢弃等待消费侧追上来 queue.offer(frame); } }这里的队列长度要按帧率和码率来估算。一个 1080P 主码流大约 4Mbps按每帧 64KB 算120 帧大约是 7.5MB 缓冲区。如果延迟要求高可以压缩到 60 帧如果播放容忍度高可以放宽到 240 帧。回调里只做offer不做任何格式转换这是关键。6.2 推流链路的验证方法从设备推到 SRS再用 ffprobe 验收推流做完了要验证结果的不只是“画面能动”还要验证端到端延迟和内容参数。我通常用ffprobe探测推上去的流看 SPS、PPS 里的分辨率、编码格式、GOP 大小是否和设备侧配置一致。如果分辨率比配置低很可能是取到了子码流但不自知。ffprobe -v error -show_streams -select_streams v:0 rtmp://127.0.0.1:1935/live/cam_101关注输出的codec_name、width、height和avg_frame_rate。如果设备配置是 1080P但这里显示 640x360通常是Streaming/Channels/102和101写反了。顺便检查一下profileH.264 High Profile 在低端播放器上兼容性差需要转码时提前发现。6.3 长时间运行后的资源回收习惯聊一个我踩过多次的坑下载句柄、播放句柄、文件句柄大量堆积。JNA 包装的 SDK 对象在你不再使用后GC 不一定会马上释放底层句柄。服务跑上几天后会出现登录成功但取流失败的现象但重启进程后一切又正常。这种问题现场耗时最长。我的习惯是所有NET_DVR_*创建出来的句柄都必须在一个finally块里显式释放并且用日志记录释放路径。登录用NET_DVR_Logout预览用NET_DVR_StopRealPlay下载用NET_DVR_StopGetFile。不要依赖finalize或者 JVM 退出时的自动清理。代码层面可以把每个句柄封装成带close()方法的资源对象团队里统一使用 try-with-resources 风格调用。这套习惯在长周期服务里比任何优化技巧都管用。记住一句话SDK 不是黑匣子但你不释放资源时它就会变成黑匣子。写到这里把我自己的经验也交代一下我早期做这套东西时最耗时间的不是写取流代码而是排查回调不触发和通道号错位这类环境型问题。后来学乖了所有设备接入前先出一个“设备自检清单”能登录、能预览、能抓图、能下载四项都过才进入业务开发。这样把问题边界提前收窄。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →