ARTICLE DETAIL

资讯详情

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

智能视频监控前端实战:行人摔倒检测系统的实时视频与告警联动

智能视频监控前端实战:行人摔倒检测系统的实时视频与告警联动 做了两三年的智能视频监控类项目前端说实话行人摔倒检测这种带AI算法的系统和传统只做视频回放的安防平台差别很大。传统平台你把视频流播出来、能回放、能PTZ控制就算完成但摔倒检测系统的前端核心难点变成了两件事一是实时视频流怎么低延迟高并发地展示二是算法产生的事件怎么及时准确地呈现给用户并形成业务闭环。这篇文章是系列的第二篇。上一篇讲了工程搭建、目录结构和前后端接口约定这次直接上干货聚焦实时视频流接入、告警事件联动、多路画面性能调优以及我在实际项目里踩过的那些坑。内容基于我负责的这套行人摔倒检测系统的前端实践涉及的技术方案不一定适用于所有场景但思路和排查路径是有共性的。这篇适合正在做或即将接手类似智能视频分析平台前端的朋友尤其是要把摄像头画面和算法结果串起来的那部分工作。1. 内容整体设计与思路拆解1.1 行人摔倒检测系统的前端到底要做什么很多人以为摔倒检测系统的前端就是放一个视频页面把摄像头画面嵌进去就完事了。实际根本不是。我梳理下来前端要承担的事情至少包含四块。第一块是实时视频墙这是操作员的眼睛。核心诉求是多路画面同时展示、画面延迟尽量低、切流不白屏。第二块是告警事件中心这是系统的灵魂。算法检测到摔倒行为后会推送事件前端要把这个事件展示出来包括抓拍图片、短视频片段、关联的相机信息、发生时间并让操作员可以对事件做确认、误报标记、生成工单等操作。第三块是历史回溯操作员要根据时间和相机维度检索事件点击事件后能回放对应时段的视频看清楚到底发生了什么。第四块是设备和系统的管理配置比如相机的增删改查、分组管理、摔倒检测的灵敏度阈值配置、轮询计划、告警策略等。这个职责范围的划分很重要它决定了你前端的技术选型。实时视频墙对播放器库依赖很重告警事件中心对WebSocket通信和状态管理要求很高历史回放又对播放器的seek能力和分段加载能力有要求。如果一开始没把职责理清后面很容易做出一个四不像的东西。1.2 前端与服务端的通信模型两条通道要分开在设计通信模型的时候我踩过一次坑一开始把视频流和事件数据混在一起考虑导致方案摇摆了很久。最后定下来的模型很简单视频流走视频流通道事件和业务数据走HTTP/WebSocket通道两者完全解耦互不干扰。视频流通道传输的是HLS或HTTP-FLV等媒体流由独立的流媒体服务分发。业务通道负责登录鉴权、设备列表、事件推送、配置下发等。摔倒检测算法服务检测到异常后会把事件数据写入消息队列业务后端消费后通过WebSocket推送给前端。前端收到事件后如果画面墙上有对应的相机画面就在这个画面上叠加告警框同时弹出通知并联动事件列表刷新。为什么要把两条通道分开因为媒体流的特点是持续、带宽占用大、对延迟敏感但不要求绝对可靠丢一两个视频帧无所谓。而业务数据的特点是短小、频率可控、对完整性和顺序性要求高摔倒事件不能丢顺序不能乱。两者放在同一条通道里视频流的带宽抖动会直接影响事件数据到达这是不可接受的。1.3 为什么选 Vue3 TypeScript Vite 这套组合技术栈上我最终选择了Vue3 TypeScript Vite这个组合在现在的前端项目里已经很主流了但我的理由可能和网上那些因为流行所以选的言论不太一样。选Vue3是因为这个项目里的界面交互有大量的响应式数据联动比如相机树的选择要联动视频墙、事件列表、地图定位相机在线状态的变化要插到列表里这些场景Vue3的响应式模型比React的受控模式写起来更顺手。TypeScript主要是为了约束接口数据类型摔倒检测系统前后端对接的接口字段非常多相机对象有十几二十个字段事件对象嵌套更深没有类型约束改接口字段的时候很容易在运行时才炸出问题。Vite就是为开发体验选的冷启动快、HMR快这个项目源码量不算小webpack在大型项目里的编译等待太痛苦了。播放器方案上最终用的是video.js加hls.js的组合页面实时墙部分自己封装了一个Vue组件。关于播放器的选型细节后面专门开一节讲。2. 核心细节解析与实操要点2.1 视频播放器选型为什么最终定了 video.js hls.js这是整个前端方案里最难选的一个环节我几乎把市面上的方案都试了一遍。原生video标签够轻但只支持Safari原生HLS能播Chrome和Firefox得靠Media Source Extensions自己拉流解析原生写法要处理的东西太多而且没有UI控制条、没有网络状态回调复用性也差。WebRTC方案延迟最低但是需要额外的信令服务和媒体网关后端成本高在相机路数多、网络环境复杂的公网场景下稳定性不好评估。至于商用的播放器SDK如果项目预算够且要上多端可以考虑但我这边只有一个网页端没必要引入商业组件。最终用了video.js加hls.js。video.js负责UI和控制逻辑hls.js负责做MSE的适配两者配合一套代码兼容Chrome、Firefox、EdgeSafari则直接用原生HLS能力。实测下来桌面端覆盖完全没有问题。这里有一个关键配置要说清楚。video.js如果要针对HLS做直播流延迟优化。默认情况下HLS直播流的延迟在10到30秒之间这个延迟对摔倒检测这种安全场景是不可接受的你看到画面上有人摔倒了其实事故可能已经在半分钟前发生。必须把延迟降下来目标控制在3到5秒内。hls.js里可以设置liveSyncDurationCount和liveMaxLatencyDurationCount这两个参数前者控制直播流同步窗口的长度后者控制允许的最大延迟时长。我把liveSyncDurationCount设为2liveMaxLatencyDurationCount设为4实际效果是延迟基本被压在5秒以内。2.2 播放器接入的完整流程与参数调优接入流程其实不复杂但每个环节都有注意点。后端侧通常会提供一个HLS播放地址格式类似https://your-domain/live/camera_001/index.m3u8。需要注意的是拿地址之前通常要先走一次鉴权接口换取一个带token的播放地址或者通过带过期时间的URL签名来访问。这个token如果过期了播放器会报错这个时候前端不能只提示播放失败应该主动去刷新token并重新拉流。前端封装播放器组件的核心逻辑我简化后大概是这样的// VideoPlayer.vue 关键部分 import videojs from video.js; import video.js/dist/video-js.css; export function createHlsPlayer(videoEl, streamUrl, options {}) { const player videojs(videoEl, { autoplay: true, muted: true, // 关键浏览器自动播放策略要求静音才能自动起播 controls: true, preload: auto, playsinline: true, // 兼容移动端 sources: [{ src: streamUrl, type: application/x-mpegURL }], liveui: true, // 显示直播UI组件比如“LIVE”标签 liveTracker: { trackingThreshold: 0 } }); player.hlsQualitySelector?.(); // 如果有画质选择器插件 player.on(error, () { // 采集错误码上报并做重连处理 handlePlayerError(player); }); player.on(stalled, () { // 卡顿处理 handleStalled(player); }); return player; }插件的使用要注意版本匹配video.js 8.x和7.x的插件API不完全兼容hls.js的版本也不能随便升级我在项目中就遇到过一次升级hls.js后Safari下的video.js回退逻辑失效的问题。建议是把这几个依赖的版本锁定不要随手更新。2.3 自动播放策略这个坑几乎人人都踩我在这里要多说几句因为这个坑几乎所有做视频流的同行都踩过。现在的浏览器对自动播放有严格限制Chrome从66开始就要求要么视频带声音并且用户和页面有交互要么视频静音才能自动起播。摔倒检测系统的监控大屏操作员不可能每开一路画面就点一次播放按钮十几二十路画面手动点播会疯掉的。所以处理策略就是默认静音起播如果用户明确点击了某个画面的取消静音操作再打开声音。这样既满足浏览器的自动播放策略又不影响正常使用。曾经接了一个需求说某个监管场景必须要有声音报警就是检测到摔倒后现场会响警报声提示周边人员。这个音频不能走视频流的音轨要走Web Audio API单独播放报警音。如果前端用自动播放起播视频后在用户没有交互的情况下调play()会被浏览器判定为媒体参与度不足而拒绝。解决方案是引导用户做一次点击交互后再启用声音或者等第一次用户点击行为后初始化一个AudioContext。2.4 多路视频墙的性能优化从一锅端到按需渲染视频墙是本系统的重头戏4路、9路、16路的监控墙是刚需有的项目甚至需要支持36路以上同时展示。直接把所有播放器实例都创建出来页面会立刻卡顿甚至浏览器崩溃。我做的第一层优化是可视区懒渲染。一进监控墙页面只初始化当前画面中可见区域的播放器实例。用IntersectionObserver监听每个视频容器是否进入了视口进入视口才创建播放器移出视口就销毁播放器并释放资源。这个思路和列表虚拟滚动的本质是一样的只不过对象从列表项换成了视频流。第二层是并发播放控制。浏览器并发拉流的数量是有限的正常情况下不超过6个TCP连接到同一域名但视频流服务经常有多个域名或IP这个限制不明显瓶颈更多在解码性能。16路1080P同时解码对GPU的负担非常大。我一般会在全屏视图下做降级画面不在全屏时拉子码流或低码率流切换全屏时再切回主码流这个切换是异步的要做好加载提示。第三层是内存泄漏预防。播放器实例如果创建了不销毁占用的内存回收不掉长时间运行后页面会越来越卡最后直接闪退。每次销毁播放器的时候先调用player.dispose()然后把video元素从DOM中移除再把src置空。特别注意canvas截图的场景如果做了定时抓帧还要把canvas的宽度高度重置否则canvas对象会持续积累。3. 实操过程与核心环节实现3.1 告警事件WebSocket消息的设计与前端处理摔倒检测的实时性要求很高事件推送给前端必须走WebSocket。如果全部靠HTTP轮询检测到摔倒到页面上看到告警延迟可能会达到10秒以上安全场景根本没法接受。我们前后端约定的WebSocket消息格式是这样的{ type: ALARM_EVENT, data: { eventId: EVT20250114153000123, cameraId: CAM001, cameraName: 门诊大厅-东侧, timestamp: 1736832600000, eventType: FALL_DOWN, confidence: 0.97, snapshotUrl: https://your-domain/fs/snapshot/CAM001/xxx.jpg, videoClipUrl: https://your-domain/fs/clip/CAM001/xxx.mp4, location: { x: 0.56, y: 0.78 }, status: PENDING } }eventType字段目前有FALL_DOWN行人摔倒等类型后续可能扩展。前端收到消息后要做几件事如果当前监控墙上有对应cameraId的画面就在画面上用一个红色的bounding box把摔倒位置标出来弹出系统通知提示操作员处理把事件插入到事件列表头部并播放一个提示音。这里面有一个细节如果操作员当前正在全屏观看某个相机弹窗不能遮住画面主体所以告警块一般设计成右下角滑出的卡片而不是居中的模态框。3.2 前端收到的告警事件如何处理前端维护告警事件状态的流转这个我单独梳理一下。事件状态至少分为PENDING待确认、CONFIRMED已确认、RESOLVED已处理、MARKED_FALSE误报等几个状态。刚开始做的时候我以为前端只管展示状态流转全由后端控制就行后来的经验是前端必须在本地维护一份事件状态副本并在用户操作后乐观更新UI再异步同步到后端。举个例子操作员在待处理列表中看到一条新告警点击确认如果前端要先等后端接口返回200再更新按钮态用户会感觉到明显的卡顿尤其在网络稍有波动的时候。我的做法是点击后立即把这条事件在本地标记为已确认按钮变成灰色防止二次点击然后发起PUT请求同步到后端如果失败再回滚并提示。这个乐观更新策略在事件处理场景里同时照顾了交互流畅度和数据一致性但前提是接口必须设计成幂等的否则重复点击会造成数据重复。3.3 事件列表、画面联动与弹窗去重事件列表是操作员的主战场也是很容易被轻视的部分。一个典型的场景是操作员盯着监控墙这时事件列表头部跳出一条新事件配图是抓拍快照操作员点一下右侧视频墙立刻切换到对应相机的实时画面同时时间轴自动定位到事件发生的时刻开始回放前30秒的视频片段。这里的技术难点在于从实时画面切换成回放片段再切回实时这个过程必须是无缝的。我的做法是维护一个回放临时状态当操作员点事件时先把当前播放器的直播流断开切到事件的videoClipUrl地址播放结束后再自动恢复直播流。如果用户此时正在操作其他事情恢复动作会延迟到这段视频播完或用户主动关闭回放面板。另外切换过程要有一个渐变遮罩过渡不然白屏闪现很影响观感。弹窗去重也很实际。同一个相机如果在短时间内连续检测到多次摔倒事件前端如果每次都弹窗操作员会被淹没。我定的规则是5秒内同一个cameraId只弹一次但事件列表里的每一条都会正常插入只是不再重复弹窗和响铃。如果不同类型的告警可以同时弹但最多同时展示三个弹窗卡片超出的按时间顺序排队展示。3.4 用状态机管理弹窗的展示与消失弹窗这个看似简单的交互如果没有状态约束后面会变成一团乱麻。我用了简单的状态机来管理// 告警弹窗的有限状态 type PopupState IDLE | SHOWING | QUEUED; // 弹窗队列管理 class AlarmPopupManager { private queue: AlarmEvent[] []; private current: AlarmEvent | null null; push(event: AlarmEvent) { if (this.shouldSuppress(event)) return; if (this.current) { this.queue.push(event); } else { this.show(event); } } private show(event: AlarmEvent) { this.current event; // 渲染弹窗并启动5秒自动收起定时器 renderPopup(event, () { this.current null; this.next(); }); } }用这个管理器的好处是弹窗的显示、排队、自动收起逻辑都收敛在一个地方不会出现这个弹窗怎么没弹出来那个弹窗怎么一直在这类问题。3.5 前端状态管理的具体组织方式整个前端用Pinia做全局状态管理按照业务域拆分了几个store。cameraStore管相机树、在线状态、分组alarmStore管事件列表、未读数量、弹窗队列viewStore管当前视角、选中的相机、回放状态。这里面我遇到的一个实际问题是相机在线状态的状态刷新频率不能太高否则后端压力大也不能太低否则画面上相机状态长时间不更新。最后定的是30秒一次全量轮询加上WebSocket推送上来的状态变更事件基本能满足需求。但是轮询的定时器在页面切换、浏览器标签页隐藏时要能自动暂停否则后台页面还在消耗网络资源白费流量。我用visibilityChange事件监听页面可见性不可见时暂停轮询重新可见时立即拉一次最新状态并恢复定时器。3.6 相机管理与配置页面的业务实现页面里还有一个比较高价值的模块相机管理。这个模块的功能主要有相机分组维护、每个相机绑定区域、开启或关闭摔倒检测算法、设置检测灵敏度。灵敏度有两个可调维度一个是检测阈值confidence threshold另一个是连续N帧满足条件才判定为摔倒这个N的默认值是5。前端配置页面用两个滑块控件分别调这两个参数配置值保存后要即时下发到算法服务同时在页面上显示配置已生效的回执。对前端来说重点是这个配置变更要立即反映到实时画面中。打个比方操作员把灵敏度从低调到高保存后如果这个相机正在视频墙展示前端应该弹出一个小的配置变更时间戳记录并在下一次事件到来时使用新的阈值判断逻辑。这里需要前后端频繁联调建议前端用mock数据先把整个交互串起来等算法服务就绪后再对接真实接口能省下大量等待联调的时间。4. 常见问题与排查技巧实录我把开发过程中遇到的高频问题整理了一张表都是我实际踩过的坑不是网上抄的。这张表给我团队的新人看基本能解决他们90%的现场问题。现象排查思路解决方法视频画面一直黑屏不加载先看Network里m3u8请求是否返回200再看流地址是否带鉴权过期参数最后看控制台是否有MSE错误确认流地址有效处理鉴权刷新缺失CORS头要后端加跨域响应头Chrome能播但Safari黑屏Safari对HLS走原生支持不走hls.jsvideo.js配置错误会导致黑屏检查播放器是否检测了Safari源码里加Hls.isSupported()判断播着播着就卡住看Network中是否出现stalled或等待请求超时多半是流服务器切片回源慢监听stalled事件做自动重连最大次数限制为3次WebSocket频繁断开重连排查Nginx的proxy_read_timeout配置默认60秒会导致闲置连接断开增大超时时间到120秒并开启WebSocket的proxy_http_version 1.1页面长时间运行后内存暴涨排查是否有失效的播放器实例未销毁是否有定时抓帧的canvas未释放实现播放器池统一管理销毁退出监控墙时手动dispose所有player弹窗重复弹出没有做同相机5秒去重在消息入口加防抖队列维护上次弹窗时间戳多路画面同时加载白屏播放器实例创建太多触发浏览器并发限制改成可视区懒渲染并发播放数量控制4.1 浏览器跨域问题与HLS加载的典型失败场景跨域问题是这个项目里最头痛的问题之一。流媒体服务器和业务服务器经常是不同域名m3u8和ts分片的请求会触发CORS。解决办法是让流媒体服务配置Access-Control-Allow-Origin响应头允许业务域名的跨域请求。如果流媒体服务器不方便改还有一个前端的变通方案是使用不带跨域限制的协议比如WebSocket转流但这会增加一层中转服务的开发成本。我遇到过的一个典型失败场景m3u8能拿到但是ts分片请求404。排查后才发现是流服务器配置了防盗链会校验Referer字段而前端请求头里Referer带的格式和后端预期的不一致。解决办法是后端在防盗链白名单里放开前端页面域名或者改成签名方式。这类问题用浏览器的Network面板很容易定位看ts请求返回的状态码就能判断。4.2 浏览器自动播放限制导致视频不播放这个现象经常出现在刚配置好新环境的时候一打开监控墙所有画面都是黑屏控制台报play() failed because the user didnt interact with the document first。这不是代码逻辑问题是浏览器策略问题。我的处理方案是让用户在首次进入页面时点击进入监控墙的按钮只有点过按钮之后才创建播放器实例这样用户交互在先视频起播的权限就足够了。如果项目有轮播自动全屏这种场景比如每10分钟自动切换全屏相机全屏请求也必须通过用户主动操作才能触发。纯自动化的全屏API会被浏览器拦截这种情况下一般只能做成提示用户点击进入全屏或者用固定大小的铺满窗口画面替代真正的浏览器全屏。4.3 HLS延迟和卡顿的调优经验HLS的延迟优化是个系统工程光靠前端改参数效果有限。前端能控制的是播放器端的buffer策略我认为有两个参数比较关键一是liveSyncDurationCount控制同步窗口设置得越小延迟越低但太小会导致频繁追帧跳动二是maxBufferLength控制最大缓冲设置得太大延迟会变高。实际项目中这两个参数是需要根据网络状况去调平衡的我给前端设计了一个延迟优先/流畅优先的切换开关默认在延迟优先模式。卡顿的另一个常见原因是上下行带宽不足。16路720P同时播放大概需要20Mbps以上的带宽如果现场网络只有10Mbps再优化播放器也没用。这时只能做码率自适应或降级到子码流。我在视频墙上做了主码流/子码流切换按钮操作员根据网络情况手动切换简单粗暴但有效。更智能的做法是通过hls.js的带宽估算API自动切换不过那个坑比较多等后续版本再深入。4.4 各类调试技巧和日志埋点调试HLS播放问题时我习惯在代码里加几行关键日志方便排查。一是每次播放器事件触发时把错误对象和当前网络状态写到日志里便于回放现场二是在webSocket收到告警事件时打印一条带有事件ID的日志用于确认前后端的数据链路畅通三是用一个全局变量记录播放器实例数监控页面性能。给前端加日志埋点这件事别的项目里可能优先级不高但在摔倒检测这种涉及安全监控的系统里日志就是事后追责的依据。出了事操作员有没有在2分钟内处理告警有没有误标事件都要能查得到。我建议前端至少在三个地方打点事件接收时间、操作员处理动作确认/误报/忽略、播放器异常和自动恢复的记录。5. 前端文档和团队协作的注意事项既然是前端文档2最后补一点关于文档维护的经验。这一系列文章的作用除了记录技术方案更重要的是让后续接手的人能快速上手。我写完后觉得文档里最有价值的部分不是用什么组件而是为什么这么定。比如播放器选型我把对比过程、踩坑经历、最终决定的原因都写清楚了后来者就不会再重复踩坑。另一个非常重要的实践是接口mock和类型定义。摔倒检测系统的前后端接口字段非常多对接过程中非常容易因为字段名不一致导致联调失败。我们团队的做法是后端先出一份OpenAPI/Swagger文档前端根据这个文档生成TypeScript类型定义然后用mock工具模拟接口数据开发前端逻辑等联调时再用真实接口替换。这样做的好处是前端不依赖后端进度后端也能从mock数据和联调结果中提前发现协议问题。前端文档中还要记录一些约定俗成的规范比如给播放器组件传参的方式、事件状态字段的取值和含义、弹窗管理的唯一入口等等。这些规范不一定能在代码里强制约束但写清楚后大家开发时就有了对照依据。最后再分享一个小技巧前端项目里对时间格式的处理一定要统一封装不要各处写new Date()然后手动拼接字符串。视频监控类页面高频出现时间选择、时间展示、时间戳转换格式不统一后面对接历史回放和事件检索的时候会非常痛苦。我做了一个统一的timeUtil模块所有页面强制走它这个决策在后续接入按时间范围检索事件功能时省了非常多的事。个人体会是项目越大越要让常见的公共逻辑收敛成模块不要散落在页面里各写各的维护成本真的会翻倍。
返回列表