Como capturar uma imagem do usuário

A maioria dos navegadores pode acessar a câmera do usuário.

Muitos navegadores agora têm a capacidade de acessar a entrada de vídeo e áudio do usuário. No entanto, dependendo do navegador, pode ser uma experiência dinâmica e inline completa ou pode ser delegada a outro app no dispositivo do usuário. Além disso, nem todos os dispositivos têm uma câmera. Como criar uma experiência que use uma imagem gerada pelo usuário que funcione bem em todos os lugares?

Comece de forma simples e progressiva

Se você quiser melhorar sua experiência progressivamente, comece com algo que funcione em todos os lugares. A maneira mais fácil é pedir ao usuário um arquivo pré-gravado.

Solicitar um URL

Essa é a opção com melhor suporte, mas menos satisfatória. Peça ao usuário um URL e use-o. Para mostrar apenas a imagem, isso funciona em qualquer lugar. Crie um elemento img, defina o src e pronto.

No entanto, se você quiser manipular a imagem de alguma forma, as coisas ficam um pouco mais complicadas. O CORS impede que você acesse os pixels reais, a menos que o servidor defina os cabeçalhos apropriados e você marque a imagem como cross-origin. A única maneira prática de contornar isso é executar um servidor proxy.

Entrada de arquivo

Também é possível usar um elemento de entrada de arquivo simples, incluindo um filtro accept que indica que você só quer arquivos de imagem.

<input type="file" accept="image/*" />

Esse método funciona em todas as plataformas. No computador, o usuário será solicitado a fazer upload de um arquivo de imagem do sistema de arquivos. No Chrome e no Safari para iOS e Android, esse método permite que o usuário escolha qual app usar para capturar a imagem, incluindo a opção de tirar uma foto diretamente com a câmera ou escolher um arquivo de imagem existente.

Um menu do Android com duas opções: capturar imagem e arquivos Um menu do iOS com três opções: tirar foto, biblioteca de fotos e iCloud

Os dados podem ser anexados a um <form> ou manipulados com JavaScript, detectando um evento onchange no elemento de entrada e lendo a propriedade files do evento target.

<input type="file" accept="image/*" id="file-input" />
<script>
  const fileInput = document.getElementById('file-input');

  fileInput.addEventListener('change', (e) =>
    doSomethingWithFiles(e.target.files),
  );
</script>

A propriedade files é um objeto FileList, que vou abordar mais adiante.

Também é possível adicionar o atributo capture ao elemento, o que indica ao navegador que você prefere receber uma imagem da câmera.

<input type="file" accept="image/*" capture />
<input type="file" accept="image/*" capture="user" />
<input type="file" accept="image/*" capture="environment" />

Adicionar o atributo capture sem um valor permite que o navegador decida qual câmera usar, enquanto os valores "user" e "environment" informam ao navegador que ele deve preferir as câmeras frontal e traseira, respectivamente.

O atributo capture funciona no Android e no iOS, mas é ignorado em computadores. No entanto, no Android, isso significa que o usuário não poderá mais escolher uma imagem existente. Em vez disso, o app de câmera do sistema será iniciado diretamente.

Arrastar e soltar

Se você já está adicionando a capacidade de fazer upload de um arquivo, há algumas maneiras fáceis de melhorar a experiência do usuário.

A primeira é adicionar um destino de soltar à página para permitir que o usuário arraste um arquivo do computador ou de outro aplicativo.

<div id="target">You can drag an image file here</div>
<script>
  const target = document.getElementById('target');

  target.addEventListener('drop', (e) => {
    e.stopPropagation();
    e.preventDefault();

    doSomethingWithFiles(e.dataTransfer.files);
  });

  target.addEventListener('dragover', (e) => {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy';
  });
</script>

Assim como na entrada de arquivo, é possível acessar um objeto FileList da propriedade dataTransfer.files do evento drop.

O manipulador de eventos dragover permite sinalizar ao usuário o que vai acontecer quando ele soltar o arquivo usando a propriedade dropEffect.

O recurso de arrastar e soltar existe há muito tempo e é compatível com os principais navegadores.

Colar da área de transferência

A última maneira de acessar um arquivo de imagem é pela área de transferência. O código para isso é muito simples, mas a experiência do usuário é um pouco mais difícil de acertar.

<textarea id="target">Paste an image here</textarea>
<script>
  const target = document.getElementById('target');

  target.addEventListener('paste', (e) => {
    e.preventDefault();
    doSomethingWithFiles(e.clipboardData.files);
  });
</script>

e.clipboardData.files é outro objeto FileList.

A parte complicada da API da área de transferência é que, para oferecer suporte total a vários navegadores, o elemento de destino precisa ser selecionável e editável. <textarea> e <input type="text"> se encaixam aqui, assim como elementos com o atributo contenteditable. Mas elas também são projetadas para editar texto.

Pode ser difícil fazer isso funcionar sem problemas se você não quiser que o usuário possa inserir texto. Truques como ter uma entrada oculta que é selecionada quando você clica em algum outro elemento podem dificultar a manutenção da acessibilidade.

Como processar um objeto FileList

Como a maioria dos métodos acima produz um FileList, preciso falar um pouco sobre o que ele é.

Um FileList é semelhante a um Array. Ele tem chaves numéricas e uma propriedade length, mas não é realmente uma matriz. Não há métodos de matriz, como forEach() ou pop(), e ele não é iterável. É claro que você pode receber uma matriz real usando Array.from(fileList).

As entradas da FileList são objetos File. Eles são exatamente iguais aos objetos Blob, exceto por terem propriedades name e lastModified somente leitura adicionais.

<img id="output" />
<script>
  const output = document.getElementById('output');

  function doSomethingWithFiles(fileList) {
    let file = null;

    for (let i = 0; i < fileList.length; i++) {
      if (fileList[i].type.match(/^image\//)) {
        file = fileList[i];
        break;
      }
    }

    if (file !== null) {
      output.src = URL.createObjectURL(file);
    }
  }
</script>

Esse exemplo encontra o primeiro arquivo que tem um tipo MIME de imagem, mas também pode processar várias imagens selecionadas/coladas/retiradas de uma só vez.

Depois de ter acesso ao arquivo, você pode fazer o que quiser com ele. Por exemplo, você pode:

  • Desenhe-o em um elemento <canvas> para poder manipulá-lo.
  • Fazer o download no dispositivo do usuário
  • Faça upload para um servidor com fetch()

Acessar a câmera de forma interativa

Agora que você já tem as bases, é hora de melhorar progressivamente.

Os navegadores modernos podem ter acesso direto às câmeras, permitindo que você crie experiências totalmente integradas à página da Web, para que o usuário nunca precise sair do navegador.

Adquirir acesso à câmera

É possível acessar diretamente uma câmera e um microfone usando uma API na especificação do WebRTC chamada getUserMedia(). Isso vai solicitar ao usuário o acesso aos microfones e câmeras conectados.

O suporte para getUserMedia() é muito bom, mas ainda não está disponível em todos os lugares. Ele não está disponível no Safari 10 ou versões anteriores, que, no momento da criação deste artigo, ainda é a versão estável mais recente. No entanto, a Apple anunciou que ele vai estar disponível no Safari 11.

No entanto, é muito simples detectar o suporte.

const supported = 'mediaDevices' in navigator;

Ao chamar getUserMedia(), é necessário transmitir um objeto que descreva o tipo de mídia que você quer. Essas escolhas são chamadas de restrições. Há várias restrições possíveis, cobrindo aspectos como se você prefere uma câmera frontal ou traseira, se quer áudio e qual é a resolução preferida para a transmissão.

No entanto, para receber dados da câmera, você precisa de apenas uma restrição, que é video: true.

Se bem-sucedida, a API vai retornar um MediaStream que contém dados da câmera. Você pode anexá-lo a um elemento <video> e reproduzi-lo para mostrar uma visualização em tempo real ou a um <canvas> para receber um instantâneo.

<video id="player" controls playsinline autoplay></video>
<script>
  const player = document.getElementById('player');

  const constraints = {
    video: true,
  };

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

Por si só, isso não é tão útil. Tudo o que você pode fazer é pegar os dados do vídeo e reproduzi-los. Se você quiser uma imagem, terá que fazer um pouco mais de trabalho.

Criar um snapshot

A melhor opção para conseguir uma imagem é desenhar um frame do vídeo em uma tela.

Ao contrário da API Web Audio, não há uma API de processamento de stream dedicada para vídeo na Web. Portanto, é necessário recorrer a um pouco de hackery para capturar uma imagem da câmera do usuário.

O processo é o seguinte:

  1. Crie um objeto de tela que vai conter o frame da câmera
  2. Ter acesso ao stream da câmera
  3. Anexar a um elemento de vídeo
  4. Quando você quiser capturar um frame preciso, adicione os dados do elemento de vídeo a um objeto de tela usando drawImage().
<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    // Draw the video frame to the canvas.
    context.drawImage(player, 0, 0, canvas.width, canvas.height);
  });

  // Attach the video stream to the video element and autoplay.
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

Depois de armazenar os dados da câmera na tela, você pode fazer muitas coisas com eles. Você pode:

  • Faça o upload diretamente no servidor
  • Armazenar localmente
  • Aplicar efeitos divertidos à imagem

Dicas

Interromper a transmissão da câmera quando não for necessário

É recomendável parar de usar a câmera quando não for mais necessário. Isso não apenas economiza bateria e capacidade de processamento, mas também dá confiança aos usuários no seu aplicativo.

Para interromper o acesso à câmera, basta chamar stop() em cada faixa de vídeo para o stream retornado por getUserMedia().

<video id="player" controls playsinline autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    context.drawImage(player, 0, 0, canvas.width, canvas.height);

    // Stop all video streams.
    player.srcObject.getVideoTracks().forEach(track => track.stop());
  });

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // Attach the video stream to the video element and autoplay.
    player.srcObject = stream;
  });
</script>

Pedir permissão para usar a câmera de forma responsável

Se o usuário não tiver concedido acesso à câmera ao seu site, assim que você chamar getUserMedia(), o navegador vai pedir ao usuário para conceder permissão ao site para usar a câmera.

Os usuários detestam receber solicitações de acesso a dispositivos poderosos na máquina e, com frequência, bloqueiam a solicitação ou a ignoram se não entenderem o contexto em que a solicitação foi criada. É recomendável solicitar o acesso à câmera somente quando for necessário. Depois que o usuário conceder o acesso, ele não será solicitado novamente. No entanto, se o usuário rejeitar o acesso, não será possível ter acesso novamente, a menos que ele mude manualmente as configurações de permissão da câmera.

Compatibilidade

Mais informações sobre a implementação de navegadores para dispositivos móveis e computadores:

Também recomendamos usar o shim adapter.js para proteger apps de mudanças de especificação e diferenças de prefixo do WebRTC.

Feedback