一、安装项目所需依赖
videojs依赖:
npm install --save-dev video.js
elementui依赖(这个图方便就不按需引入了):
npm i element-ui -S
二、main.js修改
增加以下几行:
import videojs from 'video.js'import elemenui from 'element-ui'import 'element-ui/lib/theme-chalk/index.css';Vue.prototype.$videoJs = videojs;Vue.use(elemenui)
三、准备html结构
1、准备两个组件
在components文件夹下创建两个组件videoComponent和videoPlayer——
videoComponent挂载到App组件上
videoPlayer挂载到videoComponent上
2、各组件中的html结构
先把两个组件最基本的结构搭好
videoComponent:
<template> <div class="container"> <video-player :options="videoOptions" class="video-css"></video-player> </div></template><script> import videoPlayer from './videoPlayer.vue' export default { name: 'videoComponent', components:{videoPlayer}, data(){ return{ videoOptions:{ //一些视频的配置项,见下面补充... } } }</script><style scoped> .container{ width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; } /* 视频宽高将由此样式调整 */ .video-css{ width: 800px; height: auto; }</style>
videoPlayer:
<template> <div class="videoBox"> <video ref="videoPlayer" class="video-js"></video> </div></template><script> import 'video.js/dist/video-js.css'; export default { name:'videoPlayer', }</script><style scoped> .videoBox{ box-sizing: border-box; position: relative; width: 800px; height:500px; }</style>
App:
<template> <div id="app"> <video-component></video-component> </div></template><script> import videoComponent from './components/videoComponent.vue' export default { name: 'App', components: { videoComponent } }</script><style> *{ padding: 0; margin: 0; }</style>
3、在videoPlayer组件里实例化播放器
<script> import 'video.js/dist/video-js.css'; export default { name:'videoPlayer', //接收外部组件来的参数,可实现videoPlayer组件的复用 props:{ options: { type: Object } }, data(){ return{ player:null } }, methods:{ //定义好一个实例化播放器的方法 createVideoPlayer(){ this.player = this.$videoJS(this.$refs.videoPlayer,this.options) } }, //在组件挂载时调用播放器实例化方法 mounted(){ this.createVideoPlayer() }, //组件销毁前销毁播放器 beforeDestroy(){ if(this.player){ this.player.dispose() } } }</script>
4、videoComponent传递视频参数
还有很多配置项,有需要自己查
<script> import videoPlayer from './videoPlayer.vue' export default { name: 'videoComponent', components:{videoPlayer}, data(){ return{ videoOptions:{ //controls配置项决定是否显示默认控件 controls:true, //fluid配置项根据外层css样式大小,自动填充宽高 fluid:true, //sources配置项配置视频播放源 sources:[ { //播放源 src:require('@/assets/kp.mp4'), //视频类型 type:"video/mp4" } ] } } }, }</script>
在assets文件夹下事先放好一个视频,在配置项sources里面改一下视频路径。
走到这一步,运行项目一般可以正常播放视频了:
接下来就可以实现播放器自定义控件
5、自定义控件的html结构
确定视频可以正常播放后就可以着手自定义控件了,控件写在videoPlayer这个组件里。先上结构,这里图标用的都是elementui的icon,音量没找到合适的就凑合着用了其他图标:
<template> <div class="videoBox"> <video ref="videoPlayer" class="video-js" > </video> <!-- 自定义控件 --> <div class="controlBar"> <div class="progressBar" @mousedown="isDraging = true" @mouseup="isDraging = false"> <el-slider v-model="currentTimeVal" :max="totalTimeVal" :format-tooltip="timeFormat" @change="progressUpdate" > </el-slider> </div> <div class="controlBtnBox"> <div class="left"> <i class="el-icon-video-play icon-size"></i> <span>00:00:00 / 00:00:00</span> </div> <div class="right"> <i class="el-icon-d-arrow-left icon-size" @click="back(player.currentTime())"></i> <i class="el-icon-d-arrow-right icon-size" @click="forward(player.currentTime())"></i> <i class="el-icon-bell icon-size" @click="toShowVolumeBar"></i> <div id="volumeBar"> <el-slider v-model="volume" vertical height="100px"> </el-slider> </div> </div> <div class="rateBox"> <span @click="toShowOptions">{{ratedisplay}}</span> <div class="rateOptions" v-show="isShowRateOptions"> <span v-for="r,index in rateOptions" :key="index" @click="setPlayRate(r)" > {{r}}x </span> </div> </div> </div> </div> </div></template>
结构效果图最终长这样,这里样式不多废话,跟完整代码放在文章最后面。
6、准备数据
结构搞定之后,在data里准备好后续需要用到的数据
data(){ return{ player:null, //当前播放速率 rate:1.0, //播放速率 rateOptions:[2.0,1.75,1.5,1.0,0.75,0.5], //显示速率选项 isShowRateOptions:false, //音量 volume:30, //是否暂停 isPaused:true, //当前播放时间点和视频总时长 currentTime:'00:00:00', totalTime:'00:00:00', //进度条的当前值,必须为number类型 currentTimeVal:0, //进度条最大值,必须为number类型 totalTimeVal:0, //是否在拖到进度条 isDraging:false } },
rate和rateOptions用在哪上面的代码已经写了,isShowRateOptions后面用来隐藏倍速选项那个框的这里不重要先不去理。主要是volume,它绑定在el-slider上是该滑块的默认值,后面改音量会用到;currentTime和 totalTime用于动态显示当前视频播放的具体时间,以及视频的总时长; currentTimeVal和totalTimeVal是改变进度条、实现时长跳转的主要数据;isPaused用来决定自定义控件的图标是暂停还是播放。
7、播放暂停、进度条实时跟进,拉到进度条实现跳转
首先从简单的暂停和播放做起。在html结构中,我们应该根据isPaused来决定显示哪个图标,同时将前面显示时长的假数据换成data里的currentTime和totalTime:
<i :class="[isPaused ? 'el-icon-video-play' : 'el-icon-video-pause']" class=" icon-size" v-show="isPaused"></i><span>{{currentTime}} / {{totalTime}}</span>
然后给图标绑定上一个togglePlay的函数,来响应点击后实现播放或暂停:
//控制视频的播放与暂停 togglePlay(){ this.isPaused = !this.isPaused if(!this.isPaused){ this.player.play() }else{ this.player.pause() } },
时间格式化:
//视频时长格式化timeFormat(time){ let hour = Math.floor(time / 3600), minute = Math.floor((time % 3600) / 60), second = Math.floor(time % 60); hour = hour < 10 ? "0" + hour : hour; minute = minute < 10 ? "0" + minute : minute; second = second < 10 ? "0" + second : second; return `${hour}:${minute}:${second}`;},
获取视频总时长:
//获取视频的总时长和进度条最大值getTotalTime(){ this.totalTime = this.timeFormat(this.player.duration()) this.totalTimeVal = Math.floor(this.player.duration())},
更新视频当前播放时间、进度条实时跟进:
进度条的实现原理其实就是video的timeUpdate事件可以监测到视频的播放进度,在这个事件中可以一直获取到视频当前的播放时间,然后将这个值赋给滑块绑定的currentTimeVal,这样就能在播放过程中跟着改变滑块的位置了。
//更新视频当前播放时间timeUpdate(){ //如果当前正在拉到进度条,先停止更新当前播放时间,直接return结束这个函数 //没有这一句会出现拉动进度条跳转失败的bug if(this.isDraging) return this.currentTime = this.timeFormat(this.player.currentTime()) this.currentTimeVal = this.player.currentTime() //当前时间更新到等于总时长时,要改变视频的播放状态按钮 if(this.currentTime === this.totalTime){ this.isPaused = true } },
拉到进度条跳转到指定位置播放:
这一功能的实现就是el-slider有一个change事件,在拖拽滑块松开鼠标后触发,这时只要在鼠标松开后,改变播放器的currentTime属性的值,
这里要稍微注意一下:因为我们在拉动进度条的时候,视频还处于播放状态,那么意味着上一步我们更新进度条时长的那个函数获取到的currentTime值也会改变el-slider的值,所以在上一步的函数中,我们需要监测进度条是否在拉动,如果是,我们应该停止执行那个函数。监听只需要在进度条外层的div上绑定一个mouseon和mousedown事件,鼠标按住时让isDragging等于false,然后在timeUpdate函数中通过isDragging来判断进度条是否处于拖拽的状态。
//进度条拉动时更新进度条值并从拉到的位置播放progressUpdate(val){ this.player.currentTime(val) // 虽然mouseup已经可以改变isDraging的值,但下面这句不能少,不然视频播放结束再点击播放时,进度条不会回到最开始位置 this.isDraging = false },
8、更新速率、改变音量
//改变速率setPlayRate(rate){ this.rate = rate; this.player.playbackRate(rate); this.isShowRateOptions = false;},//改变音量changeVolume(val){ this.volume = val //由于h5规定volum的值在0-1之间,所以这里要对获取到的val做一个处理(滑块的val是从0-100) this.player.volume(val / 100)},//快进forward(ct){ this.progressUpdate(ct + 10)},//后退back(ct){ this.progressUpdate(ct - 10)}
9、完整代码
VideoComponent:
<template> <div class="container"> <video-player :options="videoOptions" class="video-css" > </video-player> </div></template><script> import videoPlayer from './videoPlayer.vue' export default { name: 'videoComponent', components:{videoPlayer}, data(){ return{ videoOptions:{ //controls配置项决定是否显示默认控件,因为这里要做自定义的控件,就不显示了 controls:false, //fluid配置项根据外层css样式大小,自动填充宽高 fluid:true, //sources配置项配置视频播放源 sources:[ { //播放源 src:require('@/assets/kp.mp4'), //视频类型 type:"video/mp4" } ], } } }, }</script><style scoped> .container{ width: 800px; height: 100vh; display: flex; justify-content: center; align-items: center; } /* 视频宽高由此样式调整 */ .video-css{ width: 800px; height:auto; }</style>
VideoPlayer:
<template> <div class="videoBox"> <video ref="videoPlayer" class="video-js" @canplay="getTotalTime" @timeupdate="timeUpdate" > </video> <!-- 自定义控件 --> <div class="controlBar"> <div class="progressBar" @mousedown="isDraging = true" @mouseup="isDraging = false"> <el-slider v-model="currentTimeVal" :max="totalTimeVal" :format-tooltip="timeFormat" @change="progressUpdate" > </el-slider> </div> <div class="controlBtnBox"> <div class="left"> <i :class="[isPaused ? 'el-icon-video-play' : 'el-icon-video-pause']" class=" icon-size" @click="togglePlay()" > </i> <span>{{currentTime}}/{{totalTime}}</span> </div> <div class="right"> <i class="el-icon-d-arrow-left icon-size" @click="back(player.currentTime())></i> <i class="el-icon-d-arrow-right icon-size" @click="forward(player.currentTime())></i> <i class="el-icon-bell icon-size" @click="toShowVolumeBar"></i> <div id="volumeBar" v-show="isShowVolumeBar"> <el-slider v-model="volume" vertical height="100px" @input="changeVolume" > </el-slider> </div> </div> <div class="rateBox"> <span @click="toShowOptions">{{ratedisplay}}</span> <div class="rateOptions" v-show="isShowRateOptions"> <span v-for="r,index in rateOptions" :key="index" @click="setPlayRate(r)" > {{r}}x </span> </div> </div> </div> </div> </div></template><script> import 'video.js/dist/video-js.css'; export default { name:'videoPlayer', //接收来自父组件videoComponent的video的具体配置信息,这样可以实现对VideoPlayer组件的复用 props:{ options: { type: Object } }, //用计算属性来实现当速率为1时,显示“倍速” computed:{ ratedisplay(){ if(this.rate == 1){ return '倍速' }else{ return this.rate + 'x' } } }, data(){ return{ player:null, //当前播放速率 rate:1.0, //播放速率 rateOptions:[2.0,1.75,1.5,1.0,0.75,0.5], //显示速率选项和音量选项 isShowRateOptions:false, isShowVolumeBar:false, //音量 volume:30, //是否暂停 isPaused:true, //当前播放时间点和视频总时长 currentTime:'00:00:00', totalTime:'00:00:00', //进度条的当前值,必须为number类型 currentTimeVal:0, //进度条最大值,必须为number类型 totalTimeVal:0, //是否在拖到进度条 isDraging:false } }, methods:{ createVideoPlayer(){ this.player = this.$videoJS(this.$refs.videoPlayer,this.options) }, //显示速率选项 toShowOptions(){ this.isShowRateOptions = !this.isShowRateOptions }, toShowVolumeBar(){ this.isShowVolumeBar = !this.isShowVolumeBar }, //视频时长格式化 timeFormat(time){ let hour = Math.floor(time / 3600), minute = Math.floor((time % 3600) / 60), second = Math.floor(time % 60); hour = hour < 10 ? "0" + hour : hour; minute = minute < 10 ? "0" + minute : minute; second = second < 10 ? "0" + second : second; return `${hour}:${minute}:${second}`; }, //获取视频的总时长和进度条最大值 getTotalTime(){ this.totalTime = this.timeFormat(this.player.duration()) this.totalTimeVal = Math.floor(this.player.duration()) }, //改变速率 setPlayRate(rate){ this.rate = rate; this.player.playbackRate(rate); this.isShowRateOptions = false; }, //控制视频的播放与暂停 togglePlay(){ console.log() this.isPaused = !this.isPaused if(!this.isPaused){ this.player.play() }else{ this.player.pause() } }, //更新视频当前播放时间 timeUpdate(){ //如果当前正在拉到进度条,先停止更新当前播放时间,直接return结束这个函数 //没有这一句会出现拉动进度条跳转失败的bug if(this.isDraging) return this.currentTime = this.timeFormat(this.player.currentTime()) this.currentTimeVal = this.player.currentTime() //当前时间更新到等于总时长时,要改变视频的播放状态按钮 if(this.currentTime === this.totalTime){ this.isPaused = true } }, //进度条拉动时更新进度条值并从拉到的位置播放 progressUpdate(val){ this.player.currentTime(val) // 虽然mouseup已经可以改变isDraging的值,但下面这句不能少,不然视频播放结束再点击播放时,进度条不会回到最开始位置 this.isDraging = false }, //改变音量 changeVolume(val){ this.volume = val //由于h5规定volum的值在0-1之间,所以这里要对获取到的val做一个处理(滑块的val是从0-100) this.player.volume(val / 100) }, //快进 forward(ct){ this.progressUpdate(ct + 10) }, //后退 back(ct){ this.progressUpdate(ct - 10) } }, mounted(){ this.createVideoPlayer() }, beforeDestroy(){ if(this.player){ this.player.dispose() } } }</script><style scoped> .videoBox{ box-sizing: border-box; position: relative; width: 800px; height:500px; background-color: rgb(73, 156, 128); } .controlBar{ width: 90%; height: 55px; position:absolute; bottom: 20px; left: 5%; background-color:#817f7f5a; box-sizing: border-box; color: rgb(233, 231, 231); } .progressBar{ box-sizing: border-box; position: relative; width: 100%; padding: 10px; height: 10%; /* background-color: aliceblue; */ } .controlBtnBox{ box-sizing: border-box; width: 100%; height:60%; display: flex; justify-content: space-between; align-items: center; } /* 以下强行修改了el-slider样式 */ .progressBar /deep/ .el-slider__bar{ height: 3px; background-color: #409EFF; border-top-left-radius: 3px; border-bottom-left-radius: 3px; position: absolute; } .progressBar /deep/ .el-slider__button{ height: 8px; width: 8px; } .progressBar /deep/ .el-slider__runway{ margin-top:1px; margin-bottom: 1px; height: 3px; } .progressBar /deep/.el-slider__button-wrapper{ width: 28px; height: 33px; } .icon-size{ font-size: 25px; cursor: pointer; } .left{ padding-left:10px ; width: 50%; display: flex; align-items: center; } .left span{ margin-left: 20px; } .right{ width: 15%; display: flex; justify-content: space-around; position: relative; } .right i{ display: block; } #volumeBar{ width: 30px; height: 120px; background-color: #817f7f5a; position: absolute; top:-150px; right: 4px; display: flex; justify-content: center; align-items: center; } .rateBox{ width: 15%; cursor: pointer; } .rateOptions{ width: 80px; height: 180px; background-color: #817f7f5a; position: absolute; top:-185px; right: 50px; display: flex; flex-wrap: wrap; align-content: center; } .rateOptions span{ display: block; width: 100%; height: 30px; text-align: center; line-height: 30px; } .rateOptions span:hover{ background-color: #cec9c95a; color: #409EFF; }</style>
App:
<template> <div id="app"> <video-component></video-component> </div></template><script> import videoComponent from './components/videoComponent.vue' export default { name: 'App', components: { videoComponent } }</script><style> *{ padding: 0; margin: 0; } #app{ display: flex; justify-content: center; width: 100%; }</style>
结束,暂时还没发现什么bug。
但测试用的是的视频是放在本地的,没有涉及到资源加载,如果视频资源是走网络请求的话还得再改。。
参考链接:
以下是实现的时候参考的几篇文章
vue中自定义视频:
vue 中 自定义视频video_粥粥_的博客-CSDN博客
video.js使用教程:
Video.js使用教程一(详解)_lucky-peach的博客-CSDN博客