首页 / 资讯中心 / 文章详情

Vue视频监控后台接入云眸平台:实时预览与录像回放完整实现

Vue视频监控后台接入云眸平台:实时预览与录像回放完整实现 ★ FEATURED ARTICLE
在公司内部做过一个视频监控后台前端是Vue 2 Element UI需求就是把某安防厂商的云眸平台能力接进来做成实时预览和录像回放两个页面。当时踩了不少坑尤其是播放地址有效期、浏览器兼容、组件卸载时的内存释放这几个问题折腾了好几个晚上。这篇文章就把完整的实现思路和踩坑记录整理出来给要做类似对接的Vue开发者当个参考。1. 云眸平台接入的整体思路与需求拆解1.1 需求拆解实时预览、录像回放和权限控制看起来是个很简单的要求但拆开之后其实包含三个子问题。实时预览指的是用户点击设备列表中的某个通道浏览器立刻播放该设备的实时画面。这里有一个关键点摄像机本身用的是RTSP或者私有协议浏览器原生并不支持所以必须依赖平台侧提供的Web播放地址前端拿到地址后通过播放器解码渲染。录像回放指的是根据设备、通道、日期、起止时间查询历史录像然后播放对应的录像片段。这个比实时预览复杂因为涉及录像文件检索、时间轴展示、倍速播放、暂停、拖拽跳转。云眸平台把录像检索和取流地址都封装成了HTTP接口我们不需要关心存储介质只需要把参数传对。权限控制这边容易被忽略。云眸平台的appKey和appSecret相当于账号密码如果直接放在前端代码里等于把钥匙交给用户。所以前端不能直接调平台接口必须由后端代理。1.2 技术选型前端播放方案的3种主流思路接入云眸平台视频流前端有三条路可以走直接用官方提供的Web播放器组件、自己集成第三方播放器库、把播放地址交给视频标签播放原生支持的格式。官方Web组件是最省事的方案但通常以插件或嵌入脚本形式提供样式和交互被封装想定制截图、录制、布局比较费劲。第三方播放器库比如Video.js、flv.js、hls.js灵活可控适合封装成自己团队的公共组件但需要自己处理播放地址格式的适配。第三种直接播放MP4这类格式虽然简单但云眸的实时流走的是HLS或HTTP-FLV这两种格式原生标签都不支持所以基本不可行。我们在选择时需要考虑目标浏览器的覆盖范围。当前云眸平台的Web取流地址常见的格式有HLSm3u8和HTTP-FLVSafari对HLS原生支持Chrome则需要hls.jsHTTP-FLV则用flv.js。最终选了社区生态好、维护稳定的方案统一让后端返回自适应流地址前端封装一个播放器组件内部按需加载hls.js和flv.js避免一上来就引入一堆播放器依赖。1.3 整体架构前端Vue负责交互后端负责鉴权与取流架构上采用前后端分离的经典模式。后端维护云眸平台的appKey和appSecret负责获取access_token、按设备序列号和通道号获取播放地址、按时间范围查询录像列表。前端Vue项目只负责三件事展示设备列表和日期选择等交互组件、调用后端接口拿播放地址、把地址交给播放器渲染。好处很明显。第一密钥不暴露万一前端代码泄露也不会导致整个视频平台的凭据被滥用。第二播放地址有有效期后端可以统一刷新。第三后续如果要替换或者同时接入多个视频平台前端只需要改一张映射表后端加一个适配层即可不用大改动。2. 接入前的准备与前端的鉴权链路设计2.1 云眸平台的开发者账号与应用注册接入之前需要到云眸开放平台注册开发者账号并创建一个应用。创建成功后会拿到一对关键的凭据appKey和appSecret。它们的作用类似于账号密码appKey表明“你是谁”appSecret用来给请求签名或者换取访问令牌证明“你确实是你”。这一步有几个容易出错的地方。应用类型选择要联系实际业务如果只是视频监控后台选择对应的接入类型即可回调地址虽然有的接口不用但能填就填。还有一个坑appSecret只在创建时完整展示一次一定要保存好我见过同事把secret截图发到内部聊天群结果后端拉取之后频繁报签名错误排查半天才想起是secret中间有转义字符被截断了。2.2 access_token的获取与缓存策略云眸平台的API普遍要求请求头携带access_token。这个token不是永久的通常有有效期常见的是7天但具体以官方文档为准。频繁获取token虽然是允许的但会在业务量大的时候平白增加网络开销而且部分接口对获取频率有限制。我们后端设计了token缓存逻辑第一次调用时用appKey和appSecret请求获取token把结果连同过期时间一起放到内存缓存或者Redis设置一个提前量比如token实际还有24小时过期就当它过期去刷新避免并发请求时出现token临界失效导致大量401错误。示例逻辑用伪代码表达就是async function getAccessToken() { const cached cache.get(cm_access_token); if (cached cached.expiresAt - Date.now() RENEW_MARGIN) { return cached.value; } const response await request.openPlatform(/api/lapp/token/get, { method: POST, data: { appKey: process.env.CM_APP_KEY, appSecret: process.env.CM_APP_SECRET } }); cache.set(cm_access_token, { value: response.data.accessToken, expiresAt: Date.now() response.data.expireTime * 1000 }); return response.data.accessToken; }这里要特别注意accessToken的过期时间一般以秒为单位别直接拿来当毫秒用换算错了会导致后端一直返回token无效。2.3 播放地址的获取与参数说明获取实时播放URL时平台要求的核心参数通常是设备的序列号或设备编号和通道号。设备序列号是设备出厂时的唯一标识通道号表示这个设备上的第几路画面。有的设备是单通道永远填1有的NVR或硬盘录像机可能带很多个通道这就需要对用户屏蔽前端下拉框绑定的就是设备ID和通道号。播放URL的类型也很讲究。有的平台支持直接在URL里指定清晰度比如高清或流畅有的支持指定协议类型或者自定义转码参数名各不相同。建议后端做一层收敛前端只需要传“清晰度等级”这个语义参数后端映射成平台真正认识的参数好处是前端不感知平台差异。回放地址获取还得额外传时间范围。回放URL的有效期通常比实时地址更短因为回放涉及存储检索的开销平台不希望一个过期地址长期占用资源。因此前端在用户点击“开始回放”那一刻去请求新鲜的回放地址而不是提前申请一堆放着不是好习惯。3. 实时预览模块的实现从设备列表到画面渲染3.1 设备列表与播放器组件的整体封装实时预览页面的结构一般由三块组成左侧的设备树或分组列表、中间的播放网格、右侧的操作面板。播放网格需要支持1、4、9、16宫格切换每个格子对应一路画面。我在项目里把“一路视频”封装成了独立的MediaPlayer组件包含视频容器、加载状态、播放状态和错误提示。父组件负责设备列表渲染和宫格布局每个格子动态创建MediaPlayer子组件传入设备编号和通道号。组件内部需要有实例管理模块。为什么因为Vue组件的data变化不会自动导致视频流的销毁重建尤其是播放器实例持有DOM引用如果组件被复用比如切换宫格布局底层播放器状态可能错乱。我的做法是给每个MediaPlayer实例分配一个唯一的playerId在组件render的时候立即初始化播放器在beforeDestroy时彻底销毁并且把实例引用从父级的Map中移除避免内存泄漏。3.2 实时视频流的接入与播放后端拿到实时播放地址后返回给前端。前端判断地址后缀如果是.m3u8走hls.js如果是.flv走flv.js。代码大致是这样async function loadStream(url) { destroyCurrentPlayer(); if (url.includes(.m3u8)) { const Hls await import(hls.js); if (Hls.isSupported()) { player new Hls(); player.loadSource(url); player.attachMedia(videoElement); player.on(Hls.Events.MANIFEST_PARSED, () { videoElement.play(); }); } } else if (url.includes(.flv)) { const flvjs await import(flv.js); if (flvjs.isSupported()) { player flvjs.createPlayer({ type: flv, url }); player.attachMediaElement(videoElement); player.load(); player.play(); } } }异步加载播放器库有个好处按需加载不会因为首屏引入hls.js和flv.js把首包体积撑大。我用的是动态import构建工具会自动做代码分割。播放器加载时要把loading状态暴露给父组件否则用户会觉得点了没反应。我习惯在组件里内置一个状态机idle、loading、playing、error。loading时显示骨架屏加“加载中”playing时隐藏error时显示重试按钮。状态机的数据流清晰调试的时候也不容易迷路。3.3 播放过程中的关键兜底逻辑实时播放不是“拿到地址一路播到底”就完了。比较常见的情况是设备离线、取流失败、地址过期。地址过期这个问题尤其隐蔽画面可能在播放中突然断掉。我在组件里加了两个兜底机制。第一监听video的error事件和播放器的ERROR回调如果非用户操作导致播放中断自动重新请求后端获取新的播放地址并重试一次最多重试两次超过了就提示用户“设备暂时无法连接”。第二前端记录首次播放的时间戳针对播放地址的有效期做一个前端预警比如有效期是5分钟那我4分半的时候就主动刷新一次地址。这里强调一下刷新地址不是直接拿旧地址重新load而是重新调用后端接口获取一个新的播放地址保证地址的有效性。这是实时预览稳定运行的关键。4. 录像回放模块的实现时间检索与倍速播放4.1 按时间检索录像列表的交互设计回放页面通常包含日期选择、时间段选择、设备选择、录像列表四部分。这里的时间段选择设计很有讲究。直接让用户选择精确到秒的起止时间操作成本太高比较常见的交互是提供几个常用快捷范围最近半小时、最近一小时、今天以及一个自定义区间选择器。自定义区间选择器要限制最大跨度。通常云眸平台单次查询录像列表支持的时间跨度有限制比如单次查询不能超过24小时起止时间不能跨越多个自然日。前端把时间选择器做一次校验超出范围直接提示而不是等后端报错体验会好很多。查询结果返回的是录像片段列表每个片段有开始时间、结束时间和对应的文件ID或回放标识。前端可以将相同设备的录像段渲染成一条条带颜色的横条用时间线的方式展示这样用户可以直观地看到哪些时段有录像。轻量实现可以直接用CSS绝对定位渲染没必要上重型时间轴库除非后面要做多设备轨道对比。4.2 回放地址的获取与播放器适配录像列表确定后用户点击某个录像片段或点击“播放”按钮前端带上该片段的标识以及设备的序列号、通道号去请求回放地址。回放播放器的核心差异在于它支持暂停、恢复、停止、倍速切换还可能支持进度条拖动。而实时播放器这几个交互在业务上没太大意义。所以在组件设计上我把实时MediaPlayer和回放PlaybackPlayer拆成两个组件虽然播放底层用的同一个动态加载播放器库但暴露给父组件的属性和方法不同。在项目初期直接复用同一个组件后来发现逻辑越改越臃肿拆开之后各自维护反而清爽。回放倍速的实现在HLS和FLV协议中通常可以通过设置播放器的playbackRate实现。hls.js和flv.js都支持这个接口。但实测下来倍速档位不要做得太密0.5、1、2、4四档就够。太高倍速时部分底层解码跟不上视频会卡顿甚至花屏。4.3 录像片段边界处理与时间轴联动回放过程中还有个细节容易忽略录像片段的边界。比如录像列表显示某个片段是09:00:00到09:15:00用户拖动进度条到09:14:59时播放器还在正常播但一过09:15:00如果后端返回的地址对应的是单个录像文件画面就会自动停止。这时候需要前端监听播放结束事件自动跳转到下一个录像片段播放实现无缝衔接。做法是把时间段内的多个录像片段一次性获取回来前端构建一个播放队列。当前片段播放结束后自动播下一个片段。这里需要注意的是相邻片段之间可能存在几秒钟的缝隙播放器的ended事件触发后延迟500毫秒再加载下一个地址避免界面上出现明显的黑屏跳动感。时间轴和画面联动的另一种常见需求是打点回看。用户拖到某个时间点前端找到这个时间点落在哪个录像片段内然后以该时间点为起点请求播放地址。实现上其实就是计算目标时间点相对录像片段起始时间的偏移量转成秒传给播放器做seek。这块如果平台支持seek参数就直接用如果不支持就把目标时间当作片段起始时间重新取流代价是几秒出画的时间但在可接受范围内。5. 常见问题与排查技巧实录5.1 画面黑屏但播放器状态显示正常这类问题最诡异明明没报错就是不出画面。我遇到的情况多半是播放器的attachMedia时机问题。调用hls.js的loadSource之后不能马上play要等MANIFEST_PARSED事件触发。如果hls.js在flv.js之前加载同时页面有多个视频格子可能事件回调里拿到的videoElement引用不对。排查时要先看控制台有没有跨域报错再看播放器实例的currentTime是否在走动。如果currentTime在走但没有画面大概率是视频层的CSS问题比如video元素被打了个display:none或者z-index被其他弹层遮住了。用Vue DevTools看一遍元素样式就能很快定位。5.2 播放地址过期导致播放中断之前提到地址过期问题实际表现就是画面播放中突然卡住并停止控制台显示404。实时预览和回放都会触发。我们在后端日志里看到很多这类请求原因是前端在跨天或者长时间挂机之后旧地址早已失效。解决思路很明确在前端播放器组件中维护一个“上次心跳时间”每隔30秒检查一次当前时间与地址获取时间的差值超过厂家文档声明有效期的80%就主动刷新。刷新不等于立刻切换而是先拿到新地址存着等旧地址真正报错或者到期再无缝替换保证画面连续。我这里没有用轮询因为轮询会增加后端和平台的压力。5.3 token失效引起的大面积401项目有一次上线后全公司同时点开监控后台大批量请求返回401。查下来是后端获取token时并发请求没有做统一控制几个请求同时发现缓存里没有token各自去请求新token旧的token被后获取的覆盖其他已经拿到旧token的请求就全部失效。改进方案是在服务端对获取token做互斥锁。简单实现可以借助Redis的SETNX或者进程内用一个Promise队列只有一个请求真正去打平台接口其余请求等待同一个Promise。这种并发问题在初版项目里很难想到但真出现了才知道多痛。另外token的过期时间建议在后端的监控日志里记录下来这样出现401时能快速判断是“缓存逻辑问题”还是“平台侧token真的失效了”。5.4 内存占用过高与页面卡顿的排查集成播放器之后页面长时间挂机会出现内存持续上涨。排查发现两个原因第一个是每个播放器实例都注册了定时器比如直播流的延迟优化、自动重连组件销毁时没有清除第二个是播放器实例本身没有destroy底层WebSocket或fetch连接还留在网页上。解决办法是在Vue组件的beforeDestroy钩子里统一清理清除所有定时器、调用flv.js或hls.js的destroy方法、将video元素的src置空并移除所有事件监听器。这一步看似基础但在实际项目中经常被遗忘尤其在路由切换和宫格布局切换时旧组件销毁不全长时间运行就会出问题。还有一个容易忽略的点如果页面是多宫格同时播放多路流要注意同时连接数的限制。浏览器对同一域名下的HTTP连接数量有限制虽然HLS和FLV走的是普通HTTP但超过一定并发连接后会出现排队延迟画面加载变慢。方案是按需加载只对当前可见的格子初始化播放切到下一组画面时销毁不可见的实例而不是把16路全拉出来。6. 从一行代码到一个稳定的播放体系接入云眸平台这件事从功能层面看无非是拿地址、播地址、换地址但真正稳定运行靠的是对地址生命周期和组件生命周期的理解。我在这个项目中收获最大的一个习惯是凡是跟“时间”有关的字段落到代码里必须先统一单位。云眸返回的token过期时间用的是秒播放地址的有效期有的用分钟回放偏移量有的用毫秒前端极易混淆。建议后端统一转成毫秒返回前端只处理一种单位省掉大量debug时间。另一个建议是调试时尽量用后端返回的真实返回包而不是mock数据。我遇到过一次本地mock一切正常联调时发现真实返回的设备序列号带了大写字母而播放器拼接URL时没有做大小写保留导致取流失败。这类字符问题只有真实联调才能暴露。如果需要扩展配套可以做的方向包括录像下载功能把指定的录像片段通过后端发起下载转存、抓图能力在播放器暂停帧上截图上传业务系统、多设备轮巡自动切换宫格的播放通道。这些扩展点的实现都不会推翻现有架构都是在前端播放器组件和后端取流接口两个边界上增加能力。整个项目从需求评审到稳定跑起来周期不算长但对Vue项目来说视频模块永远算得上“看着简单、做起来有细节”的部分。希望这篇记录能帮你少走几天弯路。
阅读完成 · 觉得有帮助?
咨询建站