vue实现自定义H5视频播放器的方法步骤
脚本之家 / 编程助手:解决程序员“几乎”所有问题!
脚本之家官方知识库 → 点击立即使用
前言
前段时间基于vue写了一个自定义的video播放器组件,踩了一些小坑, 这里做一下复盘分享出来,避免日后重复踩坑...
设计阶段
这里就直接放几张完成后的播放状态图吧,界面布局基本就是flex+vw适配一把梭,也比较容易.
需要实现的几个功能基本都标注出来了; 除了还有一个视频加载失败的...下面就这届上代码了;刚开始构思的时候考虑了一下功能的实现方式: 一是用原生的DOM操作,获取video元素后,用addEventListener来监听; 二是用vue的方式绑定事件监听; 最后图方便采用了两者结合的方式,但是总感觉有点乱, 打算后期再做一下代码格式优化.
video组件实现过程
组件模板部分
主要是播放器的几种播放状态的逻辑理清楚就好了, 即: 播放中,缓存中,暂停,加载失败这几种情况,下面按功能分别说一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | <template> <div class= "video-player" > <!-- 播放器界面; 兼容ios controls--> <video ref= "video" v- if = "showVideo" webkit-playsinline= "true" playsinline= "true" x-webkit-airplay= "true" x5-video-player-type= "h5" x5-video-player-fullscreen= "true" x5-video-orientation= "portraint" style= "object-fit:fill" preload= "auto" muted= "true" poster= "https://photo.mac69.com/180205/18020526/a9yPQozt0g.jpg" :src= "src" @waiting= "handleWaiting" @canplaythrough= "state.isLoading = false" @playing= "state.isLoading = false, state.controlBtnShow = false, state.playing=true" @stalled= "state.isLoading = true" @error= "handleError" >您的浏览器不支持HTML5</video> <!-- 兼容Android端层级问题, 弹出层被覆盖 --> <img v-show= "!showVideo || state.isEnd" class= "poster" src= "https://photo.mac69.com/180205/18020526/a9yPQozt0g.jpg" alt > <!-- 控制窗口 --> <div class= "control" v-show= "!state.isError" ref= "control" @touchstart= "touchEnterVideo" @touchend= "touchLeaveVideo" > <!-- 播放 || 暂停 || 加载中--> <div class= "play" @touchstart.stop= "clickPlayBtn" v-show= "state.controlBtnShow" > <img v-show= "!state.playing && !state.isLoading" src= "../../assets/video/content_btn_play.svg" > <img v-show= "state.playing && !state.isLoading" src= "../../assets/video/content_btn_pause.svg" > <div class= "loader" v-show= "state.isLoading" > <div class= "loader-inner ball-clip-rotate" > <div></div> </div> </div> </div> <!-- 控制条 --> <div class= "control-bar" :style= "{ visibility: state.controlBarShow ? 'visible' : 'hidden'}" > <span class= "time" >{{video.displayTime}}</span> <span class= "progress" ref= "progress" > <img class= "progress-btn ignore" :style= "{transform: `translate3d(${video.progress.current}px, 0, 0)`}" src= "../../assets/video/content_ic_tutu.svg" > <span class= "progress-loaded" :style= "{ width: `${video.loaded}%`}" ></span> <!-- 设置手动移动的进度条 --> <span class= "progress-move" @touchmove.stop.prevent= "moveIng($event)" @touchstart.stop= "moveStart($event)" @touchend.stop= "moveEnd($event)" ></span> </span> <span class= "total-time" >{{video.totalTime}}</span> <span class= "full-screen" @click= "fullScreen" > <img src= "../../assets/video/content_ic_increase.svg" alt> </span> </div> </div> <!-- 错误弹窗 --> <div class= "error" v-show= "state.isError" > <p class= "lose" >视频加载失败</p> <p class= "retry" @click= "retry" >点击重试</p> </div> </div> </template> |
播放器初始化
这里有个坑点我就是当父元素隐藏即display:none时,getBoundingClientRect()是获取不到元素的尺寸数值的,后来查了MDN文档,按上面说的改了一下border也没有用,最后尝试设置元素visibility属性为hidden后发现就可以获取了.
getBoundingClientRect() : 返回元素的大小及其相对于视口的位置, 这个api在计算元素相对位置的时候挺好用的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | init() { // 初始化video,获取video元素 this .$video = this .$el.getElementsByTagName( "video" )[0]; this .initPlayer(); }, // 初始化播放器容器, 获取video-player元素 // getBoundingClientRect()以client可视区的左上角为基点进行位置计算 initPlayer() { const $player = this .$el; const $progress = this .$el.getElementsByClassName( "progress" )[0]; // 播放器位置 this .player.$player = $player; this .progressBar.$progress = $progress; this .player.pos = $player.getBoundingClientRect(); this .progressBar.pos = $progress.getBoundingClientRect() this .video.progress.width = Math.round($progress.getBoundingClientRect().width); }, |
播放 && 暂停点击
我这里把事件监听都放在只有满足正在播放视频才开始事件监听; 感觉原生监听和vue方式的监听混合在一起写有点别扭...emem...这里需要对this.$video.play()做一个异常处理,防止video刚开始加载的时候失败,如果视频链接出错,play方法调用不了会抛错,后面我也用了video的error事件去监听播放时的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // 点击播放 & 暂停按钮 clickPlayBtn() { if ( this .state.isLoading) return ; this .isFirstTouch = false ; this .state.playing = ! this .state.playing; this .state.isEnd = false ; if ( this .$video) { // 播放状态 if ( this .state.playing) { try { this .$video.play(); this .isPauseTouch = false ; // 监听缓存进度 this .$video.addEventListener( "progress" , e => { this .getLoadTime(); }); // 监听播放进度 this .$video.addEventListener( "timeupdate" , throttle( this .getPlayTime, 100, 1) ); // 监听结束 this .$video.addEventListener( "ended" , e => { // 重置状态 this .state.playing = false ; this .state.isEnd = true ; this .state.controlBtnShow = true ; this .video.displayTime = "00:00" ; this .video.progress.current = 0; this .$video.currentTime = 0; }); } catch (e) { // 捕获url异常出现的错误 } } // 停止状态 else { this .isPauseTouch = true ; this .$video.pause(); } } }, |
视频控制条显示和隐藏
这里需要加两个开关; 首次触屏和暂停触屏; 做一下显示处理即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // 触碰播放区 touchEnterVideo() { if ( this .isFirstTouch) return ; if ( this .hideTimer) { clearTimeout( this .hideTimer); this .hideTimer = null ; } this .state.controlBtnShow = true ; this .state.controlBarShow = true ; }, // 离开播放区 touchLeaveVideo() { if ( this .isFirstTouch) return ; if ( this .hideTimer) { clearTimeout( this .hideTimer); } // 暂停触摸, 不隐藏 if ( this .isPauseTouch) { this .state.controlBtnShow = true ; this .state.controlBarShow = true ; } else { this .hideTimer = setTimeout(() => { this .state.controlBarShow = false ; // 加载中只显示loading if ( this .state.isLoading) { this .state.controlBtnShow = true ; } else { this .state.controlBtnShow = false ; } this .hideTimer = null ; }, 3000); } }, |
视频错误处理和等待处理
这里错误直接用error事件, 加载中用stalled事件来监听视频阻塞状态,等待数据加载用的waiting事件; 显示对应的loading动画即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // loading动画 @keyframes rotate { 0% { transform: rotate(0deg); } 50% { transform: rotate(180deg); } 100% { transform: rotate(360deg); } } .loader { width: 58px; height: 58px; background: rgba(15, 16, 17, 0.3); border-radius: 50%; position: relative; .ball-clip-rotate { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); > div { width: 15px; height: 15px; border-radius: 100%; margin: 2px; animation-fill-mode: both; border: 2px solid #fff; border-bottom-color: transparent; height: 26px; width: 26px; background: transparent; display: inline-block; animation: rotate 0.75s 0s linear infinite; } } } |
播放时间设置
基本就是video对象的currentTime和duration这两个属性; 这里注意下视频如果没有设置预加载属性preload的话,在video元素初始化的时候是获取不到duration的...那你只能在播放的时候去拿了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 获取播放时间 getPlayTime() { const percent = this .$video.currentTime / this .$video.duration; this .video.progress.current = Math.round( this .video.progress.width * percent ); // 赋值时长 this .video.totalTime = timeParse( this .$video.duration); this .video.displayTime = timeParse( this .$video.currentTime); }, // 获取缓存时间 getLoadTime() { // console.log('缓存了...',this.$video.buffered.end(0)); this .video.loaded = ( this .$video.buffered.end(0) / this .$video.duration) * 100; }, |
手动滑动进度条控制
这里直接用touch事件即可; 注意touchend中使用e.changedTouches;因为当手指离开屏幕,touches和targetTouches中对应的元素会同时移除,而changedTouches仍然会存在元素。
- touches: 当前屏幕上所有触摸点的列表;
- targetTouches: 当前对象上所有触摸点的列表;
- changedTouches: 涉及当前(引发)事件的触摸点的列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 手动调节播放进度 moveStart(e) {}, moveIng(e) { // console.log("触摸中..."); let currentX = e.targetTouches[0].pageX; let offsetX = currentX - this .progressBar.pos.left; // 边界检测 if (offsetX <= 0) { offsetX = 0 } if (offsetX >= this .video.progress.width) { offsetX = this .video.progress.width } this .video.progress.current = offsetX; let percent = this .video.progress.current / this .video.progress.width; this .$video.duration && this .setPlayTime(percent, this .$video.duration) }, moveEnd(e) { // console.log("触摸结束..."); let currentX = e.changedTouches[0].pageX; let offsetX = currentX - this .progressBar.pos.left; this .video.progress.current = offsetX; // 这里的offsetX都是正数 let percent = offsetX / this .video.progress.width; this .$video.duration && this .setPlayTime(percent, this .$video.duration) }, // 设置手动播放时间 setPlayTime(percent, totalTime) { this .$video.currentTime = Math.floor(percent * totalTime); }, |
全屏功能
这个功能在手机上会有写兼容性问题...有待完善
1 2 3 4 5 6 7 8 9 10 | // 设置全屏 fullScreen() { console.log( '点击全屏...' ); if (! this .state.fullScreen) { this .state.fullScreen = true ; this .$video.webkitRequestFullScreen(); } else { this .state.fullScreen = false ; document.webkitCancelFullScreen(); } |
坑点汇总
1.视频预加载才能获取时长
需要设置预加载 preload="auto"
2.Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置
父元素设置display:none时获取不到尺寸数据民谣改为visibility:hidden
3.play()方法异常捕获
try{ xxxxx.play } catch(e) { yyyyyy }
4.安卓手机video兼容性处理, 视频播放时层级置顶,会影响全局弹出层样式
我这里做的处理是当弹出层出现时把视频给隐藏掉(宽高为0,或者直接去掉),用封面图来替代
5.ios下全屏处理
设置相应属性即可, playsinline
代码直通车: https://github.com/appleguardu/vue-h5-video
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
微信公众号搜索 “ 脚本之家 ” ,选择关注
程序猿的那些事、送书等活动等着你
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 reterry123@163.com 进行投诉反馈,一经查实,立即处理!
相关文章
解决Vue.js 2.0 有时双向绑定img src属性失败的问题
下面小编就为大家分享一篇解决Vue.js 2.0 有时双向绑定img src属性失败的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧2018-03-03vue3使用threejs实现3D卡片水平旋转效果的示例代码
这篇文章主要介绍了在vue3中使用threejs实现3D卡片水平旋转效果,文中通过代码示例讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下2024-04-04vue 中使用 vxe-table 制作可编辑表格的使用过程
这篇文章主要介绍了vue 中使用 vxe-table 制作可编辑表格的使用过程,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2021-08-08
最新评论