Search code examples
javascripthtml5-canvasdom-eventshtml5-video

Copying from video to canvas in javascript


I'm trying to load a video and draw a thumbnail from it into a canvas. The problem I have is that I cannot find an event that fires when the video image is ready to copy. When the loadeddata event fires the size is set correctly but the canvas image is blank. If I uncomment the timer to delay for a second then the canvas is filled, but without the delay it isn't. Is there some event I've overlooked, or some other way of telling when the video element is able to copy? I've tried load, canplay, canplaythrough, loadeddata, progress, but none seems to come in at the right time.

<html>
  <head>
    <script>
      var video, image, width, height;
      function copy(src) {
        video = document.getElementById('video');
        video.addEventListener('loadeddata', x => finishCopy()); 
        video.src = src;
      }
      function finishCopy() {
        width = document.defaultView.getComputedStyle(video).width.replace(/[^.0-]/g, '');
        height = document.defaultView.getComputedStyle(video).height.replace(/[^.0-]/g, '');
        image = document.getElementById('image');
        image.width = width;
        image.height = height;
        //setTimeout(draw, 1000);
        draw();
      }

      function draw() {
        let ctxt = image.getContext('2d');
        ctxt.drawImage(video, 0, 0, width, height);
      }

    </script>
  </head>
  <body >
    <video id="video" autostart="false" ></video>
    <canvas id="image"></canvas>
  </body>
</html>

Solution

  • Though the loadeddata event fires as soon as the data for the current frame is loaded, it does not give any hint what that actually means.

    In practice however it means that in some cases, or in some browsers it might be able to render the first frame - though you can't depend on it.

    The solution is to look for an event, which fires later on in the process of loading/processing the video.

    The order those events are happening is as follows:

    1. loadstart
    2. durationchange
    3. loadedmetadata
    4. loadeddata
    5. progress
    6. canplay

    So if we simply take the canplay event instead of loadeddata, things should work as expected.

    Here's an example:

    var video, image, width, height;
    
    function copy(src) {
      video = document.getElementById('video');
      video.addEventListener('canplay', x => finishCopy());
      video.src = src;
    }
    
    function finishCopy() {
      width = parseInt(document.defaultView.getComputedStyle(video).width);
      height = parseInt(document.defaultView.getComputedStyle(video).height);
      image = document.getElementById('image');
      image.width = width;
      image.height = height;
      draw();
    }
    
    function draw() {
      let ctxt = image.getContext('2d');
      ctxt.drawImage(video, 0, 0, width, height);
    }
    
    copy("https://www.w3schools.com/html/mov_bbb.mp4")
    <video id="video" autostart="false"></video>
    <canvas id="image"></canvas>

    Edit

    After some testing with different browsers it seems that my solution fails in Chrome. Nevertheless I found a workaround which also works in Chrome.

    There's a rather undocumented parameter you can append to your video's URL which let's you specify a start position or even a range for your video (#t=).

    Now if you set the start position to something low like 0.000001 you will still see the first frame of your video AND you're able to draw to the canvas.

    However my testing has also shown that this just works in FireFox and Chrome, if you use the canplaythrough event instead of canplay or loadeddata.

    Here's the modified example:

    var video, image, width, height;
    
    function copy(src) {
      video = document.getElementById('video');
      video.addEventListener('canplaythrough', x => finishCopy());
      video.src = src;
    }
    
    function finishCopy() {
      width = parseInt(document.defaultView.getComputedStyle(video).width);
      height = parseInt(document.defaultView.getComputedStyle(video).height);
      image = document.getElementById('image');
      image.width = width;
      image.height = height;
      draw();
    }
    
    function draw() {
      let ctxt = image.getContext('2d');
      ctxt.drawImage(video, 0, 0, width, height);
    }
    
    copy("https://www.w3schools.com/html/mov_bbb.mp4#t=0.000001")
    <video id="video" autostart="false"></video>
    <canvas id="image"></canvas>