web-dev-qa-db-ja.com

JavaScript:ビデオフレームを確実に抽出

ユーザーがビデオファイルを提供し、それに基本的な操作を適用できるようにするクライアント側プロジェクトに取り組んでいます。ビデオからフレームを確実に抽出しようとしています。現時点では、選択したビデオをロードする_<video>_があり、次のように各フレームを引き出します。

  1. 最初に求める
  2. ビデオを一時停止する
  3. _<video>_を_<canvas>_に描画
  4. .toDataUrl()を使用してキャンバスからフレームをキャプチャします
  5. 1/30秒(1フレーム)先に進みます。
  6. すすぎ、繰り返し

これはかなり非効率的なプロセスであり、フレームがスタックすることがよくあるため、より具体的には、信頼性が低いことが判明しています。これは、キャンバスに描画する前に実際の_<video>_要素を更新していないためと思われます。

フレームを分割するためだけに元のビデオをサーバーにアップロードしてから、クライアントにダウンロードし直す必要はありません。

これを行うためのより良い方法についての提案は大歓迎です。唯一の注意点は、ブラウザーがサポートする任意の形式で動作させる必要があることです(JSでのデコードは優れたオプションではありません)。

24
Ian Wizard

この素晴らしい答えから主に取られますGameAlchemist による:

ブラウザはビデオのフレームレートを尊重しないため、代わりに「映画のフレームレートと画面のリフレッシュレートを一致させるためにいくつかのトリックを使用する」ため、30秒ごとに新しいフレーム塗装はかなり不正確です。
ただし、 timeupdate event は、currentTimeが変更されたときに発生し、新しいフレームがペイントされたと想定できます。

だから、私はそうするでしょう:

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();
}
<input type="file" accept="video/*" />
<p id="progress"></p>
20
Kaiido

この質問 から調整された作業関数を次に示します。

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 (https://stackoverflow.com/q/38062864/993683)
    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);
  });
});

}

使用法:

let frames = await extractFramesFromVideo("https://example.com/video.webm");

ffmpeg.js を使用しない限り、ビデオの実際の/自然なフレームレートを判別する簡単な方法は現在ありませんが、これは10+メガバイトのJavaScriptファイルです(実際のEmscriptenポートであるため) ffmpegライブラリ、明らかに巨大です)。

6
user993683