How to Control Camera Focus with JavaScript

Using the getUserMedia API, we can open cameras on the web with JavaScript. It is supported on most browsers and Chrome for Android has additional support for controlling the camera’s zoom, torch, focus, and so on, giving more possibilities for making a camera web app.

In this article, we are going to talk about how to control camera focus on the web. We will create a demo directly using getUserMedia and another demo using Dynamsoft Camera Enhancer.

getUserMedia can control the focus mode and the focus distance, but it does not provide the “tap to focus” ability. Dynamsoft Camera Enhancer has this ability by calculating the contrast of the image.

Online demo

How Focus Works

Lights are reflected through the Camera’s lens to enter the sensor so that we can capture a view which is much larger than the sensor.

diagram

Focus distance is the distance between the lens and the object. We can adjust the position of the lens to modify the focus distance so that we can have a clear image of the target.

The process of calculating a suitable focus distance is called autofocus. There are many ways to do this, like sensoring the distance and calculating the contrast.

The Need for Controlling the Focus

When we open the camera in the browser, autofocus is enabled by default. We can control the focus if it fails to work or we need to turn off autofocus and keep the focus distance.

Out-of-focus image:

out of focus and blurry

In-focus image:

infocus

Control Camera Focus in a Web App

Let’s do this in steps.

Open the Camera with getUserMedia

  1. Create a new HTML file with the following template:

    <!DOCTYPE html>
    <html>
    <head>
      <title>Camera Focus Demo</title>
      <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
      <style>
        #container {
          height: 480px;
          width: 100%;
          background: lightgray;
          position: relative;
        }
    
        video {
          position: absolute;
          height: 100%;
          width: 100%;
          left:0;
          top:0;
          object-fit: contain;
        }
           
        button {
          font-family: monospace;
        }
      </style>
    </head>
    <body>
      <h2>Camera Focus Demo</h2>
      <label>
        Camera:
        <select id="select-camera"></select>
      </label>
      <label>
        Resolution:
        <select id="select-resolution">
          <option value="640x480">640x480</option>
          <option value="1280x720">1280x720</option>
          <option value="1920x1080" selected>1920x1080</option>
          <option value="3840x2160">3840x2160</option>
        </select>
      </label>
      <button onclick="startCamera();">Start Camera</button>
      <br/>
      <div id="container">
        <video id="video" muted autoplay="autoplay" playsinline="playsinline" webkit-playsinline></video>
      </div>
      <script type="text/javascript">
      </script>
    </body>
    </html>
    
  2. Request camera permission.

    async function requestCameraPermission() {
      try {
        const constraints = {video: true, audio: false};
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        closeStream(stream);
      } catch (error) {
        console.log(error);
        throw error;
      }
    }
    
    function closeStream(stream){
      if (stream) {
        const tracks = stream.getTracks();
        for (let i=0;i<tracks.length;i++) {
          const track = tracks[i];
          track.stop();  // stop the opened tracks
        }
      }
    }
    
  3. List cameras.

    let cameras = []
    async function listCameras(){
      let cameraSelect = document.getElementById("select-camera");
      let allDevices = await navigator.mediaDevices.enumerateDevices();
      for (let i = 0; i < allDevices.length; i++){
        let device = allDevices[i];
        if (device.kind == 'videoinput'){
          cameras.push(device);
          cameraSelect.appendChild(new Option(device.label,device.deviceId));
        }
      }
    }
    
  4. Start the selected camera with the desired resolution.

    async function startCamera(){
      let selectedCamera = cameras[document.getElementById("select-camera").selectedIndex];
      closeStream(document.getElementById("video").srcObject);
      let selectedResolution = document.getElementById("select-resolution").selectedOptions[0].value;
      let width = parseInt(selectedResolution.split("x")[0]);
      let height = parseInt(selectedResolution.split("x")[1]);
         
      const videoConstraints = {
        video: {width:width, height:height, deviceId: selectedCamera.deviceId}, 
        audio: false
      };
       const cameraStream = await navigator.mediaDevices.getUserMedia(videoConstraints);
       document.getElementById("video").srcObject = cameraStream;
    }
    

Control Camera Focus

  1. First, check whether the browser supports the focus feature.

    function checkBrowserCapabilities(){
      if (navigator.mediaDevices.getSupportedConstraints().focusMode) {
        console.log("Browser supports focus mode");
      }else{
        alert("The browser does not support focus mode.");
      }
    }
    
  2. Check whether the selected camera supports focus. If it is supported, load the range of focus distance. The distance is measured in meters.

    function checkCameraCapabilities(){
      const video = document.querySelector("video");
      const videoTracks = video.srcObject.getVideoTracks();
      track = videoTracks[0];
      let capabilities = track.getCapabilities();
      console.log(capabilities);
      if (!('focusMode' in capabilities)) {
        alert("This camera does not support focus");
      }else{
        if (!('focusDistance' in capabilities)) {
          alert("This camera does not control focus distance");
        }else{
          loadFocusDistanceRange(capabilities);
        }
      }
    }
       
    function loadFocusDistanceRange(cap){
      step = cap.focusDistance.step;
      min = cap.focusDistance.min;
      max = cap.focusDistance.max;
      currentDistance = track.getSettings().focusDistance;
      let range = document.getElementById("distance");
      range.value = currentDistance;
      range.min = min;
      range.max = max;
      range.step = step;
    }
    
  3. A select element is added to set the focus mode and an input element is added to set the focus distance of the camera.

    <label>
      Mode:
      <select id="select-mode">
        <option value="manual" selected>manual</option>
        <option value="continuous">continuous</option>
      </select>
    </label>
    <br/>
    <label>
      Focus distance:
      <input type="range" id="distance" min="0" max="3" value="0" step="0" onchange="changeDistance()"/>
    </label>
    
  4. Set focus if the input element’s value is changed.

    async function changeDistance(){
      let mode = document.getElementById("select-mode").selectedOptions[0].value;
      //https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
      let distance = document.getElementById("distance").value;
      const constraints = {advanced: [{
        focusMode: mode,
        focusDistance: distance,
      }]};
      await track.applyConstraints(constraints);
    }
    

Using Dynamsoft Camera Enhancer

Dynamsoft Camera Enhancer is a library which makes it easier to control the camera and has features to enhance the camera. It is based on getUserMedia internally.

Here are the equivalent functions using Dynamsoft Camera Enhancer v4:

  1. List the cameras.

    let cameras = await cameraEnhancer.getAllCameras();
    
  2. Open the selected camera with the desired resolution.

    let selectedCamera = cameras[document.getElementById("select-camera").selectedIndex];
    let selectedResolution = document.getElementById("select-resolution").selectedOptions[0].value;
    let width = parseInt(selectedResolution.split("x")[0]);
    let height = parseInt(selectedResolution.split("x")[1]);
    await cameraEnhancer.selectCamera(selectedCamera);
    await cameraEnhancer.setResolution({width:width, height:height});
    await cameraEnhancer.open();
    

    HTML:

    <div id="enhancerUIContainer"></div>
    
  3. Get focus distance’s range and step.

    cameraEnhancer.getCapabilities().focusDistance //{max: 3, min: 0, step: 0.12}
    
    
  4. Set focus.

    We can set the focus mode and the focus distance:

    await cameraEnhancer.setFocus({
        mode: "manual",
        distance: 1
    });
    

    We can also specify an area to focus, which makes it possible to enable the “tap to focus” feature. The unit can be in pixels or percentages, such as “500px” or “50%”.

    cameraEnhancer.setFocus({
        mode: mode,
        area: {
          centerPoint: {
              x: (x-25) + "px",
              y: (y-25) + "px",
          },
          width: "50px",
          height: "50px"
        }
    });
    
  5. Draw the area tapped. We can draw a rect around the point tapped to give the user some feedback.

    let container = document.getElementById("enhancerUIContainer");
    container.addEventListener("click",function(e){
      let {offsetX,offsetY} = calculateOffset();
      let x = (e.offsetX - offsetX)/(container.offsetWidth - offsetX * 2);
      let y = (e.offsetY - offsetY)/(container.offsetHeight - offsetY * 2);
      focus(x,y);
    })
    
    function focus(x,y){
      let video = cameraView.getVideoElement();
      x = video.videoWidth * x;
      y = video.videoHeight * y;
      let drawingItems = new Array(
        new Dynamsoft.DCE.RectDrawingItem({
            x: x-25,
            y: y-25,
            width: 50,
            height: 50,
            isMeasuredInPercentage: false
        }));
      drawingLayer.addDrawingItems(drawingItems);
      let mode = document.getElementById("select-mode").selectedOptions[0].value;
      cameraEnhancer.setFocus({
          mode: mode,
          area: {
            centerPoint: {
                x: (x-25) + "px",
                y: (y-25) + "px",
            }
          }
      });
      setTimeout(function(){
        drawingLayer.clearDrawingItems();
      },2000)
    }
    

    There may be some offset between the container and the actual video content:

    offset

    We can calculate the offset values in object-fit:contain mode with the following code:

    function calculateOffset(){
      let containerWidth = document.getElementById("enhancerUIContainer").offsetWidth;
      let containerHeight = document.getElementById("enhancerUIContainer").offsetHeight;
      let video = cameraView.getVideoElement();
      let videoWidth = video.videoWidth;
      let videoHeight = video.videoHeight;
      let containerRatio = containerWidth/containerHeight;
      let videoRatio = videoWidth/videoHeight;
      let offsetX = 0;
      let offsetY = 0;
      if (containerRatio > videoRatio) { //has offset in horizontal direction
        let displayRatio = containerHeight/videoHeight;
        let displayWidth = displayRatio * videoWidth;
        offsetX = (containerWidth - displayWidth) /2
      }else{  //has offset in vertical direction
        let displayRatio = containerWidth/videoWidth;
        let displayHeight = displayRatio * videoHeight;
        offsetY = (containerHeight - displayHeight) /2
      }
      return {offsetX:offsetX, offsetY:offsetY};
    }
    

    Demo video:

Source Code

Get the source code of the demos to have a try:

https://github.com/tony-xlh/getUserMedia-demos/tree/main/focus