Lendo QR Code através do navegador via Javascript

Em certas situações, é necessário que nosso sistema leia um QR Code para obter informações de um produto ou item a ser processado. Utilizar um programa externo para essa tarefa pode ser trabalhoso. Uma alternativa mais prática é recorrer à API de Câmera do navegador junto com a biblioteca jsQR, permitindo a leitura e decodificação do QR Code de forma simples e eficiente.

Capturando imagens da Câmera

Para iniciar, é necessário capturar uma imagem pelo navegador. Para isso, utilizamos a interface MediaDevices, que permite o acesso a dispositivos de mídia, como câmera e microfone. A captura é realizada por meio do método navigator.mediaDevices.getUserMedia, que solicita ao usuário permissão para utilizar a mídia e retorna um MediaStream contendo o conteúdo capturado. Esse método recebe um objeto de restrições (constraints), especificando os tipos de mídia desejados, como vídeo e áudio, e retorna uma promise que resolve em um MediaStream. Esse fluxo de mídia pode então ser atribuído a um elemento <video> no HTML, permitindo que o usuário visualize a transmissão da câmera em tempo real.

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Captura de Vídeo</title>

    <script>
        document.addEventListener('DOMContentLoaded', ev =>{
            const cam = document.querySelector("#cam");
            navigator.mediaDevices.getUserMedia({
                video: true,
                audio: false
            }).then( stream => {
                cam.srcObject = stream;
            })
        });
    </script>
</head>
<body>

    <video autoplay id="cam" width="300" height="300"></video>
    
</body>
</html>

Veja o exemplo

Se o dispositivo possuir mais de uma câmera, é possível listar todas elas utilizando o método navigator.mediaDevices.enumerateDevices(). Esse método retorna um array de objetos MediaDeviceInfo, contendo informações sobre cada dispositivo de mídia disponível. Entre os campos retornados, destacam-se: deviceId (identificador do dispositivo), kind (tipo do dispositivo, podendo ser audioinput ou videoinput), label (descrição do dispositivo) e groupId (identificador do grupo de dispositivos relacionados).

Vale ressaltar que o método enumerateDevices() não solicita permissão para acessar os dispositivos. Portanto, é necessário chamar previamente a função getUserMedia(), que solicita a autorização do usuário antes de listar os dispositivos disponíveis.

Para utilizar um dispositivo específico, basta passar seu deviceId no objeto de restrições (constraints) ao chamar o método getUserMedia().

{
    video: {
        deviceId: {exact: deviceId}
    }
}

Outro aspecto importante é que, em dispositivos móveis, se um stream já estiver sendo exibido em um elemento <video>, é necessário liberar o stream atual antes de iniciar um novo. Isso pode ser feito acessando todas as tracks do stream, iterando sobre elas e interrompendo-as com o método stop().

...

    <script>
        document.addEventListener('DOMContentLoaded', ev =>{
            const devicesSelect = document.querySelector('#devices');
            let currentStream = null;
            function previewCam(deviceId){
                if(deviceId == null) return;
                const cam = document.querySelector('#cam');
                
                if(currentStream){
                    currentStream.getTracks().forEach(track => track.stop());
                }

                navigator.mediaDevices.getUserMedia({
                    video: {
                        deviceId: {exact: deviceId}
                    }
                }).then( stream =>{
                    currentStream = stream;
                    cam.srcObject = stream;
                })
            }

            async function getDevices(){
                try{
                    await navigator.mediaDevices.getUserMedia({ video: true });
                    const devices = await navigator.mediaDevices.enumerateDevices();
                    devices.forEach( d =>{
                            if(d.kind == 'videoinput'){
                                devicesSelect.innerHTML += `<option value="${d.deviceId}">${d.label}</option>`;
                            }
                    });
                    previewCam(devicesSelect[devicesSelect.selectedIndex].value) 
                }catch(e){
                    console.log("Erro ao acessar a camera");
                }
            }
            
            devicesSelect.addEventListener('change', ev => {                
                previewCam( ev.target.value )
            });

            getDevices();
            
        })
    </script>
</head>
<body>
    <select id="devices"></select>

    <video autoplay id="cam" width="300" height="300"></video>
</body>
....

Veja o exemplo

No código anterior, definimos duas funções: getDevices, responsável por listar todos os dispositivos de vídeo em um elemento <select>, e previewCam, que exibe o stream do dispositivo selecionado em um elemento <video>.

Na função getDevices, na linha 27, chamamos o método getUserMedia, ignorando seu retorno, apenas para solicitar permissão de uso da câmera. Em seguida, na linha 28, utilizamos o método enumerateDevices() para obter a lista de dispositivos disponíveis. Iteramos sobre os objetos MediaDeviceInfo com forEach e, para cada item do tipo videoinput, adicionamos um elemento <option> ao <select>, definindo o value como o deviceId e o texto exibido como o label. Por fim, na linha 34, selecionamos o primeiro item do <select>, obtemos seu value (correspondente ao deviceId) e o passamos para a função previewCam.

Na função previewCam, recebemos o deviceId como parâmetro. Na linha 11, verificamos se há um stream ativo; caso positivo, obtemos todas as suas tracks, iteramos sobre elas e as interrompemos com o método stop(). Na linha 15, chamamos o método getUserMedia(), passando um objeto de configuração onde o atributo video recebe o deviceId. Quando a promise retorna, armazenamos o stream na variável currentStream, permitindo fechá-lo posteriormente, se necessário, e o atribuimos ao elemento <video> para exibição.

Pegando uma imagem da câmera

O próximo passo é capturar a imagem do stream de vídeo para enviá-la à biblioteca jsQR. Para isso, precisamos de um elemento <canvas> onde vamos desenhar um frame do vídeo. O elemento <canvas> pode ser oculto, se necessário.

Abaixo está um exemplo de código para capturar a imagem de um elemento de vídeo e desenhá-la em um <canvas> ao clicar em um botão.

<script>
...
const btCapturar = document.querySelector('#capturar');
btCapturar.addEventListener('click', ev =>{
    const cam = document.querySelector('#cam');
    const canvas = document.querySelector("#canvas");
    const ctx = canvas.getContext('2d');
    canvas.width = cam.videoWidth;
    canvas.height = cam.videoHeight;
    ctx.drawImage(cam, 0, 0, canvas.width, canvas.height);
});
...
</scrip>

...

<select id="devices"></select>
        
<video autoplay id="cam" width="300" height="300"></video>
    
<button id="capturar">Capturar</button>
 
<canvas id="canvas" width="300" height="300"></canvas>
...

Veja o exemplo

Finalmente lendo o QR Code

Agora que temos a imagem, podemos utilizar a biblioteca jsQR. Para instalá-la, basta usar o npm.

npm install jsqr --save
// ES6 import
import jsQR from "jsqr";

jsQR(...);

Ou você pode baixar o código de distribuição do GitHub, jsQR.js, e incluí-lo diretamente via script.

<script src="jsQR.js"></script>
<script>
  jsQR(...);
</script>

Agora, para decodificar a imagem e extrair o QR Code, basta obter o DataImage do <canvas> utilizando o método ctx.getImageData e chamar a função fornecida pela biblioteca jsQR, passando o DataImage e as dimensões do canvas. A função retornará o conteúdo do QR Code.

jsQR(imageData.data, imageData.width, imageData.height);
<script>
...
const btCapturar = document.querySelector('#capturar');
btCapturar.addEventListener('click', ev =>{
    const cam = document.querySelector('#cam');
    const canvas = document.querySelector("#canvas");
    const qrcode = document.querySelector("#qrcode");
    console.log(canvas);
    const ctx = canvas.getContext('2d');
    canvas.width = cam.videoWidth;
    canvas.height = cam.videoHeight;
    ctx.drawImage(cam, 0, 0, canvas.width, canvas.height);

    const imageData = ctx.getImageData(0,0,canvas.width, canvas.height);
    const code = jsQR(imageData.data, imageData.width, imageData.height);
    if(code){
        qrcode.value = code.data;
    }

});
...
</script>
..
<select id="devices"></select>
        
<video autoplay id="cam" width="300" height="300"></video>
        
<button id="capturar">Capturar</button>

<input type="text" id="qrcode">
        
<canvas style="display:none" id="canvas" width="300" height="300"></canvas>

Veja o exemplo

Como você pode ver, ler um QR Code é bem simples, com a biblioteca jsQR fazendo todo o trabalho. Agora, basta adaptar a solução conforme suas necessidades. T++!