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 相关文章推荐
两个DIV等高的JS的实现代码
Dec 23 Javascript
jQuery中的bind绑定事件与文本框改变事件的临时解决方法
Aug 13 Javascript
js常用代码段整理
Nov 30 Javascript
如何设置iframe高度自适应在跨域情况下的可用方法
Sep 06 Javascript
javascript不同类型数据之间的运算的转换方法
Feb 13 Javascript
require.js配合插件text.js实现最简单的单页应用程序
Jul 12 Javascript
使用JS 插件qrcode.js生成二维码功能
Feb 20 Javascript
BootStrap表单宽度设置方法
Mar 10 Javascript
three.js实现3D影院的原理的代码分析
Dec 18 Javascript
轻松搞定jQuery+JSONP跨域请求的解决方案
Mar 06 jQuery
微信小程序引用iconfont图标的方法
Oct 22 Javascript
jQuery实现飞机大战小游戏
Jul 05 jQuery
基于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
咖啡常见的种类
2021/03/03 新手入门
PHP中addslashes与mysql_escape_string的区别分析
2016/04/25 PHP
PHP新特性详解之命名空间、性状与生成器
2017/07/18 PHP
php判断文件上传图片格式的实例详解
2017/09/30 PHP
jQuery创建自己的插件(自定义插件)的方法
2010/06/10 Javascript
jquery制作LED 时钟特效
2015/02/01 Javascript
jQuery解析XML与传统JavaScript方法的差别实例分析
2015/03/05 Javascript
Javascript实现网络监测的方法
2015/07/31 Javascript
JavaScript图片轮播代码分享
2015/07/31 Javascript
JS文字球状放大效果代码分享
2015/08/19 Javascript
javascript日期处理函数,性能优化批处理
2015/09/06 Javascript
node.js cookie-parser 中间件介绍
2016/06/06 Javascript
JS实现简单易用的手机端浮动窗口显示效果
2016/09/07 Javascript
Jquery Easyui进度条组件Progress使用详解(8)
2020/03/26 Javascript
vue + webpack如何绕过QQ音乐接口对host的验证详解
2018/07/01 Javascript
用原生 JS 实现 innerHTML 功能实例详解
2019/04/03 Javascript
小程序接口的promise化的实现方法
2019/12/11 Javascript
[01:29]2017 DOTA2国际邀请赛官方英雄手办展示
2017/03/18 DOTA
简介Django框架中可使用的各类缓存
2015/07/23 Python
解决python nohup linux 后台运行输出的问题
2018/05/11 Python
Python使用pickle模块实现序列化功能示例
2018/07/13 Python
Python遍历文件夹 处理json文件的方法
2019/01/22 Python
python实现简单图书管理系统
2019/11/22 Python
Python解释器及PyCharm工具安装过程
2020/02/26 Python
python 制作本地应用搜索工具
2021/02/27 Python
澳大利亚波西米亚风情网上商店:Czarina
2019/03/18 全球购物
葡萄牙语专业个人求职信
2013/12/10 职场文书
考试违纪检讨书
2014/02/02 职场文书
《灯光》教学反思
2014/02/08 职场文书
信息技术教学反思
2014/02/12 职场文书
幼儿园开学寄语
2014/04/03 职场文书
营销与策划实训报告
2014/11/05 职场文书
接收函
2019/04/22 职场文书
Python中os模块的简单使用及重命名操作
2021/04/17 Python
VS2019连接MySQL数据库的过程及常见问题总结
2021/11/27 MySQL
Vscode中SSH插件如何远程连接Linux
2022/05/02 Servers