vue 手写视频的control组件

效果

功能介绍:根据点击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>

猜你喜欢

转载自blog.csdn.net/qq_45689385/article/details/124168909