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 相关文章推荐
线路分流自动智能跳转代码,自动选择最快镜像网站(js)
Oct 31 Javascript
JavaScript高级程序设计阅读笔记(十六) javascript检测浏览器和操作系统-detect.js
Aug 14 Javascript
JavaScript控制各种浏览器全屏模式的方法、属性和事件介绍
Apr 03 Javascript
JS基于面向对象实现的放烟花效果
May 07 Javascript
jQuery插件EasyUI校验规则 validatebox验证框
Nov 29 Javascript
原生JavaScript实现滚动条效果
Mar 24 Javascript
AngularJS中过滤器的使用与自定义实例代码
Sep 17 Javascript
bootstrap基础知识学习笔记
Nov 02 Javascript
JS作用域链详解
Jun 26 Javascript
layui 弹出删除确认界面的实例
Sep 06 Javascript
vue+elementui通用弹窗的实现(新增+编辑)
Jan 07 Vue.js
vue实现书本翻页动画效果实例详解
Apr 08 Vue.js
基于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
反射调用private方法实践(php、java)
2015/12/21 PHP
PHP获取用户客户端真实IP的解决方案
2016/10/10 PHP
PHP从零开始打造自己的MVC框架之类的自动加载实现方法详解
2019/06/03 PHP
JavaScript几种形式的树结构菜单
2010/05/10 Javascript
非常强大的 jQuery.AsyncBox 弹出对话框插件
2011/08/29 Javascript
基于jquery实现点击左右按钮图片横向滚动
2013/04/11 Javascript
轻松学习jQuery插件EasyUI EasyUI表单验证
2015/12/01 Javascript
jQuery实现的超简单点赞效果实例分析
2015/12/31 Javascript
JavaScript的ExtJS框架中表格的编写教程
2016/05/21 Javascript
使用js获取地址栏参数的方法推荐(超级简单)
2016/06/14 Javascript
jquery实现(textarea)placeholder自动换行
2016/12/22 Javascript
SpringMVC简单整合Angular2的示例
2017/07/31 Javascript
微信小程序开发animation心跳动画效果
2017/08/16 Javascript
JS中的算法与数据结构之集合(Set)实例详解
2019/08/20 Javascript
浅谈Vue3.0之前你必须知道的TypeScript实战技巧
2019/09/11 Javascript
js实现简单点赞操作
2020/03/17 Javascript
vue实现给div绑定keyup的enter事件
2020/07/31 Javascript
Vue双向数据绑定(MVVM)的原理
2020/10/03 Javascript
[34:47]完美世界DOTA2联赛PWL S2 Magma vs LBZS 第一场 11.18
2020/11/18 DOTA
[07:25]DOTA2-DPC中国联赛2月5日Recap集锦
2021/03/11 DOTA
python读写LMDB文件的方法
2018/07/02 Python
TensorFlow实现简单的CNN的方法
2019/07/18 Python
微信小程序python用户认证的实现
2019/07/29 Python
Python根据服务获取端口号的方法
2019/09/25 Python
解析PyCharm Python运行权限问题
2020/01/08 Python
CSS3中Transform动画属性用法详解
2016/07/04 HTML / CSS
Expedia瑞典官网:预订度假屋、酒店、汽车租赁、机票等
2021/01/23 全球购物
2014全国两会心得体会
2014/03/17 职场文书
岗位说明书范文
2014/05/07 职场文书
军训口号
2014/06/13 职场文书
如何写早恋检讨书
2014/09/10 职场文书
党的群众路线教育实践活动个人整改落实情况汇报
2014/10/28 职场文书
合伙经营协议书范本(通用版)
2014/12/03 职场文书
六一领导慰问欢迎词
2015/01/26 职场文书
重阳节简报
2015/07/20 职场文书
Jupyter notebook 输出部分显示不全的解决方案
2021/04/24 Python