Captura de áudio e vídeo em HTML5

A captura de áudio/vídeo é o "Santo Graal" do desenvolvimento da Web há muito tempo. Por muitos anos, dependemos de plug-ins de navegadores (Flash ou Silverlight) para fazer nosso trabalho. Vamos lá!

HTML5 ao resgate. Talvez não seja aparente, mas o aumento do HTML5 trouxe um aumento no acesso ao hardware do dispositivo. Geolocation (GPS), a API Orientation (acelerômetro), WebGL (GPU) e a API Web Audio (hardware de áudio) são exemplos perfeitos. Esses recursos são ridiculamente poderosos, expondo APIs JavaScript de alto nível que ficam acima dos recursos de hardware subjacentes do sistema.

Este tutorial apresenta uma nova API, GetUserMedia, que permite que os apps da Web acessem a câmera e o microfone de um usuário.

O caminho para getUserMedia()

Se você não conhece a história, a maneira como chegamos à API getUserMedia() é uma história interessante.

Várias variantes das "APIs Media Capture" evoluíram nos últimos anos. Muitas pessoas reconheceram a necessidade de acessar dispositivos nativos na Web, mas isso levou todos a criar uma nova especificação. As coisas ficaram tão confusas que o W3C finalmente decidiu formar um grupo de trabalho. O único propósito deles? Entenda a loucura! O grupo de trabalho da política de APIs de dispositivos (DAP, na sigla em inglês) foi encarregado de consolidar e padronizar a grande quantidade de propostas.

Vou tentar resumir o que aconteceu em 2011…

Rodada 1: captura de mídia em HTML

A captura de mídia HTML (link em inglês) foi a primeira tentativa do DAP para padronizar a captura de mídia na Web. Ele funciona sobrecarregando o <input type="file"> e adicionando novos valores para o parâmetro accept.

Se você quiser permitir que os usuários tirem uma foto de si mesmos com a webcam, isso é possível com capture=camera:

<input type="file" accept="image/*;capture=camera">

A gravação de um vídeo ou áudio é semelhante:

<input type="file" accept="video/*;capture=camcorder">
<input type="file" accept="audio/*;capture=microphone">

Legal, né? Gosto particularmente de reutilizar uma entrada de arquivo. Semanticamente, isso faz muito sentido. A "API" específica não tem a capacidade de fazer efeitos em tempo real, por exemplo, renderizar dados de webcam ao vivo para um <canvas> e aplicar filtros WebGL. A captura de mídia HTML só permite gravar um arquivo de mídia ou fazer um instantâneo por tempo.

Suporte:

  • Navegador do Android 3.0: uma das primeiras implementações. Confira este vídeo para ver como funciona.
  • Chrome para Android (0.16)
  • Firefox Mobile 10.0
  • Safari e Chrome para iOS6 (suporte parcial)

Rodada 2: elemento do dispositivo

Muitas pessoas achavam que a Media Capture do HTML era muito limitada. Por isso, uma nova especificação surgiu com suporte a qualquer tipo de dispositivo (futuro). Não é de surpreender que o design tenha chamado por um novo elemento, o elemento <device>, que se tornou o antecessor do getUserMedia().

O Opera foi um dos primeiros navegadores a criar implementações iniciais de captura de vídeo com base no elemento <device>. Logo depois (no mesmo dia, para ser preciso), o WhatWG decidiu descartar a tag <device> em favor de outra em ascensão, desta vez uma API JavaScript chamada navigator.getUserMedia(). Uma semana depois, o Opera lançou novos builds que incluíam suporte à especificação getUserMedia() atualizada. Mais tarde naquele ano, a Microsoft entrou na festa lançando um Lab para IE9 com suporte à nova especificação.

Veja como o <device> ficaria:

<device type="media" onchange="update(this.data)"></device>
<video autoplay></video>
<script>
  function update(stream) {
    document.querySelector('video').src = stream.url;
  }
</script>

Suporte:

Infelizmente, nenhum navegador lançado nunca incluiu <device>. Uma API a menos com que se preocupar :) <device> tinha duas grandes vantagens: 1. era semântica, e 2.) era facilmente extensível para oferecer suporte a mais do que apenas dispositivos de áudio/vídeo.

Respire fundo. As coisas mudam muito rápido!

Rodada 3: WebRTC

O elemento <device> acabou sendo descontinuado.

O ritmo para encontrar uma API de captura adequada foi acelerado graças ao maior esforço do WebRTC (Comunicação em tempo real na Web). Essa especificação é supervisionada pelo Grupo de Trabalho do W3C WebRTC. O Google, o Opera, a Mozilla e alguns outros têm implementações.

getUserMedia() está relacionado ao WebRTC porque é o gateway para esse conjunto de APIs. Ele fornece os meios para acessar o fluxo local de câmera/microfone do usuário.

Suporte:

O getUserMedia() tem suporte desde o Chrome 21, Opera 18 e Firefox 17.

Primeiros passos

Com o navigator.mediaDevices.getUserMedia(), podemos finalmente usar a entrada de webcam e microfone sem um plug-in. O acesso à câmera agora está a uma chamada, não a uma instalação de distância. Ele é integrado diretamente ao navegador. Está animado?

Detecção de recursos

A detecção de recursos é uma verificação simples da existência de navigator.mediaDevices.getUserMedia:

if (navigator.mediaDevices?.getUserMedia) {
  // Good to go!
} else {
  alert("navigator.mediaDevices.getUserMedia() is not supported");
}

Como receber acesso a um dispositivo de entrada

Para usar a webcam ou o microfone, precisamos solicitar permissão. O primeiro parâmetro de navigator.mediaDevices.getUserMedia() é um objeto que especifica os detalhes e os requisitos de cada tipo de mídia que você quer acessar. Por exemplo, se você quiser acessar a webcam, o primeiro parâmetro será {video: true}. Para usar o microfone e a câmera, transmita {video: true, audio: true}:

<video autoplay></video>

<script>
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: true })
    .then((localMediaStream) => {
      const video = document.querySelector("video");
      video.srcObject = localMediaStream;
    })
    .catch((error) => {
      console.log("Rejected!", error);
    });
</script>

Ok. O que está acontecendo aqui? A captura de mídia é um exemplo perfeito de novas APIs HTML5 trabalhando juntas. Ele funciona com nossos outros amigos do HTML5, <audio> e <video>. Não definimos um atributo src nem incluímos elementos <source> no elemento <video>. Em vez de alimentar o vídeo com um URL para um arquivo de mídia, definimos srcObject para o objeto LocalMediaStream que representa a webcam.

Também estou informando o <video> para autoplay. Caso contrário, ele seria congelado no primeiro frame. Adicionar controls também funciona como você espera.

definir restrições de mídia (resolução, altura, largura);

O primeiro parâmetro de getUserMedia() também pode ser usado para especificar mais requisitos (ou restrições) no fluxo de mídia retornado. Por exemplo, em vez de indicar que você quer acesso básico ao vídeo (por exemplo, {video: true}), você pode exigir que o stream seja em HD:

const hdConstraints = {
  video: { width: { exact:  1280} , height: { exact: 720 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);
const vgaConstraints = {
  video: { width: { exact:  640} , height: { exact: 360 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);

Para ver mais configurações, consulte a API constraints.

Como selecionar uma fonte de mídia

O método enumerateDevices() da interface MediaDevices solicita uma lista dos dispositivos de entrada e saída de mídia disponíveis, como microfones, câmeras, fones de ouvido etc. A promessa retornada é resolvida com uma matriz de objetos MediaDeviceInfo que descrevem os dispositivos.

Neste exemplo, o último microfone e câmera encontrados são selecionados como a origem do stream de mídia:

if (!navigator.mediaDevices?.enumerateDevices) {
  console.log("enumerateDevices() not supported.");
} else {
  // List cameras and microphones.
  navigator.mediaDevices
    .enumerateDevices()
    .then((devices) => {
      let audioSource = null;
      let videoSource = null;

      devices.forEach((device) => {
        if (device.kind === "audioinput") {
          audioSource = device.deviceId;
        } else if (device.kind === "videoinput") {
          videoSource = device.deviceId;
        }
      });
      sourceSelected(audioSource, videoSource);
    })
    .catch((err) => {
      console.error(`${err.name}: ${err.message}`);
    });
}

async function sourceSelected(audioSource, videoSource) {
  const constraints = {
    audio: { deviceId: audioSource },
    video: { deviceId: videoSource },
  };
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
}

Confira a ótima demonstração de Sam Dutton sobre como permitir que os usuários selecionem a fonte de mídia.

Segurança

Os navegadores mostram uma caixa de diálogo de permissão ao chamar navigator.mediaDevices.getUserMedia(), o que dá aos usuários a opção de conceder ou negar acesso à câmera/microfone. Por exemplo, esta é a caixa de diálogo de permissões do Chrome:

Caixa de diálogo de permissões no Chrome
Caixa de diálogo de permissão no Chrome

Fornecer substituto

Para usuários que não têm suporte a navigator.mediaDevices.getUserMedia(), uma opção é usar um arquivo de vídeo existente se a API não tiver suporte e/ou a chamada falhar por algum motivo:

if (!navigator.mediaDevices?.getUserMedia) {
  video.src = "fallbackvideo.webm";
} else {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  video.srcObject = stream;
}