一、安装项目所需依赖
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。
但测试用的是的视频是放在本地的,没有涉及到资源加载,如果视频资源是走网络请求的话还得再改。。