Bootstrap

选择本地视频上传至cos,上传cos前截取视频第一帧做视频封面

最近的需求是将原来上传cos的视频,在上传cos之前截取下视频的第一帧做封面(至于为啥不用cos里自带的截取封面功能,这个领导安排不纠结哈)。
先简单说下这边上传cos的方式,前端通过调用后端同学的获取签名接口,拿到签名和上传文件所要存储的路径,然后将存储桶路径和存储路径和签名拼接在一起就形成了上传cos必须的鉴权url(也可以后端同学自己拼接好直接返回完整的鉴权url),代码如下:
封装好的上传视频的子组件A页面

      // 视频上传
      async uploadcos(file) {
        let _this = this
        console.log('要上传的文件:', file)
        const authData = await getYourAuth({ fileName: file.name })
        this.videoUrl = authData.key
        this.videoData = file//这一步是为了将本地文件的文件流传递给父组件用以截取视频封面
        var xhr = new XMLHttpRequest()
        xhr.open('PUT', '你们自己的存储桶访问域名' + authData.key + '?sign=' + authData.sign, true)
        xhr.onload = function(e) {
          console.log('上传成功', xhr.status, xhr.statusText)
        }
        xhr.onerror = function(e) {
          _this.videoUrl = ''//cos上传出错记得把链接情况,以免实际上传失败结果却显示了错误的预览(虽然腾讯云一般情况下不会出错)
          _this.$message({ message: '上传出错,请联系管理人员处理', type: 'warning', center: true })
          console.log('上传出错', xhr.status, xhr.statusText)
        }
        xhr.send(file) // file 是要上传的文件对象
      },
      startUploadFile(e) {
        if (e) {
          const file = e.file
          var fileSize = file.size / 1024 / 1024 < 200
          if (!file) {
            return
          }
          if (!fileSize) {
            this.$message({ message: '视频大小不能超过200M', type: 'warning', center: true })
            return false
          }
          this.uploadcos(file)
        }
      },
      handleClose(){
       let vm = this
       vm.$refs['uploadVideoRef'].validate((valid) => {
          this.$emit('close', { url: vm.videoUrl, cosStatusflag: 3,         videoFile: vm.videoData })
          this.fileList = []
          if (valid) {
          } else {
            return false
          }
        })
}

好了,上传cos的前端工作已经做完了,下面说截取视频封面。
思路就是将从子组件拿到的视频资源,放到写好的隐藏的video标签内,然后监听视频加载事件,当视频加载完之后,通过canvas绘制封面,将绘制好的base64格式的封面图转成后端同学需要的格式单独上传或者和同一表单的其他字段一起上传即可。(我这里是单独上传封面,拿到后端返的链接形式的封面图再和其他字段一起提交到服务器)
使用子组件的父组件B页面

closeVideoDialog(res) {
  const _this = this
  if (res) {
    this.editImgFrom.tableData[this.currentIndex].videoData = URL.createObjectURL(res.videoFile);//视频的文件流--用以预览

    let file=res.videoFile;
    let dataURL = ''
    let video = document.getElementById('noseeVideo'+this.currentIndex)
    video.addEventListener('loadeddata', function() {
      let canvas = document.createElement('canvas')
      canvas.getContext('2d').drawImage(video, 0, 0, '300', '200') //绘制canvas
      dataURL = canvas.toDataURL('image/jpeg') //转换为base64
      //因为这边的上传图片接口支持base64格式的,所以没有用到转化,直接传了base64因此下面这些转换格式的代码给注释掉了
      /*_this.editImgFrom.tableData[_this.currentIndex].posterImg = dataURL
      let blob = _this.base64ToBlob(dataURL,'image/jpeg');
      console.log('图片二进制流:', blob)
      let file = _this.base64ToFile(dataURL, '视频封面1', null)
      _this.editImgFrom.tableData[_this.currentIndex].posterImgFile=file;;
      console.log('图片文件流:', file)*/

      let imgcode=dataURL.split(',')[1];
      var fileEnd = file.name.substring(file.name.indexOf('.'));
      _this.uploadImg(imgcode).then(function(res){
        _this.editImgFrom.tableData[_this.currentIndex].posterImg = res;//封面图赋值--只这样赋值是无法更新视图的
        _this.editImgFrom.tableData[_this.currentIndex] = {..._this.editImgFrom.tableData[_this.currentIndex]}//...这种方式可以直接更新视图,和Vue.set(),vm.$set通等功效,非常方便
      }).catch(err=>{
        console.log(err.mesage)
      })
    })
  }
  this.videoVisible = false
},

下附base64流转换为二进制blob或者直接转file的两个方法

base64ToBlob(dataURI, type) {//base64转blob
  let binary = atob(dataURI.split(',')[1]);
  let array = [];
  for (let i = 0; i < binary.length; i++) {
    array.push(binary.charCodeAt(i));
  }
  return new Blob([new Uint8Array(array)], { type: type });
},
base64ToFile(dataURL, fileName, mimeType = null) {//base64转file
  const arr = dataURL.split(','),
    defaultMimeType = arr[0].match(/:(.*?);/)[1],
    bStr = atob(arr[1]),
    u8arr = new Uint8Array(n);
  let n = bStr.length
  while (n--) {
    u8arr[n] = bStr.charCodeAt(n)
  }
  return new File([u8arr], fileName, {type: mimeType || defaultMimeType})
},

20210531更改,通过上述方式虽然可以实现以formdata的传参方式将图片的文件流传递,但是调用后端同学提供的上传接口时却出现了报错,status是200,response是-2,找后端同学帮忙查看发现,他们拿到文件流之后会针对文件流中的name,type字段做处理,比如图片的name只接受xx.jpg这种形式的,而我通过上述直接转file或者直接转blob,或者通过blob再转file之后的name要么是xx.mp4(因为图片是截取的视频),要么是写死的字符串xx均不符合后端逻辑,因此导致上传失败,明白了问题出在哪里对照着要求命名就行了。说实话当时为了找这个问题我自己看了一晚上,完全没想到后端同学会从name中截取文件的类型进行二次判断,我以为他们只需要file中的type即可。不管如何,问题总算解决了。另外,整个找问题的过程,我发现了这种通过base64流转换file和从本地文件中直接选取图片的不同差异。比如转换生成的file中size不正确(明显小于实际值),而且不管以何种方式转换后的都没有uid,size不对就转blob之后再转file,如果后端同学的逻辑需要uid可以自己生成uid,添加进去。我的最终选择就是通过将base64转成blob之后再转成file,并用时间戳给图片命名,成功实现了上传。上传方法改成了下面代码这样

async uploadImg(imgData) {//上传封面(参数为base64)
        let vm = this;
        let imgUrl;
        let timestamp=new Date().getTime();
        let action = location.origin + '/sftp/upload'
        var blob = vm.dataURLtoBlob(imgData);
        var file=new File([blob ], `视频封面${timestamp}.jpg`,{type:'image/jpg'})
        let formData = new FormData();
        formData.append("file", file);
        var xhr = new XMLHttpRequest()
        xhr.open('POST', action, true)
        xhr.setRequestHeader('Auth-token', vm.headers)
        xhr.onload = function() {
          let obj=xhr.response;
          if((typeof obj=='object')&&obj.constructor==Object){
            obj = obj;
          }else{
            obj  = eval("("+obj+")");
          }
          imgUrl = obj.data;
          vm.$forceUpdate();//{...obj}这种方式会导致表单其他字段事件无效,因此改用直接赋值前强更新的方式来解决
          vm.editImgFrom.tableData[vm.currentIndex].coverImg = imgUrl;
        }
        xhr.onerror = function() {
          imgUrl = '';
          vm.$forceUpdate();
          vm.editImgFrom.tableData[vm.currentIndex].coverImg = imgUrl;
          _this.$message({ message: '上传出错,请联系管理人员处理', type: 'warning', center: true })//{...obj}这种方式会导致表单其他字段事件无效,因此改用直接赋值前强更新的方式来解决
        }
        xhr.send(formData)
      },

希望帮到大家,避免踩坑。

;