54

I want to capture a frame from video every 5 seconds.

This is my JavaScript code:

video.addEventListener('loadeddata', function() {
    var duration = video.duration;
    var i = 0;

    var interval = setInterval(function() {
        video.currentTime = i;
        generateThumbnail(i);
        i = i+5;
        if (i > duration) clearInterval(interval);
    }, 300);
});

function generateThumbnail(i) {     
    //generate thumbnail URL data
    var context = thecanvas.getContext('2d');
    context.drawImage(video, 0, 0, 220, 150);
    var dataURL = thecanvas.toDataURL();

    //create img
    var img = document.createElement('img');
    img.setAttribute('src', dataURL);

    //append img in container div
    document.getElementById('thumbnailContainer').appendChild(img);
}

The problem I have is the 1st two images generated are the same and the duration-5 second image is not generated. I found out that the thumbnail is generated before the video frame of the specific time is displayed in < video> tag.

For example, when video.currentTime = 5, image of frame 0s is generated. Then the video frame jump to time 5s. So when video.currentTime = 10, image of frame 5s is generated.

Lin
  • 736
  • 1
  • 8
  • 20
  • 1
    What is theCanvas on your generateThumbnail function? Can you please provide the html tags for this question to be more useful? I'm trying to do the same thing but I'm not sure how theCanvas should be declare on the page. Thanks! – alejosoft Nov 20 '15 at 16:45
  • Hi Lin, do you still have source code for this question you asked? can you please provide a link? – The Dead Man Jun 22 '19 at 14:55

2 Answers2

65

Cause

The problem is that seeking video (by setting it's currentTime) is asynchronous.

You need to listen to the seeked event or else it will risk take the actual current frame which is likely your old value.

As it is asynchronous you must not use the setInterval() as it is asynchronous too and you will not be able to properly synchronize when the next frame is seeked to. There is no need to use setInterval() as we will utilize the seeked event instead which will keep everything is sync.

Solution

By re-writing the code a little you can use the seeked event to go through the video to capture the correct frame as this event ensures us that we are actually at the frame we requested by setting the currentTime property.

Example

// global or parent scope of handlers
var video = document.getElementById("video"); // added for clarity: this is needed
var i = 0;

video.addEventListener('loadeddata', function() {
    this.currentTime = i;
});

Add this event handler to the party:

video.addEventListener('seeked', function() {

  // now video has seeked and current frames will show
  // at the time as we expect
  generateThumbnail(i);

  // when frame is captured, increase here by 5 seconds
  i += 5;

  // if we are not past end, seek to next interval
  if (i <= this.duration) {
    // this will trigger another seeked event
    this.currentTime = i;
  }
  else {
    // Done!, next action
  }
});
ashleedawg
  • 20,365
  • 9
  • 72
  • 105
  • 2
    I tried using your code. Now I have another problem. Because of seeked, when I seeked the video by clicking at timeline, the image is generated as well. But I don't want that. **Edit** I solved this problem already. Thank you. – Lin Oct 04 '13 at 08:11
  • `video.currentTime = i;` doesn't trigger any seeked event for me. – Michael Jan 28 '14 at 04:58
  • @Matian2040 no, unfortunately – Michael Feb 01 '17 at 16:48
  • oh you can actually compare frames and make sure they are different before displaying a "new one". here's a clue. https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas – MartianMartian Feb 02 '17 at 02:51
  • @Matian2040 sure, a simple method is to use the "difference" blending combined with saturation filter (0%) and count pixels > 0. Use a threshold based on percentage to avoid too much noise/compression influence. There are always chance for false positives but it can reduce number of different thumbs if that is a goal (or use a longer interval depending on the purpose). –  Feb 02 '17 at 05:42
  • @user1693593 your jsfiddle link is not working. can u plz post the correct link. I do also have similar use case to capture image. thanks in advance. – Gautam Tyagi May 14 '19 at 11:01
  • Hii, Your link does not work seems link is dead , can you please provide source code for this solution?? thanks – The Dead Man Jun 22 '19 at 14:51
  • @user9964622 no, I didn't find it. the link opens with 404 page not found. – Gautam Tyagi Jun 23 '19 at 17:36
0

If you'd like to extract all frames from a video, see this answer. The example below assumes that you want to extract a frame every 5 seconds, as OP requested.


This answer requires WebCodecs which is supported in Chrome and Edge as of writing.

<canvas id="canvasEl"></canvas>
<script type="module">
  import getVideoFrames from "https://deno.land/x/get_video_frames@v0.0.8/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);

  const saveFrameEverySeconds = 5;
  let elapsedSinceLastSavedFrame = 0;
  await getVideoFrames({
    videoUrl,
    onFrame(frame) {  // `frame` is a VideoFrame object: 
      elapsedSinceLastSavedFrame += frame.duration / 1e6; // frame.duration is in microseconds, so we convert to seconds
      if(elapsedSinceLastSavedFrame > saveFrameEverySeconds) {
        ctx.drawImage(frame, 0, 0, canvasEl.width, canvasEl.height);
        elapsedSinceLastSavedFrame = 0;
      }
      frame.close();
    },
    onConfig(config) {
      canvasEl.width = config.codedWidth;
      canvasEl.height = config.codedHeight;
    },
  });
  
  URL.revokeObjectURL(fileOrBlob); // revoke URL to prevent memory leak
</script>
joe
  • 3,752
  • 1
  • 32
  • 41