上传video时读取首屏生成封面图的坑

4,183 阅读1分钟

需求简介

需求:上传一个视频文件,截取视频的第一帧作为封面图 使用 vue+vant

文件上传

[注意项] 1、在safari中由于无点击事件无法触发video的loadeddata事件,所以需要上传视频文件后手动点击生成poster

<van-uploader
  v-if="!form.videoUrl"
  accept="video/*"
  v-model="videoList"
  max-count="1"
  :preview-image="false"
  :after-read="onVideoUpload"
/>

监听after-read钩子获取到文件实例

async onVideoUpload(file) {
  try {
    this.videoUploading = true;
    const blob = new Blob([file.file]);
    const localUrl = URL.createObjectURL(blob);

    // 校验video合法并且返回video 此处会监听 loadeddata 事件在ios无效
    const video = await validateVideo(localUrl);
    if (!video) {
      // safari下无法播放自动生成首图 切无法获取视频时长
      // 这种情况下让用户手动触发获取视频播放状态
      this.videoLoadError = true;
    } else {
      // 把video转化为file
      const res = await videoCapture(video);
      const picFile = await base64toFile(res);
      const picUrl = await uploadFile(picFile);
      this.form.videoCoverImg = picUrl;
    }

    const resultUrl = await uploadFile(file.file);
    file.url = resultUrl;
    this.form.videoUrl = resultUrl;
    this.videoUploading = false;
  } catch (err) {
    this.videoUploading = false;
    this.videoLoadError = false;
    this.videoList = [];
    this.$toast({
      position: 'bottom',
      message: err.message
    });
  }
},

校验视频loadeddata事件,并返回video本身

注意 1、如果视频需要跨域处理,即外部链接,则video标签需要添加crossOrigin="anonymous"属性

export const validateVideo = videourl => {
  const video = document.createElement('video');
  video.src = videourl;
  video.currentTime = 0.1;
  video.load();
  return new Promise((resolve, reject) => {
    video.addEventListener('loadeddata', function() {
      if (video.duration > 16) {
        reject({ message: '上传视频不符合规范' });
      } else {
        resolve(video);
      }
    });
    // 超过三秒钟则直接退出
    setTimeout(() => {
      resolve(false);
    }, 3000);
  });
};

使用canvas画出第一帧并保存为base64格式

export default video => {
  return new Promise((res, rej) => {
    try {
      const vw = video.videoWidth;
      const vh = video.videoHeight;
      const scale = 0.25;

      const canvas = document.createElement('canvas');
      canvas.width = vw * scale;
      canvas.height = vh * scale;
      const fill = canvas.getContext('2d');
      fill.drawImage(video, 0, 0, canvas.width, canvas.height);
      res(canvas.toDataURL('image/png'));
    } catch (err) {
      rej(err);
    }
  });
};

把base64图片转为file类型并上传给服务器

1、这里有个坑是如果正则表达式在safari浏览器上由于解码原因会报错

[Works in Chrome, but breaks in Safari: Invalid regular expression: invalid group specifier name /(?<=/)([^#]+)(?=#*)/ [duplicate]](stackoverflow.com/questions/5…)

export const base64toFile = base64 => {
  return new Promise((resolve, reject) => {
    try {
      var arr = base64.split(',');
      var mime = arr[0].match(/:(.*?);/)[1];
      var bstr = atob(arr[1]);
      var n = bstr.length;
      var u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }

      resolve(new File([u8arr], 'poster.png', { type: mime }));
      // resolve(new Blob([u8arr], { type: mime }));
    } catch (err) {
      reject({ message: '转码失败' });
    }
  });
};

超过3秒未响应loadeddata事件,则使用点击事件触发生成预览图

<video
  ref="video"
  crossOrigin="anonymous"
  :src="form.videoUrl"
  :poster="form.videoCoverImg"
></video>

<van-button
  v-if="videoError"
  class="preview-butn"
  native-type="button"
  @click="genereteVideoPoster"
>
  生成预览图
</van-button>

async genereteVideoPoster() {
  try {
    const video = this.$refs.video;

    const videoDom = await videoLoadeddata(video);

    const res = await videoCapture(videoDom);

    const picFile = await base64toFile(res);
    const picUrl = await uploadFile(picFile);
    this.form.videoCoverImg = picUrl;
    this.videoPaused = false;
  } catch (err) {
    this.$toast({
      position: 'bottom',
      message: err.message
    });
  }
}

结语

1、文章随手粘贴的代码,本人水平有限

  • 在safari中由于无点击事件无法触发video的loadeddata事件,所以需要上传视频文件后手动点击生成poster
  • 如果视频需要跨域处理,即外部链接,则video标签需要添加crossOrigin="anonymous"属性
  • 如果正则表达式在safari浏览器上由于解码原因会报错Invalid regular expression: invalid group specifier name

2、关于参考的链接