原生video元素自定义播放控件controls
一、背景
之所以要写自定义播放控件,是因为使用**navigator.mediaDevices.getUserMedia()**做视频流的时候,现实中人的右手在视频中显示在左边【如图1】,所以要做video镜像,但通过css(transform:scale(-1,1))虽然可以将视频镜像,但是镜像后在Chrome中自带的播放控件也被翻转了【如图2】
【图1】
【图2】
虽然可以通过video::-webkit-media-controls属性控制播放控件的翻转,但在某些情况下,比如全屏时,以下css可能不生效,所以这种方法只能作为一种参考。。。
<style lang="scss" scoped>
video::-webkit-media-controls {
transform: scale(-1, 1) !important; /* Chrome 和 Safari */
}
video::-moz-media-controls {
transform: scale(-1, 1) !important; /* Firefox */
}
video::-ms-media-controls {
transform: scale(-1, 1) !important; /* Edge */
}
</style>
基于以上原因,就只能自定义播放控件了
二、自定义播放控件
实现基础功能(使用vue2+element-ui)
- 播放、暂停按钮
- 当前播放时间/总时长
- 进度条、点击及拖动进度条可正常播放
- 音量控制
- 使用transition元素实现播放控件fade显隐
最终实现结果如下图
1、播放、暂停视频
主要利用video自带的play\pause方法控制,没有什么特别的。
另外,为了在点击除按钮外的其他视频区域也能控制播放,在video上增加了click事件,执行togglePlay方法
// html--start
// 视频播放区
<video
ref="videoPlayer"
width="100%"
height="400"
class="answerVideo"
:src="item.answerVideoUrl"
preload="auto"
@timeupdate="updateProgressBar"
@loadedmetadata="getDuration"
@ended="onVideoEnded"
@click="togglePlay"
></video>
// 控制播放按钮
<el-button @click="togglePlay" type="text" size="mini">
<i :class="isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'"></i>
</el-button>
// html--end
// methods
togglePlay() {
const video = this.$refs.videoPlayer;
if (video.paused) {
video.play();
this.isPlaying = true;
} else {
video.pause();
this.isPlaying = false;
}
}
2、当前播放时间/总时长
(1)视频总时长duration:在视频数据加载完的loadedmetadata方法中获取video.duration进行更新
(2)当前播放的时间位置:通过video的timeupdate方法更新currentTime
注:有些视频的duration可能为Infinity,即无限大时长,这是由于视频文件可能存在异常,导致无法读取视频时长,所以在getDuration方法做了异常处理
// html
<span class="video_time">
{{ formatTime(currentTime) }}
<span>/ {{ formatTime(duration) }}</span>
</span>
// methods
// 获取视频总时长
getDuration() {
this.videoPlayer = this.$refs.videoPlayer;
if (this.videoPlayer) {
// 异常处理,其中item.duration为当前录制视频的时长,在录制时记录,如果不存在录制场景,请忽略,都没取到给个默认值5s,防止进度条异常
this.duration = Number.isFinite(this.videoPlayer.duration)
? this.videoPlayer.duration
: this.item.duration || 5;
}
},
// 更新当前播放的时间
updateProgressBar() {
this.getDuration();
// 如果在拖拽中、不更新时间
if (this.isDraging) {
return;
}
if (this.videoPlayer) {
this.currentTime = this.videoPlayer.currentTime;
}
},
// 格式化时间
formatTime(seconds) {
const format = val => `0${Math.floor(val)}`.slice(-2);
const hours = seconds / 3600;
const minutes = (seconds % 3600) / 60;
seconds = seconds % 60;
if (hours < 1) {
return [minutes, seconds].map(format).join(":");
}
return [hours, minutes, seconds].map(format).join(":");
}
3、进度条
进度条的位置:currentTime,进度条max=duration总时长
拖动进度条播放需要注意两种情况:
(1)视频暂停时播放:
拖动结束后,需要从拖动的终止位置开始自动播放
解决:changeProgress中调用video.play方法即可
(2)视频播放中拖动播放: 拖动进度条播放不能从拖动的终止位置开始播放,而是从拖动之前的位置开始播放
原因:因为是在播放中,所以视频的timeupdate方法一直在执行,也就是currentTime一直在更新,这也就导致el-slider的当前位置也在更新
也就是说currentTime变化受以下两种情况的影响
【1】 用户手动拖动进度条
【2】是视频播放时timeupdate的事件执行的updateProgressBar一直在执行
所以需要在用户开始拖动时禁止更新updateProgressBar中的currentTime
同时由于el-slider 没有监听开始拖拽的事件,所以使用原生js监听mousedown事件
// html
<el-slider
ref="progressSlider"
class="video_control_progress"
v-model="currentTime"
:show-tooltip="false"
:max="duration"
@change="changeProgress"
></el-slider>
mounted() {
// 由于el-slider 没有监听开始拖拽的事件,所以使用原生js监听mousedown事件
// 获取滑块的DOM元素
const sliderButton = this.$refs.progressSlider.$el.querySelector(
".el-slider__button-wrapper"
);
// 监听mousedown事件
sliderButton.addEventListener("mousedown", this.handleDragStart);
sliderButton.addEventListener("touchstart", this.handleDragStart);
},
// methods
// 更新当前播放时间、进度条位置
updateProgressBar() {
this.getDuration();
// 如果在拖拽中、不更新时间
if (this.isDraging) {
return;
}
if (this.videoPlayer) {
this.currentTime = this.videoPlayer.currentTime;
}
},
// 滑块结束拖动
changeProgress(value) {
this.isDraging = false;
this.videoPlayer.currentTime = value;
this.videoPlayer.play();
this.isPlaying = true;
},
// 进度开始拖拽,停止视频播放
handleDragStart() {
this.isDraging = true;
this.videoPlayer.pause();
}
4、音量控制
音量控制很简单,更新video的volume属性即可
// html
<span class="video_volume_wrap">
<el-slider
class="video_volume"
v-model="volume"
@change="changeVolume"
></el-slider>
音量
</span>
// methods
// 音量变化
changeVolume(value) {
this.videoPlayer.volume = value / 100;
}
5、控件显隐
使用transition元素+mouseleave/mouseover组合实现,就不做赘述,详见下面的完整代码
以上就是播放/暂停控制、当前播放时间/总时长、进度条控制、音量控制几个基础功能的实现了,以下是完整代码
三、完整代码
<template>
<div
class="customer_video_wrap"
@mouseleave="hideControls"
@mouseover="showControls"
>
<video
ref="videoPlayer"
width="100%"
height="400"
class="answerVideo"
:class="{ isMirror: item.isMirror }"
:src="item.answerVideoUrl"
preload="auto"
@timeupdate="updateProgressBar"
@loadedmetadata="getDuration"
@ended="onVideoEnded"
@click="togglePlay"
></video>
<!-- 自定义视频控件 -->
<transition name="el-fade-in-linear">
<div
class="video_control"
v-show="isShowingControls"
style="transition: opacity 0.5s;"
>
<div class="video_control_top">
<div>
<el-button @click="togglePlay" type="text" size="mini">
<i
:class="
isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'
"
></i>
</el-button>
<span class="video_time">
{{ formatTime(currentTime) }}
<span>/ {{ formatTime(duration) }}</span>
</span>
</div>
<span class="video_volume_wrap">
<el-slider
class="video_volume"
v-model="volume"
@change="changeVolume"
></el-slider>
音量
</span>
</div>
<el-slider
ref="progressSlider"
class="video_control_progress"
v-model="currentTime"
:show-tooltip="false"
:max="duration"
@change="changeProgress"
></el-slider>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
// 当前录制视频的视频问答题详情
item: {
type: Object,
default: () => {
return {
id: "",
index: 0
};
}
}
},
data() {
return {
// 自定义视频控件
isPlaying: false,
currentTime: 0,
duration: 1,
volume: 30,
isShowingControls: true,
isDraging: false,
videoPlayer: null
};
},
mounted() {
// 获取滑块的DOM元素
const sliderButton = this.$refs.progressSlider.$el.querySelector(
".el-slider__button-wrapper"
);
// 监听mousedown事件
sliderButton.addEventListener("mousedown", this.handleDragStart);
sliderButton.addEventListener("touchstart", this.handleDragStart);
},
methods: {
// 切换播放/暂停
togglePlay() {
const video = this.$refs.videoPlayer;
if (video.paused) {
video.play();
this.isPlaying = true;
} else {
video.pause();
this.isPlaying = false;
}
},
// 视频播放结束
onVideoEnded() {
this.isPlaying = false;
},
// 获取视频时长
getDuration() {
this.videoPlayer = this.$refs.videoPlayer;
if (this.videoPlayer) {
this.duration = Number.isFinite(this.videoPlayer.duration)
? this.videoPlayer.duration
: this.item.duration || 5;
}
},
// 更新进度条
updateProgressBar() {
this.getDuration();
if (this.isDraging) {
return;
}
if (this.videoPlayer) {
this.currentTime = this.videoPlayer.currentTime;
}
},
// 滑块结束拖动
changeProgress(value) {
this.isDraging = false;
this.videoPlayer.currentTime = value;
this.videoPlayer.play();
this.isPlaying = true;
},
// 进度开始拖拽
handleDragStart() {
this.isDraging = true;
this.videoPlayer.pause();
},
// 音量变化
changeVolume(value) {
this.videoPlayer.volume = value / 100;
},
// 格式化时间
formatTime(seconds) {
const format = val => `0${Math.floor(val)}`.slice(-2);
const hours = seconds / 3600;
const minutes = (seconds % 3600) / 60;
seconds = seconds % 60;
if (hours < 1) {
return [minutes, seconds].map(format).join(":");
}
return [hours, minutes, seconds].map(format).join(":");
},
// 展示控制条
showControls() {
this.isShowingControls = true;
},
// 隐藏控制条
hideControls() {
this.isShowingControls = false;
}
}
};
</script>
<style lang="scss" scoped>
.customer_video_wrap {
position: relative;
width: 100%;
}
.answerVideo {
z-index: 1;
}
video.isMirror {
transform: scale(-1, 1);
}
// 自定义视频控件
.video_control {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 20px;
background: linear-gradient(0deg, #000000 5%, #00000073, transparent);
.video_control_top {
display: flex;
justify-content: space-between;
align-items: center;
color: #fff;
i {
font-size: 20px;
color: #fff;
}
.video_time {
transform: translateY(-2px);
display: inline-block;
margin-left: 8px;
}
.video_volume_wrap {
display: flex;
align-items: center;
font-size: 13px;
}
.video_volume {
width: 70px;
margin-right: 8px;
}
}
/deep/.el-slider {
.el-slider__runway {
background-color: #cbcbcb;
height: 4px;
margin: 4px 0;
}
.el-slider__bar {
height: 4px;
background-color: #fff;
}
.el-slider__button-wrapper {
top: -16px;
}
.el-slider__button {
width: 10px;
height: 10px;
border: none;
}
}
}
</style>
代码之路漫漫~ 一坑接一坑,但填坑的过程也有那么点意思,哈哈~ 与君共勉!
以上就是今天的全部内容啦,如有错误,欢迎指正~~
转载自:https://juejin.cn/post/7376575006535778341