Render Captured Video to Full Page Canvas

March 17th 2017 HTML 5 JavaScript

In modern HTML 5 browsers you can render video from your camera inside a web page using the video element. However, to further process the captured video (e.g. parse a QR code) or add some custom rendering on top of it, the canvas element needs to be used. Due to the ever changing APIs in this field, it's not easy to find up-to-date working sample code for achieving this.

Directly Render Video from Camera

To capture camera video, we need to use MediaDevices.getUserMedia API:

var video = document.getElementById('video');

if (navigator.mediaDevices.getUserMedia) {
  var successCallback = function(stream) {
    video.srcObject = stream;
  };
  var errorCallback = function(error) {
    console.log(error);
  };
  navigator.mediaDevices.getUserMedia({
    audio: false,
    video: { facingMode: { ideal: 'environment' } } // prefer rear-facing camera
  }).then(successCallback, errorCallback);
}

The code assumes a video element with id="video" on the page:

<video id="video" autoplay="true"></video>

It's a good idea to also add WebRTC adapter to your page, so that the code will also work in browsers, which don't support MediaDevices.getUserMedia yet and still require the use of the older navigator.getUserMedia API. Unfortunately, Safari supports neither, so there's still no way to make this work in iOS. Additionally, camera preference isn't respected by all devices, therefore you might need to implement a manual camera selection.

Render Captured Video on Canvas

To render the video on a canvas instead, we will register a callback at the end of the above initialization procedure using requestAnimationFrame:

requestAnimationFrame(renderFrame);

This will ensure that our method gets called on each repaint. Inside it we will draw the current image from the camera:

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');

function renderFrame() {
  // re-register callback
  requestAnimationFrame(renderFrame);
  // set internal canvas size to match HTML element size
  canvas.width = canvas.scrollWidth;
  canvas.height = canvas.scrollHeight;
  if (video.readyState === video.HAVE_ENOUGH_DATA) {
    // scale and horizontally center the camera image
    var videoSize = { width: video.videoWidth, height: video.videoHeight };
    var canvasSize = { width: canvas.width, height: canvas.height };
    var renderSize = calculateSize(videoSize, canvasSize);
    var xOffset = (canvasSize.width - renderSize.width) / 2;
    context.drawImage(video, xOffset, 0, renderSize.width, renderSize.height);
  }
}

A couple of things probably require further explanation:

  • We need to re-register our callback with requestAnimationFrame on every call, because it would otherwise only get called once.
  • If we allow our canvas HTML element to resize, we need to make sure that the size of the internal canvas always matches the current size of the HTML element. This does not happen automatically as the two sizes are independent.
  • Camera captures the video at a fixed resolution. We want to scale it to match the canvas size, while preserving the aspect ratio:

      function calculateSize(srcSize, dstSize) {
        var srcRatio = srcSize.width / srcSize.height;
        var dstRatio = dstSize.width / dstSize.height;
        if (dstRatio > srcRatio) {
          return {
            width:  dstSize.height * srcRatio,
            height: dstSize.height
          };
        } else {
          return {
            width:  dstSize.width,
            height: dstSize.width / srcRatio
          };
        }
      }
    

Of course, we can expend renderFrame to draw anything else on the canvas. Or as I did, to grab the render image from the camera and scan it for QR codes using jsQR:

var imageData = this.context.getImageData(
  xOffset, 0, renderSize.width, renderSize.height);
var qrData = jsQR.decodeQRFromImage(
  imageData.data, imageData.width, imageData.height);

Stretch Canvas to Full Page

The code above now requires our HTML to contain a canvas element with id="canvas" as well:

<div id="container">
  <video id="video" autoplay="true"></video>
  <canvas id="canvas"></canvas>
</div>

I wrapped both in a single div container which fills the full size of the parent element, thanks to the following CSS:

#container {
  width: 100%;
  height: 100%;
}

#video {
  display: none; /* user can see video on canvas */
}

#canvas {
  width: 100%;
  height: 100%;
}

If the parent is the page root, the following CSS will make sure that the canvas will fill the page completely:

html, body {
  height: 100%;
}

I created a Plunk, where you can see everything in action, but haven't embedded it, because modern browsers won't allow the use of camera if the whole page is not served using https.

Copyright
Creative Commons License