<video>旋转视频

365 阅读4分钟
  • 背景

    需求:基于原生的h5 的功能,增加控制左右旋转视频的功能

  • 尝试失败的方案

    1. 修改原生controls

      无法解决以下问题:

      1. 控制栏在#top-layer层,无法操作dom,只能做极有限的样式配置
      2. 通过css transform: rotate(),旋转video时,工具栏随着video旋转,无法定位到视频下方
    2. 寻找现成组件

      查找了video.js等相关插件,没找到满足需求的轮子

  • 最终方案

    • 隐藏原生工具栏

      video::-webkit-media-controls {
        display: none !important;
      }
      
    • 重写整个视频控制工具栏

    image.png

    image.png

    1. 播放

      // 播放
      // videoSelector 作为属性传入到组件中
      play() {
        const video = document.querySelector(this.videoSelector) as any;
        video && video.play();
      }
      
    2. 暂停

      // 暂停播放
      // videoSelector 作为属性传入到组件中
      pause() {
        const video = document.querySelector(this.videoSelector) as any;
        video && video.pause();
      }
      
    3. 全屏

      // 全屏
      private isFullScreen = false;
      // videoContainerSelector video 外层dom 作为属性传入到组件中
      fullscreen() {
        const videoBox = document.querySelector(this.videoContainerSelector) as any;
        videoBox && (videoBox.className = 'video-wrap fullscreen'); // 给video增加全屏样式
        this.isFullScreen = true;
      }
      
    4. 退出全屏

      // 全屏
      // videoContainerSelector video 外层dom 作为属性传入到组件中
      exitFullscreen() {
        const videoBox = document.querySelector(this.videoContainerSelector) as any;
        videoBox && (videoBox.className = 'video-wrap'); // 给video去掉全屏样式
        this.isFullScreen = false;
      }
      
    5. 旋转视频

      // 旋转视频
      rotate(rotation) {
        const video = document.querySelector(this.videoSelector) as any;
        const transformValue = video?.style.transform;
        const matches = (transformValue as any).match(/rotate(([^)]+))/);
        let angle = 0;
        if (matches) {
          angle = parseFloat(matches[1]);
        }
        video && (video.style.transform = 'rotate(' + (angle + rotation) + 'deg)');
      }
      
    6. 播放速度

      // 播放速度
      private rate = 1;
      changePlayBackRate(rate) {
        const video = document.querySelector(this.videoSelector) as any;
        video && (video.playbackRate = Number(rate));
        video && (this.rate = video.playbackRate);
      }
      
    7. 画中画

      // 画中画
      pictureInPiacture() {
        const video = document.querySelector(this.videoSelector) as any;
        video && video?.requestPictureInPicture();
      }
      
    8. 退出画中画

      // 退出画中画
      exitPitctureInPicture() {
        if ((document as any).pictureInPictureElement) {
          (document as any).exitPictureInPicture();
        }
      }
      
    9. 播放进度条

      <div class="progress">
        <el-progress
          :percentage="(currentTime * 100) / Number(duration)"
          :show-text="false"
          color="#fff"
          :stroke-width="4"
        ></el-progress>
      </div>
      
      <script>
        private currentTime = 0;
        private videoPlaying = false;
        mounted() {
          let checkVideoTimer: number | null = setInterval(() => {
            const video = document.querySelector(this.videoSelector) as any;
            if (video) {
              video.ontimeupdate = (e) => {
                this.videoPlaying = true;
                this.currentTime = video?.currentTime;
              };
              video.onpause = () => {
                this.videoPlaying = false;
              };
              checkVideoTimer && clearInterval(checkVideoTimer);
              checkVideoTimer = null;
            }
          }, 200);
        }
        ...
        private duration: number | string = 0;
        private durationchange(e) {
          this.duration = e.target.duration.toFixed(0);
        }
      </script>
      
  • 完整代码

    ```
    <template>
      <div>
        <el-button v-if="isFullScreen" icon="el-icon-close" class="ex-btn" circle @click="exitFullscreen"></el-button>
        <div class="custom-video-controls">
          <div class="controls">
            <div class="left">
              <img
                v-if="!videoPlaying"
                src="@/assets/icons/video/play.png"
                @click="play"
                alt="播放"
              />
              <img
                v-else
                src="@/assets/icons/video/pause.png"
                @click="pause"
                alt="暂停"
              />
              <span class="duraction">{{ _formatTime(Number(currentTime).toFixed(0)) }} /
                {{ _formatTime(duration) }}</span>
              <img
                src="@/assets/icons/video/rotateleft.png"
                @click="rotate(-90)"
                alt="左转90°"
              />
              <img
                src="@/assets/icons/video/rotateright.png"
                @click="rotate(90)"
                alt="右转90°"
              />
            </div>
            <div>
              <img
                v-if="!isFullScreen"
                src="@/assets/icons/video/fullscreen.png"
                @click="fullscreen"
                alt="全屏"
              />
              <img
                v-else
                src="@/assets/icons/video/exitfullscreen.png"
                @click="exitFullscreen"
                alt="退出全屏"
              />
    
              <el-dropdown
                @command="moreCommand"
                trigger="click"
                :hide-on-click="false"
              >
                <el-button circle><img
                    src="@/assets/icons/video/more.png"
                    alt="更多"
                  /></el-button>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="download"><span class="dropdown-link">
                      <img
                        src="@/assets/icons/video/download.png"
                        alt="下载"
                      />下载
                    </span></el-dropdown-item>
                  <el-dropdown-item command="rate">
                    <el-dropdown
                      @command="changePlayBackRate"
                      trigger="click"
                      placement="left"
                    >
                      <span class="dropdown-link">
                        <img
                          src="@/assets/icons/video/rate.png"
                          alt="播放速度"
                        />播放速度
                      </span>
                      <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item
                          command="0.25"
                          :class="rate == 0.25 ? 'active' : ''"
                        ><span class="rate-option">0.25</span></el-dropdown-item>
                        <el-dropdown-item
                          command="0.5"
                          :class="rate == 0.5 ? 'active' : ''"
                        ><span class="rate-option">0.5</span></el-dropdown-item>
                        <el-dropdown-item
                          command="0.75"
                          :class="rate == 0.75 ? 'active' : ''"
                        ><span class="rate-option">0.75</span></el-dropdown-item>
                        <el-dropdown-item
                          command="1"
                          :class="rate == 1 ? 'active' : ''"
                        ><span class="rate-option">正常</span></el-dropdown-item>
                        <el-dropdown-item
                          command="1.25"
                          :class="rate == 1.25 ? 'active' : ''"
                        ><span class="rate-option">1.25</span></el-dropdown-item>
                        <el-dropdown-item
                          command="1.5"
                          :class="rate == 1.5 ? 'active' : ''"
                        ><span class="rate-option">1.5</span></el-dropdown-item>
                        <el-dropdown-item
                          command="1.75"
                          :class="rate == 1.75 ? 'active' : ''"
                        ><span class="rate-option">1.75</span></el-dropdown-item>
                        <el-dropdown-item
                          command="2"
                          :class="rate == 2 ? 'active' : ''"
                        ><span class="rate-option">2</span></el-dropdown-item>
                      </el-dropdown-menu>
                    </el-dropdown></el-dropdown-item>
                  <el-dropdown-item command="picture"><span class="dropdown-link">
                      <img
                        src="@/assets/icons/video/picture.png"
                        alt="画中画"
                      />画中画
                    </span></el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </div>
          </div>
          <div class="progress">
            <el-progress
              :percentage="(currentTime * 100) / Number(duration)"
              :show-text="false"
              color="#fff"
              :stroke-width="4"
            ></el-progress>
          </div>
        </div>
      </div>
    </template>
    <script lang="ts">
    import { Component, Vue, Prop } from 'vue-property-decorator';
    @Component({
      name: 'VideoControl'
    })
    export default class VideoControl extends Vue {
      @Prop({
        default: 0
      })
      duration: number | string;
      @Prop({
        default: ''
      })
      videoContainerSelector: string;
      @Prop({
        default: ''
      })
      videoSelector: string;
      private currentTime = 0;
      private videoPlaying = false;
      mounted() {
        let checkVideoTimer: number | null = setInterval(() => {
          const video = document.querySelector(this.videoSelector) as any;
          if (video) {
            video.ontimeupdate = (e) => {
              this.videoPlaying = true;
              this.currentTime = video?.currentTime;
            };
            video.onpause = () => {
              this.videoPlaying = false;
            };
            checkVideoTimer && clearInterval(checkVideoTimer);
            checkVideoTimer = null;
          }
        }, 200);
      }
      // 播放
      play() {
        const video = document.querySelector(this.videoSelector) as any;
        video && video.play();
      }
      // 暂停播放
      pause() {
        const video = document.querySelector(this.videoSelector) as any;
        video && video.pause();
      }
      private _formatTime(seconds) {
        // 计算小时、分钟和秒数
        const hours = Math.floor(seconds / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        const remainingSeconds = seconds % 60;
    
        // 将单个数字的小时、分钟和秒数格式化为两位数
        const formattedHours = hours < 10 ? '0' + hours : hours;
        const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
        const formattedSeconds =
          remainingSeconds < 10 ? '0' + remainingSeconds : remainingSeconds;
    
        // 返回格式化后的时间字符串
        if (hours) {
          return formattedHours + ':' + formattedMinutes + ':' + formattedSeconds;
        } else {
          return formattedMinutes + ':' + formattedSeconds;
        }
      }
      // 全屏
      private isFullScreen = false;
      fullscreen() {
        const videoBox = document.querySelector(this.videoContainerSelector) as any;
        videoBox && (videoBox.className = 'video-wrap fullscreen');
        this.isFullScreen = true;
      }
      // 全屏
      exitFullscreen() {
        const videoBox = document.querySelector(this.videoContainerSelector) as any;
        videoBox && (videoBox.className = 'video-wrap');
        this.isFullScreen = false;
      }
      rotate(rotation) {
        const video = document.querySelector(this.videoSelector) as any;
        const transformValue = video?.style.transform;
        const matches = (transformValue as any).match(/rotate(([^)]+))/);
        let angle = 0;
        if (matches) {
          angle = parseFloat(matches[1]);
        }
        video && (video.style.transform = 'rotate(' + (angle + rotation) + 'deg)');
      }
      // 下载视频
      downloadVideo() {
        const video = document.querySelector(this.videoSelector) as any;
        const videoSrc = video?.currentSrc || '';
        fetch(videoSrc, {
          headers: new Headers({
            Origin: location.origin
          }),
          mode: 'cors'
        })
          .then((res) => res.blob())
          .then((blob) => {
            const a = document.createElement('a');
            document.body.appendChild(a);
            a.style.display = 'none';
            const url = window.URL.createObjectURL(blob);
            a.href = url;
            a.download = '视频.mp4';
            a.click();
            document.body.removeChild(a);
            window.URL.revokeObjectURL(url);
          });
      }
      // 播放速度
      private rate = 1;
      changePlayBackRate(rate) {
        const video = document.querySelector(this.videoSelector) as any;
        video && (video.playbackRate = Number(rate));
        video && (this.rate = video.playbackRate);
      }
      // moreCommand
      moreCommand(command) {
        switch (command) {
          case 'download':
            this.$message.warning('请点击鼠标右键,选择“视频另存为…');
            return;
          case 'rate':
            return;
          case 'picture':
            this.pictureInPiacture();
            return;
          default:
            return;
        }
      }
      // 画中画
      pictureInPiacture() {
        const video = document.querySelector(this.videoSelector) as any;
        video && video?.requestPictureInPicture();
      }
      // 退出画中画
      exitPitctureInPicture() {
        if ((document as any).pictureInPictureElement) {
          (document as any).exitPictureInPicture();
        }
      }
    }
    </script>
    <style lang="scss" scoped>
    .custom-video-controls {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      background-color: rgba(0, 0, 0, 0.4);
      .controls {
        color: #fff;
        display: flex;
        align-items: center;
        justify-content: space-between;
        img {
          width: 18px;
          height: 18px;
          margin: 12px 8px 2px 8px;
          cursor: pointer;
        }
        button {
          background: transparent;
          border: none;
        }
        .left {
          display: flex;
          align-items: center;
          .duraction {
            margin: 12px 12px 2px 4px;
          }
        }
      }
      .progress {
        margin: 8px;
        ::v-deep .el-progress-bar__outer {
          background-color: #909399 !important;
        }
      }
    
      .rate-option {
        padding: 4px 24px;
        display: block;
      }
    }
    .dropdown-link {
      display: flex;
      align-items: center;
      padding: 8px 24px 8px 0px;
      img {
        width: 18px;
        height: 18px;
        margin: 0px 20px 0px 0px;
      }
    }
    .active {
      color: #38bba9;
    }
    .ex-btn {
      position: absolute;
      top: 40px;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: rgba(0, 0, 0, 0.4);
      color: #fff;
      border: none;
      opacity: 0;
    }
    </style>
    ```