El video de Vue2 toma fotogramas y establece la portada.

Obtenga fotogramas de vídeo, configure la portada, vue2+ Element+ vue-cropper+vue-core-video-player

principal。js

importar VueCoreVideoPlayer desde 'vue-core-video-player'

Vue.use(VueCoreVideoPlayer, {

  simplemente: 'zh-CN'

})


Componentes de la aplicación de interfaz

<template>
  <div class="box">
    <h1>视频帧提取封面</h1>
    <video class="videos" controls :src="videoForm.videoUrl"></video>
    <div class="btns">
      <el-button size="large" class="button">
        选择视频文件
        <input id="video-file" type="file" accept="video/*" @change="fileChange" />
      </el-button>
      <el-button size="large" color="#fe3355" style="color: red; position: relative; z-index: 999" @click="shows">
        提取封面
      </el-button>
    </div>
    <videoCover ref="videoCover" :file="videoForm.file" :is-show="videoForm.comIsShow" @closeDialog="close" @confirmImg="confirmImg"></videoCover>
    <div class="look_img">
      <img :src="imgLookUrl" alt="" />
    </div>
  </div>
</template>

<script>
import videoCover from './video.vue'
export default {
  components: {
    videoCover
  },
  data() {
    return {
      videoForm: {
        videoUrl: '',
        file: {},
        comIsShow: false
      },
      imgLookUrl: ''
    }
  },

  created() {},
  methods: {
    fileChange(e) {
      let videoFile = e.target.files[0]
      if (videoFile) {
        this.videoForm.videoUrl = URL.createObjectURL(videoFile)
        this.videoForm.file = videoFile
        this.$refs.videoCover.changeFile(videoFile)
        console.log(videoFile, 'videoFilevideoFile', typeof videoFile)
      }
    },
    // 打开组件
    shows() {
      this.videoForm.comIsShow = true
      this.$refs.videoCover.changeFile(this.videoForm.file)
      console.log(3333)
    },
    //关闭组件回调
    close() {
      this.videoForm.comIsShow = false
    },
    //确认封面回调 data返回值
    confirmImg(data) {
      this.imgLookUrl.value = data.url
      console.log(data)
      // console.log(blobUrl);
    }
  }
}
</script>
<style lang="scss" scoped>
.button {
  position: relative;
}
.button input {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.box {
  h1 {
    font-size: 40px;
    text-align: center;
    margin-bottom: 50px;
    // color: #fe3355;
    font-weight: normal;
    margin-top: 60px;
  }
  .videos {
    width: 200px;
    height: 100px;
    margin: 0 auto;
    display: block;
    margin-bottom: 30px;
  }
  .btns {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 50px;
  }
}
.look_img {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 50px;
  img {
    width: auto;
    height: 200px;
  }
}
</style>

Embalaje de componentes

<template>
  <div>
    <el-dialog :visible.sync="isShow" title="" :close-on-click-modal="false" width="1000px" class="dialog-dfl" :before-close="beforeClose">
      <div>
        <el-tabs v-model="activeName" class="demo-tabs">
          <el-tab-pane label="封面截取" name="one">
            <div class="conts" style="height: 450px">
              <div v-show="!loading">
                <div class="look_img">
                  <img :src="imgForm.url" alt="" />
                </div>
                <div class="imgs_list_box">
                  <div class="imgs_list">
                    <div v-for="(item, index) in imgForm.img_list" :key="index" class="imgs_item">
                      <img :src="item" alt="" />
                    </div>
                    <div class="slider-dfl">
                      000898{
   
   { imgForm.videoTime }} {
   
   { sliderVal }}
                      <el-slider v-model="sliderVal" :step="0.01" :min="0" :max="imgForm.videoTime" placement="bottom" :format-tooltip="formatTooltip" @change="sliderChange" />
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </el-tab-pane>
          <el-tab-pane label="本地上传" name="two">
            <div v-loading="cropperForm.loadings" element-loading-background="#fff" element-loading-text="解析中" style="height: 450px">
              <div v-if="!cropperForm.loadings" class="conts_right">
                <div class="l">
                  <div
                    :style="{
                      backgroundColor: cropperForm.bgColor
                    }"
                    v-html="cropperForm.imgLookUrl"
                  ></div>
                  <!-- {
   
   { cropperForm.imgLookUrl }} -->
                </div>
                <div class="r">
                  <vueCropper ref="cropperRef" :img="imgForm.urlTwo" :output-size="option.outputSize" :output-type="option.outputType" :info="option.info" :can-scale="option.canScale" :auto-crop="option.autoCrop" :auto-crop-width="option.autoCropWidth" :auto-crop-height="option.autoCropHeight" :fixed-box="option.fixedBox" :fixed="option.fixed" :fixed-number="option.fixedNumber" :can-move="option.canMove" :can-move-box="option.canMoveBox" :original="option.original" :center-box="option.centerBox" :info-true="option.infoTrue" :full="option.full" :enlarge="option.enlarge" :mode="option.mode" :fill-color="cropperForm.bgColor" @real-time="previewHandle"></vueCropper>
                </div>
              </div>
            </div>
          </el-tab-pane>
        </el-tabs>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button v-show="activeName == 'one'" color="#fe3355" @click="confirmCover">
            确认封面
          </el-button>
          <div v-show="activeName == 'two'" class="bottom_btn">
            <div class="l">
              <el-button style="margin-right: 10px; position: relative">
                <span>选择本地封面</span>
                <input id="img-file" type="file" accept="image/*" @change="fileChange" />
              </el-button>
              <el-color-picker v-model="cropperForm.bgColor" :locale="'zhCn'" show-alpha />
            </div>
            <div class="r">
              <el-button color="#fe3355" @click="confirmCoverTwo">
                确认封面
              </el-button>
            </div>
          </div>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script>
import { VueCropper } from 'vue-cropper'

export default {
  components: { VueCropper },
  props: {
    // 验证码类型: true:不校验手机号是否在库; false:校验手机号是否在库
    isShow: {
      type: Boolean,
      default: () => true
    }
    //传过来的视频文件
    // file: {
    //   type: Object,
    //   default: () => ({})
    // }
  },
  data() {
    return {
      loading: false,
      file: null,
      activeName: 'one',
      sliderVal: 0,
      sliderVal2: 20,
      cropperRef: {},
      imgForm: {
        url: '', //封面预览地址
        urlTwo: '', //封面预览地址
        blob: {}, //封面blob对象
        img_list: [], //底部预览条图片数组
        videoTime: 0, //视频时长
        oldVideoFile: {} //旧的视频文件
      },
      option: {
        img: '',
        outputSize: 1, // 裁剪生成图片的质量
        outputType: 'jpeg', // 裁剪生成图片的格式 jpeg, png, webp
        info: true, // 裁剪框的大小信息
        canScale: true, // 图片是否允许滚轮缩放
        autoCrop: true, // 是否默认生成截图框
        autoCropWidth: 200, // 默认生成截图框宽度
        autoCropHeight: 200, // 默认生成截图框高度
        fixedBox: false, // 固定截图框大小 不允许改变
        fixed: false, // 是否开启截图框宽高固定比例
        fixedNumber: [1, 1], // 截图框的宽高比例 [ 宽度 , 高度 ]
        canMove: true, // 上传图片是否可以移动
        canMoveBox: true, // 截图框能否拖动
        original: false, // 上传图片按照原始比例渲染
        centerBox: false, // 截图框是否被限制在图片里面
        infoTrue: true, // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
        full: false, // 是否输出原图比例的截图
        enlarge: '1', // 图片根据截图框输出比例倍数
        mode: 'contain' // 图片默认渲染方式 contain , cover, 100px, 100% auto,
      },
      cropperForm: {
        imgLookUrl: '', //裁剪实时预览
        bgColor: '#fff', //裁剪图片底色
        loadings: true
      }
    }
  },
  watch: {
    file(newVal, oldVal) {
      console.log(oldVal, 'file')
      if (this.isShow == true && this.file.type) {
        if (this.file.type.includes('video')) {
          //通过验证
          if (newVal[0].name != this.imgForm.oldVideoFile.name && newVal[0].size != this.imgForm.oldVideoFile.size) {
            //是否已经选择了这个视频文件 选择了相同的文件就不用初始化了 如果不同就初始化
            this.loading = true
            this.cropperForm.loadings = true
            this.activeName = 'one'
            this.init()
          }
        } else {
          //未通过验证
          this.$message({
            message: '请选择视频格式的文件',
            grouping: true,
            type: 'error'
          })
          this.$emit('closeDialog', false)
        }
      } else if (this.isShow == true) {
        //未通过验证
        this.$message({
          message: '请选择视频格式的文件',
          grouping: true,
          type: 'error'
        })
        this.$emit('closeDialog', false)
      }
    },
    isShow(newVal, oldVal) {
      console.log(newVal, oldVal)
      //   if (this.isShow == true && this.file.type) {
      //     if (this.file.type.includes('video')) {
      //       //通过验证
      //       if (newVal[0].name != this.imgForm.oldVideoFile.name && newVal[0].size != this.imgForm.oldVideoFile.size) {
      //         //是否已经选择了这个视频文件 选择了相同的文件就不用初始化了 如果不同就初始化
      //         this.loading = true
      //         this.cropperForm.loadings = true
      //         this.activeName = 'one'
      //         this.init()
      //       }
      //     } else {
      //       //未通过验证
      //       this.$message({
      //         message: '请选择视频格式的文件',
      //         grouping: true,
      //         type: 'error'
      //       })
      //       this.$emit('closeDialog', false)
      //     }
      //   } else if (this.isShow == true) {
      //     //未通过验证
      //     this.$message({
      //       message: '请选择视频格式的文件',
      //       grouping: true,
      //       type: 'error'
      //     })
      //     this.$emit('closeDialog', false)
      //   }
    }
  },
  created() {},
  methods: {
    changeFile(newVal) {
      console.log('=-=-=-', this.isShow, this.file, newVal)
      this.file = newVal
      if (this.file.type) {
        if (this.file.type.includes('video')) {
          //通过验证
          if (newVal.name != this.imgForm.oldVideoFile.name && newVal.size != this.imgForm.oldVideoFile.size) {
            //是否已经选择了这个视频文件 选择了相同的文件就不用初始化了 如果不同就初始化
            this.loading = true
            this.cropperForm.loadings = true
            this.activeName = 'one'
            console.log('---changeFile', newVal)
            this.init()
          }
        } else {
          //未通过验证
          this.$message({
            message: '请选择视频格式的文件',
            grouping: true,
            type: 'error'
          })
          this.$emit('closeDialog', false)
        }
      } else if (this.isShow == true) {
        //未通过验证
        this.$message({
          message: '请选择视频格式的文件',
          grouping: true,
          type: 'error'
        })
        this.$emit('closeDialog', false)
      }
    },
    init() {
      this.imgForm.url = ''
      this.imgForm.blob = {}
      this.imgForm.img_list = []
      this.imgForm.videoTime = 0
      this.imgForm.oldVideoFile = this.file
      let reader = new FileReader()
      console.log(this.file, '0-0-0-')
      let that = this
      //获取视频时长
      reader.onload = function(e) {
        let video = document.createElement('video')
        // @ts-ignore
        video.src = e.target.result

        video.addEventListener('loadedmetadata', async function() {
          // 这里先看900宽度能放几张图片
          const img_src = await that.captureFrame(that.file, Math.floor(video.duration))
          var img_load = document.createElement('img')
          img_load.setAttribute('src', img_src.url)
          img_load.onload = function() {
            var aspectRatio = img_load.naturalWidth / img_load.naturalHeight
            // option.fixedNumber[0] =
            //   parseFloat((img_load.width / img_load.height).toFixed(2)) - 0.2;
            that.option.fixedNumber[0] = img_load.width / img_load.height
            var width = 90 * aspectRatio
            let count = Math.floor(960 / width) // 总宽度为960 看能放几张图片

            let duration = Math.floor(video.duration) //取整
            that.imgForm.videoTime = duration
            var step = Math.floor(duration / (count - 1)) // 步长
            var result = [] // 存储结果的数组
            for (var i = 0; i < count; i++) {
              result.push(i * step)
            }
            if (result[0] == 0) {
              result[0] = 0.1
            }
            result.forEach(async (item, index) => {
              const res = await that.captureFrame(that.file, item)
              if (index == 0) {
                that.imgForm.url = res.url
                that.imgForm.urlTwo = res.url
                that.imgForm.blob = res.blob
              }
              that.imgForm.img_list.push(res.url)
            })
            that.$nextTick(() => {
              setTimeout(() => {
                that.loading = false
              }, 2000)
            })
          }
        })
      }

      // @ts-ignore
      reader.readAsDataURL(this.file)
    },
    //滑块位置改变 更滑上方主封面图
    async sliderChange(val) {
      const res = await this.captureFrame(this.file, val)
      console.log(val, res.url, 'sliderChange')
      this.imgForm.url = res.url
      this.imgForm.urlTwo = res.url
      this.imgForm.blob = res.blob
    },
    // 格式化提示时间
    formatTooltip(val) {
      console.log(val)
      var timeString = this.convertSeconds(val)
      return timeString
    },
    async handleClick(tab, event) {
      console.log(tab, event)
      if (this.activeName.value == 'two') {
        this.cropperForm.loadings = true
        this.$nextTick(() => {
          setTimeout(() => {
            this.cropperForm.loadings = false
          }, 500)
        })
      }
    },
    //关闭模态弹窗
    beforeClose() {
      this.$emit('closeDialog', false)
    },
    //确认封面选择封面
    confirmCover() {
      this.$emit('closeDialog', false)
      this.$emit('confirmImg', {
        url: this.imgForm.url,
        blob: this.imgForm.blob
      })
    },
    // 获取视频帧的封面
    captureFrame(videoFile, time = 0) {
      return new Promise(succeed => {
        const video = document.createElement('video')
        console.log('captureFrame', time)
        video.currentTime = time
        video.muted = true
        video.autoplay = true
        video.oncanplay = async () => {
          const res = await this.drawVideo(video)
          succeed(res)
        }
        video.src = URL.createObjectURL(videoFile)
      })
    },

    // 画视频
    drawVideo(video) {
      return new Promise(res => {
        const cvs = document.createElement('canvas')
        const ctx = cvs.getContext('2d')
        cvs.width = video.videoWidth
        cvs.height = video.videoHeight
        ctx.drawImage(video, 0, 0, cvs.width, cvs.height)
        cvs.toBlob(blob => {
          res({
            blob,
            url: URL.createObjectURL(blob)
          })
        })
      })
    },

    // 秒数换算时间
    convertSeconds(seconds) {
      var hours = Math.floor(seconds / 3600)
      var minutes = Math.floor((seconds % 3600) / 60)
      // var remainingSeconds = Math.floor(seconds % 60) //秒
      var millisecond = Math.floor((seconds % 60) * 10) //毫秒
      var timeString = ''
      console.log(millisecond)
      if (hours > 0) {
        timeString += hours + ':'
      }

      timeString += minutes + ':' + millisecond

      return timeString
    },

    //裁剪功能实时事件
    previewHandle(val) {
      console.log(val)
      this.cropperForm.imgLookUrl = val.html
    },
    //本地上传封面
    fileChange(e) {
      let imgFile = e.target.files[0]
      if (imgFile) {
        this.cropperForm.loadings = true

        this.imgForm.urlTwo = URL.createObjectURL(imgFile)
        this.$nextTick(() => {
          setTimeout(() => {
            this.cropperForm.loadings = false
          }, 500)
        })
      }
    },
    //本地封面确定事件
    confirmCoverTwo() {
      // @ts-ignore
      this.$refs.cropperRef.getCropData(data => {
        const image = new Image()
        image.src = data
        image.onload = () => {
          const canvas = document.createElement('canvas')
          const ctx = canvas.getContext('2d')
          canvas.width = image.width
          canvas.height = image.height
          ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
          canvas.toBlob(blob => {
            this.$emit('closeDialog', false)
            this.$emit('confirmImg', {
              url: URL.createObjectURL(blob),
              blob
            })
          })
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.conts {
  height: 450px;
  box-sizing: border-box;
  padding: 0 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  .look_img {
    display: flex;
    justify-content: center;
    margin-bottom: 45px;
    img {
      width: auto !important;
      height: 256px !important;
      border-radius: 4px;
    }
  }
  .imgs_list_box {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .imgs_list {
    position: relative;
    width: auto !important;
    height: 91px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    .imgs_item {
      img {
        width: auto !important;
        height: 88px !important;
      }
    }
  }
}
.slider-dfl {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
}
.conts_right {
  height: 450px;
  box-sizing: border-box;
  padding: 0 20px;
  padding-bottom: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  .l {
    flex: 1;
    height: 100%;
    border: 1px solid #e4e7ed;
    margin-right: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  .r {
    flex: 1;
    height: 100%;
    border: 1px solid #e4e7ed;
  }
}
.bottom_btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  .l {
    display: flex;
    align-items: center;
  }
}
#img-file {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
/deep/ .dialog-dfl .el-dialog__header {
  padding: 0 !important;
}
/deep/ .dialog-dfl .el-dialog__body {
  padding: 0 !important;
}
/deep/ .dialog-dfl .el-dialog__headerbtn {
  z-index: 3;
}
/deep/ .dialog-dfl .el-dialog__footer {
  padding: 15px 24px !important;
  border-top: 1px solid #e4e7ed !important;
}
/deep/ .dialog-dfl .el-tabs__item {
  height: 60px !important;
  font-size: 16px !important;
}
/deep/ .dialog-dfl .el-tabs__nav-wrap::after {
  height: 1px !important;
}
/deep/ .dialog-dfl .el-tabs__item:hover {
  color: #fe3355 !important;
}
/deep/ .dialog-dfl .el-tabs__nav-wrap {
  padding-left: 20px !important;
}
/deep/ .dialog-dfl .el-tabs__active-bar {
  background-color: #fe3355 !important;
  height: 3px !important;
}
/deep/ .dialog-dfl .el-tabs__item.is-active {
  color: #fe3355 !important;
}
/deep/ .dialog-dfl .el-slider__button {
  position: relative !important;
  width: 24px !important;
  height: 94px !important;
  border: 2px solid #fe3355;
  border-radius: 4px;
  transform: translateY(12%);
}
/deep/ .dialog-dfl .el-slider__button::after {
  position: absolute;
  left: 5px;
  top: 50%;
  transform: translateY(-50%);
  content: '';
  background-color: #ebebeb;
  border-radius: 1.5px;
  height: 34px;
  width: 3px;
}
/deep/ .dialog-dfl .el-slider__button::before {
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  content: '';
  background-color: #ebebeb;
  border-radius: 1.5px;
  height: 34px;
  width: 3px;
}
/deep/ .dialog-dfl .el-slider__runway {
  background-color: transparent !important;
  height: 88px !important;
  top: -31px;
}
/deep/ .el-slider__bar {
  height: 88px !important;
  // transform: translateY(-48%);
  background-color: transparent;
}
/deep/ .dialog-dfl .el-slider {
  height: 100% !important;
}
/deep/ .dialog-dfl .el-loading-spinner .path {
  stroke: #fe3355 !important;
}
/deep/ .dialog-dfl .el-loading-spinner .el-loading-text {
  color: #c1c1c1 !important;
  margin-top: 10px;
}
/deep/ .el-slider__button {
  height: 74px;
}
</style>

Supongo que te gusta

Origin blog.csdn.net/aleluye/article/details/132737574
Recomendado
Clasificación