尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VideoLineForJS:纯JS视频回放轴组件设计与海康设备对接实践

VideoLineForJS:纯JS视频回放轴组件设计与海康设备对接实践 简介面向海康威视等视频平台回放场景此资源提供一款基于JavaScript的VideoLineForJS视频轴组件适合Web前端开发者快速集成时间轴选择与视频回放联动功能。组件通过简洁的API回调将选中时间点返回给页面示例代码演示了起止时间段的构建方式便于理解核心用法。包体共7个文件、压缩后155KB包含2个JavaScript脚本、1个示例HTML页面、1个GIF演示动图、1个Markdown说明文档以及工程配置文件xml/iml轻量易部署可直接在浏览器中运行体验交互效果。该组件尤其适合在安防监控、录像回放等业务中实现按时间轴拖拽定位。目前已有393人学习下载对于需要快速为视频系统增加时间轴交互的前端开发者这套资源省去了自行封装复杂时间计算与渲染逻辑的麻烦从演示、源码到文档一应俱全可帮助缩短开发调试周期。 做安防视频接入这两年的朋友大概率碰到过这样一个需求要在网页里做一个类似监控大屏的录像回看界面中间是播放器下方是一条时间轴能显示哪段时间有录像、哪段时间是空白用户可以直接在轴上点选时间、框选时段拖动进度时画面跟着走。这类组件在C/S客户端里很常见比如海康的iVMS、大华的SmartPSS但到了B/S项目里靠谱的开源方案其实不多。VideoLineForJS 就是针对这个场景做的一个纯JS视频回放轴组件专门接海康这类监控设备的数据源解决网页端录像检索与回放联动的问题。这篇文章我把自己从选型、设计到实现对接的完整思路和踩坑记录整理出来给要接手类似需求的同学一个可参考的路线。1. 为什么需要一个“视频回放轴”需求背后的真实痛点1.1 安防B/S项目里的录像回看之痛早几年做安防平台的网页端录像回看基本是三条路一是直接跳转到海康iVMS这样的桌面客户端二是用VLC插件在浏览器里播RTSP流三是在页面上做一个简单的录像文件表格用户找到时间段再点播放。前两种方式在Chrome逐步禁掉插件后越来越难用第三种方式的交互又很反人类——用户要看“今天凌晨3点到4点有没有人经过”得先翻列表找文件再手动猜测哪一段是目标录像效率低到被客户投诉。真正让时间轴组件变成刚需的是“录像片段可视化”这个概念。监控设备的录像并不是7x24小时连续的——常态下是定时录像有人或车触发时才有移动侦测录像有些点位还有报警录像。这些片段零零散散分布在一天里用户关心的其实不是文件列表而是“哪个时段有录像”。只有把时间轴画出来用色块标出有录像的区间用户才能一眼判断该看哪里。1.2 功能清单与使用场景具体到VideoLineForJS这个组件我当时给它定义的核心功能有四块多通道切换一台NVR后面挂着十几路摄像头时间轴要跟着通道切换而变化。录像片段可视化把某个通道某天的录像片段按时间画在轴上定时、移动侦测、报警用不同颜色区分。时间点与时间段选择单击确定播放时间点拖拽框选确定导出或下载的时间段。与播放器联动点击时间轴后播放器自动跳转到对应时间并开始回放。至于谁会用到这些东西我接触下来有三类人一类是做园区、工地、连锁门店安防集成的项目组需要把海康设备能力嵌入到自己平台一类是做VMS视频管理平台产品的前端需要替代老的C/S客户端交互还有一类是接国标平台的项目平台侧拿到的录像文件列表最终也要用时间轴展示。说白了只要你的页面里要“看录像”就绕不开这条轴。1.3 为什么不用现成的视频播放器插件选型阶段肯定有人问不用VLC、不用video.js、不用现成的Web播放器为什么自己写一个轴这个问题的答案很简单——播放器插件解决的是“视频怎么播”而我要解决的是“录像怎么找”。像jessibuca、wsPlayer这些播放器通常自带一个极简进度条但那个进度条只能显示当前播放进度既不知道哪些时间段有录像也不支持多通道切换和片段类型标识更没法按需缩放。所以播放器归播放器回放轴必须单独做。VideoLineForJS从设计上就不依赖任何播放器实现它只维护“时间状态”再通过事件把时间变化通知给播放器这样两边互不绑架换播放器也不用改轴。2. 整体设计数据结构、坐标换算与渲染选型2.1 录像片段的数据模型时间轴最常见的数据来源是设备侧的录像检索接口返回的文件列表。不管底层是海康ISAPI、OpenAPI还是国标平台最终拿到的核心信息都差不多通道号这串录像属于哪个摄像头。开始时间、结束时间录像片段的起止点。录像类型0表示定时录像1是移动侦测2是报警录像不同设备厂商定义可能略有差异。我在组件里用一个很薄的对象去接收这些数据不对厂商格式做任何假设{ channelId: 1, startTime: 1704067200000, endTime: 1704067800000, type: 1, // 0定时 1移动侦测 2报警 fileName: ch01_20240101000000_20240101003000.mp4 }注意startTime和endTime一律用Unix毫秒时间戳不要用字符串。原因后面会单独说——时区问题在录像时间这块非常坑。2.2 时间到像素的坐标换算时间轴本质上是一把带刻度的标尺核心算法就是“时间转x坐标”和“x坐标转时间”。换算公式很朴素// 时间转像素 function timeToX(timeMs, viewStartMs, pixelsPerMs) { return (timeMs - viewStartMs) * pixelsPerMs; } // 像素转时间 function xToTime(x, viewStartMs, pixelsPerMs) { return viewStartMs x / pixelsPerMs; }关键在于pixelsPerMs这个值。把它设计成“当前视图覆盖的总时长”和“画布宽度”之间的关系会更直观function calcScale(viewDurationMs, canvasWidth) { return canvasWidth / viewDurationMs; // 单位换算成 像素/毫秒 }显式地持有当前视图起点和总时长这两个状态而不是存一个裸的scale值这样实现拖拽平移时就特别简单——拖拽只是改了viewStartMs滚轮缩放则是改viewDurationMs并保持鼠标位置下的时间点不动。2.3 渲染层选型Canvas为主体DOM做辅助做时间轴渲染很多人的第一反应是用DOM——div套div色块就是一堆带背景色的div。但等到一个通道一天下来有几百上千个录像片段时DOM节点的数量会让页面卡到怀疑人生。我在实测中见过有人用DOM渲染一天的移动侦测录像一个摄像头生成了4000多个短片段浏览器直接卡成幻灯片。所以VideoLineForJS的核心画布我用的是Canvas。Canvas对几千个矩形绘制的性能完全没压力而且缩放在Canvas里就是重新绘制一遍不需要处理DOM的绝对定位和层级关系。但是Canvas上的元素天然不支持鼠标事件所以“点和拖拽”的交互热区用DOM来做一个透明覆盖层或者在Canvas的click事件里做坐标命中检测。我采用的是后者——Canvas监听pointer事件然后通过坐标反算时间再遍历当前可见片段判断是否命中这样省掉一层DOM结构。2.4 只绘制可见区域这里还有一个性能关键点不管设备侧返回了多少录像片段绘制时永远只遍历“当前视图范围内”的数据。比如用户把视图缩放到只显示上午10点到10点半那程序只需要从数据里筛出这个区间内的片段而不是把全天2000个块全画一遍。配合ViewStart和ViewDuration两个状态每次渲染先做一次区间过滤再做绘制性能基本就是常数级别和总数据量无关。这也是整个组件能保持流畅最核心的一条设计原则。3. 核心功能实现从刻度到拖拽的逐个击破3.1 自适应刻度标尺刻度是时间轴的门面。画刻度时的关键问题不是“画多少条”而是“根据当前缩放级别决定画什么精度”。如果视图范围大画分钟刻度会密到挤成一团如果视图范围小画小时刻度又太稀疏。我的做法是维护一个刻度等级表先根据当前像素密度算出“步长至少要多宽”再去匹配合适的刻度等级const LEVELS [ { stepMs: 10 * 60 * 1000, labelFormat: HH:mm }, // 10分钟 { stepMs: 30 * 60 * 1000, labelFormat: HH:mm }, // 30分钟 { stepMs: 60 * 60 * 1000, labelFormat: HH:mm }, // 1小时 { stepMs: 6 * 60 * 60 * 1000, labelFormat: MM-DD HH:mm } // 6小时 ]; function findLevel(pixelsPerMs) { const minWidth 80; // 两条刻度之间至少间隔80像素 return LEVELS.find(level level.stepMs * pixelsPerMs minWidth); }刻度线和主刻度上的文字分开绘制主刻度画得长一点只给整点画文字次级刻度画短竖线不做label。这样视觉干净渲染成本也低。3.2 录像块的绘制与重叠处理录像块是时间轴信息密度最高的部分。绘制逻辑本身不复杂——拿到片段的开始和结束时间算成x1和x2再画一个矩形。但两个问题必须处理第一个是“短片段合并”。移动侦测场景下设备经常产生一些几秒钟的短片段比如一片树叶在风里晃来晃去画面变化就触发一段5秒录像导致时间轴上出现一片密密麻麻的细线。我的策略是小于30秒的片段在渲染时按30秒最小宽度显示并且相邻间隔小于等于15秒的片段直接合并成一个块。这个策略做出来视觉上毛刺感大幅减少用户也不会被几百条细线吓到。第二个是“多类型重叠”。一个时刻可能同时存在定时录像和移动侦测录像如果你先把定时录像画满全天再把侦测录像覆盖上去后面画上去的会把前面的颜色盖掉。处理方式是分两到三行绘制每行只画一种类型的片段行高设置成总彩条高度除以类型数。时间轴横向信息不变纵向用行数表达类型差异交互时通过点击判断命中了哪一行。3.3 交互拖拽、框选、滚轮缩放和播放入口交互部分用PointerEvent统一处理鼠标和触屏比单独绑mouse事件省不少力气。整个画布的状态机其实很简洁pointerdown后移动超过5像素进入“拖拽框选”模式不移动超过5像素则表现为“单击选中”。拖拽框选结束后把选中区域高亮显示同时触发一个timeRangeSelected事件外部可以用这个时间范围去下载录像或截图。双击则视为“快速定位”把播放头移动到该位置并开始回放。滚轮或触屏双指缩放时以鼠标当前点为锚点保持那个时间点在新视图中的位置不变。这部分最需要注意的其实是事件冲突。比如滚轮缩放和页面滚动、拖拽框选和播放器拖拽进度条这些事件经常打架。我最后统一做了事件判定只有当鼠标落在时间轴画布区域内时才接管滚轮事件其他情况下滚轮行为保持浏览器默认。节流方面拖拽和缩放的回调用requestAnimationFrame来驱动事件处理函数里只更新状态真正的绘制都丢给渲染循环避免高频触发导致的重复计算。3.4 与播放器联动的消息协议回放轴最终要解决的是“让播放器跳转到某个时间”。组件里不直接操作任何播放器实例只触发一个统一格式的时间跳转事件// 轴组件对外事件 onPlayheadChange({ channelId, startTime, playMode: normal });播放器侧拿到这个事件后解析RTSP或按需请求录像分发服务跳到对应时间点。反向的联动同样存在——播放器在播放过程中会持续更新时间进度轴上的播放头标记要跟着走。这个更新频率不需要太高每秒刷新4到5次播放头位置就够肉眼已经很平滑。频率过高反而是浪费性能容易给低配工控机上的浏览器造成不必要的压力。4. 与海康设备对接录像检索、取流与时间同步4.1 录像文件列表怎么拿VideoLineForJS本身不关心录像数据从哪来但实际项目里十有八九都是接海康设备。海康设备的录像检索最常用的是ISAPI协议通过HTTP POST一个检索请求到设备的管理端口通常80路径是/ISAPI/ContentMgmt/search。请求体是一个XML结构大致包含检索的通道号、时间段和录像类型条件响应里返回符合条件的录像文件列表。不同设备固件对返回字段的细节略有不同有的返回UTC时间有的返回本地时间命名也不完全一致这块强烈建议以设备实际返回为准。如果是在海康云眸、萤石这类云平台上做对接一般不直接打ISAPI而是走海康提供的OpenAPI通过accessToken换取录像文件列表。两种情况组件的接入方式都一样——把返回结果标准化成我之前讲的那套{channelId,startTime,endTime,type}结构喂给轴组件。4.2 RTSP取流与播放器选型录像列表拿到之后真正播放视频时海康设备最常用的取流方式是RTSP。海康IPC和NVR的RTSP地址格式一般是rtsp://用户名:密码IP地址:554/Streaming/Channels/101路径末尾的101表示第一通道的主码流102表示第一通道的子码流201是第二通道主码流依此类推。如果你的设备走的是GB/T 28181国标平台接入取流地址通常由平台侧动态生成需要从平台接口里拿。浏览器不能直接播RTSP所以网页端的播放器方案基本就是两种一种是后端做转流把RTSP转成WebRTC或HLS再交给浏览器播放另一种是在网页里集成wsPlayer这类支持传输层解析的播放器通过WebSocket接一个流媒体网关。这里组件要做的事情很单纯播放器跳转时需要知道目标时间对应的取流地址这个由业务侧根据实际方案拼接轴组件只负责把时间点交出去。4.3 时间同步录像偏移的罪魁祸首海康设备在录像检索时返回的时间是基于设备自身系统时钟的。如果设备时间和服务器时间不一致或者设备的时区设置和浏览器所在时区不一致时间轴上显示的录像块位置就会整体偏移。我踩过一个很典型的坑设备在北京时区返回的录像片段时间带的是UTC时间戳而我在前端直接按本地时间显示结果所有录像块整体往前偏移了8个小时。用户选晚上8点的录像实际跳到凌晨4点。排查半天才发现是时间基准不一致。处理方案是两条设备侧强制开启NTP时间同步让设备时间与服务器时间保持一致。前端统一用Unix毫秒时间戳在标准化接口返回数据时把字符串时间统一解析成时间戳。渲染和事件跳转全部基于时间戳计算只在标签上显示时按本地时区格式化。5. 高频问题排查速查表做这个组件的过程中我在不同项目里、不同浏览器和设备环境下遇到了不少问题挑几个最有代表性的列成表格现象可能原因解决办法录像块整体偏移几小时设备时区与前端时区不一致统一使用UTC时间戳设备开启NTP同步回放跳转后黑屏播放器无法定位到片段起点跳转时向播放器传入小一点的时间偏移提前100ms片段很多时Canvas绘制卡顿绘制了全部数据而未过滤可见区增加可见区域数据筛选逻辑只绘制视图范围内的块缩放时鼠标位置下的时间点“飘走”缩放锚点计算错误缩放前后保持锚点时间对应的x坐标不变拖拽框选和单击冲突没有移动距离阈值判断pointermove超过5像素才进入框选状态时间轴渲染正常但点击无响应Canvas上未绑定事件或坐标换算错误绑定事件时用getBoundingClientRect修正坐标偏移浏览器控制台报跨域错误播放器或检索接口跨域后端代理转发或由网关做CORS放行还有一个会被很多人忽略的小问题当设备返回的录像片段之间有几秒的“空洞”但时间轴上看起来像是连续的用户单击那个位置时播放器去seek结果老半天加载不出来。这是因为部分设备对录像的索引有时间粒度限制并不是你给出精确到毫秒的seek时间就能定位的。我的做法是在跳转时做一个容错把请求的播放时间向前调整0.5秒到1秒让设备落在上一个关键帧上播放出来的画面能很快追上目标时间。这种“往回退一点再放”的策略在对接真实设备时比强行精确seek靠谱得多。6. 集成后的心得与可扩展方向这套视频回放轴组件在几个园区项目里跑了半年多我自己总结下来最有价值的经验有三条。第一做这种组件的时候最忌讳的就是把组件和具体的业务服务耦死。VideoLineForJS对外只关心“给我一份标准化的录像片段列表”至于这份列表来自ISAPI、OpenAPI还是自己平台数据库组件完全不管。接入方只需要写一个适配层把后端返回的各厂商数据转成统一结构。这个思路也让组件在后续接大华、宇视设备时几乎零成本——换的只是一层适配代码核心的时间轴交互逻辑完全不用动。第二时间轴这种交互密集型组件状态管理一定要集中。我见过不少失败的做法是把时间、缩放级别、选中片段分散在各种事件闭包里改一个地方要连带改三个函数。正确的做法是让组件内部维护一份唯一的状态对象所有交互事件都通过“更新状态 - 重绘”这个单向流程走出问题时也好排查。第三别把所有录像类型都硬编码在组件里。不同项目有不同录像分类有的还有故障录像、手动录像、重点标记等类型。组件做成“类型与颜色映射表”可配置的甚至支持接入方传入自定义图标和标签文案这样在不同客户的定制需求里复用时不用反复改组件源码。后续想扩展的方向可以有很多支持多通道同屏对比时的时间轴同轴联动、移动端触屏手势的进一步优化、接入AI事件标签在轴上显示人形/车辆触发的标记点都是把这条时间轴从“够用”推向“好用”的路子。但核心的架构定好了扩展不过是加功能而已。我自己在实际项目里最满意的一点是这个组件从设计之初就没有依赖框架现在不管接到Vue2、Vue3还是React的项目里都是拿过去就能用省了非常大的维护精力。本文还有配套的精品资源点击获取
返回列表