ARTICLE DETAIL

资讯详情

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

自研VideoLineForJS:解决海康威视视频回放时间轴痛点

自研VideoLineForJS:解决海康威视视频回放时间轴痛点 简介VideoLineForJS是一套基于JavaScript的视频轴视频回放轴实现方案主要面向海康威视等监控视频播放场景帮助前端开发者快速在页面中集成时间轴选择、回放时段标记与进度回调能力。压缩包共7个文件整体仅155KB以2个核心JS脚本为主含jQuery依赖与时间轴逻辑并配有HTML示例页、GIF动态演示、Markdown说明文档及IDE配置结构轻量清晰。通过初始化VideoLine实例并传入时间回调函数即可把输出时间按“yyyy-MM-dd HH:mm:ss”格式展现示例中还给出了多段起止时间数据方便理解片段回放区间如何控制。开发者可直接参考示例页调整参数或抽取脚本嵌入现有监控平台快速实现具备海康风格交互的视频回放轴。资源已有393人学习适合具备一定JavaScript基础、正在搭建监控系统或视频回放模块的开发者参考使用。 做海康威视视频回放功能的时候我一开始真没打算自己写一个视频轴。官方Demo里那条进度条虽然简陋但努努力也能拖动定位。直到产品连续提了三个需求24小时录像要在同一画面上随意缩放、报警事件必须在时间轴上打点、多路摄像头要共用一个时间轴做联动回放——原生的那个控件直接做不了。被逼无奈之下我自己写了 VideoLineForJS一个纯JS实现的视频回放轴组件核心就是解决海康这类设备在Web端回放场景下的时间轴交互问题。这篇文章把整个组件的设计思路、刻度算法、还有跟海康播放器对接的完整过程整理出来正在做监控平台Web端、被时间轴折磨过的朋友可以参考。1. 先说清楚海康威视原生时间轴为什么让我决定自己写在动手之前我先花了两个晚上确认了一件事不是我不会用是原生时间轴真的做不到业务要的效果。搞清楚这个问题很关键否则很容易陷入官方没做好我也不敢随便动的惯性思维里。1.1 原生方案不是不能看是没法用海康的设备端和Web开发包迭代了很多版本从早期的WebControl插件到现在的无插件H5播放方案。平心而论这些东西在视频播放本身做得是稳的但播放器自带的时间轴控件完全不是同一个水准差在四个地方没有缩放能力。这是最致命的。安防回放的特点是时间跨度极大用户可能前一秒在定位某个具体事件下一秒就要拉到昨天同一时段做对比。原生进度条的固定比例让精确找点和全局浏览变成了两件互相矛盾的事要么拖半天才能到目标位置要么拖过头还得来回微调。样式和交互深度绑死。原生产物的颜色、高度、按钮布局都是写死的想融入自己平台的UI体系基本不可能。监控平台的客户通常对界面要求不低一个风格突兀的进度条摆在回放页面里验收阶段就会被打回来。业务数据叠加不进去。真实监控回放页面里时间轴不只是显示现在几点它还要显示报警记录、门禁事件、移动侦测时间点、设备离线区间这些业务信息。原生时间轴没有这种数据层的概念想在这个基础上扩展等于在别人的毛坯房里改承重墙。和业务系统联动困难。回放页面往往同时有事件列表、通道列表、录像片段列表。点击列表里的一条报警要驱动时间轴跳到对应位置反过来拖动时间轴要反过来更新列表高亮。原生组件没有设计这种外部事件入口和内部状态回吐机制。一句话总结海康做的是播放器时间轴只是播放器的附属品而我们的业务需要的是一个回放交互中枢这两者定位完全不同。1.2 安防业务里时间轴承担的不只是拖一拖我把实际项目的场景拉出来列了一遍发现时间轴在回放业务里串联了几乎所有的交互逻辑事件定位回放用户在报警列表里看到一条记录点击后平台自动跳转时间轴到事件发生的时刻再控制播放器从该时刻开始回放。通道对比分析四路画面同时回放四个播放器要共享同一条时间轴拖动一次四路全部同一时刻跳转方便对比不同角度的现场。录像时段感知用户要直观看到哪个时间段有录像、哪个没有不能拖到一段空白时间才发现画面是黑的。范围选择与导出框选一段时间段把录像导出交给后端任务处理这个交互天然发生在时间轴上。这些场景没一个能被拖一拖进度条满足。所以我决定做一个独立的时间轴组件把算法、渲染、交互从业务里抽出来同时保留足够的事件钩子让业务自由接入。2. 组件边界与渲染选型VideoLineForJS 动工前的三个关键决定写代码之前有三个决定直接影响了组件后续的走向组件边界划在哪、用Canvas还是DOM渲染、内部怎么分层。我把这三个问题想清楚了才开始写后面基本没推倒重来。2.1 只做时间轴不碰播放器这是最重要的一个决定。VideoLineForJS 从一开始就明确组件内部不直接管理任何播放器实例不管是海康的还是别的品牌的。原因很简单。海康的Web接入形态太多了老项目还在用ActiveX插件新项目可能用官方H5无插件SDK还有些项目走的是GB28181国标平台或者第三方流媒体服务。如果组件内部和某一种播放器强耦合那每换一种接入方式时间轴就要跟着改一遍这违背了组件的复用价值。VideoLineForJS 对外只暴露几个时间相关的事件比如onTimeChange时间变化、onRangeChange可视范围变化、onSelect范围选择完成。至于时间变了之后要去调哪个播放器的哪个seek方法那完全是业务层的事情。我在项目里写过三种不同的适配代码分别对接海康插件、海康H5 SDK和国标平台底层时间轴组件一个字没改。这种解耦思路其实也降低了联调难度我先把时间轴组件用假数据跑通再单独调试播放器最后用一行事件订阅把它们串起来排查问题时边界非常清晰。2.2 Canvas 渲染还是 DOM 渲染我选 Canvas 的理由这是我犹豫最久的一个点因为两种方案各有一批拥护者。我把它们放在一起做了个对比对比维度DOM 渲染Canvas 渲染实现门槛低CSS绝对定位就能做稍高需要自己算坐标和绘制刻度密集时的性能元素数量多页面卡顿明显绘制过程轻量性能稳定事件命中天然支持点谁就是谁要自己做坐标换算和命中判断样式定制依赖CSS能力有边界几乎无限颜色、粗细、渐变都能画文字清晰度高浏览器渲染注意设备像素比否则会模糊我最后选了Canvas。理由很直接视频轴的核心场景就是大跨度时间密集刻度比如24小时视图下按分钟显示刻度如果每个刻度都生成一个DOM节点那一屏就有上千个节点拖拽和缩放的时候必然卡。Canvas没有DOM节点数量的问题所有刻度都是一次绘制指令性能瓶颈只在绘制本身。组件里Canvas用于渲染主画布刻度、时间线、录像区间、进度块这块是高频重绘区域像自定义弹窗这种低频交互我还是用DOM浮层实现各取其长。2.3 内部按五层拆各管一摊组件内部没有搞复杂的架构只是按职责分了五层便于维护调度层维护当前可视时间范围、缩放级别、播放进度状态是所有计算的入口。算法层刻度间隔计算、时间戳与像素坐标换算、可见刻度索引计算纯函数方便单测。渲染层负责Canvas绘制从调度层和算法层拿结果画出来。交互层处理鼠标拖拽、滚轮缩放、触摸事件换算成时间变化后交给调度层。事件层向外部发布onTimeChange、onRangeChange、onSelect等回调同时接收外部传入的标记数据。这五层里最核心、最容易写错的就是算法层也是下一节要展开的重点。3. 刻度计算与坐标换算视频轴的心脏是这样跳动的如果整个组件是一辆车刻度算法就是发动机。它解决的核心问题是在任意时间跨度、任意像素宽度下算出刻度应该隔多远画一条以及时间戳和像素坐标怎么互相换算。这两件事搞透了时间轴就完成了一半。3.1 刻度分级先算间隔再决定画什么假设现在可视范围是1分钟轴的宽度是1200像素那么大概每秒钟有20个像素此时刻度精确到秒是合理的但如果可视范围切到24小时每秒的像素宽度约0.014像素这时候还按秒画刻度会画出一万多个刻度线叠在一起画出来也是一团黑。所以正确做法是先根据当前可视范围算出合适的刻度间隔再按间隔去画。我维护了一个候选间隔列表单位是秒const STEP_CANDIDATES [ 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 21600, 43200, 86400 ]; function pickStep(pixelsPerSecond, minPx 80) { const minSeconds minPx / pixelsPerSecond; return STEP_CANDIDATES.find(step step minSeconds) || 86400; }逻辑很简单先把每个刻度至少需要的像素宽度我用的80px换算成最小时间间隔然后到候选列表里找第一个不小于这个值的间隔。这里的80px是我调出来的经验值——在常规字号下一个刻度标签宽度大约50~70px留出20px以上的间距才不会显得拥挤。如果想要刻度更密可以把阈值降到60或50但视觉上会变挤。这个列表还有一个隐藏优点刻度间隔单调上升且覆盖从秒到天的所有常用粒度。因为候选值都是整数秒换算成秒级-分钟级-小时级-天级的标签后用户不会看到刻度顺序错乱的问题。选定间隔后还需要确定从哪个时间点开始对齐。为了让数字好看通常取可视范围起点向下取整到间隔的整数倍然后从这个对齐点开始循环加间隔直到超出可视范围function getTicks(rangeStart, rangeEnd, step) { const ticks []; let t Math.floor(rangeStart / step) * step; while (t rangeEnd) { ticks.push(t); t step; } return ticks; }3.2 时间戳与像素坐标的双向换算时间轴的本质是一个从时间戳到像素坐标的线性映射。我统一用毫秒时间戳处理内部逻辑不直接操作Date对象这样换算函数可以写得非常干净// 每秒对应的像素数 const pixelsPerSecond axisWidth / (rangeEnd - rangeStart); // 时间戳 - x坐标 function timeToX(t) { return (t - rangeStart) * pixelsPerSecond; } // x坐标 - 时间戳 function xToTime(x) { return rangeStart x / pixelsPerSecond; }很多第一次写时间轴的人会忽略一个问题组件内部的所有时间戳必须使用同一种基准并且明确是本地时间还是UTC时间。我在项目里吃过这个亏后面专门在4.2节讲海康对接时再展开。组件内部我固定用毫秒时间戳 本地时区语义也就是用户看到的刻度时间是他本地的时间换算时不调用toISOString()避免字符串格式化时被转成UTC。3.3 绘制时容易被忽略的细节有了刻度数组和坐标换算渲染本身其实很机械但有几个细节直接影响观感主刻度、次刻度分开画。默认做法是每个刻度都画一条短线每隔5个或者10个刻度画一条长线并附上时间文字。次刻度和主刻度用不同颜色和高度区分视觉层级立刻清晰。文字格式要跟随刻度间隔变化。间隔为秒时显示HH:mm:ss间隔为分时显示HH:mm间隔超过1小时还要在头部显示日期MM-DD HH:mm。如果跨度跨天光显示时间不显示日期用户很容易搞混。只遍历可见区间的刻度。一次绘制循环里不要从rangeStart遍历到rangeEnd而是先通过 Math.ceil 和 Math.floor 把可见区间的起止索引算出来只循环这一段。对于24小时视图这种刻度多的场景这个优化能少绘制几十甚至上百个刻度。Canvas高分屏模糊问题。默认Canvas在高DPI屏幕上会发虚需要在初始化时读取window.devicePixelRatio把Canvas的物理像素设为逻辑像素乘devicePixelRatio再用ctx.scale(dpr, dpr)缩放上下文。这个坑不处理发布到4K屏上刻度文字会糊得没法看。4. 对接海康回放的全链路seek、时区坑与状态同步时间轴本身跑通之后真正费劲的是跟海康播放器对接。这个环节的问题不在播放本身而在于时间语义的错位和状态同步的节奏控制。我把链路拆成三段来看播放形态、时间传参、状态回传。4.1 两种常见的海康Web回放接入形态根据项目里接触过的实际情况海康Web回放大体有两种接入形态老式Web控件方案。这种方案依赖浏览器插件ActiveX/WebPlugin常见于一些存量项目。接入后通过插件对象的方法发起回放传入设备编号、通道号、开始时间、结束时间拉起一个回放窗口。优点是能力全、稳定性经过多年验证缺点是需要插件环境部署和兼容性都是麻烦事。新版无插件H5方案。海康近几年的Web SDK支持纯H5拉流和回放通过WebSocket或者其他私有协议取流配合官方提供的JavaScript SDK操作播放器其中有播放、暂停、seek之类的接口。回放seek本质是重新定位到指定时间点并继续解码播放。两个形态的接口名在不同版本里会有变化我接触过的项目里老插件方案往往用StartPlayback这类命名新H5方案则是startPlayback配合time参数或者单独的seekToTime之类的方法。具体方法名要看你们引入的SDK版本以官方文档为准。这也是为什么VideoLineForJS坚持不碰播放器的原因——适配代码写在外层SDK升级了只改适配层就行。4.2 最容易翻车的8小时时区偏差这是一个能让第一次接入的人排查到怀疑人生的坑。海康的录像检索和回放定位接口很多要求时间参数是YYYY-MM-DD HH:mm:ss格式的本地时间字符串而不是Unix毫秒时间戳。问题出在前端常用的转换方法上// 错误示例这样会丢失8小时 const params { startTime: new Date(startTime).toISOString().slice(0, 19).replace(T, ) };toISOString()会把时间转成UTC标准时间字符串。中国在东八区北京时间比UTC快8小时所以转出来的字符串比本地时间整整少了8小时。举个例子用户点时间轴想定位到2024-06-01 10:00:00接口收到的却是2024-06-01 02:00:00回放的画面和预期完全不匹配。我提供的排查建议是先别怀疑播放器把传给接口的时间字符串和本地时间对着看一遍如果差了8小时基本都是时区转换问题。处理方式不复杂自己拼本地时间字符串function formatLocalTime(d) { const pad n String(n).padStart(2, 0); return ${d.getFullYear()}-${pad(d.getMonth() 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}; }除了时区还有一个容易忽略的点设备端存储的录像时间标识是设备本地时间服务器端的时间区间也是服务器本地时间。如果设备跨时区部署或者服务器时区和设备时区不一致按服务器时间直接去定位还是会偏。最稳妥的做法是统一约定一个时间标准我建议以所在平台的服务器时间为准回调给设备端时做一次时区差值修正这个处理一般在后端完成。4.3 播放器时间上报与拖动定位的状态协调时间轴和播放器对接本质是双向状态同步播放器 - 时间轴播放器在回放过程中持续上报当前播放位置。老插件方案通常有周期性的时间回调新H5方案也提供类似的进度事件。收到上报后时间轴要更新进度块的位置同时把当前时刻反映到刻度线上。时间轴 - 播放器用户拖动或点击时间轴定位到新的时间点触发播放器的seek操作。这两个方向如果做不好协调用户体验会非常割裂。我总结了一套状态机逻辑用户在时间轴上按住拖动时组件进入预览状态此时只更新UI上的目标时间线不触发seek。鼠标松开组件一次性触发onTimeChange业务层调用播放器seek到目标时间。播放器seek完成后开始上报新的播放位置时间轴进度块回归跟随播放状态。播放器暂停、播放、停止时时间轴要同步切换对应的状态显示。这种拖动时不打扰播放器松手再seek的策略是从实际教训里来的。之前做过一版拖动过程中实时seek的方案结果播放器卡得几乎没法用具体的排查过程放在5.2节细说。5. 实测性能优化与功能扩展从跑通到顶得上组件写完能跑只是第一步真正投入生产环境后遇到的全是性能问题和超出预期的业务需求。这一节挑几个典型案例讲讲。5.1 36小时录像跨度下的刻度渲染优化有一次客户要求时间轴默认显示36小时跨度同时要叠加录像区间数据。我的第一版在缩放结束的高频回调里直接重算刻度并重绘整个Canvas结果在低配电脑上明显卡顿缩放操作不跟手。定位问题时我用Performance面板抓了下发现每次缩放事件里做了太多无谓的计算。优化做了三件事控制计算时机。滚动缩放过程本身非常高频每秒可能触发几十次。我改成缩放过程中只更新缩放视觉反馈真正的刻度重算要等交互结束后比如mouseup或者连续缩放事件停止300ms后才执行。缓存刻度数组。同样的时间范围和缩放级别下刻度计算结果是一样的。我加了一层以rangeStart、rangeEnd、step为key的缓存二次渲染直接复用数组不重新跑循环。重绘前做可见性裁剪。36小时跨度下Candidate数组选出的step可能是30分钟一天就有48个主刻度两天就近100个加上次级刻度更多。Canvas重绘前先移除完全不落在可见区域的刻度减少无意义的绘制调用。优化之后在普通办公电脑上拖拽和缩放都保持流畅CPU占用明显下降。5.2 拖动时频繁seek把播放器拖死的解法这可能是对接海康回放时最典型的一个坑。早期版本里我监听时间轴的onTimeChange事件每次用户拖动都实时调用播放器seek。当时想着这样用户体验最连贯画面能跟着鼠标走。实际一测问题立刻暴露播放器seek一次的开销远超前端接口调用的消耗每秒触发十几次seek播放器直接进入假死状态画面卡住、声音断续、甚至一段时间后无响应。整个排查链路是这样的先怀疑是播放器实例有问题换了一路视频测试依旧再看网络请求发现回放网络流在频繁重建最后在播放器调用层加日志发现seek接口每次拖动都会触发十几次问题就出在频率上。最终方案就是我前面提到的状态机拖动过程中只管UI不触发任何seek鼠标松开后才触发一次seek。如果产品确实要求拖动时实时预览可以做降频处理比如1秒最多触发一次seek并且丢弃中间的多次拖动目标值只取最后一次。实际体验下来松手后seek的方式反而是用户最容易接受的——它符合定位再播放的直觉。5.3 录像存在性区间与事件标记的扩展VideoLineForJS跑了一段时间后最受欢迎的两个扩展功能都不是我一开始规划的完全是被业务推着做的。录像存在性区间。海康后端提供查询录像时间表的接口能拿到通道在某段时间内有录像的片段列表。我把这些片段转成了时间轴上的色带有录像显示为实心色带无录像显示为空白。这个功能极大改善了回放的确定性——用户一眼就能看出哪个时间段能放不用拖过去看黑屏再拖回来。实现上就是在Canvas绘制层增加了一个数据层先画色带再画刻度色带的坐标系跟刻度完全是同一套换算逻辑算好起止坐标直接fillRect即可。事件标记打点。把报警、门禁、移动侦测等事件按发生时间映射到时间轴上的垂直标记线不同类型用不同颜色点击标记可以弹出事件详情。事件数据量大时我先按step粒度做了一次归并同一个刻度区间内的多条事件在UI上合并成一个可展开的聚合标记避免标记线密密麻麻叠在一起。这两个扩展做下来我的一个体感是时间轴组件想要好用必须预留数据注入的通道。VideoLineForJS 对外提供了setVideoSegments和setEventMarks这样的数据入口业务层只需要把后端数据塞进来渲染层按统一坐标换算绘制组件内不关心数据来源。最后再分享一个从实战里悟出来的小技巧给时间轴初始化一个合理的默认时间范围通常取当前时刻往前推30分钟到1小时比默认显示一整天对用户友好得多也让刻度算法在最舒适的位置起步。VideoLineForJS 目前在我的项目里稳定跑了半年多接入过三种不同的播放器方案后续如果有时间我打算把它单独抽出来做成一个通用组件把这一路踩过的坑沉淀成文档让更多做监控平台的朋友少走弯路。本文还有配套的精品资源点击获取
返回列表