vue实现自定义H5视频播放器的方法步骤


Posted in Javascript onJuly 01, 2019

前言

前段时间基于vue写了一个自定义的video播放器组件,踩了一些小坑, 这里做一下复盘分享出来,避免日后重复踩坑...

设计阶段

这里就直接放几张完成后的播放状态图吧,界面布局基本就是flex+vw适配一把梭,也比较容易.

vue实现自定义H5视频播放器的方法步骤

vue实现自定义H5视频播放器的方法步骤

vue实现自定义H5视频播放器的方法步骤

需要实现的几个功能基本都标注出来了; 除了还有一个视频加载失败的...下面就这届上代码了;刚开始构思的时候考虑了一下功能的实现方式: 一是用原生的DOM操作,获取video元素后,用addEventListener来监听; 二是用vue的方式绑定事件监听; 最后图方便采用了两者结合的方式,但是总感觉有点乱, 打算后期再做一下代码格式优化.

video组件实现过程

组件模板部分

主要是播放器的几种播放状态的逻辑理清楚就好了, 即: 播放中,缓存中,暂停,加载失败这几种情况,下面按功能分别说一下

<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在计算元素相对位置的时候挺好用的.

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事件去监听播放时的错误

// 点击播放 & 暂停按钮
  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();
    }
   }
  },

视频控制条显示和隐藏

这里需要加两个开关; 首次触屏和暂停触屏; 做一下显示处理即可

// 触碰播放区
  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动画即可

// 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的...那你只能在播放的时候去拿了.

// 获取播放时间
  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: 涉及当前(引发)事件的触摸点的列表
// 手动调节播放进度
  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);
  },

全屏功能

这个功能在手机上会有写兼容性问题...有待完善

// 设置全屏
  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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript判断两种格式的输入日期的正确性的代码
Mar 25 Javascript
js中自定义方法实现停留几秒sleep
Jul 11 Javascript
使用node.js半年来总结的 10 条经验
Aug 18 Javascript
jQuery获取file控件中图片的宽高与大小
Aug 04 Javascript
JS实现漂亮的时间选择框效果
Aug 20 Javascript
浅谈javascript alert和confirm的美化
Dec 15 Javascript
jQuery实现文字自动横移
Jan 08 Javascript
原生JS发送异步数据请求
Jun 08 Javascript
解决vue中虚拟dom,无法实时更新的问题
Sep 15 Javascript
微信小程序实现卡片层叠滑动效果
Jun 21 Javascript
node 解析图片二维码的内容代码实例
Sep 11 Javascript
详解Vue.js 响应接口
Jul 04 Javascript
基于Vue SEO的四种方案(小结)
Jul 01 #Javascript
JavaScript一元正号运算符示例代码
Jun 30 #Javascript
重学JS之显示强制类型转换详解
Jun 30 #Javascript
JavaScript判断浏览器运行环境的详细方法
Jun 30 #Javascript
微信小程序如何自定义table组件
Jun 29 #Javascript
微信小程序如何调用图片接口API并居中显示
Jun 29 #Javascript
微信小程序如何调用json数据接口并解析
Jun 29 #Javascript
You might like
isset和empty的区别
2007/01/15 PHP
php实现无限级分类
2014/12/24 PHP
PHP数组操作类实例
2015/07/11 PHP
详解PHP序列化和反序列化原理
2018/01/15 PHP
PHP基于GD2函数库实现验证码功能示例
2019/01/27 PHP
jQuery AJAX回调函数this指向问题
2010/02/08 Javascript
javascript实现节点(div)名称编辑
2014/12/17 Javascript
JavaScript使用Prototype实现面向对象的方法
2015/04/14 Javascript
深入浅析javascript中的作用域(推荐)
2016/07/19 Javascript
jquery根据一个值来选中select下的option实例代码
2016/08/29 Javascript
json定义及jquery操作json的方法
2016/10/03 Javascript
ionic2懒加载配置详解
2017/09/01 Javascript
BootStrap TreeView使用实例详解
2017/11/01 Javascript
详解vue-cli之webpack3构建全面提速优化
2017/12/25 Javascript
js中forEach,for in,for of循环的用法示例小结
2020/03/14 Javascript
JS如何实现封装列表右滑动删除收藏按钮
2020/07/23 Javascript
详解MySQL数据类型int(M)中M的含义
2016/11/20 Python
​如何愉快地迁移到 Python 3
2019/04/28 Python
python使用socket实现的传输demo示例【基于TCP协议】
2019/09/24 Python
PyTorch里面的torch.nn.Parameter()详解
2020/01/03 Python
如何利用Python识别图片中的文字
2020/05/31 Python
python不同系统中打开方法
2020/06/23 Python
python PyAUtoGUI库实现自动化控制鼠标键盘
2020/09/09 Python
Python实现随机爬山算法
2021/01/29 Python
借助HTML5 Canvas来绘制三角形和矩形等多边形的方法
2016/03/14 HTML / CSS
美国电视购物:QVC
2017/02/06 全球购物
三星印度官网:Samsung印度
2019/08/03 全球购物
给朋友的道歉信
2014/01/09 职场文书
入党申请自荐书范文
2014/02/11 职场文书
电子商务专业求职信
2014/07/10 职场文书
公司租房协议书范本
2014/10/08 职场文书
教师调动申请报告
2015/05/18 职场文书
婚庆公司开业主持词
2015/06/30 职场文书
2016春季田径运动会广播稿
2015/12/21 职场文书
导游词之无锡梅园
2019/11/28 职场文书
vue实现移动端div拖动效果
2022/03/03 Vue.js