Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generate a thumbnail/snapshot of a video file selected by a file input at a specific time

How do I grab a snapshot of a video file selected via <input type="file"> at a specific time in the video silently in-the-background (i.e. no visible elements, flickering, sound, etc.)?

like image 237
wilsonzlin Avatar asked Apr 27 '16 07:04

wilsonzlin


People also ask

How do I make video thumbnails preview?

Making a video thumbnail is relatively straightforward. You can take a screenshot from your video that best explains its contents, or you can use a tool like Photoshop or Canva to overlay text or icons on the image. To make a screen capture on a PC, use the Windows Snipping Tool.

How do you make a video thumbnail in HTML?

Approach: Sometimes the user wants to display a specific image of his choice as the thumbnail of the video. This can be simply done by using the poster attribute. All you have to do is create a poster attribute in the video tag and place the URL of the thumbnail image in it.


1 Answers

There are four major steps:

  1. Create <canvas> and <video> elements.
  2. Load the src of the video file generated by URL.createObjectURL into the <video> element and wait for it to load by listening for specific events being fired.
  3. Set the time of the video to the point where you want to take a snapshot and listen for additional events.
  4. Use the canvas to grab the image.

Step 1 - Create the elements

This is very easy: just create one <canvas> and one <video> element and append them to <body> (or anywhere really, it doesn't really matter):

var canvasElem = $( '<canvas class="snapshot-generator"></canvas>' ).appendTo(document.body)[0];
var $video = $( '<video muted class="snapshot-generator"></video>' ).appendTo(document.body);

Notice that the video element has the attribute muted. Don't put any other attributes like autoplay or controls. Also notice that they both have the class snapshot-generator. This is so we can set the style for both of them so that they are out of the way:

.snapshot-generator {
  display: block;
  height: 1px;
  left: 0;
  object-fit: contain;
  position: fixed;
  top: 0;
  width: 1px;
  z-index: -1;
}

Some browsers work with them set to display: none, but other browsers will have serious problems unless they are rendered on the page, so we just make them minuscule so that they are essentially invisible. (Don't move them outside the viewport though, as otherwise you may see some ugly scrollbars on your page.)

Step 2 - Load the video

Here's where things start to get tricky. You need to listen to events to know when to continue. Different browsers will fire different events, different times and in different orders, so I'll save you the effort. There are three events that must always fire at least once before the video is ready; they are:

  • loadedmetadata
  • loadeddata
  • suspend

Set up the event handler for these events and keep track how many have fired. Once all three have fired, you are ready to proceed. Keep in mind that, since some of these events may fire more than once, you only want to handle the first event of each type that is fired, and discard subsequent firings. I used jQuery's .one, which takes care of this.

var step_2_events_fired = 0;
$video.one('loadedmetadata loadeddata suspend', function() {
    if (++step_2_events_fired == 3) {
        // Ready for next step
    }
}).prop('src', insert_source_here);

The source should just be the object URL created via URL.createObjectURL(file), where file is the file object.

Step 3 - Set the time

This stage is similar to the previous: set the time and then listen for an event. Inside our if block from the previous code:

$video.one('seeked', function() {
    // Ready for next step
}).prop('currentTime', insert_time_here_in_seconds);

Luckily its only one event this time, so it's pretty clear and concise. Finally...

Step 4 - Grab the snapshot

This part is just using the <canvas> element to grab a screenshot. Inside our seeked event handler:

canvas_elem.height = this.videoHeight;
canvas_elem.width = this.videoWidth;
canvas_elem.getContext('2d').drawImage(this, 0, 0);
var snapshot = canvas_elem.toDataURL();

// Remove elements as they are no longer needed
$video.remove();
$(canvas_elem).remove();

The canvas needs to match the dimensions of the video (not the <video> element) to get a proper image. Also, we are setting the canvas's internal .height and .width properties, not the canvas height/width CSS style values.

The value of snapshot is a data URI, which is basically just a string that starts with data:image/jpeg;base64 and then the base64 data.

Our final JS code should look like:

var step_2_events_fired = 0;
$video.one('loadedmetadata loadeddata suspend', function() {
  if (++step_2_events_fired == 3) {
    $video.one('seeked', function() {
      canvas_elem.height = this.videoHeight;
      canvas_elem.width = this.videoWidth;
      canvas_elem.getContext('2d').drawImage(this, 0, 0);
      var snapshot = canvas_elem.toDataURL();

      // Delete the elements as they are no longer needed
      $video.remove();
      $(canvas_elem).remove();
    }).prop('currentTime', insert_time_here_in_seconds);
  }
}).prop('src', insert_source_here);

Celebrate!

You have your image in base64! Send this to your server, put it as the src of an <img> element, or whatever.

For example, you can decode it into binary and directly write it to a file (trim the prefix first), which will become a JPEG image file.

You could also use this to offer previews of videos while they are uploaded. If you are putting it as the src of an <img>, use the full data URI (don't remove the prefix).

like image 185
wilsonzlin Avatar answered Oct 17 '22 19:10

wilsonzlin