vue.js组件之音视频播放器 - zhouted/zhouted.github.io GitHub Wiki
标签: javascript vue
<template>
<div class="view-audio">
<audio ref="audio"
class="audio"
:src="src"
:autoplay="autoplay"
:loop="loop"
:muted="muted"
controlsList="nodownload"
@contextmenu="onContextMenu"
@load="onReadyState"
@loadstart="onReadyState"
@loadedmetadata="onReadyState"
@loadeddata="onReadyState"
@durationchange="onReadyState"
@seeked="onSeeked"
@volumechange="onVolume"
@ratechange="onRate"
@pause="onPause"
@play="onPlay"
@playing="onPlaying"
@progress="onProgress"
@timeupdate="onTime"
@ended="onEnded"
@error="onError">
<p>Your browser doesn't support HTML5 audio.</p>
<slot></slot>
</audio>
<div class="audio-controls"
ref="controls"
v-if="controls">
<div class="audio-btn"
@click="clickPlay">
<i class="iconfont"
:class="isPlaying ? 'icon-pause' : ' icon-play'" />
</div>
<view-slider class="audio-progress"
:value="currentTime"
:max="duration"
:format-tooltip="null"
@input="onChangeCurrentTime"></view-slider>
<div class="audio-time">
{{ currentTime | formatTime }} / {{ duration | formatTime }}
</div>
<view-rates :value="playbackRate"
@change="onChangeRate"></view-rates>
<div class="audio-btn"
@click="clickMuted">
<i class="iconfont"
:class="muteVolume ? 'icon-sound' : ' icon-sound-mute'" />
</div>
<view-slider class="audio-volume"
:value="muteVolume"
:format-tooltip="null"
@input="onChangeVolume"></view-slider>
</div>
</div>
</template>
<script>
import ViewSlider from './ViewSlider.vue';
import ViewRates from './ViewRates.vue';
import { formatTime } from './ViewSlider.vue';
export default {
name: 'ViewAudio',
components: {
[ViewSlider.name]: ViewSlider,
[ViewRates.name]: ViewRates
},
props: {
src: String,
controls: {
type: Boolean,
default: true
},
autoplay: {
type: Boolean,
default: false
},
muted: {
type: Boolean,
default: false
},
loop: {
type: Boolean,
default: false
}
},
data: function () {
return {
duration: 0, //时长
currentTime: 0, //当前播放时长
playbackRate: 1, //倍速
volume: 0, //音量
isMuted: false, //静音
isPlaying: false, //播放中
states: [] //记录视频初始化状态
};
},
computed: {
muteVolume() {
return this.isMuted ? 0 : this.volume;
}
},
filters: {
formatTime: function (value) {
return formatTime(value);
}
},
methods: {
onChangeCurrentTime(value) {
this.$refs.audio.currentTime = value;
},
onChangeVolume(value) {
this.$refs.audio.volume = value;
},
onChangeRate(value) {
this.$refs.audio.playbackRate = value;
},
clickPlay() {
const audio = this.$refs.audio;
audio.paused ? audio.play() : audio.pause();
},
clickMuted() {
this.$refs.audio.muted = !this.$refs.audio.muted;
},
onReadyState(e) {
this.pushState(e);
this.duration = e.target.duration;
this.playbackRate = e.target.playbackRate;
this.volume = e.target.volume;
this.isMuted = e.target.muted;
},
onRate(e) {
this.pushState(e);
this.playbackRate = e.target.playbackRate;
},
onVolume(e) {
this.pushState(e);
this.volume = e.target.volume;
this.isMuted = e.target.muted;
},
onSeeked(e) {
this.pushState(e);
},
onContextMenu(e) {
this.pushState(e);
//e.preventDefault(); return false;//禁用右键菜单,避免用户下载视频(其实没什么用)
},
onPause(e) {
this.pushState(e);
this.isPlaying = !e.target.paused;
},
onPlay(e) {
this.pushState(e);
this.isPlaying = !e.target.paused;
},
onPlaying(e) {
this.pushState(e);
this.isPlaying = !e.target.paused;
},
onProgress(e) {
// console.log(e);
},
onTime(e) {
// console.log(e);
this.currentTime = e.target.currentTime;
},
getCurrentTime() {
return this.currentTime;
},
onEnded(e) {
this.pushState(e);
},
onError(e) {
this.pushState(e);
},
pushState(e) {
// console.log(e);
const states = this.states;
const len = states.length;
if (!len || states[len - 1] != e.type) {
states.push(e.type);
}
},
formatTime(value) {
return formatTime(value);
}
}
};
</script>
<style lang="less">
.view-audio {
.audio {
margin: auto;
}
.audio-controls {
position: relative;
// top: 50%;
// transform: translateY(-50%);
display: flex;
align-items: center;
background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.9));
color: #fff;
font-size: 0.9rem;
.audio-btn {
flex: 0 0 2rem;
padding: 0.5rem 0;
border-radius: 50%;
cursor: pointer;
}
.audio-btn:first-of-type {
margin-left: 0.5rem;
}
.audio-btn:last-of-type {
margin-right: 0.5rem;
}
.audio-btn:hover {
// background-color: #333;
}
.audio-progress {
flex: 12 0 2rem;
}
.audio-time {
flex: 0 2 auto;
margin: 0 1rem 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.audio-volume {
flex: 1 0 1rem;
}
}
}
</style>
<template>
<!-- @mousemove="onHoverVideo(1)" -->
<div class="view-video">
<video class="video"
ref="video"
:src="src"
:poster="poster"
:autoplay="autoplay"
:loop="loop"
:muted="muted"
:playsinline="playsinline"
:webkit-playsinline="playsinline"
controlsList="nodownload"
@contextmenu="onContextMenu"
@click="clickPlay"
@load="onReadyState"
@loadstart="onReadyState"
@loadedmetadata="onReadyState"
@loadeddata="onReadyState"
@durationchange="onReadyState"
@seeked="onSeeked"
@volumechange="onVolume"
@ratechange="onRate"
@pause="onPause"
@play="onPlay"
@playing="onPlaying"
@progress="onProgress"
@timeupdate="onTime"
@ended="onEnded"
@error="onError">
<slot />
<p>Your browser doesn't support HTML5 video.</p>
</video>
<transition name="controls">
<div class="video-controls"
ref="controls"
v-show="showControls">
<div class="video-btn"
@click="clickPlay">
<i class="iconfont"
:class="isPlaying ? 'icon-pause' : ' icon-play'" />
</div>
<view-slider class="video-progress"
:value="currentTime"
:max="duration"
:format-tooltip="formatTime"
@change="onChangeCurrentTime"></view-slider>
<div class="video-time">
{{ currentTime | formatTime }} / {{ duration | formatTime }}
</div>
<view-rates :value="playbackRate"
@change="onChangeRate"></view-rates>
<div class="video-btn"
@click="clickMuted">
<i class="iconfont"
:class="muteVolume ? 'icon-sound' : ' icon-sound-mute'" />
</div>
<view-slider class="video-volume"
:value="muteVolume"
@input="onChangeVolume"></view-slider>
<div class="video-btn"
@click="clickFullscreen">
<i class="iconfont"
:class="fullscreen ? 'icon-fullscreen-exit' : 'icon-fullscreen'" />
</div>
</div>
</transition>
</div>
</template>
<script>
//import { Button, Slider } from 'element-ui';
import ViewSlider from './ViewSlider.vue';
import ViewRates from './ViewRates.vue';
import { formatTime } from './ViewSlider.vue';
export default {
name: 'ViewVideo',
components: {
[ViewSlider.name]: ViewSlider,
[ViewRates.name]: ViewRates
},
props: {
src: String,
poster: String,
autoplay: {
type: Boolean,
default: false
},
muted: {
type: Boolean,
default: false
},
loop: {
type: Boolean,
default: false
},
playsinline: {
type: Boolean,
default: false
}
// controls: {
// type: Boolean,
// default: false
// }
},
data: function () {
return {
videoWidth: 0, //视频宽
videoHeight: 0, //视频高
duration: 0, //视频时长
currentTime: 0, //当前播放时长
playbackRate: 1, //倍速
volume: 0, //音量
isMuted: false, //静音
isPlaying: false, //播放中
fullscreen: false, //全屏显示
states: [], //记录视频初始化状态
pictureInPictureEnabled: false, //是否支持画中画
adjustValue: 0,
showRateList: false,
showControls: 1
};
},
computed: {
muteVolume() {
return this.isMuted ? 0 : this.volume;
}
},
filters: {
formatTime: function (value) {
return formatTime(value);
}
},
methods: {
onChangeCurrentTime(value) {
this.$refs.video.currentTime = value;
},
onChangeVolume(value) {
this.$refs.video.volume = value;
},
onChangeRate(value) {
this.$refs.video.playbackRate = Number.parseFloat(value);
},
clickPlay() {
const video = this.$refs.video;
video.paused ? video.play() : video.pause();
},
clickMuted() {
this.$refs.video.muted = !this.$refs.video.muted;
},
//reference: https://www.wikimoe.com/?post=82
clickFullscreen() {
const vue = this;
if (!vue.fullscreen) {
const video = vue.$refs.video;
const view = video.parentElement;
if (view.requestFullscreen) {
view.requestFullscreen();
} else if (view.webkitRequestFullscreen) {
view.webkitRequestFullscreen();
} else if (view.mozRequestFullScreen) {
view.mozRequestFullScreen();
} else if (view.msRequestFullscreen) {
view.msRequestFullscreen();
} else if (video.webkitEnterFullScreen) {
//iOS不支持非video元素全屏
video.webkitEnterFullScreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
},
onFullscreen() {
this.fullscreen =
document.fullscreen ||
document.webkitIsFullScreen ||
document.mozFullScreen ||
!!document.msFullscreenElement;
},
clickInPicture() {
if (!document.pictureInPictureElement) {
this.$refs.video.requestPictureInPicture();
} else {
document.exitPictureInPicture();
}
},
onReadyState(e) {
this.pushState(e);
this.videoWidth = e.target.videoWidth;
this.videoHeight = e.target.videoHeight;
this.duration = e.target.duration;
this.playbackRate = e.target.playbackRate;
this.volume = e.target.volume;
this.isMuted = e.target.muted;
},
onRate(e) {
this.pushState(e);
this.playbackRate = e.target.playbackRate;
},
onVolume(e) {
this.pushState(e);
this.volume = e.target.volume;
this.isMuted = e.target.muted;
},
onSeeked(e) {
this.pushState(e);
},
onContextMenu(e) {
this.pushState(e);
//e.preventDefault(); return false;//禁用右键菜单,避免用户下载视频(其实没什么用)
},
onPause(e) {
this.pushState(e);
this.isPlaying = !e.target.paused;
},
onPlay(e) {
this.pushState(e);
this.isPlaying = !e.target.paused;
},
onPlaying(e) {
this.pushState(e);
this.isPlaying = !e.target.paused;
},
onProgress(e) {
// console.log(e);
},
onTime(e) {
// console.log(e);
this.currentTime = e.target.currentTime;
},
getCurrentTime() {
return this.currentTime;
},
onEnded(e) {
this.pushState(e);
},
onError(e) {
this.pushState(e);
},
onHoverVideo(value) {
const vue = this;
vue.showControls = value; //鼠标移动即显示controls
if (!vue.showControlsTimer) {
//过几秒钟没有鼠标活动即隐藏controls
vue.showControlsTimer = setTimeout(() => {
vue.showControlsTimer = null;
if (vue.showControls < 0.1) {
vue.showControls = 0;
} else {
vue.onHoverVideo(vue.showControls / 2, 'timer');
}
}, 1000);
}
},
pushState(e) {
// console.log(e);
const states = this.states;
const len = states.length;
if (!len || states[len - 1] != e.type) {
states.push(e.type);
}
},
formatTime(value) {
return formatTime(value);
}
},
created() {
this.pictureInPictureEnabled = document.pictureInPictureEnabled;
},
mounted() {
//全屏事件
document.addEventListener('fullscreenchange', () => {
this.onFullscreen();
});
document.addEventListener('webkitfullscreenchange', () => {
this.onFullscreen();
});
document.addEventListener('onmozfullscreenchange', () => {
this.onFullscreen();
});
document.addEventListener('MSFullscreenChange', () => {
this.onFullscreen();
});
// document.addEventListener('click', e => {
// //点击文档其他区域时隐藏弹出项
// const video = this.$refs.video;
// const view = video && video.parentElement;
// if (view && !view.contains(e.target)) {
// this.showControls = 0;
// }
// });
}
};
</script>
<style lang="less">
.view-video {
position: relative;
height: 516px;
.video {
width: 100%;
height: 100%;
}
.video-controls {
position: absolute;
left: 0;
bottom: 0;
right: 0;
display: flex;
align-items: center;
background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.9));
color: #fff;
font-size: 0.9rem;
.video-btn {
flex: 0 0 2rem;
padding: 0.5rem 0;
border-radius: 50%;
cursor: pointer;
}
.video-btn:first-of-type {
margin-left: 0.5rem;
}
.video-btn:last-of-type {
margin-right: 0.5rem;
}
.video-btn:hover {
// background-image: radial-gradient(#888, #000);
}
.video-progress {
flex: 12 0 2rem;
}
.video-time {
flex: 0 2 auto;
margin: 0 1rem 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-volume {
flex: 1 0 1rem;
}
}
.controls-enter-active,
.controls-leave-active {
transition: opacity 1s ease;
}
.controls-enter,
.controls-leave-to {
opacity: 0;
}
}
</style>
<template>
<div class="view-rate" ref="rates" @click="showRateList = !showRateList">
<span>{{ value }}X</span>
<div class="view-rate-list" v-show="showRateList">
<div
class="rate-item"
v-for="(s, i) in rates"
:key="i"
@click="onChangeRate(s)"
>
<span>{{ s }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ViewRates',
props: {
value: Number,
rates: {
type: Array,
default: function() {
return ['10x', '2.0', '1.5', '1.2', '1.0', '0.8', '0.5'];
}
}
},
data: function() {
return { showRateList: false };
},
computed: {},
model: {
prop: 'value',
event: 'change'
},
methods: {
onChangeRate(s) {
this.$emit('change', Number.parseFloat(s));
}
},
mounted() {
document.addEventListener('click', e => {
//点击文档其他区域时隐藏弹出项
const rates = this.$refs.rates;
if (rates && !rates.contains(e.target)) {
this.showRateList = false;
}
});
}
};
</script>
<style lang="less">
.view-rate {
position: relative;
flex: none;
padding: 1rem 1rem;
.view-rate-list {
position: absolute;
left: 0.25rem;
bottom: 0;
display: flex;
flex-direction: column;
background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.9));
.rate-item {
padding: 0.25rem 1rem;
text-align: right;
}
.rate-item:hover {
background-color: #fff;
color: #050509;
}
}
cursor: pointer;
}
</style>
<template>
<div class="view-slider" :style="sliderStyles">
<div
class="view-slider-bak"
:style="sliderBakStyles"
@mousedown="onAdjust"
@mousemove.prevent="onAdjust"
@mouseup="onAdjust"
@mouseleave="onAdjust"
>
<div class="view-slider-bar">
<div class="view-slider-rate" :style="sliderRateStyles"></div>
</div>
<div
class="view-slider-pos"
:style="sliderPosStyles"
@mouseout="onHoverPos"
@mouseover="onHoverPos"
>
<div class="pos-tip" v-show="(hoverPos || adjusting) && tooltip">
{{ tooltip }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ViewSlider',
props: {
value: {
type: Number,
default: 0
},
max: {
type: Number,
default: 1
},
size: {
type: Number,
default: 1
},
formatTooltip: {
type: Function
}
},
data: function() {
return {
adjusting: false, //正在调节
adjustValue: this.value,
hoverPos: false
};
},
model: {
prop: 'value',
event: 'changed'
},
watch: {
value: function(newValue, oldValue) {
console.debug(`ViewSilder::watch.value:${oldValue}-->${newValue}`);
this.adjustValue = newValue;
}
},
computed: {
sliderRate() {
return (100 * this.adjustValue) / this.max;
},
sliderRateStyles() {
return { height: this.size / 2 + 'rem', width: this.sliderRate + '%' };
},
sliderPosStyles() {
const styles = {
top: this.size + 'rem',
height: this.size + 'rem'
};
styles.width = styles.height;
styles.left = this.sliderRate + '%';
styles.transform = `translateX(-50%)`;
styles.transform += `scale(${this.hoverPos || this.adjusting ? 1.2 : 1})`;
return styles;
},
sliderBakStyles() {
return {
padding: this.size * 1.25 + 'rem' + ' 0'
};
},
sliderStyles() {
return {
margin: '0 ' + this.size + 'rem'
};
},
tooltip() {
if (typeof this.formatTooltip == 'function') {
return this.formatTooltip(this.adjustValue);
} else if (this.formatTooltip === null) {
return null;
}
return this.sliderRate.toFixed();
}
},
filters: {},
methods: {
onAdjust(e) {
if (e.type == 'mousedown') {
this.adjusting = true;
}
if (
(e.type == 'mousemove' || e.type == 'mouseleave') &&
!this.adjusting
) {
return;
}
const rect = e.currentTarget.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
let value = (this.max * offsetX) / rect.width;
if (value <= 0) {
value = 0;
} else if (value > this.max) {
value = this.max;
}
this.adjustValue = value;
this.$emit('input', this.adjustValue);
if (e.type == 'mouseup' || e.type == 'mouseleave') {
this.$emit('change', this.adjustValue);
this.adjusting = false;
}
},
onHoverPos(e) {
this.hoverPos = e.type == 'mouseover';
}
}
};
export function formatTime(value) {
let time = parseInt(value, 10);
if (Number.isNaN(time)) return '';
let strs = [];
let hour = Math.floor(time / 3600);
if (hour) {
strs.push(hour < 10 ? '0' + hour : hour);
}
let minute = Math.floor((time - hour * 3600) / 60);
strs.push(minute < 10 ? '0' + minute : minute);
let second = time - hour * 3600 - minute * 60;
strs.push(second < 10 ? '0' + second : second);
return strs.join(':');
}
</script>
<style lang="less">
.view-slider {
.view-slider-bak {
position: relative;
.view-slider-bar {
width: 100%;
background-color: #aaa;
.view-slider-rate {
background-color: #fff;
border: solid 1px #aaa;
}
}
.view-slider-pos {
position: absolute;
border-radius: 50%;
background-color: #fff;
transition: transform 0.2s ease 0s;
.pos-tip {
position: absolute;
padding: 20% 40%;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
font-size: 0.5em;
color: #050509;
background-color: #fff;
border-radius: 12%;
}
.pos-tip:after {
content: '\00a0';
width: 0;
height: 0;
display: block;
position: absolute;
bottom: -1.1rem;
left: 50%;
transform: translateX(-50%);
border-style: solid;
border-width: 0.7rem;
border-color: #fff transparent transparent transparent;
}
}
}
cursor: pointer;
}
</style>