ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java对接海康威视摄像头:从RTSP到Web播放的完整方案

Java对接海康威视摄像头:从RTSP到Web播放的完整方案 1. 网页为什么直接播不了海康的视频先看清三条路再动手我前段时间接了一个需求Java后端对接一批海康威视摄像头在现有管理后台里做页面实时播放和录像回放。需求文本就一句话真正落地时才发现这里面的链路比想象中长不少。很多第一次接触海康设备的人都会有一个直觉海康不是有SDK吗Java调一下不就行了真不是这么回事。这篇文章就把我实践下来的一套完整方案拆开讲清楚SDK怎么选、后端怎么取流、流格式怎么转、前端播放器怎么接最后再把我踩过的一个黑屏问题完整复盘一遍。先说最核心的矛盾海康摄像头IPC和录像机NVR输出的标准视频流是RTSP协议。RTSP本身不是一种浏览器能直接消费的媒体协议它更像是一个“控制协议”负责协商播放、暂停、跳转这些动作真正的视频数据跑在RTP包里面。而浏览器只认HTTP(S)协议下的媒体资源所以你在地址栏里输一个rtsp://192.168.1.64:554/Streaming/Channels/101浏览器只会一脸茫然。1.1 浏览器不能播RTSP这件事不是版本问题是协议问题很多人问“海康摄像头取流地址”怎么拼这其实是所有坑里最容易解决的一个。海康设备默认的RTSP地址格式大概是这样的rtsp://admin:password192.168.1.64:554/Streaming/Channels/101101通道1的主码流102通道1的子码流201通道2的主码流以此类推这个地址拿给VLC、ffmpeg、PotPlayer都能直接播但到了网页里就走不通。原因很简单浏览器内核里的媒体栈只实现了HTTP的拉流能力没有实现完整的RTSP客户端。这不是说你换个浏览器版本就能解决而是协议栈层面的缺失。所以在做方案之前你得先接受一个事实想实现“页面实时播放和回放”中间必有一层转换这层转换负责把海康的RTSP或SDK原始码流变成浏览器能播的格式。1.2 三条主流路线先想清楚再动手目前做海康Web接入市场上基本是三条路线路线一海康自带WebControl插件。这是IE时代遗留的方案基于ActiveX/NPAPI。Chrome很早就宣布停止支持NPAPI插件Edge也不再支持ActiveX所以这套东西在2024年的今天基本可以宣判死刑。除非你的项目还锁死在老IE内核的浏览器上否则我劝你别碰。路线二中间件/流媒体网关。用ffmpeg或者现成的流媒体服务比如SRS、ZLMediaKit把海康的RTSP拉出来转成HLS或HTTP-FLV输出给前端。这个方案优点是可以复用成熟组件缺点是它独立于你的Java业务系统。你要自己做设备状态管理、令牌鉴权、动态启停还得维护一堆ffmpeg进程的生命周期出问题的时候排查链路很长。路线三自建轻量级流媒体网关用海康设备网络SDK在Java后端取流。这是我最终采用并验证可行的方案。核心思路就是Java通过JNA调用海康官方C语言SDKHCNetSDK在回调里拿到原始码流经过解封装后重新封装成FLV格式以HTTP-FLV的方式推给前端播放器用flv.js或类似的MSE播放器。三条路线里路线三的工程量其实没有想象中大而且它的优势是深度可控预览、回放、暂停、拖动、按时间查录像这些操作都能和你的业务系统无缝集成。后面所有内容都围绕路线三展开。2. 技术选型与整体架构设备网络SDK HTTP-FLV flv.js2.1 为什么是设备网络SDK而不是VisionMaster在搜资料的时候很多人会看到海康VisionMasterVM软件这个词在这里必须先澄清一下。VisionMaster是海康的机器视觉软件主要用来做视觉定位、尺寸测量、缺陷检测这类工业场景它对应的相机通常是MV系列的面阵/线阵工业相机走的是GigE或USB3.0接口和视频监控领域里的网络摄像头IPC、网络录像机NVR完全不是一套体系。如果你的项目是监控摄像头的Web播放要用的就是海康的“设备网络SDK”也就是HCNetSDK。这个SDK官方只提供C/C接口动态库在Windows下是HCNetSDK.dllLinux下是libhcnetsdk.so。Java要做的是通过JNA或者JNI去调用它目前社区里用的最多的是JNA方案没有太多玄学成分就是把C接口翻译成Java接口把结构体翻译成Java类。2.2 公网访问环境下的架构链路整个系统的数据链路和控制链路可以这样理解数据链路摄像头 → 海康设备网络SDK → Java流媒体网关Netty → HTTP-FLV → flv.js → 浏览器控制链路前端页面 → Java业务接口 → Java流媒体网关 → SDK控制接口 → 海康设备这里有一个重要的经验Java进程既是SDK客户端同时也充当了流媒体服务端。也就是说浏览器不再直接面对设备所有与设备相关的操作都统一走后端这个“代理”。为什么这么设计最直接的原因是安全和可管理性。如果让前端直接拿RTSP地址播放等于把IPC的取流口令暴露给所有用户而且浏览器也播不了。后端代理之后前端只需要请求一个普通HTTP URL播放权限、访问控制、操作行为都可以在Java层做统一管理。2.3 依赖清单和版本选择这次实践用到的核心组件列成表格更直观组件用途选型建议JDK运行环境8即可项目里用8或11都行JNAJava调用C动态库JNA 5.xNetty构建HTTP流式服务Netty 4.x海康设备网络SDK取流、控制、回放下载官方最新版flv.js前端解析播放FLV开源社区版或引用CDNHCNetSDK要去海康官方开发者社区注册并实名认证后下载这点和很多国外SDK不同。下载时注意匹配你的操作系统位数Windows下要区分32位和64位Java进程的位数要和动态库位数一致。如果JVM是64位却加载了32位的DLL启动时就会报错。Linux部署还要额外检查动态库依赖比如libhcnetsdk.so依赖一些系统基础库缺失的话需要安装。实测下来CentOS 7和Ubuntu 18.04以上版本基本都不需要额外折腾但老系统要注意。3. Java对接HCNetSDK的关键细节JNA映射、登录与会话管理海康SDK官方没有Java版本所以所有接Java的人第一步都是在网上找一份HCNetSDK的JNA接口定义。网上这类封装版本很多质量参差不齐。从我实际使用来看结构体映射如果出了问题后面排查起来非常痛苦。3.1 JNA结构体映射最容易翻车的三个细节结构体是Java对接海康SDK时最大的工程量和最大的坑。HCNetSDK里的结构体动辄几十个字段、上百字节而且里面包含大量预留字段。JNA在加载动态库时会按照你定义的Structure计算内存布局如果你定义的结构体大小和C库预期不一致轻则字段读出来是乱码重则导致SDK内部缓冲区溢出。第一个细节字段顺序不能改字段类型必须严格对应。很多C结构体里的BOOL其实是int或者byteJNA里一定要映射成byte或int不能映射成Java的boolean。JNA处理boolean时默认只占1字节如果C结构体里是4字节的int型BOOL整个结构体的偏移就全乱了。我遇到过的情况就是登录结构体前面字段全对唯独一个布尔字段类型写错导致后面字节流错位设备地址读出来是乱的排查了很久才发现。第二个细节预留字段和填充字段不能省。海康SDK的结构体在多个版本里反复演进很多结构体里有一大片byRes[]预留区域这些字段虽然你现在不用但它们在结构体里的内存布局作用不可省略。如果你图省事只映射用到的字段结构体大小就会比实际的小SDK写入数据时会越界。第三个细节字符串数组用byte[]别用String。登录结构体里的设备地址、用户名、密码都是定长byte数组。JNA虽然支持把String自动转成native char数组但在结构体场景下用byte[]更可控特别要注意编码问题。海康SDK的密码字段是字节数组如果密码里有特殊字符Java默认的UTF-8转出来和设备端编码不一致登录就会莫名其妙失败。3.2 初始化、登录和错误码定位海康SDK的Java接入第一步是加载动态库并执行初始化。JNA加载动态库的标准写法HCNetSDK sdk Native.load(HCNetSDK, HCNetSDK.class); sdk.NET_DVR_Init(); sdk.NET_DVR_SetConnectTime(2000, 1); sdk.NET_DVR_SetReconnect(10000, true);这里NET_DVR_SetReconnect我建议一定要开。设备重启、网络抖动时SDK能自动重连否则你的流媒体网关会时不时出现没有画面的情况而且重连参数里的间隔时间根据实际情况调整我这里设了10秒重连间隔。登录设备的核心代码大致长这样NET_DVR_USER_LOGIN_INFO loginInfo new NET_DVR_USER_LOGIN_INFO(); loginInfo.sDeviceAddress ip.getBytes(); loginInfo.wPort (short) 8000; loginInfo.sUserName username.getBytes(); loginInfo.sPassword password.getBytes(); loginInfo.write(); NET_DVR_DEVICEINFO_V40 deviceInfo new NET_DVR_DEVICEINFO_V40(); int userId sdk.NET_DVR_Login_V40(loginInfo, deviceInfo); if (userId -1) { int errorCode sdk.NET_DVR_GetLastError(); // 根据errorCode查阅官方错误码表定位问题 }海康默认的SDK通信端口是8000RTSP的554是媒体流端口这是两码事。很多人在防火墙只开了554结果SDK登录一直失败其实就是8000端口被挡了。错误码定位有一个习惯要养成任何SDK调用返回失败返回-1或false立刻调NET_DVR_GetLastError()。海康的SDK错误码表很长方向大概分几类参数错误、通道号错误、用户权限不足、网络超时、连接数满、设备资源不足。其中“连接数满”在项目上线后非常常见因为设备和平台授权是有限额的。如果用户反馈设备看不了频繁出现连接数类错误大概率是并发预览路数超过了设备授权或SDK允许的上限。3.3 多设备多通道的会话管理一个项目通常不止一台设备需要设计一个会话管理器。登录层面每台设备用一个userId管理登录会话。同一个设备不要反复登录登出SDK登录和登出都是有开销的频繁操作会导致设备端连接资源释放不及时。预览和回放层面SDK的每次取流调用都会返回一个句柄realHandle或playbackHandle这是标识一路媒体会话的唯一凭证。我的做法是维护一个MaprealHandle - ChannelSession playbackHandle - PlaybackSessionChannelSession里面放设备IP、通道号、当前连接的订阅者列表、最近活跃时间等。这样当设备断线重连时可以快速定位哪些会话失效并通知前端重连。还有一个很关键的经验一路视频流不要给每个浏览器用户都建立一路SDK取流。同理一个通道多个人同时看如果每个人都走后端拉一路流带宽和设备的并发承载都会被迅速打满。正确做法是维护一个“一源多播”的机制同一通道只从设备拉一路流然后通过后端的发布订阅模型把这一路流分发给多个前端播放器。这个设计在海康设备平台授权有限的项目里尤其重要能直接决定系统能不能在多人同时在线时稳定运行。4. 实时预览的实现拆解从SDK回调到浏览器画面海康SDK的预览流程从代码逻辑上可以拆成三步建立预览、接收回调数据、推送前端。4.1 预览接口调用和回调数据分类实时预览的核心调用是NET_DVR_RealPlay_V40参数里需要指定登录的userId、预览通道、码流类型等。关键参数说明lChannel设备通道号从1开始和设备端配置的通道编号对应dwStreamType0表示主码流1表示子码流。多路预览场景建议用子码流能省不少带宽dwLinkMode0表示TCP取流网络环境复杂时TCP比UDP稳定bBlocked1表示阻塞式取流取流失败时接口会一直等待0表示非阻塞立即返回调用成功后海康SDK会在内部线程里回调你注册的回调函数。回调函数的签名里有几个重要参数dwDataType和pBuffer。回调数据分类是刚接触SDK时最容易被忽视的地方。dwDataType有几种取值系统头数据SYSHEAD和音视频流数据STREAMDATA。第一次回调来的时候通常是系统头里面包含SPS/PPS这些关键信息后面的回调才是真正的音视频码流。如果代码不分类型直接把第一包数据当成视频帧去处理FLV封装出来就是坏的。回调函数里要做几件事收到SYSHEAD时解析里面的SPS/PPS保存下来准备用于封装FLV的AVC sequence header收到STREAMDATA时把pBuffer里的PS包解析出来抽取出H.264视频帧和AAC音频帧把抽取出来的数据按时间戳顺序写入FLV封装器FLV封装器产生完整的FLV tag后通过HTTP-FLV输出给订阅的客户端4.2 PS流解封装和H.264/AAC抽取海康SDK回调里拿到的数据不是裸H.264而是PS封装格式。PS是MPEG-2系统流的一种和海康设备内部存储格式有关里面包含了PS包头、PSM节目流映射、PES包体等结构。这块的解法有两条路路一自己写PS解复用器。网上能找到PS封装格式的文档解析逻辑不算复杂核心是从PS包的负载里找到PES包再从PES包里取出PES payload最后从PES payload里按起始码剥离出H.264的NALU。音频是AAC的话则是从PES里提取AAC帧。这个工作大概需要几百行代码不算多但细节多PES头长度、PTS/DTS提取、NALU边界处理都要照顾到。路二用社区封装好的项目。海康SDK的Java封装在GitHub上有不少一些项目已经实现了PS解封装和FLV封装。我做了过一轮筛选后发现不能直接无脑用。因为不同SDK版本回调出来的数据结构可能有差异而且有些社区项目只实现了预览没有回放部分。我的建议是拿社区项目做参考把PS解析、FLV封装这两个核心类抽出来其他和业务耦合的部分用自己的代码实现。PS解封装有一个性能建议回调函数是在海康SDK的内部线程里执行的不要在回调里做任何耗时操作比如网络发送、文件IO。我的做法是回调里只做一件事把数据拷贝到一个有界队列由一个专门的流推送线程从队列消费。否则一旦某个前端客户端慢回调线程被阻塞整个SDK的取流都会被拖住而且极难排查。4.3 FLV打包和HTTP流式输出FLV格式本身很简单本质上是一个FLV头加上一连串FLV tag。每个tag由tag头、tag数据、PreviousTagSize组成。视频tag里的数据是VideoTagHeader加AVCPacketType等字段音频tag则是AudioTagHeader加AAC PacketType等字段。实战里有几个关键点第一FLV的第一个视频tag必须是AVC sequence header。这个tag里放的不是视频帧而是SPS和PPS组成的AVCDecoderConfigurationRecord。浏览器端的flv.js看到这个tag才会初始化H.264解码器。如果这个tag缺失或顺序不对画面就会黑屏。第二拿到SPS/PPS后立刻推sequence header不要等第一个关键帧。如果服务器在关键帧到达时才去推头用户点击播放后要等一个GOP间隔通常是1到2秒才能看到画面。先推头、再推视频帧可以实现接近秒开的效果。第三视频帧要转成AVCC格式。海康PS包里解析出来的是AnnexB格式NALU以00 00 00 01或00 00 01起始码分割但播放器通常希望AVCC格式NALU前有一个4字节的长度字段。flv.js对AnnexB格式的兼容性不稳定最好在后端就转成AVCC。这个转换逻辑不复杂把起始码替换成4字节大端长度再组装成VideoTagHeader。HTTP输出层我用Netty实现了一个轻量HTTP服务接口格式类似GET /live/{sessionId}.flv响应头设置Content-Type: video/x-flv Cache-Control: no-cache Access-Control-Allow-Origin: *这里注意如果用Nginx做反向代理必须关闭缓冲也就是设置proxy_buffering off。否则Nginx默认缓冲整块数据再转发直播流的延迟会飙升甚至出现前端长时间不出画面的问题。5. 录像回放不止是换个接口这么简单实时预览搞定后很多人会觉得回放就是把NET_DVR_RealPlay_V40换成NET_DVR_StartPlayBackByTime其他逻辑复用。这个想法有一半是对的另一半会踩坑。5.1 按时间回放的基础流程海康SDK里按时间回放的调用是NET_DVR_StartPlayBackByTime核心参数是一个回放条件结构体包含起始时间和结束时间格式是NET_DVR_TIME本质就是年、月、日、时、分、秒。关键前提这个时间是设备端时间不是服务器时间。如果设备的NTP时间没配置好它内部的时钟可能和你服务器差好几分钟。你用服务器当前时间往前推一小时去回放结果发现设备上根本没有这个时间段的录像或者录像时间对不上。这个问题在项目上线后很容易被误判为“设备没录像”其实是时间基准不一致。稳妥的做法是在登录成功后主动读一次设备时间计算与服务器时间的偏移量所有回放时间条件都按设备时间传。回放的回调数据和预览一样同样是PS封装格式所以上一章写的PS解封装和FLV封装逻辑可以复用只是会话类型要区分开。5.2 录像文件枚举与时间轴处理按时间回放的一次完整调用需要有两个阶段先枚举录像再发起回放。用NET_DVR_FindFile_V40枚举指定通道和时间段内的录像文件会返回一组录像文件信息每个文件中包含开始时间、结束时间、文件大小等字段。把这些字段提取后返回给前端前端就可以在时间轴上用色块标识出“有录像”的区域。这一步非常推荐做原因在于如果不先枚举录像段直接把一个时间段传给设备去回放当时间段跨越多个录像文件时有些设备固件会在文件之间跳转时出现卡顿或黑屏。而先枚举出录像段让用户选择具体哪一段录像来播整个过程就变得非常稳定可控。我的前端交互设计是回放页面先请求一个“查询录像段”接口返回当天所有录像段列表页面在进度条上用不同颜色标识可播区域。用户点击具体时间段后后端从录像段列表里挑出包含该时间点的录像段发起回放。如果用户拖到了一个没有录像的空白区域后端会明确返回“该时间段无录像”前端置灰操作按钮而不是让播放器停留在黑屏状态。5.3 暂停、恢复、seek这些控制操作的正确姿势回放过程中用户肯定会拖动进度条、暂停、继续播放这些操作对应SDK里的NET_DVR_PlayBackControl_V40。控制码定义要去SDK头文件里查JNA封装里也都会有对应的常量。实战经验是拖动进度条不要重新建立回放会话。很多人遇到seek功能就直接关掉当前回放再按新时间开一路新回放这样会有一段空白等待时间而且回放进度会重新从关键帧开始缓冲体验很差。正确做法是在同一路回放句柄上发seek指令。seek的规范流程大概是暂停回放 → 发送SEEK指令 → 等待SDK定位完成 → 恢复播放。中间如果跳过暂停直接seek部分设备会不响应或返回错误。所以控制层设计接口时前端要按这个时序来调后端的接口也最好封装成“定位并继续播放”一个原子操作避免前端自己拆出多个请求导致时序错位。5.4 回放没有声音的问题很多做回放的人会遇到“画面正常但没有声音”的情况这个大概率不是代码问题而是设备端配置问题。海康摄像头的音频输入/输出默认可能是关闭的录像计划里也有独立的音频录制开关。如果设备端没有启用音频采集功能回放数据流里就不存在音频PES包后端封装FLV时自然没有音频tag。这个需要从两个方向排查一是去设备管理后台确认音频输入是否开启二是看后端解封装日志里音频PES包是否为零。另外还要注意音频编码格式如果IPC输出的音频是G.711或G.726而你的FLV封装器只支持下AAC那也需要在后端做音频转码才能出声音。我的建议是在设备上把音频编码统一设置成AAC避免引入额外的转码组件。6. 前端播放器集成与一次完整的排错链路后端链路全部打通后前端反而是相对简单的部分但也不是接入一个播放器就能完事。6.1 flv.js接入和生命周期管理播放HTTP-FLV流最成熟的前端方案是flv.js。它本质上是把FLV容器格式的数据在浏览器MSE能力之上转换成浏览器能播放的片段底层用的是video标签。核心代码大概是这样if (flvjs.isSupported()) { const player flvjs.createPlayer({ type: flv, isLive: true, url: /live/ sessionId .flv }); player.attachMediaElement(videoElement); player.load(); player.play(); }注意回放场景里isLive要设成false因为回放不是无限长度的直播流。前端在组件销毁时一定要调用player.destroy()否则浏览器会持续持有解码器资源和网络连接。我实际见过一个页面反复切换设备后内存持续上涨的案例最后定位就是播放器实例没释放。6.2 实测踩坑画面一直黑屏的完整排查过程这里把我遇到的一个非常典型的问题完整复盘一遍这个问题在论坛里反复有人问但很少有人把排查链路写清楚。现象是后端日志显示SDK登录成功、预览会话建立、回调函数里持续收到数据前端flv.js也加载成功了但页面上就是黑屏播放器没有抛任何网络错误。第一步排查单独验证后端流是否正常。我用VLC播放器直接请求后端的HTTP-FLV地址结果VLC能正常出画面。这说明后端输出的FLV数据流本身是能播的问题大概率出在flv.js与流格式的兼容性上。第二步排查抓包对比。我用Wireshark抓取后端返回的HTTP响应体再和使用官方Demo播放器时的响应体做对比。发现问题出在FLV文件头的处理上。FLV标准要求第一个视频tag必须是AVC sequence header而我当时的实现在某些场景下是先推了关键帧没有在最前面把SPS/PPS单独打包成一个tag推出去。VLC播放器对这种格式有一定容错会自动去数据流里找SPS/PPS但flv.js对格式要求更严格没有sequence header直接不能初始化解码器所以画面就一直黑着。第三步修复调整封装逻辑。在PS解封装后一旦解析出SPS/PPS就立刻构造AVC sequence header并作为FLV的第一个视频tag写入不管后续是不是关键帧。改完后前端画面秒出问题解决。这个案例说明一个经验VLC能播不代表浏览器能播。VLC的容错能力远强于浏览器里的JS播放器。所以做这种流媒体转换功能自测时一定要同时用flv.js页面和VLC分别验证两者都正常才算真的通。6.3 上线前必须处理掉的问题清单根据这次项目经验整理一份上线前检查清单按这个过一遍能少踩很多坑设备时间是否配置了NTP同步回放时间基准是否一致摄像头视频编码是否统一为H.264H.265在浏览器MSE里兼容性很差音频编码是否统一为AAC设备是否开启了音频输入8000端口和554端口是否在防火墙放行海康SDK动态库的位数是否和JVM位数一致设备断线重连后旧的预览/回放会话是否会被正确清理视频并发订阅机制是否实现了一源多播前端播放器实例在组件切换时是否彻底销毁反向代理是否关闭了缓冲Nginx或网关超时时间是否设置过短导致长连接被切断这套方案上线后在局域网环境里实时预览的延迟可以控制在1秒以内回放拖动响应也很流畅。回放功能在实现时最花时间的其实不是取流而是录像段的枚举和设备时间的校准。只要能把这个底层逻辑理清楚把PS解封装和FLV封装这两个核心环节写稳后面接什么前端播放器都是顺水推舟的事。
返回列表