JavaScript:可靠地提取视频帧

Ian*_*ard 23 javascript video html5 canvas

我正在开发一个客户端项目,它允许用户提供视频文件并对其应用基本操作.我正在尝试可靠地从视频中提取帧.目前我有一个<video>我正在加载所选视频,然后按如下方式拉出每个帧:

  1. 寻求开始
  2. 暂停视频
  3. <video>到一个<canvas>
  4. 用画布从画布中捕捉帧 .toDataUrl()
  5. 向前搜索1/30秒(1帧).
  6. 冲洗并重复

这是一个相当低效的过程,更具体地说,证明是不可靠的,因为我经常遇到卡住的帧.这似乎是因为它不会<video>在绘制到画布之前更新实际元素.

我宁愿不必将原始视频上传到服务器只是为了分割帧,然后将它们下载回客户端.

任何有关更好的方法的建议都非常感谢.唯一需要注意的是,我需要它使用浏览器支持的任何格式(在JS中解码不是一个很好的选择).

Kai*_*ido 17

从这个伟大的答案大多采取GameAlchemist:

由于浏览器不尊重视频的帧速率,而是"使用一些技巧在电影的帧速率和屏幕的刷新率之间进行匹配",你的假设是每隔30秒,一个新的框架将被绘制是非常不准确的.
但是,当currentTime发生变化时,timeupdate事件应该触发,我们可以假设画了一个新帧.

所以,我会这样做:

document.querySelector('input').addEventListener('change', extractFrames, false);

function extractFrames() {
  var video = document.createElement('video');
  var array = [];
  var canvas = document.createElement('canvas');
  var ctx = canvas.getContext('2d');
  var pro = document.querySelector('#progress');

  function initCanvas(e) {
    canvas.width = this.videoWidth;
    canvas.height = this.videoHeight;
  }

  function drawFrame(e) {
    this.pause();
    ctx.drawImage(this, 0, 0);
    /* 
    this will save as a Blob, less memory consumptive than toDataURL
    a polyfill can be found at
    https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Polyfill
    */
    canvas.toBlob(saveFrame, 'image/jpeg');
    pro.innerHTML = ((this.currentTime / this.duration) * 100).toFixed(2) + ' %';
    if (this.currentTime < this.duration) {
      this.play();
    }
  }

  function saveFrame(blob) {
    array.push(blob);
  }

  function revokeURL(e) {
    URL.revokeObjectURL(this.src);
  }
  
  function onend(e) {
    var img;
    // do whatever with the frames
    for (var i = 0; i < array.length; i++) {
      img = new Image();
      img.onload = revokeURL;
      img.src = URL.createObjectURL(array[i]);
      document.body.appendChild(img);
    }
    // we don't need the video's objectURL anymore
    URL.revokeObjectURL(this.src);
  }
  
  video.muted = true;

  video.addEventListener('loadedmetadata', initCanvas, false);
  video.addEventListener('timeupdate', drawFrame, false);
  video.addEventListener('ended', onend, false);

  video.src = URL.createObjectURL(this.files[0]);
  video.play();
}
Run Code Online (Sandbox Code Playgroud)
<input type="file" accept="video/*" />
<p id="progress"></p>
Run Code Online (Sandbox Code Playgroud)

  • 我的假设不是每隔30秒会有一个新的帧,但如果我每1/30秒得到1帧,我最终会得到30帧的FPS.我犯的错误是假设我得到的帧实际上是正确的当前帧.我很感激帮助.timeupdate事件非常有用.TY :) (2认同)
  • 这种方法并不完全可靠:它曾经返回 4172 帧,然后在第二次运行时返回 4573,根据 ffmpeg,视频实际上只有 250 帧。10 秒 @ 25 fps:http://www.w3schools.com/html/mov_bbb.mp4 (2认同)

小智 7

这是一个根据此问题调整的工作功能:

async function extractFramesFromVideo(videoUrl, fps=25) {
  return new Promise(async (resolve) => {

    // fully download it first (no buffering):
    let videoBlob = await fetch(videoUrl).then(r => r.blob());
    let videoObjectUrl = URL.createObjectURL(videoBlob);
    let video = document.createElement("video");

    let seekResolve;
    video.addEventListener('seeked', async function() {
      if(seekResolve) seekResolve();
    });

    video.src = videoObjectUrl;

    // workaround chromium metadata bug (/sf/ask/2664400511/)
    while((video.duration === Infinity || isNaN(video.duration)) && video.readyState < 2) {
      await new Promise(r => setTimeout(r, 1000));
      video.currentTime = 10000000*Math.random();
    }
    let duration = video.duration;

    let canvas = document.createElement('canvas');
    let context = canvas.getContext('2d');
    let [w, h] = [video.videoWidth, video.videoHeight]
    canvas.width =  w;
    canvas.height = h;

    let frames = [];
    let interval = 1 / fps;
    let currentTime = 0;

    while(currentTime < duration) {
      video.currentTime = currentTime;
      await new Promise(r => seekResolve=r);

      context.drawImage(video, 0, 0, w, h);
      let base64ImageData = canvas.toDataURL();
      frames.push(base64ImageData);

      currentTime += interval;
    }
    resolve(frames);
  });
});
Run Code Online (Sandbox Code Playgroud)

}

用法:

let frames = await extractFramesFromVideo("https://example.com/video.webm");
Run Code Online (Sandbox Code Playgroud)

请注意,除非有可能使用ffmpeg.js,否则目前没有简单的方法来确定视频的实际/自然帧速率,但这是一个10兆以上的javascript文件(因为它是实际ffmpeg库的脚本端口,显然这很大) )。

  • 有趣的是,在您回答之后我就碰到了这一点。我将给出一个类似的答案(但要详细得多),指出使用ontimeupdate的问题,并建议使用设置video.currentTime的解决方案。优秀作品! (2认同)
  • @ParthibanRajendran您可以使用现有的画布并将其清除,然后再开始在其上绘制。如果适合您的需求,这实际上可能会更好地提高性能! (2认同)

joe*_*joe 6

2023年答案:

如果您想可靠地提取所有帧(即没有“寻找”和丢失帧),并尽可能快地提取(即不受播放速度或其他因素的限制),那么您可能需要使用 WebCodecs API。截至撰写本文时,Chrome 和 Edge 支持它。其他浏览器很快就会跟进 - 希望到 2023 年底能够得到广泛支持。

我为此编写了一个简单的库,但它目前仅支持 mp4 文件。这是一个例子:

<canvas id="canvasEl"></canvas>
<script type="module">
  import getVideoFrames from "https://deno.land/x/get_video_frames@v0.0.9/mod.js"

  let ctx = canvasEl.getContext("2d");

  // `getVideoFrames` requires a video URL as input.
  // If you have a file/blob instead of a videoUrl, turn it into a URL like this:
  let videoUrl = URL.createObjectURL(fileOrBlob);

  await getVideoFrames({
    videoUrl,
    onFrame(frame) {  // `frame` is a VideoFrame object: https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame
      ctx.drawImage(frame, 0, 0, canvasEl.width, canvasEl.height);
      frame.close();
    },
    onConfig(config) {
      canvasEl.width = config.codedWidth;
      canvasEl.height = config.codedHeight;
    },
  });
  
  URL.revokeObjectURL(fileOrBlob); // revoke URL to prevent memory leak
</script>
Run Code Online (Sandbox Code Playgroud)

(请注意,@Kaiido 的优秀答案中提到了 WebCodecs API,但不幸的是,仅此 API 并不能解决问题 - 上面的示例使用mp4box.js来处理 WebCodecs 无法处理的内容。也许 WebCodecs 最终会支持容器方面的事情和这个答案将变得几乎无关紧要,但在那之前我希望这是有用的。)