效果
功能介绍:根据点击title跳转到当前对应的时间节点播放 ,本文项目用的是vue+antd开发 如果不是antd 有些样式请自行修改,功能还是好的
话不多说,直接上代码,根据prop传入标题,和地址即可,注意 头部用的滚动用的是swiper,请导入后使用,本文是直接在index.html中映入的
<template>
<div id="videoBox" class="videoBox">
<!--视频分段播放组件 created by shuqianchuang -->
<!-- 加载蒙层 -->
<div class="mask" v-show="videoInfo.markFlag" @click="playPause">
<a-icon slot="indicator" type="loading" style="font-size: 60px" spin />
</div>
<!-- 点击视频播切换放蒙层 -->
<div class="play-pause" @click="playPause"></div>
<template v-if="videoSrc !== '' && videoFlag">
<!-- 视频 -->
<video
autoplay
ref="videoBox"
@canplay="getDuration"
@timeupdate="updateTime"
@pause="pause"
@play="play"
@seeking="seeking"
@seeked="seeked"
@playing="playing"
@waiting="waiting"
>
<source :src="videoSrc" type="video/mp4" />
</video>
<div class="control">
<!-- 标题内容 -->
<div class="title-data">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(item, index) in videoTimeSlotArr" :key="index">
<div @click.stop="videoTitleSwitch(item.time)">
<p v-if="titleTimeVisible">
{
{ changeSecondsToHours(item.time) }}
</p>
<p>
{
{ item.title }}
</p>
</div>
</div>
</div>
</div>
<!-- 滚动条 -->
<div class="navigationBar">
<!-- 开始播放与暂停播放切换 -->
<div class="leftBar">
<div class="switchPlay">
<a-icon type="pause" @click="playPause" v-if="videoInfo.playFlag" />
<a-icon type="caret-right" @click="playPause" v-else />
</div>
</div>
<!-- 当前播放时间 -->
<div class="time">
{
{ videoInfo.playTimeText }}
</div>
<!-- 进度条 -->
<div class="line" ref="line">
<!-- 进度条背景 -->
<div class="bgc"></div>
<!-- 当前播放的位置 -->
<div class="preload" ref="preload">
<div class="after" ref="after" @mousedown="dragProgressBar"></div>
</div>
<!-- 透明蒙板层 点击切换播放时间 -->
<div class="load" @click.stop="clickPlayProgressBar" @mousemove.stop="moveChange" @mouseout.stop="mouseout">
<!-- 在进度条中每个标题时间段展示的标记 -->
<template v-for="(item, index) in videoTimeSlotArr">
<a-tooltip placement="top" :key="index">
<template slot="title">
<span>{
{ item.title }}</span>
</template>
<div
class="after"
:style="{ left: item.left + 'px' }"
@click.stop="videoTitleSwitch(item.time)"
@mousemove.stop="mouseout"
></div>
</a-tooltip>
</template>
<!-- 悬浮进度条上展示提示时间 -->
<div
class="timecode ghost-timecode"
:style="{ display: videoInfo.opacityJudge ? 'block' : 'none', left: videoInfo.hoverLeft + 'px' }"
>
<div class="box">{
{ videoInfo.hoverTime }}</div>
</div>
</div>
</div>
<!-- 总时间 -->
<div class="time">
{
{ videoInfo.totalTimeText }}
</div>
<!-- 倍数 -->
<div class="multiple">
倍数
<div class="multiple-model">
<p
v-for="item in videoInfo.multipleArr"
:key="item"
:class="{ active: videoInfo.multiple == item }"
@click.stop="multipleChange(item)"
>
{
{ item == 1 || item == 2 || item == 3 ||item == 4 ? item + '.0' : item }}X
</p>
</div>
</div>
<div class="rightBar">
<!-- 声音 -->
<div class="voice">
<img
v-show="videoInfo.voicePercent > 0"
@click.stop="voiceSwitch(true)"
src="../../assets/play.png"
alt=""
/>
<img
v-show="videoInfo.voicePercent <= 0"
src="../../assets/pause.png"
@click.stop="voiceSwitch(false)"
alt=""
/>
<!-- 声音滚动条 -->
<div class="voiceBox">
<div class="num">{
{ (videoInfo.voicePercent * 100).toFixed(0) }}%</div>
<div>
<div class="voice-bgc" ref="voiceBgc"></div>
<div class="voice-progress" ref="voiceProgress">
<div class="after" @mousedown.stop="dragSoundProgressBar"></div>
</div>
</div>
<div class="voice-model" @click.stop="voiceClick"></div>
</div>
</div>
</div>
<!-- 全屏 -->
<div class="multiple">
<img
v-if="videoInfo.fullScreen"
src="../../assets/cancelFullScreen.png"
@click.stop="fullScreenSwitch"
alt=""
title="取消全屏"
/>
<img v-else src="../../assets/fullScreen.png" @click.stop="fullScreenSwitch" alt="" title="全屏" />
</div>
</div>
</div>
</template>
</div>
</template>
<script>
import moment from 'moment'
export default {
name: 'videoCmp',
props: {
videoSrc: {
//当前播放视频的地址
type: String,
required: true
},
videoTimeArr: {
//分时间段间隔播放数组 示例 videoTimeArr=[{time:100,title:'标题1'},{time:200,title:'标题2'}] time表示时间 格式:HH:mm:ss title:标题
type: Array,
required: true,
default: () => []
},
titleTimeVisible: {
//标题的时间展示状态 默认不展示
type: Boolean,
default: false
}
},
data() {
return {
videoInfo: {
playTimeText: '00:00:00', //展示播放时间
totalTimeText: '00:00:00', //展示的总时间
totalTime: 0, //总时间
playTime: 0, //当前播放时间
playFlag: false, //播放状态
proBarX: 0, //进度条拖动x轴的位置
proWidth: 100, //拖动的距离
flag: true, //拖动标杆
voicePercent: 0, //音量
voicePercentCopy: 0, //音量备份 以便静音后回复
multipleArr: [4.0,3.0,2.0, 1.5, 1.25, 1.0, 0.75, 0.5], //倍数数组
multiple: 1.0, //倍数
fullScreen: false, //是否全屏
hoverLeft: 0, //进度条悬浮提示
opacityJudge: false, //进度条悬浮提示是否显示
hoverTime: '', //悬浮提示信息
markFlag: false //加载蒙层
},
videoFlag: true, //强制刷新video
videoTimeSlotArr: [], //时间段数据
mySwiper: null //Swiper实例
}
},
mounted() {},
watch: {
videoSrc(newValue) {
//监听视频地址 值为真变化时 强制刷新
console.log(this.videoSrc)
if (newValue) {
// 初始化总时间 和 当前播放时间 已经间隔数组
;(this.videoInfo.playTimeText = '00:00:00'), //展示播放时间
(this.videoInfo.totalTimeText = '00:00:00'), //展示的总时间
(this.videoInfo.totalTime = 0), //总时间
(this.videoInfo.playTime = 0), //当前播放时间
(this.videoTimeSlotArr = []) //时间段数据清空
this.videoInfo.markFlag = false //当前加载蒙层隐藏
//强制刷新
this.videoFlag = false
// setTimeout(() => {
// this.videoFlag = true
// }, 100)
}
}
},
methods: {
moment,
//计算每个时间段的距离
getTimeDistance(dataArr) {
console.log(dataArr)
//清空原数据
this.videoTimeSlotArr = []
//重新添加
dataArr.forEach((item, index) => {
let time = moment.duration(item.time).as('seconds') //把时分秒转换为秒
this.videoTimeSlotArr.push({
title: item.title, //标题
time, //时间
left: (time / this.$refs.videoBox.duration) * this.$refs.line.offsetWidth //进度条上标记的距离
})
})
//添加拖动
this.$nextTick(() => {
if (this.mySwiper) {
//如果存在先销毁 在添加
this.mySwiper.destroy
this.mySwiper = null
}
this.mySwiper = new Swiper('.title-data', {
slidesPerView: 7
})
})
},
//点击标题切换
videoTitleSwitch(time) {
this.videoInfo.playTimeText = this.changeSecondsToHours(time) //展示当前时长文字
this.$refs.preload.style.width = (time / this.videoInfo.totalTime) * this.$refs.line.offsetWidth + 'px' //当前播放进度条
this.$refs.videoBox.currentTime = time //跟新当前时长
},
//暂停播放切换
playPause() {
if (this.videoInfo.flag) {
if (!this.videoInfo.playFlag) this.$refs.videoBox.play()
else this.$refs.videoBox.pause()
}
},
//暂停
pause() {
this.videoInfo.playFlag = false
},
//播放
play() {
this.videoInfo.playFlag = true
},
//当前播放时间以更改
updateTime() {
if (this.$refs.videoBox) {
//获取到当前的video实例
//动态更改当前时长
this.videoInfo.playTimeText = this.changeSecondsToHours(this.$refs.videoBox.currentTime)
this.videoInfo.playTime = this.$refs.videoBox.currentTime
if (this.videoInfo.flag) {
//如果没有拖动 则修改播放进度条长度
this.$refs.preload.style.width =
(this.$refs.videoBox.currentTime / this.videoInfo.totalTime) * this.$refs.line.offsetWidth + 'px'
}
} //当前播放时间
},
//初始化后获取视频数据
getDuration() {
if (this.$refs.videoBox) {
this.videoInfo.totalTimeText = this.changeSecondsToHours(this.$refs.videoBox.duration) //总时长
this.videoInfo.totalTime = this.$refs.videoBox.duration
this.videoInfo.voicePercent = this.$refs.videoBox.volume //声音
this.videoInfo.multiple = this.$refs.videoBox.playbackRate //倍数
this.$refs.voiceProgress.style.height = (1 - this.$refs.videoBox.volume) * 100 + 'px' //声音播放条
this.videoInfo.playTimeText = this.changeSecondsToHours(this.$refs.videoBox.currentTime) //当前播放时长
//视频加载完成后 处理时间段数据
this.getTimeDistance(this.videoTimeArr)
}
},
//进度条拖动
dragProgressBar(event) {
var event = event || window.event
//得到按下时与x轴的距离
this.videoInfo.proBarX = event.clientX
let that = this
let time = null
//得到当前视频播放的长度
that.videoInfo.proWidth = that.$refs.preload.clientWidth
that.videoInfo.flag = false //拖动时警用调其它事件 如正在播放updateTime事件
//按下滑动
document.onmousemove = function(event) {
var event = event || window.event
//拖动的x轴减去按下的x轴 就等于拖动的距离
let dis = event.clientX - that.videoInfo.proBarX
//当前的宽度加上滑动的距离就等于最新的距离
that.$refs.preload.style.width = that.videoInfo.proWidth + dis + 'px'
console.log(that.$refs.preload.offsetWidth, that.$refs.line.offsetWidth)
//如果滑动的距离大于总距离 就直接填充为百分百
if (that.$refs.preload.offsetWidth >= that.$refs.line.offsetWidth + 6) {
that.$refs.preload.style.width = '100%'
that.videoInfo.proWidth = dis
that.videoInfo.proBarX = that.videoInfo.proBarX
}
//计算出当前时间
let durationPercent = that.$refs.preload.offsetWidth / that.$refs.line.offsetWidth
that.$refs.videoBox.currentTime = that.videoInfo.totalTime * durationPercent
that.videoInfo.playTimeText = that.changeSecondsToHours(that.$refs.videoBox.currentTime)
}
//鼠标抬起
document.onmouseup = function() {
//存储当前的正在播放进度条的宽度
if (that.$refs.preload) that.videoInfo.proWidth = that.$refs.preload.clientWidth
//清除悬浮事件
document.onmousemove = null
//恢复其它事件
if (time) clearTimeout(time)
time = setTimeout(() => {
that.videoInfo.flag = true
}, 200)
//清除全局up事件
setTimeout(() => {
document.onmouseup = null
}, 300)
}
},
//点击进度条
clickPlayProgressBar(event) {
var event = event || window.event
//当前的宽度加上滑动的距离
this.$refs.preload.style.width = event.offsetX + 'px'
this.videoInfo.proWidth = event.offsetX
let durationPercent = this.$refs.preload.offsetWidth / this.$refs.line.offsetWidth
this.$refs.videoBox.currentTime = this.videoInfo.totalTime * durationPercent
this.videoInfo.playTimeText = this.changeSecondsToHours(this.$refs.videoBox.currentTime)
},
//秒转分钟
changeSecondsToHours(value) {
const time = moment.duration(value, 'seconds')
const hours = time.hours()
const minutes = time.minutes()
const seconds = time.seconds()
return moment({ h: hours, m: minutes, s: seconds }).format('HH:mm:ss')
},
//鼠标移入到进度条显示提示文字
moveChange(e) {
//鼠标移入
var ev = e || window.event //兼容各个浏览器
//计算出当前悬浮的时间
this.videoInfo.hoverTime = this.changeSecondsToHours(
(ev.offsetX / this.$refs.line.offsetWidth) * this.$refs.videoBox.duration
)
//显示悬浮展示的div
this.videoInfo.opacityJudge = true
this.videoInfo.hoverLeft = ev.offsetX //45是左侧颜色有的固定宽
},
//鼠标移出进度条清除文字提示
mouseout() {
// 鼠标移出
this.videoInfo.opacityJudge = false
},
//拖动声音滚动进度
dragSoundProgressBar() {
var event = event || window.event //获得鼠标的拖动事件
//记录下当前的坐标点
let y0 = event.clientY
let that = this
let height = that.$refs.voiceProgress.offsetHeight
let time = null
that.videoInfo.flag = false
console.log(height)
//获得当前div在所包含的祖先的位置
//用于记录当前声量的比例
document.onmousemove = function(event) {
var event = event || window.event //获得鼠标的拖动事件
var dis = 0
dis = event.clientY - y0
that.$refs.voiceProgress.style.height = height + dis + 'px'
//大于整体高度
if (that.$refs.voiceProgress.offsetHeight >= that.$refs.voiceBgc.offsetHeight) {
that.$refs.voiceProgress.style.height = '70%'
that.videoInfo.voicePercent = 0
//小于整体高度
} else if (that.$refs.voiceProgress.offsetHeight <= 0) {
that.$refs.voiceProgress.height = '0px'
that.videoInfo.voicePercent = 1
} else {
//当前音量大小
that.videoInfo.voicePercent =
1 - that.$refs.voiceProgress.offsetHeight / (that.$refs.voiceBgc.offsetHeight / 100) / 100
}
// 设置音量
that.$refs.videoBox.volume = that.videoInfo.voicePercent
}
document.onmouseup = function(event) {
event.stopPropagation()
// voicePercent = (voiceBarInner.offsetHeight / voiceBar.offsetHeight) * 100
document.onmousemove = null
if (time) clearTimeout(time)
time = setTimeout(() => {
that.videoInfo.flag = true
}, 200)
//清除全局up事件
setTimeout(() => {
document.onmouseup = null
}, 300)
}
},
//点击声音滚动条
voiceClick(event) {
var event = event || window.event
//当前的宽度加上滑动的距离
this.$refs.voiceProgress.style.height = event.offsetY + 'px'
this.videoInfo.voicePercent = 1 - event.offsetY / 100
this.$refs.videoBox.volume = this.videoInfo.voicePercent
},
//点击声音图片 静音或者非静音
voiceSwitch(bol) {
if (bol) {
//有声音
this.videoInfo.voicePercentCopy = this.videoInfo.voicePercent
this.videoInfo.voicePercent = 0
this.$refs.voiceProgress.style.height = '70%'
} else {
//点击静音
this.videoInfo.voicePercent = this.videoInfo.voicePercentCopy
this.$refs.voiceProgress.style.height = (1 - this.videoInfo.voicePercent) * 100 + 'px'
}
this.$refs.videoBox.volume = this.videoInfo.voicePercent
},
//倍数
multipleChange(item) {
this.videoInfo.multiple = item
this.$refs.videoBox.playbackRate = item
},
//全屏
fullScreenSwitch() {
this.videoInfo.fullScreen = !this.videoInfo.fullScreen
if (this.videoInfo.fullScreen) {
//全屏
var element = document.getElementById('videoBox')
if (element.requestFullScreen) {
element.requestFullScreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen()
}
} else {
//取消全屏
var element = document
if (element.exitFullscreen) {
element.exitFullscreen()
} else if (element.mozCancelFullScreen) {
element.mozCancelFullScreen()
} else if (element.webkitExitFullscreen) {
element.webkitExitFullscreen()
}
if (typeof window.ActiveXObject !== 'undefined') {
var wscript = new ActiveXObject('WScript.Shell')
if (wscript !== null) {
wscript.SendKeys('{Esc}')
}
}
}
},
//当用户已移动视频中的新位置时触发。
seeked() {
this.videoInfo.markFlag = false
},
//当用户开始移动视频中的新位置时触发。
seeking() {
this.videoInfo.markFlag = true
},
//当视频在因缓冲而暂停或停止后已就绪时触发。
playing() {
this.videoInfo.markFlag = false
},
//当视频由于需要缓冲下一帧而停止时触发。
waiting() {
this.videoInfo.markFlag = true
}
}
}
</script>
<style lang="less" scoped>
.videoBox {
height: 100%;
overflow: hidden;
position: relative;
video{
width: 100%;
height: 100%;
}
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
background: rgba(255, 255, 255, 0.4);
height: calc(100vh - 280px);
display: flex;
align-items: center;
justify-content: center;
}
.play-pause {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100vh - 280px);
background: rgba(255, 255, 255, 0);
z-index: 999;
}
.control {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.8));
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}
.title-data {
// overflow-y: hidden;
// overflow-x: auto;
width: 100%;
overflow: hidden;
// white-space: nowrap;
padding-right: 5px;
::v-deep .swiper-slide > div {
display: inline-block;
width: 98%;
height: 50px;
background: rgba(0, 0, 0, 0.6);
border-radius: 4px;
padding: 4px;
margin-left: 5px;
color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
cursor: pointer;
p {
margin: 0;
padding: 0;
display: inline-block;
}
> p:last-child {
margin-left: 10px;
}
}
}
.navigationBar {
// height: 60px;
margin-bottom: 10px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: center;
.leftBar {
display: flex;
color: #fff;
align-items: center;
justify-content: center;
.switchPlay {
font-size: 28px;
margin-right: 4px;
::v-deep .anticon {
cursor: pointer;
}
}
}
.multiple {
color: #fff;
cursor: pointer;
margin-left: 10px;
position: relative;
&:hover .multiple-model {
display: flex;
}
.multiple-model {
position: absolute;
left: -30px;
bottom: 22px;
width: 80px;
height: 200px;
border-radius: 4px;
z-index:9999;
background-color: rgba(0, 0, 0, 0.8);
flex-direction: column;
display: none;
p {
margin: 0;
padding: 0;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.active {
color: #0292d7;
}
p:hover {
background: rgba(255, 255, 255, 0.1);
}
}
}
.rightBar {
display: flex;
color: #fff;
align-content: center;
justify-content: center;
margin-left: 10px;
.voice {
cursor: pointer;
position: relative;
&:hover .voiceBox {
display: block;
}
.voiceBox {
position: absolute;
bottom: 32px;
left: -4px;
width: 40px;
height: 140px;
background-color: rgba(0, 0, 0, 0.8);
display: none;
border-radius: 4px;
z-index:9999;
.num {
text-align: center;
line-height: 30px;
}
> div > div {
position: absolute;
left: 48%;
top: 34px;
width: 3px;
}
.voice-bgc {
background: #0292d7;
height: 70%;
}
.voice-model {
position: absolute;
left: 48%;
top: 34px;
width: 3px;
height: 70%;
background: rgba(0, 0, 0, 0);
z-index: 999;
}
.voice-progress {
background: #fff;
height: 70%;
.after {
position: absolute;
width: 10px;
height: 10px;
border-radius: 5px;
z-index: 9999;
background: #fff;
bottom: -4px;
right: -4px;
}
}
}
}
}
.time {
font-size: 14px;
color: #fff;
}
.line {
flex: 1;
position: relative;
margin: 0 10px;
> div {
position: absolute;
left: 0;
height: 4px;
cursor: pointer;
border-radius: 4px;
}
.ghost-timecode {
transition: opacity linear 250ms;
// opacity: 0;
display: none;
.box {
cursor: pointer;
color: white;
background-color: #9d0300;
display: block;
}
.box::after {
border-top-color: #9d0300;
}
}
.timecode {
position: absolute;
display: block;
box-sizing: border-box;
top: -39px;
.box {
position: relative;
background-color: black;
border-radius: 0.5em;
box-shadow: 0 0 4px 0 black;
color: #fff;
height: 20px;
line-height: 20px;
box-sizing: border-box;
font-size: 12px;
padding: 0 8px;
white-space: nowrap;
text-align: center;
cursor: -webkit-grab;
cursor: grab;
cursor: -moz-grab;
display: inline-block;
font-family: Verdana, sans-serif;
left: -50%;
}
.box::after {
top: 100%;
left: 50%;
border: solid transparent;
content: ' ';
height: 15px;
width: 0px;
position: absolute;
border-top-color: black;
border-width: 5px;
margin-left: -3px;
}
}
}
.bgc {
width: 100%;
background: rgba(255, 255, 255, 0.1);
}
.preload {
width: 0px;
background: rgba(255, 255, 255, 0.6);
.after {
position: absolute;
width: 10px;
height: 10px;
border-radius: 5px;
z-index: 9999;
background: #fff;
top: -4px;
right: -10px;
}
}
.load {
width: 100%;
z-index: 999;
background: rgba(255, 255, 255, 0);
position: relative;
.after {
position: absolute;
width: 10px;
height: 4px;
border-radius: 2px;
z-index: 9999;
background: #fff;
top: 0px;
}
}
}
}
</style>