likes
comments
collection
share

原生video元素自定义播放控件controls

作者站长头像
站长
· 阅读数 33

原生video元素自定义播放控件controls

一、背景

之所以要写自定义播放控件,是因为使用**navigator.mediaDevices.getUserMedia()**做视频流的时候,现实中人的右手在视频中显示在左边【如图1】,所以要做video镜像,但通过css(transform:scale(-1,1))虽然可以将视频镜像,但是镜像后在Chrome中自带的播放控件也被翻转了【如图2

【图1】

原生video元素自定义播放控件controls

【图2】

原生video元素自定义播放控件controls 虽然可以通过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)

  1. 播放、暂停按钮
  2. 当前播放时间/总时长
  3. 进度条、点击及拖动进度条可正常播放
  4. 音量控制
  5. 使用transition元素实现播放控件fade显隐

最终实现结果如下图

原生video元素自定义播放控件controls

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)当前播放的时间位置:通过videotimeupdate方法更新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
评论
请登录