本地上传视频生成第一帧截图

2021/8/2 canvasvideo

需求来源于一个页面上会展示很多视频让用户进行选择,如果都是直接以 video 标签的形式放在上面对加载的影响比较大,看了一下爱腾优的视频列表实际上也都只是显示一张图片,因为列表中放视频第一帧的截图基本上就能看出来这是什么视频了。

现在问题变成了上传视频的同时生成对应的第一帧截图,这个工作可以后台处理也可以前台处理,这边主要描述一下怎么在前台生成好截图传给后台。

首先是怎么根据视频截图,可以调用 canvas 的 drawImage (opens new window) 方法来绘制图像,然后通过 toDataURL (opens new window) 返回一个包含图片展示的 data URI (opens new window)

  const canvas = document.createElement('canvas');
  const video = document.getElementById('video');
  const img = document.getElementById('img');
  canvas.width = video.clientWidth;
  canvas.height = video.clientHeight;
  
  // 监听media中的首帧已经加载事件
  video.onloadeddata = () => {
    canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
    img.src = canvas.toDataURL('image/png');
  }
1
2
3
4
5
6
7
8
9
10
11

如果是截图一个已经加载好的视频可以通过上述方式操作,如果是处理本地上传的视频,那么就需要在上传之前先创建 URL 对象给 video,然后再调用上述方法操作。

注意:video 不能直接播放本地的视频,需要部署到服务器作为资源或者是用户上传的时候可以读取文件流。

const video = document.createElement("video");

// 创建一个URL对象作为video的src,不要忘记通过revokeObjectURL释放
video.src = window.URL.createObjectURL(file);

// 需要配置静音且默认播放,不然可能截图的可能是个空白
video.muted = true;
video.autoplay = true;

video.onloadeddata = () => {
  // 如果还是出现空白可以手动播放一下
  // video.play();
  
  // 进行之前类似的操作进行截图就行了
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过上述步骤就可以在用户上传视频的时候截图了,如果把 video 的 src 改成一个在线的 src 也可以对任意在线视频进行截图。不过一个光秃秃的图片看着有点不太像是视频的样子,如果右下角加上视频的时长就更好了。video (opens new window) 在 loadedmetadata 事件之后就可以获取到一些 meta 信息了,比如 duration 就是一个以秒为单位的视频时长,转换成 00: 01 这种表现形式通过 canvas 的 fillText 放在右下角就行了。

完整例子:

const captureImage = (videoDom: HTMLVideoElement) => {
  const img = document.getElementById('img');
  const canvas = document.createElement("canvas");
  canvas.width = 160; //可以根据实际需求调整
  canvas.height = (canvas.width * videoDom.videoHeight) / videoDom.videoWidth;
  
  const ctx = canvas.getContext("2d");
  
  if (ctx) {
    // 视频第一帧
    ctx.drawImage(videoDom, 0, 0, canvas.width, canvas.height);
  
    // 视频时长
    ctx.font = "12px";
    ctx.fillStyle = "#fff";
    const duration = formatSeconds(round(videoDom.duration)); // 格式化秒为hh:mm:ss
    
    // 获取了一下文本宽度,因为fillText的坐标是左上角
    const textWidth = ctx.measureText(duration).width;
    ctx.fillText(
      duration,
      canvas.width - textWidth - 10,
      canvas.height - 10,
      textWidth
    );
    img.src = canvas.toDataURL('image/png');
  }
};

const handleUpload = (file: File) => {
  const video = document.createElement("video");
  const src = window.URL.createObjectURL(file);
  video.src = src;
  video.muted = true;
  video.autoplay = true;
  video.onloadeddata = () => {
    captureImage(video);
    window.URL.revokeObjectURL(src);
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40