纯前端实现视频首帧截取(含真机兼容方案)

295 阅读4分钟

背景

当我们在做视频相关的业务时,常见的需求就是希望可以拿到视频的首帧作为视频的封面图,本文主要介绍了纯前端(浏览器端)方案,如何通过 videocanvas 截取视频的首帧,并将其绘制为图片,供我们实际业务使用。

目录

  • 技术方案
  • 效果示例
  • 兼容方案

技术方案

  • 技术原理:在 canvas 中绘制 video 标签的内容,并将其输出为图片。
  • 核心代码:
    const getVideoInfo = async (videoUrl) => {
      return new Promise((resolve) => {
        let video = document.createElement("video");
        video.src = videoUrl;
        video.currentTime = 0.1;
        video.preload = "metadata";
        video.addEventListener("loadeddata", async () => {
          let canvas = document.createElement("canvas"),
            width = video.videoWidth, //canvas的尺寸和图片一样,保证清晰度
            height = video.videoHeight;
    
          canvas.width = width;
          canvas.height = height;
    
          const delay = (ms) =>
            new Promise((resolve) => setTimeout(resolve, ms)); // 使用 await 等待 delay 函数完成,最好就是100ms,尝试过0、50ms都不行,画出来的图是黑屏
    
          await delay(100);
    
          canvas
            .getContext("2d")
            .drawImage(video, 0, 0, canvas.width, canvas.height); //绘制canvas
          const thumb = canvas.toDataURL("image/jpeg"); // 上传base64
          // 释放资源,防止移动端网页内存泄漏,导致页面崩溃
          canvas.width = 0;
          canvas.height = 0;
          video.src = "";
          video.load();
          video.remove();
    
          video = null;
          canvas = null;
    
          resolve(thumb);
        });
      });
    };
    
  • 使用方法:只需要传入对应的 video 的视频源地址,即可获取对应的截图,可以将上述方法直接当作项目中的工具函数使用,或者直接应用在业务逻辑中。

效果示例

理论上 Chrome 电脑端可以直接使用,所以本文的案例偏向于移动端 H5 界面。主要使用了桌面端的 Chrome 的手机端模拟器,以及真机 iPhone 12 的微信浏览器(iPhone 的 IOS 系统中所有浏览器基本都是基于 Safari 的 webkit 内核,因此可以当作 Safari 浏览器看待)。

  • Chrome 的移动端模拟器
    • 默认视频显示效果 图2-1.png

    • 点击截图按钮后效果(不设置 video 的 poster 属性)

      图2-2.png

    • 点击截图按钮后效果(设置 video 的 poster 属性) 图2-2.png

  • iPhone 12 的微信浏览器
    • 默认视频显示效果 图3-1.jpg
    • 点击截图按钮后效果(不设置 video 的 poster 属性) 图3-2.jpg
    • 点击截图按钮后效果(设置 video 的 poster 属性) 图3-3.jpg

兼容方案

从上面的效果可以看到,在 iPhone 中的浏览器,默认不会加载视频首帧作为视频封面,需要开发者手动设置 poster 属性,而在 Chrome 的手机端模拟器上是会默认选择视频的首帧来作为视频封面的。

因此可以理解为本文的核心代码作为一种兼容方案,解决了 iPhone 手机浏览器中视频没有封面的兼容性问题。

以下为效果示例中的完整代码,同时也可以作为如何使用 getVideoInfo 方法的一种示例效果。大家可以根据实际情况来尝试 poster 的设置,以及视频帧的截取,调整 getVideoInfo 中的 currentTime 时间可以截取到预期的 video 播放画面。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Canvas + Video 实现首帧截取</title>
  </head>
  <body>
    <h3>Canvas + Video 实现首帧截取</h3>
    <video
      width="100%"
      id="video"
      src="./assets/video/test.mp4"
      controls
    ></video>
    <div>
      <button id="btn">点我开始截取</button>
    </div>
    <h3>截取后的首帧截图</h3>
    <div>
      <img width="100%" id="img" src="" />
    </div>
  </body>
  <script>
    const video = document.getElementById("video");
    const btn = document.getElementById("btn");
    const img = document.getElementById("img");

    btn.addEventListener("click", async () => {
      const thumb = await getVideoInfo(video.src);
      img.src = thumb;
      // 设置 video 的 poster 属性
      video.poster = thumb;
    });

    const getVideoInfo = async (videoUrl) => {
      return new Promise((resolve) => {
        let video = document.createElement("video");
        video.src = videoUrl;
        // 根据实际项目可以调整期望截取的视频帧
        video.currentTime = 0.1;
        video.preload = "metadata";
        video.addEventListener("loadeddata", async () => {
          let canvas = document.createElement("canvas"),
            width = video.videoWidth, //canvas的尺寸和图片一样,保证清晰度
            height = video.videoHeight;

          canvas.width = width;
          canvas.height = height;

          const delay = (ms) =>
            new Promise((resolve) => setTimeout(resolve, ms)); // 使用 await 等待 delay 函数完成,最好就是100ms,尝试过0、50ms都不行,画出来的图是黑屏

          await delay(100);

          canvas
            .getContext("2d")
            .drawImage(video, 0, 0, canvas.width, canvas.height); //绘制canvas
          const thumb = canvas.toDataURL("image/jpeg"); // 上传base64
          // 释放资源,防止移动端网页内存泄漏,导致页面崩溃
          canvas.width = 0;
          canvas.height = 0;
          video.src = "";
          video.load();
          video.remove();

          video = null;
          canvas = null;

          resolve(thumb);
        });
      });
    };
  </script>
</html>

杂谈

  • 本文利用了浏览器的 video 标签和 canvas 绘制了视频首帧的获取,实际项目中可以借助服务端对视频做裁剪也能实现相同效果。其中可以在服务端利用无头浏览器,或者使用 FFmpeg,或者使用第三方多媒体服务等方式实现。
  • 在我们处理 video 标签相关的逻辑和效果时,不要完全依赖桌面端的浏览器模拟器,要时刻关注 Chrome 和 Safari 的差异,以及移动端不同浏览器之间的差异,避免等到上线时才发现兼容性问题,临时更改方案,造成风险。

参考文档

浏览知识共享许可协议

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。