web-dev-qa-db-fra.com

JavaScript: extraire des images vidéo de manière fiable

Je travaille sur un projet côté client qui permet à un utilisateur de fournir un fichier vidéo et de lui appliquer des manipulations de base. J'essaie d'extraire les images de la vidéo de manière fiable. Pour le moment, j'ai un <video> Dans lequel je charge la vidéo sélectionnée, puis je retire chaque image comme suit:

  1. Cherchez au début
  2. Mettre la vidéo en pause
  3. Dessinez <video> Dans un <canvas>
  4. Capturez le cadre du canevas avec .toDataUrl()
  5. Avance de 1/30 seconde (1 image).
  6. Rincer et répéter

Il s'agit d'un processus plutôt inefficace, et plus précisément, il s'avère peu fiable car je suis souvent bloqué par des trames. Cela semble provenir du fait qu'il ne met pas à jour l'élément réel <video> Avant qu'il ne dessine dans le canevas.

Je préfère ne pas avoir à télécharger la vidéo d'origine sur le serveur juste pour diviser les images, puis les télécharger à nouveau sur le client.

Toutes les suggestions pour une meilleure façon de le faire sont grandement appréciées. La seule mise en garde est que j'en ai besoin pour travailler avec n'importe quel format pris en charge par le navigateur (le décodage dans JS n'est pas une excellente option).

24
Ian Wizard

Principalement tiré de cette excellente réponse par GameAlchemist :

Étant donné que les navigateurs ne respectent pas les fréquences d'images des vidéos, mais plutôt "l'utilisation de quelques astuces pour faire une correspondance entre la fréquence d'images du film et la fréquence de rafraîchissement de l'écran", votre hypothèse est que, tous les 30 secondes, une nouvelle le cadre sera peint est assez inexact.
Cependant, l'événement timeupdate devrait se déclencher lorsque le paramètre CurrentTime a changé, et nous pouvons supposer qu'un nouveau cadre a été peint.

Donc, je le ferais comme ça:

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

Voici une fonction qui a été modifiée à partir de cette question :

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);
  });
});

}

Usage:

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

Notez qu'il n'y a actuellement aucun moyen facile de déterminer la fréquence d'images réelle/naturelle d'une vidéo à moins que vous n'utilisiez peut-être ffmpeg.js , mais c'est un fichier javascript de 10+ mégaoctets (car c'est un port emscripten du bibliothèque ffmpeg, qui est évidemment énorme).

6
user993683