ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Vue集成海康威视H5player播放器:WebAssembly视频监控实战指南

Vue集成海康威视H5player播放器:WebAssembly视频监控实战指南 做监控平台最烦的一件事就是你辛辛苦苦把业务页面写完了结果前端播放器掉链子。之前在Vue项目里接入海康威视摄像头预览第一版用的还是老一套web控件方案客户现场全是新版Chrome插件安装被拦、浏览器升级后控件失效运维一边打电话一边远程装控件体验一言难尽。后来换成海康威视官方H5视频播放器H5player开发包V2.1.2这才算真正把视频接入从“能不能用”变成了“怎么用得更顺手”。这篇文章就围绕Vue集成H5player V2.1.2这件事把我实际落地过程中的资源放置、组件封装、生命周期管理、报错排查和nginx配合一次性讲透不说废话全是能直接照着抄的。1. 为什么是H5player而不是web插件一次被迫迁移1.1 webcontrol的时代痛点海康威视老的web开发包核心是webcontrol控件走的是ActiveX或者NPAPI技术路线。在IE时代这套东西确实能用但放到现在简直是灾难Chrome从45版本开始彻底不认NPAPIEdge默认禁止ActiveXFirefox更不用说。就算客户愿意装老版本的浏览器还得给每台电脑装一次插件内网批量部署倒还好碰上跨网段的机器或者权限受限的办公环境光一个控件注册就能折腾半天。还有一个隐藏痛点老方案的画面不是真正嵌在DOM里而是通过控件在自己窗口里绘制。这就带来两个问题一个是页面弹窗、Tab切换、CSS动画时控件画面容易被遮挡或者残留另一个是前端工程化很难介入你没法用Vue的响应式数据去控制它所有的操作都只能走那个控件的全局方法和现在的组件化开发方式格格不入。1.2 H5player V2.1.2的定位和技术底牌H5player是海康威视官方出的纯网页播放方案核心思路是“去控件化”。V2.1.2这个版本底层用WebAssembly做解码把这个能力也带上了所以只要浏览器支持WebAssembly基本就能跑不再依赖任何本地安装程序。它和webcontrol最大的区别是画面渲染在Canvas里播放器的生命周期完全由前端代码控制这意味着可以在Vue单页应用里按需创建和销毁也可以把它封装成一个独立的业务组件。我把H5player V2.1.2和常见的video.js / hls.js方案对比了一下差异很直观对比项H5player V2.1.2video.js hls.js海康私有取流协议原生支持能直连符合条件的设备或平台不支持必须转成HLS/FLV多窗口分屏自带窗口管理和分屏API需要自己布局多个video标签抓图、录像、对讲提供封装好的接口需要自己实现或找第三方库依赖安装无纯前端无纯前端调试和坑位文档偏少兼容性细节多社区活跃资料多这个表不是黑谁而是想说明选型逻辑如果你的项目只需要播放m3u8流用H5player反而大材小用还要背它的包体积和兼容性包袱但如果你业务里大量用到海康的设备需要直连取流、窗口分割、实时抓图这些能力那H5player V2.1.2就是最贴近需求的方案。2. 拿到开发包后的资源安置方案2.1 开发包里到底有什么H5player V2.1.2开发包从海康开放平台下载后解压出来是一堆静态资源和示例代码。核心文件大概是这么几类h5player.min.js播放器主逻辑压缩包整个播放器的入口。h5player.min.css默认样式主要是窗口分割和控件按钮的样式。wasm/目录WebAssembly解码器按编码格式拆分成多个文件。worker.js或类似名字的Web Worker脚本负责解码计算和主线程通信。demo/目录官方示例HTML页面和引入脚本的写法。这里要特别注意wasm文件和worker脚本是通过运行时路径去加载的不是通过前端模块系统去import的。很多人第一次用把h5player.min.js当成普通npm包去import结果运行时报错说找不到wasm就是因为这些资源没有被正确拷贝到部署目录。2.2 为什么不能放src目录以及正确的public安排Vue CLI和Vite默认会把src目录里的文件交给webpack或者Rollup处理图片会被base64内联字体文件会被重命名加hash。这对业务代码是好事但对wasm和worker来说就是灾难它们需要以原文件名、原相对路径被浏览器加载一旦被构建工具改了hash播放器内部就找不到对应资源了。所以最稳的方式是——整个开发包原样放进public目录Vite项目同样适用让它不被构建流程处理。我推荐的项目结构是这样的public/ h5player/ css/ h5player.min.css js/ h5player.min.js worker.js wasm/ xxx-decoder.wasm demo/ # 可以删掉留着也没什么影响然后在public/index.html里用script标签直接引!DOCTYPE html html head meta charsetutf-8 / title监控平台/title /head body div idapp/div !-- 放在这里或body底部都可以 -- script src/h5player/js/h5player.min.js/script /body /html为什么不放到组件里用import导入有两个原因。第一CSP内容安全策略和非模块化全局变量的问题官方开发包设计上就是往window上挂一个全局对象import进来的效果和script标签差不多但会增加打包复杂度第二wasm路径是相对运行时的location解析的script标签方式最容易保持开发环境和生产环境一致。2.3 引入方式和全局对象探测V2.1.2的全局对象在不同小版本里有时叫H5player有时叫JSPlugin。为了避免在代码里写死导致版本切换后报错我习惯封装一个获取播放器构造函数的函数export function getHikPlayerConstructor() { if (typeof window.H5player ! undefined) { return window.H5player } if (typeof window.JSPlugin ! undefined) { return window.JSPlugin } throw new Error(H5player加载失败请检查script引入顺序) }用这个函数去获取构造函数能兼容大部分官方开发包的版本差异。如果两个都没有那就说明h5player.min.js没有被正确加载多半是路径问题回到上一节检查。3. Vue组件里的封装与生命周期管理3.1 最小可用组件封装H5player的第一步是把它变成一个标准的Vue组件。下面这份代码是我在项目里验证过的骨架删掉了业务无关的东西保留了最核心的初始化、播放和销毁逻辑template div classhik-player-container div :idplayerDomId classhik-player-dom/div /div /template script import { getHikPlayerConstructor } from /utils/hikPlayer let playerSeq 0 export default { name: HikH5Player, props: { url: { type: String, required: true }, channelId: { type: String, default: }, streamType: { type: Number, default: 1 } }, data() { return { playerDomId: , player: null } }, watch: { url() { this.reloadStream() } }, created() { playerSeq 1 this.playerDomId hik-player-${Date.now()}-${playerSeq} }, mounted() { this.$nextTick(() { this.initPlayer() }) }, beforeDestroy() { this.destroyPlayer() }, methods: { initPlayer() { const PlayerConstructor getHikPlayerConstructor() const instance new PlayerConstructor({ szId: this.playerDomId, iType: 2, iWidth: this.$el.clientWidth, iHeight: this.$el.clientHeight, iMaxSplit: 1, iHeartBeatMode: 1 }) instance.JS_StartService(HikH5Player, { szPluginPath: /h5player/js/, iServicePortStart: 15900, iServicePortEnd: 15910, iHeartBeatMode: 1 }).then(() { this.player instance this.playStream() }).catch((err) { console.error(H5player服务启动失败, err) }) }, playStream() { if (!this.player) return const playInfo { playURL: this.url, channelID: Number(this.channelId) || 1, streamType: this.streamType, mode: 1 } this.player.JS_Play(1, playInfo, playInfo).catch((err) { console.error(播放失败, err) }) }, reloadStream() { if (!this.player) return this.stopStream() this.$nextTick(() { this.playStream() }) }, stopStream() { if (!this.player) return try { this.player.JS_Stop(1) } catch (e) { // 忽略停止时的异常 } }, destroyPlayer() { if (!this.player) return try { this.player.JS_StopService this.player.JS_StopService() } catch (e) { // 忽略销毁时的异常 } this.player null } } } /script style scoped .hik-player-container { position: relative; width: 100%; height: 100%; } .hik-player-dom { width: 100%; height: 100%; } /style这个组件有几个细节值得说。第一个细节playerDomId必须在created里生成而不是在mounted里写死。考虑的是组件复用场景如果同一个页面渲染了多个播放器实例重复的id会导致后面的实例找不到容器。用Date.now()加上自增序号能保证同一时间不会撞id。第二个细节player实例不要放进data()里的响应式对象中而是作为实例属性赋值。我最早就是把player写在data里结果Vue给这个对象套了一层Proxy代理JSPlugin内部有些方法依赖对象自身的属性遍历在代理对象上执行时会被拦截出现一些很诡异的“方法存在但调用报错”的问题。写成this.player instance不经过响应式系统反而干净。3.2 初始化服务JS_StartService的参数JS_StartService是H5player初始化时绕不开的方法作用是让播放器服务在指定端口范围内拉起一个本地服务进程后续播放器内部的取流、解码和渲染都通过这个服务进行。参数里最值得关注的是iServicePortStart和iServicePortEnd这两个参数划定了服务端口范围。实际部署时端口范围不要只给一个端口因为浏览器并行播放多路视频时服务需要在多个端口上同时工作。我在内网环境里遇到过一种情况只给了15900到15901两个端口打开第八路视频画面时卡住不动把端口范围扩大到15900到15920就好了。另外要注意同一个页面如果有多个H5player实例服务端口范围不能互相重叠否则后启动的实例会抢占先启动实例的端口导致之前能正常播放的窗口突然全部断开。szPluginPath这个参数也必须指向开发包js所在的目录。它决定播放器内部加载worker和wasm时的基础路径如果写成相对路径一旦当前路由层级很深或者nginx做了二级目录代理就很容易拼出一个不存在的地址。保持绝对路径/h5player/js/是最省事的。3.3 播放与事件回调播放调用是JS_Play第一个参数是窗口索引从1开始后面的参数是播放信息对象。这个对象里playURL是必须的取流地址可以是海康私有协议地址也可以是平台下发的播放地址mode字段用来区分预览还是回放具体取值以你拿到的开发包demo为准。事件回调在这里扮演的角色很关键。H5player不是一发播放命令就万事大吉的播放过程中会频繁抛事件比如在线状态变化、断流、重连、窗口点击等等。以Vue的思维去理解这些事件其实是一个个“总线消息”你可以把它们统一转发给业务层this.player.JS_SetCallBack((code, data) { // 根据code判断事件类型 this.$emit(player-event, { code, data }) if (code 某离线code) { this.$emit(offline, data) } })事件回调的code值是纯数字不同版本含义可能有点出入所以封装组件时不要把所有code都硬编码在业务代码里而是先建立一个code到业务含义的映射表后续版本升级时只需要集中维护这一张表。3.4 组件销毁时的清理Vue组件的beforeDestroy里很多人只写了JS_Stop忘了JS_StopService。这就会导致一个问题组件销毁后后台服务进程还占着端口下一次再进入页面初始化新的播放器时会因为端口被占用而失败。我踩过一次很典型的坑在路由里从监控页跳转到配置页再跳回来第一次播放正常第二次播放直接报服务启动失败。查了端口才发现是上一次的服务进程没被杀掉。后来把销毁逻辑改成先JS_Stop停掉所有窗口的播放再JS_StopService停掉服务最后把this.player置空这个问题就再也没出现过。还有一点播放器容器是v-if控制的还是在keep-alive组件里的生命周期走向不同。v-if销毁会走beforeDestroy但keep-alive缓存组件只触发deactivated这时不能粗暴销毁播放器否则切回来画面是黑的。正确做法是在deactivated里暂停播放activated里恢复。比如activated() { if (this.url) { this.$nextTick(() this.playStream()) } }, deactivated() { this.stopStream() }这种策略既保留了keep-alive带来的状态缓存又不会让流一直挂着占用带宽。4. 多窗口、分屏和常见业务扩展4.1 窗口索引和分屏布局监控平台最常见的需求就是大屏分屏。H5player的iMaxSplit控制最多能分割成几个窗口1、4、9、16都是常用值。初始化之后调用窗口分割API就能把播放区域切成对应的格子。分屏状态下的核心概念是“窗口索引”。文档约定窗口索引从1开始JS_Play的第一个参数就是往哪个窗口里播放。业务层需要维护一个“当前选中了第几个窗口”的状态比如data() { return { currentWnd: 1, splitCount: 4 } }, methods: { setSplit(count) { this.splitCount count this.player.JS_SetWindowSplit(count) this.currentWnd 1 }, playInWindow(url, wndIndex 1) { const playInfo { playURL: url, mode: 1 } this.player.JS_Play(wndIndex, playInfo, playInfo) } }拖动分割条或者点击某个窗口时播放器会抛事件事件数据里带了当前窗口索引及时更新currentWnd后续操作就知道往哪个窗口去播放。多窗口真正的难点不在拆分而在和Vue列表渲染的协调。我在表格里渲染数据点击某一行要把对应视频流切到当前窗口如果表格和播放器是两个独立组件就需要状态提升到父组件用provide/inject或者Vuex/Pinia管理“当前播放窗口索引”和“当前通道信息”。这块属于常规的状态管理但特别容易出问题因为窗口索引的同步是异步的先在父组件改了状态再用新状态去调播放器必须保证播放器已经完成了窗口切换的渲染。4.2 与Vue业务状态同步的几个细节播放器画面本身是Canvas业务层如果要画电子围栏、标注框、区域线不要直接覆盖在Canvas上面否则鼠标事件会被Canvas拦截。正确做法是在播放器容器上叠加一层绝对定位的SVG图层坐标通过播放器提供的坐标换算接口把像素坐标转成业务坐标。另一个细节是组件尺寸变化。页面从全屏切换到分屏播放器容器宽高会变但Canvas本身不会自动跟着DOM尺寸变需要手动调用播放器的尺寸刷新接口。监听方式可以这样// 在组件mounted里 this.resizeObserver new ResizeObserver(() { if (this.player) { this.player.JS_Resize() } }) this.resizeObserver.observe(this.$el) // beforeDestroy里断开 this.resizeObserver this.resizeObserver.disconnect()用ResizeObserver而不是window.resize事件的原因有两个一是ResizeObserver能感知元素级别的大小变化即使父容器因为侧边栏折叠导致的尺寸变化也能捕捉到二是它不会因为窗口滚动条的出现消失而频繁触发全局事件性能更好。多路播放时还要特别注意带宽和内存。一路1080P主码流的带宽大约2到4Mbps16路同时预览客户端带宽至少要30Mbps以上否则画面会频繁卡顿。开发大屏方案时我会在业务层加一个“分页预览”策略16个窗口分两页每页8路页面切换时自动停止非当前页的流只保留当前页播放。用户感知上差别不大但对服务器和带宽的压力能降一半。5. 从实际报错出发的排查链路集成H5player过程中我整理过一份报错排查清单按照真实排错的顺序列出来几乎每个问题都在客户端现场或者测试环境遇到过。5.1 一堆404资源路径与nginx现象是播放器初始化报错浏览器Network面板一堆红色404尤其集中在wasm和worker文件上。排查链路是这样的先看请求的完整URL如果路径是/h5player/js/xxx.wasm而文件实际放在/h5player/wasm/xxx.wasm那就是szPluginPath配置有问题。如果URL是正确的但还是404那就是nginx的静态资源路径没配对。用alias配置时特别容易犯一个错误# 错误示例alias会直接把location后的路径拼接到alias路径后 location /h5player/ { alias /opt/www/h5player/; }如果/opt/www/h5player/js/h5player.min.js真实存在上面这种配置其实是能用的。真正容易错的是把alias写成root还少了一级目录。遇到404先curl一下完整URL看文件是否真实存在。另外就是dist部署场景Vue构建后的dist目录拷贝到服务器h5player整个文件夹也要一并拷贝。5.2 wasm编译失败MIME与压缩表现是浏览器控制台出现wasm streaming compile failed播放器无法解码。原因有两种。第一种是nginx没有给.wasm文件配置正确的MIME类型默认可能是application/octet-stream部分旧版nginx会因为MIME不对拒绝流式编译wasm。解决办法是在nginx的http块里加上types { application/wasm wasm; }或者在location /h5player/里加上避免影响全局配置。第二种是nginx开启了gzip且压缩级别过高导致wasm文件传输后无法流式编译。wasm本身已经是高度压缩的二进制格式强行再gzip不仅收益低还可能引发兼容问题。我解决这个问题的方法是给wasm目录单独关闭gziplocation /h5player/wasm/ { gzip off; add_header Cache-Control no-cache; }这样浏览器拿到的就是原原始wasm文件流式编译直接成功加载速度也更快。5.3 服务端口起不来端口范围与防火墙表现是初始化时JS_StartService返回失败或者player初始化后长时间不触发成功回调。先在浏览器端打开几个调试请求多半会看到端口连接被拒绝。用命令行检查端口被谁占用netstat -ano | findstr 15900如果端口被占就把iServicePortEnd扩大同时检查Windows防火墙或Linux安全组是否放行这个端口范围。内网部署时还有一个隐藏点如果服务器有多个网卡JS_StartService绑定本机地址时可能绑到了127.0.0.1而浏览器访问的是另一个内网IP导致连接被拒。遇到这种情况在Demo里找找是否存在绑定地址的配置项没有的话就只能在部署层面统一用本机地址访问。5.4 混合内容拦截HTTPS页面与HTTP取流表现是页面使用HTTPS部署但H5player请求取流地址是http://浏览器直接把请求拦成灰色控制台报Mixed Content。这个问题的本质是所有现代浏览器都强制要求HTTPS页面内不能发HTTP请求。解决办法有两条路。第一条是能上HTTPS就全上HTTPS取流地址也用https://或者wss://这需要后端配合部署证书。第二条是如果现场环境实在做不到就把整个应用降级为HTTP访问页面是HTTP浏览器就不拦HTTP的取流请求。第三条路是让后端提供一个HTTPS的反向代理网关前端始终走https://域名/live/xxx代理再转给后端的HTTP取流服务。5.5 RTSP和m3u8的边界哪些能直接播很多第一次用H5player的人习惯性地把RTSP地址直接传给playURL然后发现一直转圈。这里要明确一个边界浏览器本身没有RTSP协议的解析能力H5player V2.1.2能直连的通常是海康私有协议或者平台下发的HTTP-FLV/WS-FLV流。设备端的RTSP地址需要经过海康的流媒体服务或自己搭一个转码服务转成H5player支持的协议后才能播。至于m3u8H5player并不是专门为HLS设计的播放器。如果你手里的流是标准HLS用hls.js或video.js会更合适别让H5player去硬解HLS那样既增加维护成本又容易踩兼容性的坑。如果业务里既有海康私有流又有少量m3u8地址我建议用H5player管私有流用独立的hls.js播放器管m3u8各管各的互不干扰。6. 后端、nginx和移动终端的协同配置6.1 nginx静态资源与代理配置H5player部署到生产环境后涉及两层nginx配置。第一层是静态资源第二层是取流代理。下面这份配置是我在项目里用得比较顺手的模板server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /opt/www/dist; try_files $uri $uri/ /index.html; } # H5player开发包资源 location /h5player/ { alias /opt/www/h5player/; gzip off; types { application/wasm wasm; } add_header Cache-Control no-cache; } # 取流代理示例把 /live/ 前缀转给后端流媒体服务 location /live/ { proxy_pass http://127.0.0.1:8088; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; } }proxy_read_timeout 3600s很关键取流是长连接默认的60秒超时会让视频画面每隔一分钟断一次。现场查过一次“画面一分钟准时黑屏”的问题就是这个超时时间在作怪。设置成1小时以上基本能覆盖绝大多数监控场景。如果取流走的是WebSocket注意Upgrade和Connection两个header要带上否则ws://握手会失败。6.2 移动端和低性能设备上的降级思路H5player虽然能在移动端浏览器跑但性能远不如PC。手机Chrome和Safari对WebAssembly的支持总体是好的但解码多路高清流时手机会明显发热掉电很快卡顿也频繁。所以移动端页面我一般做两件事。第一件事是限制单页并发路数手机页面默认只拉主码流或者子码流尽量避免同时播放超过4路。分辨率上优先选择子码流具体可以在playURL地址或者播放信息里切换码流类型。第二件事是增加手动开关。页面提供“标清/高清”切换默认标清用户需要看清楚细节再切高清。这样既照顾了性能也保留了用户看清晰画面的可能性。实测下来这种做法在4G网络环境下特别管用卡顿率比无脑拉高清至少下降一半。移动端另一个容易忽视的点是自动播放策略。iOS Safari对自动播放有严格限制没有用户手势的播放大概率会被拒绝。所以移动端页面在进入时不要一上来就JS_Play要先弹一次“点击进入监控”的引导按钮用户点击后再初始化播放器并触发播放。这样既符合浏览器的自动播放策略也给了用户知情权算是一举两得。最后再补充一个开发包版本管理的经验。H5player V2.1.2是官方开发包没有发布到npm仓库所以版本统一靠“下载后整体提交到项目里”来管理。我习惯在项目根目录放一个THIRD_PARTY.md记录开发包版本号、下载时间、从哪个页面获取的、解压后是否改过文件。这个文件在后续接手同事排错时价值很大能快速判断他用的包版本和线上是不是一致。官方升级新版本后先跑通官方demo再替换到项目里别拿着旧配置直接套新包我见过好几次升级后wasm路径变了导致整个播放器白屏的案例。
返回列表