ARTICLE DETAIL

资讯详情

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

Vue3接入海康视频Web插件:实现社会视频资源实时预览与多画面轮巡

Vue3接入海康视频Web插件:实现社会视频资源实时预览与多画面轮巡 做社会视频资源整合接入汇聚系统这个项目时我把大部分精力都压在了后端GB/T28181网关接入、设备编目、流媒体转发、录像存储觉得只要视频流能汇聚上来Web端实时预览就是顺手的事。真到联调那天我才发现自己想简单了。几十路社会侧点位商铺门口、园区出入口、桥洞卡口的视频汇聚上来之后浏览器端的实时预览成了整个验收环节里最难啃的骨头。用video标签放不了RTSP转成HLS延迟又高得离谱最后绕了一圈还是回到海康视频Web插件这条路。这篇是系列方案第五篇专门写这一层在Vue3项目里如何正确接入海康视频Web插件v1.5.5把社会视频资源的实时预览、分屏、轮巡这些能力稳稳落地。如果你正在搞视频接入、视频汇聚类项目前端正好是Vue3技术栈这篇应该能帮你省下至少一周的试错时间。1. 四期方案做下来为什么前端播放反而成了最容易被低估的环节先说个背景。整个社会视频资源整合接入汇聚系统大体上分四层最下面是前端感知层也就是各个社会单位自己装的摄像机、NVR、DVR中间是接入层通过GB/T28181、ONVIF这些协议把设备注册上来再往上是平台服务层负责设备管理、流媒体网关、转码分发、录像存储最上面才是用户真正天天面对的应用层包括Web端、移动端、大屏端。系列前面的内容基本把前三层讲完了。到了第五篇按理说应该讲应用层的各种业务功能比如实时预览、录像回放、云台控制、报警联动。但我想把Web端实时预览单独拎出来原因很简单在这个项目里它是最容易被技术负责人低估、却直接决定验收成败的模块。为什么这么说后端做得再好设备接入率再高甲方打开浏览器看到的仍然是黑屏或者画面卡成幻灯片前面所有工作都会被一笔勾销。而Web端实时预览恰恰是整个链路里环境最复杂的一环要面对浏览器对私有协议的限制、要处理多路并发带来的性能压力、要兼容不同版本的内核和插件环境、还要在项目交付现场解决各种莫名其妙的环境问题。更现实的是社会视频资源接入项目和纯自建监控系统不一样。纯自建系统用的是同一厂商的设备协议统一后端再做个流媒体网关前端接HLS或者WebRTC都舒服。但社会视频资源汇聚这个场景里前端的设备五花八门很多还是老旧NVR虽然通过国标GB/T28181统一注册上来了可到了最终展示环节通常还是按原有方式取流最稳。所以在方案设计阶段我就给自己定了个原则前端播放能力不作为项目最后阶段的锦上添花而是作为独立的子任务从第一天就并行推进。这篇的内容就是把这个子任务从选型到落地的完整过程拆给你看。2. 海康视频Web插件v1.5.5的接入前提与运行边界先说结论在目前的浏览器技术环境下选海康视频Web插件v1.5.5不是因为它完美而是因为它解决了一个核心问题——让浏览器页面能够低延迟地播放设备私有协议的视频流并保留完整的控制能力。2.1 为什么标准video标签搞不定RTSP做视频接入的人都知道浏览器原生不支持RTSP。你用video srcrtsp://192.168.1.64/...Chrome直接不认。这不是海康的问题是所有浏览器厂商的一致决定因为RTSP这类协议又老又复杂还有严重的端口安全隐患。那为什么项目里不能全链路走标准流媒体协议比如让后端统一把RTSP转成HLS或者HTTP-FLV能但有代价。我把当时的方案对比整理成了下面这张表播放方案延迟表现部署复杂度控制能力适用定位HLS(ts/m3u8)3~10秒切片越大延迟越高低后端转封装即可弱只能播放录像回放、监控墙不严苛场景HTTP-FLV1~3秒中需要合适播放器弱有一定实时要求的预览WebRTC0.2~1秒高后端需完整信令与媒体协商弱对延迟极端敏感的场景厂商Web插件0.2~0.5秒中需客户端安装插件强支持云台/对讲/抓图社会视频资源汇聚项目主力方案这张表看下来WebRTC理论最好但在社会视频资源项目里它要把大量异构设备统一转成WebRTC流后端工程量非常大。插件方案看似老土实际延迟最低、控制能力最强而且是海康设备原生支持的方式省去了转码网关的压力。2.2 插件的运行架构页面、本地服务、流媒体网关三者的关系海康视频Web插件v1.5.5不是纯JavaScript库它的本质是一个本地代理服务。页面上引入的SDK只是信令end真正的解码和渲染工作是插件安装时写入操作系统的本地服务完成的。大致流程是这样的安装插件后Windows上会注册一个本地服务监听固定端口段比如15900到15909。浏览器页面通过SDK的JS方法向本地服务发送命令登录设备/平台、开始预览、停止预览。本地服务收到命令后向指定的设备或流媒体网关发起取流解码后把画面渲染到页面上指定的div容器里。页面本身不接触视频流只负责显示渲染区域和处理控制命令。理解了这套架构才能理解为什么插件的调试方式和普通前端库完全不同。它依赖本地服务是否启动、端口是否被防火墙拦截、插件版本是否匹配这些在代码层面看不到的问题往往才是项目交付现场的大坑。2.3 版本与运行环境的边界v1.5.5这个版本号是海康视频Web插件里比较新的一代。在项目选型时我关注的不是最新而是可用。几个运行边界必须提前说清楚操作系统目前主要支持Windows平台。生产环境如果用户用macOS或者Linux访问插件方案直接不可用需要后端提供替代方案比如HLS降级。浏览器Chrome和Edge的较新版本基本都能跑但要求页面必须通过http://协议访问不能直接用file://打开。内核位数需要注意浏览器本身是64位还是32位插件服务会不会被安全软件拦截这些属于交付现场的高频问题。安装包与SDK版本要一致项目里如果前端用的是1.5.5的SDK客户端必须安装对应版本的插件否则初始化会报错或者直接找不到本地服务。这一点我后面还会反复强调。注意在方案评审时一定要把需要安装客户端插件这个前提明确写进需求文档。很多甲方默认浏览器打开就能看到了验收才发现还要装插件如果早说清楚后面会省掉大量沟通成本。3. Vue3工程里引入插件SDK的完整过程接入方式选择正确后面能少踩一半的坑。我在接入海康视频Web插件v1.5.5时踩过把SDK当普通npm模块的坑这里把正确姿势完整写出来。3.1 把SDK当原生JS库而不是ES模块来处理拿到海康的Web开发包里面通常包含一个安装包exe、一个JS文件比如webcontrol.min.js或者webVideoCtrl.js、一份开发文档、若干示例页面。很多第一次接入的人会试图把JS文件用import方式引入Vue3工程。结果大概率遇到两个问题一是SDK内部用了不少浏览器全局对象webpack或Vite打包时要么报错、要么在严格模式下行为异常二是SDK维护了自己内部的全局状态被模块化之后容易出各种诡异问题。推荐做法是把SDK的JS文件放到public目录在index.html里用传统script标签全局引入。这样做的理由SDK与业务代码在生命周期上解耦插件加载失败只影响SDK相关功能不影响Vue应用本身。避免打包工具改动SDK内部代码最大程度保持其原始行为。全局变量方式也符合插件SDK的设计预期它就是给传统网页用的。以Vite工程为例目录结构大概是public/ hikvision/ webcontrol.min.js install-v1.5.5.exe index.html 中引入 script src/hikvision/webcontrol.min.js/script3.2 工程里的全局类型声明与初始化封装JS文件用script引入后window上会挂载SDK暴露的对象。在TypeScript项目里需要先声明全局类型否则window.WebControl直接报成员不存在。我在src/types/hikvision.d.ts里做了这样的声明export {}; declare global { interface Window { WebControl: any; WebVideoCtrl: any; } }有人会问为什么用any因为SDK的类型定义不一定和实际版本完全一致强行写完整类型反而增加维护成本。用any先跑通等封装成组件后再内部消化类型问题。初始化封装的思路是把创建插件实例和启动/关闭服务统一管理起来。我写了一个createPlugin.ts作为唯一SDK入口/** * 创建海康Web插件实例 * 注意基于 v1.5.5 开发包的常见写法具体方法名以实际SDK文档为准 */ export function createHikPlugin(containerId: string) { if (!window.WebControl) { throw new Error(海康Web插件SDK未加载请检查public目录下的js文件); } const plugin new window.WebControl({ szPluginContainerID: containerId, iServicePortStart: 15900, iServicePortEnd: 15909, // szClassId 是插件安装时注册的本地控件ID // 不同开发包/项目会不一样以包内文档或安装目录注册表为准 szClassId: 填入你SDK包里的ClassId, iProtocolVersion: 2, bBUpdate: false, }); return plugin; }这里有一个容易卡住的点szClassId填什么。它不是自己编的而是跟随安装包走的。打开SDK开发包里的demo页面通常能找到示例里写好的这个值或者exe安装后用注册表编辑器查插件对应的ClassId。不同项目拿到的包可能不同绝对不要照抄网上别人分享的值。3.3 初始化失败的通用排查顺序插件初始化失败是接入期最高频的问题我在项目里遇到过不下三次。按照下面的顺序排查基本能覆盖绝大多数情况确认插件安装包已经安装成功且版本和SDK匹配。打开任务管理器看本地服务进程是否在跑。用浏览器直接访问http://localhost:15900之类的端口确认端口服务通。确认页面是通过http://localhost或者http://局域网IP访问的file://协议基本不行。在DevTools控制台执行window.WebControl确认SDK对象真的挂载上来了。查看网络面板确认webcontrol.min.js文件非404、非缓存旧版本。经验之谈项目现场如果换了一台电脑就初始化失败90%是插件没装或者版本不对记住一个动作——把SDK包里自带的demo页面拷过去直接双击打开测试。demo能跑说明插件环境正常问题出在你的工程demo也跑不了问题在插件安装环境别在业务代码里瞎找。4. 从零封装一个可以复用的VideoViewer播放组件接入SDK只是第一步。要让业务开发同学不关心底层插件逻辑把播放能力当成普通组件用还需要封装。4.1 组件对外暴露的Props与事件设计封装组件之前先想清楚一个播放容器到底要暴露什么。我最终给的Props设计如下interface VideoViewerProps { deviceId: string; // 设备编码比如 34020000001320000001 channelNo: number; // 通道号 streamType?: main | sub; // 主码流/子码流 mode?: real | playback; // 实时预览/录像回放 userId?: string; // 平台认证登录后的会话标识 width?: string; // 容器宽度 height?: string; // 容器高度 autoPlay?: boolean; // 挂载后自动开始播放 }事件上对外暴露三件事就够了start播放成功后触发带当前设备信息。stop播放停止后触发。error任何异常统一走这个事件组件内不吞错误。这个设计看起来简单但有个很重要的取舍不要把插件登录和设备播放混在一个组件里。插件的登录态和平台登录态并不是一回事账号管理、会话刷新应该由更上一层的服务去维护播放组件只负责拿到登录态之后把画面放出来。4.2 挂载、预览、释放三个关键时机的代码落地组件核心代码我用Vue3的组合式API来写三个关键生命周期时机非常重要。第一个时机onMounted。插件SDK要求容器真实存在于DOM里才能初始化。在onMounted里拿到ref对应的DOM节点再创建插件实例、初始化插件窗口template div refcontainerRef classvideo-viewer !-- 播放区域由插件创建 -- /div /template script setup langts import { onMounted, onUnmounted, ref, markRaw } from vue; const props withDefaults(definePropsVideoViewerProps(), { streamType: sub, mode: real, autoPlay: true, }); const emit defineEmits([start, stop, error]); const containerRef refHTMLDivElement(); let pluginInstance: any null; onMounted(async () { try { // 注意插件实例不要放进 reactive 或 ref它是复杂对象 // 用 markRaw 或普通模块变量保存即可 pluginInstance markRaw(await initPlugin(containerRef.value)); await pluginStartPreview(); emit(start, { deviceId: props.deviceId }); } catch (error) { emit(error, error); } }); /script第二个时机播放前的登录与会话准备。预览之前组件内部要保证插件已经登录了设备或平台。但如上所述登录态应该从外部统一管理。实际项目中我是这样处理的组件只接收userId在pluginStartPreview内部判断当前插件是否已经登录如果没登录就调用SDK的登录方法传入登录参数。这块代码不展开所有实现说一个关键点SDK的登录和预览都是异步回调风格不是Promise。我封装时统一包了一层Promise避免业务代码里出现回调地狱function pluginStartPreview(): Promisevoid { return new Promise((resolve, reject) { pluginInstance.JS_Request({ szCommand: startPreview, deviceId: props.deviceId, channelNo: props.channelNo, streamType: props.streamType, success: () resolve(), error: (error: any) reject(error), }); }); }第三个时机onUnmounted。这一步是项目上线前内存不断增长的最常见根源。组件卸载时必须先停止预览再关闭窗口最后停止本地服务。顺序错了插件本地服务的句柄可能不会被释放反复切换页面后内存就被吃光onUnmounted(() { try { emit(stop, { deviceId: props.deviceId }); pluginInstance?.JS_Request({ szCommand: stopPreview, deviceId: props.deviceId, channelNo: props.channelNo, }); pluginInstance?.JS_CloseWindow?.(); } catch (error) { console.warn(插件释放异常, error); } });4.3 预览切换与回放模式的扩展封装组件时我额外做了两个扩展项目里非常常用。预览切换设备A切到设备B不能直接带着旧句柄去播放新设备。正确做法是先停止当前预览再用新参数重新startPreview。如果漏掉停止这步画面上很可能出现两个画面叠影或者花屏。回放模式回放和实时预览的差异主要在请求参数上。实时预览传设备编码和通道号回放要多传时间范围开始时间、结束时间。我在组件里通过mode字段区分底层SDK调用走不同的命令。这样业务层可以看到同一套组件传不同参数给不同能力不用为回放单独再写一套页面。5. 从单路预览到四宫格轮巡多实例并发的关键设计单路预览跑通后真正的挑战是多路并发。社会视频资源汇聚平台最常见的展示场景是一屏看多个点位还要按顺序自动轮巡。这部分如果设计不好页面很容易卡死或者浏览器直接崩溃。5.1 一个插件实例还是多个插件实例这个问题在项目里争论了很久。我的结论是优先用一个插件实例多窗口显示。原因有三点插件本地服务本身是独立进程每创建一个插件实例就多一份本地服务负担成本非常高。大多数SDK的设计就是单实例多窗口多个实例互相之间还可能抢占资源。业务侧的轮巡、分屏逻辑本质是在不同窗口里播放不同设备和实例数量无关。所以我把VideoViewer组件做成了两层底层是PluginContainer一个页面全局只有一个负责管理插件实例和所有窗口上层才是业务看到的播放组件向底层申请一个窗口。5.2 多窗口的调度和复用逻辑多窗口需要一个调度器。我设计了几个核心的数据结构来管理interface PreviewWindow { index: number; // 窗口索引 deviceId: string; channelNo: number; status: idle | playing | stopped; containerId: string; }四宫格场景窗口索引就是0、1、2、3。调度器的职责是某个业务组件申请播放时找到当前空闲的最小索引窗口。没有空闲窗口时抛出窗口已满的错误由业务层决定是排队等待还是提示用户。某窗口停止播放后立即回收索引。实际代码里调度器可以做成一个简单的单例模块不用Pinia因为插件实例不是普通响应式对象放Pinia会触发深度代理导致性能问题甚至报错class PreviewScheduler { private windows: PreviewWindow[] []; acquire(): PreviewWindow | null { const idle this.windows.find((w) w.status idle); return idle ? idle : null; } release(index: number) { const win this.windows[index]; if (win) win.status idle; } }这里有一个很实用的细节多窗口切换时窗口的div容器不需要销毁重建只需要把SDK的播放对象指向新的div就行。频繁销毁重建DOM会带来明显的闪烁和卡顿而通过调度器复用窗口切换过程非常平滑。5.3 自动轮巡与资源控制轮巡功能本质是个定时器每隔N秒把所有窗口的设备编号做一次整体平移。比如四宫格显示设备1、2、3、45秒后整体变成设备5、6、7、8再5秒后变成9、10、11、12。实现这种效果有几个经验不要在JS的setInterval里直接对每个窗口挨个停止再播放。这样会产生瞬时的高峰请求插件本地服务经常被打懵。我采用的是错峰切换同一时间只切换一个窗口间隔200毫秒再切下一个。页面切换到后台时轮巡应该暂停。用document.visibilitychange事件判断页面隐藏时清掉定时器回到前台再恢复。否则浏览器后台标签页会限制定时器精度导致画面不同步。主码流和子码流要分流。四宫格预览时强制用子码流只有点开单窗口全屏预览时才切换主码流。这不只是节约带宽更是保护客户端电脑的解码性能。四路主码流同时解码一般办公电脑的CPU会直接拉满。6. 黑屏、内存增长、断流重连上线前的关键排错记录最后分享几个上线前必须排掉的坑。这几个问题在项目任何阶段都可能遇到而且比重很高。6.1 黑屏的完整排查链路黑屏是所有播放问题里最让人头疼的因为原因太多了。我整理了一个排查链路每次遇到黑屏都照这个顺序走排查步骤判断标准处理方式1. 插件SDK是否加载window.WebControl是否存在重新放置JS文件并刷新2. 本地服务是否启动任务管理器里能否看到服务进程、端口是否可访问重新安装插件或手动启动服务3. 容器div是否有宽高DevTools检查元素尺寸给div设置明确宽度高度4. 登录态是否有效调用SDK查询登录状态重新登录刷新会话5. 播放参数是否正常设备编码、通道号能否在IE demo里播放用demo验证设备本身是否可取流6. 网络是否打通页面所在电脑到设备/平台网络是否通排查防火墙、路由这个表里最有价值的其实是第4步。在社会视频资源项目里平台侧的登录会话通常有时效前端一直开着页面不动会话过期之后再去播放就是黑屏。平台侧做了自动踢出时客户也最容易遇到这种情况。6.2 组件销毁不干净导致的内存增长这个坑遇到过不止一次症状非常典型页面反复进入退出内存持续上涨最终浏览器崩溃。根本原因是插件本地服务的窗口句柄没有被释放。Vue3组件的onUnmounted钩子里如果只做了emit(stop)而没有真正调用SDK的关闭窗口方法本地服务会一直认为窗口还开着。排错建议是写一个独立的插件自检页页面里只做三件事初始化、开一路预览、关页面。反复操作十几次观察任务管理器里内存变化。能稳定复现增长说明释放逻辑有问题如果自检页正常再去排查业务代码里是不是忘了解绑事件或者清了定时器。另外有一个细节如果项目用了Vue的KeepAlive缓存组件onUnmounted根本不会触发要改用onDeactivated来停止预览。这是我当时在路由缓存场景下踩过的一个隐蔽坑。6.3 断流重连和会话过期处理网络抖动、设备重启、平台重启都会导致预览中断。项目验收时最怕这个画面看着看着就断了手动刷新页面又恢复甲方会觉得系统不可靠。处理思路分两步第一步监听SDK暴露的断开事件。插件在取流中断时通常会触发类似的onError或者onDisconnect回调。我在封装层统一拦截转成组件的error事件往外抛。第二步实现有限次数自动重连。不要无限重连否则设备离线时会一个劲儿刷请求把平台打崩。我用的是指数退避策略let retryCount 0; const MAX_RETRY 3; function handleDisconnect() { if (retryCount MAX_RETRY) { emit(error, { message: 连接断开重连次数已用完 }); return; } retryCount 1; const delay Math.min(1000 * 2 ** retryCount, 15000); setTimeout(() { // 重新登录、重新预览 pluginStartPreview().catch(handleDisconnect); }, delay); }这里有个容易被忽略的点重连之前要重新做一遍停止旧播放、关闭旧窗口的操作。因为断流时插件里的旧窗口状态并不可靠不清理直接重开很容易在新窗口上叠加旧画面。这个坑我是吃过亏的。另一个关键点重连时如果会话过期要先做一次重新登录再开始预览。所以在封装层重连逻辑和登录逻辑是串在一起的不能只重播不管登录。项目实际交付阶段我还养成了一个习惯把上面这些排错结论整理成一页纸的《现场排错手册》让实施同事带着。每次遇到黑屏或断流先查手册再远程定位效率比在微信群里来回沟通高得多。这一步看着不起眼却是项目顺利验收的重要保障。最后再分享一个很实际的心得视频插件这一类东西版本一旦定下来项目周期内就不要轻易升级。海康视频Web插件v1.5.5在当前阶段完全够用但中间如果因为某个新功能贸然升级版本很可能连带影响SDK的API调用方式和客户端的安装包回归测试成本非常高。踩过几次坑之后你会发现稳定压倒一切前端接入的活儿能少变就少变把时间留给真正影响业务的功能优化上去。
返回列表