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.
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.
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:
In-focus image:
Control Camera Focus in a Web App
Let’s do this in steps.
Open the Camera with getUserMedia
-
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>
-
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 } } }
-
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)); } } }
-
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
-
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."); } }
-
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; }
-
A
select
element is added to set the focus mode and aninput
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>
-
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:
-
List the cameras.
let cameras = await cameraEnhancer.getAllCameras();
-
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>
-
Get focus distance’s range and step.
cameraEnhancer.getCapabilities().focusDistance //{max: 3, min: 0, step: 0.12}
-
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" } });
-
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:
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