ARTICLE DETAIL

资讯详情

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

基于SpringBoot+Netty的海康摄像头ISUP接入与HLS播放方案

基于SpringBoot+Netty的海康摄像头ISUP接入与HLS播放方案 在物联网安防项目里“海康威视摄像头ISUP原EHOME协议”接入需求一直很硬核。做视频平台的团队最怕的不是数据库不是前端而是设备侧各种私有协议。今天这篇就直接讲透一件事用SpringBoot Java Netty自建ISUP服务端让海康摄像头主动注册进来再配合ZLMediaKit把实时视频流转成HLS最后Vue前端用hls.js在浏览器里直接播放。整个方案我落地过不是SDK拉RTSP那种内网玩法而是真正适合跨公网接入的ISUP主动注册模式适合正在做安防平台、视频中台、物联网设备接入的同学参考。1. 项目背景与整体方案拆解1.1 ISUP原EHOME协议到底是什么海康老设备上有个功能叫EHOME新固件里统一改叫ISUPInternet Serial Union Protocol本质是一套基于TCP的私有协议。它的核心思想是让摄像头主动去连接平台服务器而不是平台去探测设备。这个设计对跨网络场景特别友好摄像头在客户现场可能没有公网IP、在NAT后面、甚至4G拨号上网平台不需要知道设备在哪只要设备能访问到平台服务器就行。很多人第一反应是用海康官方SDK拉RTSP流但那套方案要求设备端和取流端网络互通要么在同一局域网要么做端口映射整个部署链路非常脆弱。ISUP不一样设备主动拨号到平台注册、心跳、取流指令、媒体流回传都在这条TCP链路上跑公网环境下反而稳定得多。也正因为ISUP是私有协议海康没有公开完整的协议文档通常需要走官方渠道申请对接文档所以不少团队被吓退。但实际拆解下来核心就几块设备注册、心跳保活、信令交互、媒体流接收。搞明白消息结构之后用Netty完全能自己实现一个服务端。1.2 技术选型为什么是SpringBoot Netty ZLMediaKit Vue这个组合不是拍脑袋定的是我对比过好几种方案后的实际选择。SpringBoot负责上层业务比如设备管理、预览会话管理、鉴权、播放地址生成。做平台类项目Spring的生态和团队熟悉度都是最优解。NettyISUP是长连接TCP私有协议Netty在高并发长连接场景下的性能和内存管理比手撸Socket强太多社区成熟解决粘包拆包也很方便。ZLMediaKit流媒体服务器。ISUP媒体流回传需要接收、转封装、分发这层如果自己做工程量非常大。ZLM支持RTP/RTSP/RTMP/HLS/FLV/WebRTC等多种协议而且提供了REST控制API非常适合做二次开发。Vue hls.js前端要“免安装播放器”HLS是最稳的方案Safari原生支持Chrome/Edge用hls.js兜底。虽然延迟比WebRTC高几秒但胜在兼容性好摄像机实时预览场景完全够用。整体链路是海康摄像头通过ISUP注册到Netty服务端业务层下发预览指令后设备开始回传视频流流媒体服务器接收并转成HLS浏览器端通过Vue组件绑定播放地址渲染画面。1.3 整体链路与模块划分模块技术载体职责设备接入层Netty TCP服务端口7660接收ISUP注册、心跳、信令业务管理层SpringBoot设备管理、预览会话、鉴权、播放地址生成流媒体层ZLMediaKit端口8090/1935/554接收视频流、转封装、输出HLS/RTSP/RTMP前端展示层Vue2/Vue3 hls.js拉取HLS流并播放提前说明一下ISUP服务端和ZLM的集成度取决于你们项目的媒体流回传方式我在下文会给出一种经过验证的媒体流转发思路。2. 后端工程搭建与环境准备2.1 SpringBoot基础工程初始化项目用Maven构建SpringBoot版本我建议用2.7.x或者3.x按团队习惯选。如果项目还要对接MinIO做录像存储注意SpringBoot 3对javax到jakarta的切换避免依赖冲突。我这边以SpringBoot 2.7.18为例Java 8/11都能跑。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.100.Final/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency /dependencies用Hutool主要是图方便HTTP调用ZLM控制API、JSON解析、缓存工具都很顺手。如果公司禁止引入第三方工具库也可以用OkHttp加Jackson效果一样。2.2 关键依赖说明Netty版本选4.1.x稳定且内置的拆包器够用。ISUP协议的报文头里通常会包含消息总长度字段所以最核心的就是LengthFieldBasedFrameDecoder。这里有个坑ISUP的报文长度字段偏移量不是固定的不同设备固件、不同协议版本可能不一样千万别抄网上代码直接写死一定要拿抓包数据确认偏移位置。SpringBoot方面除了web和Netty基本不需要额外视频依赖。很多新手以为Java处理视频需要引入什么神秘的媒体库其实真正干重活的是ZLMediaKit后端只负责信令和控制。2.3 ZLMediaKit流媒体服务部署ZLM建议直接用Docker部署省掉编译的麻烦。官方镜像或者社区镜像都行只要暴露端口一致。docker run -d \ --name zlm \ -p 1935:1935 \ -p 8080:80 \ -p 8443:443 \ -p 8554:554 \ -p 10000:10000/udp \ -p 10000:10000/tcp \ -p 8000:8000/udp \ -v /opt/zlm:/opt/media/bin/www \ registry.cn-hangzhou.aliyuncs.com/zlmediakit/zlmediakit:master启动后修改config.ini里的api.secret这是ZLM控制API的鉴权密钥后端调用时需要带上。http.port默认80映射到宿主机8080方便区分。确认ZLM启动成功的方法很简单浏览器直接访问后台HTTP API接口例如http://127.0.0.1:8080/index/api/getServerConfig?secret你的密钥能返回JSON就说明服务正常。3. ISUP协议接入与信令交互实现3.1 ISUP报文结构快速认知ISUP协议报文由消息头和消息体组成。消息头里一般包含设备标识、协议类型、消息类型、消息序列号、消息长度等字段。消息体根据消息类型不同而不同比如注册报文里面会带设备型号、固件版本、通道数等信息。具体字段和二进制格式必须以PDF协议文档为准我这里只说通用认知。你在写解码器前先抓一份设备注册的完整报文用Wireshark或者tcpdump导出来对照文档逐字节分析比看十篇文章都管用。抓包分析时优先确认这几个字段的位置消息起始标识、消息总长度、消息类型、源设备ID、目标设备ID。只要这五个字段定位准确后面的解码器基本不会跑偏。3.2 Netty服务端核心实现先定义ISUP服务端启动器监听7660端口。这个端口是协议默认端口可以在设备端配置平台上改但建议保持默认降低设备配置出错率。Slf4j Component public class IsupServer implements ApplicationRunner { Resource private IsupChannelInitializer isupChannelInitializer; Override public void run(ApplicationArguments args) { EventLoopGroup boss new NioEventLoopGroup(1); EventLoopGroup worker new NioEventLoopGroup(); try { ServerBootstrap bootstrap new ServerBootstrap(); bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true) .childHandler(isupChannelInitializer); bootstrap.bind(7660).sync(); log.info(ISUP server started at port 7660); } catch (Exception e) { log.error(ISUP server start failed, e); } } }TCP_NODELAY一定要打开ISUP是长连接实时信令关闭Nagle算法可以减少小包延迟对信令响应速度有帮助。ChannelInitializer里加上拆包器、解码器和业务Handler顺序很关键不能乱。public class IsupChannelInitializer extends ChannelInitializerSocketChannel { Override protected void initChannel(SocketChannel ch) { ChannelPipeline pipeline ch.pipeline(); // 长度字段偏移量和长度字段字节数按你们协议文档调整 pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 20, 4, 0, 0)); pipeline.addLast(new IsupMessageDecoder()); pipeline.addLast(new IsupServerHandler()); pipeline.addLast(new IdleStateHandler(90, 0, 0, TimeUnit.SECONDS)); } }IdleStateHandler用来做读超时判断90秒内没收到设备心跳或任何数据就触发userEventTriggered在这条通道的处理器里主动关闭连接。3.3 设备注册、心跳与在线管理设备注册是接入流程里的第一件大事。设备上线后会发一包注册报文里面带着设备编码。服务端解析后要做三件事把设备编码和Channel关联起来、回复注册成功、同时触发业务层设备上线回调。Slf4j ChannelHandler.Sharable Component public class IsupServerHandler extends SimpleChannelInboundHandlerIsupMessage { Resource private DeviceSessionManager deviceSessionManager; Override protected void channelRead0(ChannelHandlerContext ctx, IsupMessage msg) { if (msg.getMsgType() MsgType.LOGIN) { String deviceId msg.getSourceId(); DeviceSession session new DeviceSession(); session.setDeviceId(deviceId); session.setChannel(ctx.channel()); session.setRemoteAddress(ctx.channel().remoteAddress().toString()); deviceSessionManager.onDeviceOnline(session); IsupMessage ack buildLoginAck(msg); ctx.writeAndFlush(ack); log.info(device online: {}, deviceId); } else if (msg.getMsgType() MsgType.HEARTBEAT) { deviceSessionManager.refreshHeartbeat(msg.getSourceId()); ctx.writeAndFlush(buildHeartbeatAck(msg)); } else { eventPublisher.publishEvent(new IsupMessageEvent(ctx.channel(), msg)); } } Override public void channelInactive(ChannelHandlerContext ctx) { deviceSessionManager.removeByChannel(ctx.channel()); ctx.close(); } Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof IdleStateEvent) { log.warn(device heartbeat timeout, close channel: {}, ctx.channel()); deviceSessionManager.removeByChannel(ctx.channel()); ctx.close(); } } }设备Session管理我建议直接内存Map套AtomicInteger计数如果设备量大或者需要多实例部署再上Redis。这里可以顺带说一句设备在线状态和通道状态最好分开存通道可能独立离线别一设备掉线就把所有通道状态清了。3.4 实时预览指令的下发时机与报文组织设备注册成功只是第一步预览才是业务核心。前端点击“播放”按钮后端收到请求后要做的事先确认设备在线再确认通道号和码流类型然后通过ISUP链路向设备发送实时视音频请求信令。预览指令里需要填写的关键参数一般包括目标设备ID、通道号、码流类型主码流/子码流、平台接收媒体流的地址和端口。这里平台接收媒体流的地址就是ZLM上为这个预览会话开的RTP接收端口。public void startPreview(DeviceSession session, int channelNo, int streamType) { IsupMessage playReq new IsupMessage(); playReq.setTargetId(session.getDeviceId()); playReq.setMsgType(MsgType.REAL_TIME_PLAY); playReq.setBody(buildPlayBody(session.getDeviceId(), channelNo, streamType)); session.getChannel().writeAndFlush(playReq); }设备收到预览指令后如果参数正确就会向平台注册时上报的媒体接收地址回传PS流。这里最容易出问题的是媒体接收地址配置很多设备在注册时上报的是内网地址导致平台端收不到流后面章节我会专门讲排查方法。4. 视频媒体流处理与HLS输出4.1 为什么需要ZLMediaKit这一层有的同学可能会问Netty已经把流收到了直接用Java写个Socket服务向浏览器吐数据不行吗理论上可以但视频流不是普通字节流它涉及到PS解封装、音视频解码、时间戳对齐、多路并发分发、转协议等一系列问题自己从零做就是一个中型流媒体项目。ZLMediaKit把这些脏活累活都干了我们只需要把ISUP回传的媒体流按它要求的方式喂进去然后把播放地址交给前端。更关键的是ZLM自带HLS切片能力内部把RTP/PS流转换成HLS分片后前端拿到m3u8索引文件按列表拉ts分片播放整个链路工程化程度很高。4.2 把ISUP回传的PS流送入流媒体的实现思路我这里采用的方案是把ISUP回传的PS流按RTP封装推给ZLM。ZLM提供了控制API可以动态开启一个RTP接收端口并关联到指定流ID。大致流程后端调用ZLM的openRtpServer接口参数传流ID和端口号传0表示自动分配端口ZLM返回一个实际端口号。Netty收到设备回传的PS流数据后在内存里做一次RTP打包把PS流塞进RTP载荷加上RTP头然后通过UDP发送到刚才ZLM分配的那个端口。ZLM收到RTP数据后自动完成PS解封装和转封装生成HLS分片。播放地址直接拼接http://zlm服务器IP:8080/{app}/{streamId}.hls.m3u8。这里说一下我为什么不用FFmpeg中转。ISUP回传的媒体流是在TCP私有连接里透传的FFmpeg无法直接读取这条私有连接要么先把裸流重新转发成一个本地UDP/TCP端口再用FFmpeg拉这样中间又多了进程和临时端口故障点多。直接RTP封装给ZLM整条链路都在进程内逻辑清晰、还少一层IO拷贝。RTP打包的核心代码逻辑就是构造RTP头然后按MTU大小切分PS流数据。如果设备回传PS流本身比较规整甚至可以先不分包直接一个RTP包塞一帧PS数据简单粗暴也能跑。但遇到大帧时容易超过MTU导致丢包所以建议还是按标准分片。4.3 通道会话管理与播放地址生成每个预览请求都要生成一个全局唯一的流ID我习惯用deviceId_channelNo_streamType加上一个短雪花ID做后缀看起来像这样4201001234567890_1_main_820391。这样在ZLM后台排查流的时候一眼就能看出是哪个设备的哪路通道。SpringBoot业务层需要维护一个预览会话表记录字段说明streamId唯一流IDdeviceId设备编码channelNo通道号rtpPortZLM分配的RTP接收端口playUrl生成的HLS播放地址startTime预览开始时间预览结束后调用ZLM的closeRtpServer关闭端口同时给设备下发停止预览指令。如果不关设备会一直推流浪费带宽也占ZLM资源。再强调一下这个步骤最容易漏很多线上事故就是预览会话泄漏导致ZLM端口耗尽。5. Vue前端播放与组件封装5.1 播放方案对比HLS、FLV、WebRTC前端播放视频流有很多方案我列一下实际体验HLSm3u8兼容性最好浏览器原生支持和hls.js兜底延迟大概3到10秒部署简单。适合预览场景也是目前最主流的“免安装播放器”方案。FLVHTTP-FLV需要flv.js库延迟能压到1到3秒但移动端H5兼容性一般对网络丢包更敏感。WebRTC延迟最低500毫秒内但ZLM的WebRTC播放需要额外信令服务前端联调工作量更大。考虑到标题场景是浏览器Vue播放m3u8而且大多数监控预览并不要求极低延迟HLS是性价比最高的方案。5.2 Vue视频播放器组件实现新建一个LivePlayer.vue组件封装hls.js的加载、播放和销毁逻辑。template video refvideoEl classvideo-player controls autoplay muted playsinline /video /template script import Hls from hls.js; export default { name: LivePlayer, props: { src: { type: String, required: true } }, data() { return { hls: null }; }, mounted() { this.initPlayer(); }, methods: { initPlayer() { const video this.$refs.videoEl; if (!this.src) return; if (Hls.isSupported()) { this.hls new Hls({ liveSyncDurationCount: 3, liveMaxLatencyDurationCount: 6 }); this.hls.loadSource(this.src); this.hls.attachMedia(video); this.hls.on(Hls.Events.ERROR, (event, data) { if (data.fatal) { this.$emit(play-error, data); } }); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生 HLS 支持 video.src this.src; video.play(); } } }, beforeDestroy() { if (this.hls) { this.hls.destroy(); this.hls null; } } }; /script几个细节说下muted属性必须加上浏览器自动播放策略不允许带声音自动播放不加的话画面会被浏览器拦截。playsinline属性是针对iOS Safari的加了这个视频才能在页面内联播放不会自动全屏。liveSyncDurationCount控制了HLS直播追帧的延迟值越小延迟越低但太小会导致频繁跳帧3是比较平衡的配置。5.3 断线重连与播放体验优化摄像头设备经常掉线重连HLS流也会因为设备网络波动短暂中断。前端做自动重连很有必要。基本思路是监听video的error事件和hls.js的fatal事件如果是网络错误就销毁当前实例等2秒重新加载播放地址。注意重连之前最好让父组件重新向后端要一次播放地址因为重新推流后streamId可能变了。tryReconnect() { this.$emit(reconnect-request); }多路视频同时预览时页面上可能挂十几个video标签建议用v-if控制组件的挂载和卸载切走就销毁回来再重建避免内存暴涨。hls.js实例非常占内存每路直播在120MB到220MB内存之间长期驻留很容易把前端页面拖垮。6. 常见问题排查与避坑实录6.1 设备侧注册问题现象1设备一直显示离线优先检查设备的ISUP配置服务器地址、端口、设备ID是否填对。很多设备默认服务器地址是web.hik-online.com这种要改成你们平台的公网地址或内网地址。然后看平台侧日志TCP连接是否建立注册报文是否收到。现象2注册成功但心跳不稳定检查设备网络环境如果是4G网络运营商NAT超时时间不同心跳周期要设备端配置合理一般90秒发一次平台端读超时可以放大到120秒更稳妥。另外防火墙别把7660端口拦了长连接端口一旦被断开回包就全断了。6.2 后端信令问题现象3Netty收到的报文解析乱码大概率是拆包器长度字段位置写错了。ISUP协议报文头里长度字段的偏移是固定的但不同文档版本可能不一致老EHOME和新ISUP就有差异。老老实实抓包把HEX数据对照文档一字节一字节比对。我踩过一次坑把4字节长读成2字节所有大报文全部解析失败查了大半天。现象4注册成功但预览指令无响应先确认设备通道号是否存在主码流还是子码流用错了。有些设备子码流分辨率低但码率更低适合公网传输。其次检查预览指令里媒体接收地址填的是不是平台能真正监听的地址如果设备在公网外千万别填127.0.0.1得填平台服务器的公网IP或者映射到公网的网卡IP。6.3 流媒体和前端播放问题现象5ZLM收不到ISUP回传的PS流用ZLM的getAllSession接口查看RTP会话是否存在。如果会话存在但码率一直是0问题出在设备没有把流发到这个端口或者源端口不匹配。可以用tcpdump在ZLM服务器上抓一下UDP端口判断数据有没有到达。现象6播放地址返回404m3u8文件还没生成。ZLM生成HLS切片需要一点时间通常收到码流后1到2秒可访问。如果一直404检查流ID是否和ZLM里实际注册的流ID一致不要拼错路径。现象7前端一直黑屏先单独用浏览器打开m3u8地址测试如果能播放说明后端和流媒体链路正常问题在前端。优先查浏览器是否自动播放被拦截加muted解决。另外hls.js版本别太旧建议使用1.x版本对HLS直播兼容性更好。6.4 一条重要的安全提醒视频流播放地址一定要加鉴权不要直接对外开放。ZLM本身支持通过控制接口设置播放鉴权或者你在SpringBoot层做一层代理播放地址用带有效期的token换取。摄像机画面属于敏感数据生产环境如果不做访问控制后果谁都担不起。7. 写在最后的经验与扩展想法这个项目从设备注册到浏览器播放整个链路走通差不多花了两周时间最耗时的不是写代码而是ISUP协议报文结构的确认和设备调试。我第一次对接时没有申请到完整协议文档全靠抓包反推消息头走了不少弯路。如果你们项目进度紧我的建议是先向海康官方申请ISUP对接文档同时找一台支持ISUP的实体设备进行抓包分析。前期把报文结构、设备ID规则、长度字段偏移确认清楚后面Netty解码和信令交互写起来会非常快。千万别上来就写代码协议都没搞清楚写出来的解码器大概率不能用。这个方案后续还可以扩展录像回放功能。ISUP协议本身支持文件回放指令设备可以回传录像文件流接入逻辑和实时预览类似。把收到的视频流在ZLM侧按需录像然后用MinIO做分片存储前端再做个时间轴就是一个完整的视频云平台雏形了。另外想多提一句ISUP和GB28181虽然都是安防设备接入协议但侧重点不同。GB28181是国家标准大部分厂商设备都支持适合多厂商混合场景ISUP是海康私有协议设备接入更轻量但可移植性差。做平台型产品最好两者都支持但第一次做的话先把ISUP打通再啃GB28181就轻松很多。如果你们公司有其他品牌的摄像头这套架构也能平滑扩展流媒体层不用动只需要新增一个适配器把对应协议的信令翻译成统一的流会话管理就行。这也是为什么我把协议接入和流媒体转发拆得很干净目的就是让模块之间能独立演进。
返回列表