Tirando fotos com JS

Hoje em dia todo computador, notebook ou smartphone tem uma (ou mais) câmera(s).

Antigamente, a única maneira de aproveitar essas câmeras em uma aplicação Web era usando Flash.

Mas, hoje em dia, podemos tirar fotos com JS. Para isso, vamos utilizar o WebRTC.

WebRTC é um padrão Web que define protocolos e APIs em JS para enviar vídeo, áudio e dados entre navegadores de maneira peer-to-peer e em tempo real. Está disponível no Chrome para Desktop e Android, Firefox, Opera e Edge.

Pra começar, um <video> e um <canvas>

Precisamos de uma estrutura mínima no nosso HTML.

A tag <video> com a propriedade autoplay, ficará responsável por mostrar para o usuário as imagens obtidas da câmera.

Para guardar a imagem da foto, vamos usar um <canvas>, que começara invisível.

Finalmente, precisaremos de um <button> para que o usuário informe o momento em que deve ser tirada a foto a partir do vídeo.

Os três elementos mencionados ficarão dentro de uma <div>.

<div class="camera">
  <video id="video" class="foto" autoplay>Vídeo não disponível.</video>
  <canvas id="canvas" class="foto" style="display: none;"></canvas>
  <button id="tira-foto">Tirar foto</button>
</div>

Um pouquinho de css

Para posicionar os elementos e definir uma borda, teremos um pequeno trecho de css:

.camera {
  width: 340px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.5em;
}
.foto {
  width: 320px;
  height: 240px;
  border: 1px solid black;
  margin: 1em;
}

Mude o CSS à vontade, para deixar mais estiloso!

Mostrando o vídeo

No nosso código JS, obteremos o elemento cujo id é video:

var video = document.querySelector('#video');

Para obter as imagens da câmera, precisamos usar o método getUserMedia do navigator.

Uma coisa chata é que a API ainda não está consolidada. Por isso, o getUserMedia tem prefixos diferentes para cada navegador:

  • navigator.webkitGetUserMedia – para o Chrome e Opera
  • navigator.mozGetUserMedia – para o Firefox
  • navigator.msGetUserMedia – para o Edge

Testaremos o getUserMedia correto, armazendo-o em uma variável e ajustando o this através de um bind com o objeto navigator:

var getUserMedia = (navigator.webkitGetUserMedia ||
                       navigator.mozGetUserMedia ||
                          navigator.msGetUserMedia).bind(navigator);

Ao chamar o getUserMedia, devemos passar 3 parâmetros:

  • um objeto de configuração que diz se deve ser obtido vídeo ou áudio da câmera, entre outros detalhes
  • uma função chamada quando houve sucesso ao obter mídia da câmera. Nessa função, obtemos um MediaStream, que devemos passar para a propriedade srcObject do nosso elemento video. Guardaremos o MediaStream em uma variável, para uso posterior.
  • uma função chamada em caso de erro
var configuracaoMedia = {video: true, audio: false};

var mediaStream;
function iniciaVideo (stream) {
  video.srcObject = stream;
  mediaStream = stream;
}

function trataErroMedia (erro) {
  console.error('Erro: ' + erro);
}

getUserMedia(configuracaoMedia, iniciaVideo, trataErroMedia);

O resultado até aqui pode ser encontrado em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/1/

Obtendo uma foto

Devemos obter o canvas e o botão a partir de seus ids:

var canvas = document.querySelector('#canvas');
var botaoTiraFoto = document.querySelector('#tira-foto');

Precisamos registrar um tratador do evento de clique no botão.

botaoTiraFoto.addEventListener('click', function (e) {
  //tratamento aqui...
});

Dentro da função de tratamento do clique, devemos obter o contexto 2D a partir do canvas e utilizá-lo para desenhar uma imagem a partir do vídeo. Nesse momento, a foto é tirada.

Podemos obter os dados através do método toDataURL do canvas.

botaoTiraFoto.addEventListener('click', function (e) {
  canvas.getContext('2d').drawImage(video, 0, 0, 320, 240);
  var dados = canvas.toDataURL('image/png');
  //fazer algo com os dados...
});

O método toDataURL retorna uma Data URI, uma maneira de representar imagens e outros tipos de dados como um texto. Por exemplo, um .png com um pontinho preto pode ser representado como: data:image/png;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=

Podemos enviar essa Data URI para o servidor com uma chamada AJAX.

Do lado do servidor, você pode gravar a Data URI no BD para uso posterior como src de um <img> ou ainda transformá-lo em um arquivo.

Em PHP, usaríamos str_replace, base64_decode e file_put_contents para salvar a imagem em um arquivo no servidor.

Você pode testar o código até essa parte em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/1/

Mostrando a foto e escondendo o vídeo

Quando o usuário clicar o botão, é interessante mostrarmos o <canvas> que tem a imagem da foto tirada e, em seguida, escondermos o vídeo da câmera.

botaoTiraFoto.addEventListener('click', function (e) {
  //código omitido...
  video.style.display = 'none';
  canvas.style.display = '';
});

Ao exibirmos a imagem, percebemos que há uma distorção. Para resolver, é importante fixar a altura e largura do <canvas>:

canvas.width = 320;
canvas.height = 240;

A câmera continuará filmando, mas a imagem ficará parada, sendo alterada só quando o usuário clicar no botão.

Você pode acompanhar o resultado em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/2/

Parando a câmera ao obter foto

Para suspender a filmagem logo depois do clique no botão, podemos usar o seguinte código:

mediaStream.getVideoTracks().forEach(function (media) {
  media.stop();
});

É interessante alternar entre filmagem com a câmera e a exibição da foto tirada. Para isso, alternamos entre exibição do <video> e do <canvas>. Além disso, alternamos entre parar e voltar com o funcionamento da câmera.

botaoTiraFoto.addEventListener('click', function (e) {
  if (mediaStream) {
    //código omitido...
    mediaStream.getVideoTracks().forEach(function (media) {
      media.stop();
    });
    video.style.display = 'none';
    canvas.style.display = '';
    mediaStream = null;
  } else {
    getUserMedia(configuracaoMedia, iniciaVideo, trataErroMedia);
    video.style.display = '';
    canvas.style.display = 'none';
  }

O código até aqui está em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/3/

Para funcionar no Chrome do Android

Com o código anterior, ocorre um bug quando acessado pelo Chrome do Android: é mostrada uma imagem preta no vídeo.

Para corrigí-lo, é necessário fixar as propriedades opcionais maxWidth e minWidth na configuração de getUserMedia:

var configuracaoMedia = {
  video: {
    optional: [
      { maxWidth: 320},
      { maxHeight: 240}
    ]
  }, 
  audio: false
};

O código final pode ser encontrado em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/4/

Consideração importante: tem que ser HTTPS
Para que o código acima funcione no Chrome e Opera quando publicado em um servidor real, não em sua máquina local, é preciso que seja usado HTTPS.
Para o Firefox não há essa restrição.