8

I'm trying to build a function that extracts frames from a video in JavaScript. Here the code I came up with. The function receives a source and a callback. From there, I create a video with the source and I want to draw frames of the video in the canvas with a set interval.

Unfortunately, the frames returned are all transparent images.

I tried a few different things, but I can't make it work. Can someone help? Thanks.

const extractFramesFromVideo = function(src, callback) {
  var video = document.createElement('video');
  video.src = src;

  video.addEventListener('loadeddata', function() {
    var canvas = document.createElement('canvas');
    var context = canvas.getContext('2d');
    canvas.setAttribute('width', video.videoWidth);
    canvas.setAttribute('height', video.videoHeight);

    var frames = [];
    var fps = 1; // Frames per seconds to
    var interval = 1 / fps; // Frame interval
    var maxDuration = 10; // 10 seconds max duration
    var currentTime = 0; // Start at 0

    while (currentTime < maxDuration) {
      video.currentTime = currentTime; 
      context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
      var base64ImageData = canvas.toDataURL();
      frames.push(base64ImageData);

      currentTime += interval;

      if (currentTime >= maxDuration) {
        console.log(frames);
        callback(frames);
      }
    }
  });
}

export default extractFramesFromVideo;
Steven Fabre
  • 91
  • 1
  • 2
  • If I move the `context.drawImage` outside of the while, it draws the first frame. – Steven Fabre Jun 01 '17 at 02:51
  • Possible duplicate of [JavaScript: Extract video frames reliably](https://stackoverflow.com/questions/32699721/javascript-extract-video-frames-reliably) – Kaiido Jun 01 '17 at 23:32
  • 2
    I think the problem with this is that after changing `video.currentTime`, you need to actually wait for the video to seek to that position and update the frame data, then you can extract it. It may need to download some data, so it's not synchronous. The `loadeddata` event just indicates that the first frame as be downloaded (at least). I think, after changing `currentTime`, you'd need to either wait for the [`seeked`](https://developer.mozilla.org/en-US/docs/Web/Events/seeked) event. –  Sep 16 '18 at 18:43

2 Answers2

9

After tweaking your code to wait for the seeked event, and fixing a few bits and pieces, it seems to work fine:

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.addEventListener('loadeddata', async function() {
      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;
      let duration = video.duration;

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

    // set video src *after* listening to events in case it loads so fast
    // that the events occur before we were listening.
    video.src = videoObjectUrl; 

  });
}

Usage:

let frames = await extractFramesFromVideo("https://example.com/video.webm");
  • Where to get all the frames which are getting extracted from this code? – EdG Oct 25 '18 at 09:12
  • Nice answer ! You could improve it by removing all of the Promise stuff and seeked event listener and replacing `await new Promise(r => seekResolve=r)` by a single `await once(video, 'seeked')` – Theo Martinez Mar 30 '22 at 16:28
-1

currentComponent.refs.videopreview is <video ref="videopreview" autoPlay></video> in HTML page Below is the code to extract the frame from the video.

   const getFrame = () => {

        const video = currentComponent.refs.videopreview;
        if(!video.srcObject) {
          return null;
        }
        const canvas = document.createElement('canvas');
        canvas.width = canvasWidth;
        canvas.height = canvasHeight;
        canvas.getContext('2d').drawImage(video, 0,0);
        const data = canvas.toDataURL('image/jpeg');     
        return data; // frame data
      }

getFrame function can be called at required interval.

user2010672
  • 79
  • 1
  • 12