生成由文件输入在特定时间选择的视频文件的缩略图/快照

如何在视频中的特定时间在后台静默获取通过 <input type="file"> 选择的视频文件的快照(即没有可见元素、闪烁、声音等)?

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.)?

推荐答案

主要有四个步骤:

  1. 创建 <canvas><video> 元素.
  2. URL.createObjectURL生成的视频文件的src加载到元素中,等待加载通过监听被触发的特定事件.
  3. 将视频的时间设置为您想要拍摄快照并监听其他事件.
  4. 使用画布抓取图像.
  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.

<小时>

第 1 步 - 创建元素

这很简单:只需创建一个 <canvas> 和一个 <video> 元素并将它们附加到 <body> (或任何地方,这并不重要):


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

请注意,视频元素具有 muted 属性.不要放置任何其他属性,例如 autoplaycontrols.另请注意,它们都有类 snapshot-generator.这样我们就可以为它们设置样式,使它们不碍事:

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

有些浏览器会将它们设置为 display: none,但其他浏览器会出现严重问题,除非它们在页面上呈现,所以我们只是将它们缩小,以便它们本质上是不可见的.(但不要将它们移到视口之外,否则您可能会在页面上看到一些难看的滚动条.)

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.)

这就是事情开始变得棘手的地方.您需要收听事件以了解何时继续.不同的浏览器会触发不同的事件、不同的时间和不同的顺序,所以我会为你省力.在视频准备好之前,三个事件必须始终至少触发一次;他们是:

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:

  • 加载的元数据
  • 加载数据
  • 暂停

为这些事件设置事件处理程序并跟踪触发了多少.一旦所有三个都被解雇了,你就可以继续了.请记住,由于其中一些事件可能会触发多次,因此您只想处理每个触发的类型的第一个事件,并丢弃后续触发.我使用了 jQuery 的 .一个,它负责这个.

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

源应该只是通过 URL.createObjectURL(file) 创建的对象 URL,其中 file 是文件对象.

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

此阶段与上一个阶段类似:设置时间,然后监听事件.在我们之前代码的 if 块中:

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...

这部分只是使用 <canvas> 元素来抓取屏幕截图.在我们的 seeked 事件处理程序中:

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

画布需要匹配视频的尺寸(不是 <video> 元素)才能获得正确的图像.此外,我们正在设置画布的内部 .height.width 属性,不是画布的高度/宽度 CSS 样式值.

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.

snapshot的值是一个数据URI,基本上就是一个以data:image/jpeg;base64开头的字符串,然后是base64数据.

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.

我们最终的 JS 代码应该是这样的:

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

庆祝!

你的图片是base64的!将此发送到您的服务器,将其作为 <img> 元素的 src 或其他任何内容.

Celebrate!

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

比如可以解码成二进制,直接写入文件(先剪掉前缀),会变成JPEG图片文件.

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.

您还可以使用它在上传视频时提供视频预览.如果您将其作为 <img>src,请使用完整的数据 URI(不要删除前缀).

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).

相关文章