Captura de áudio e vídeo em HTML5

Introdução

A captura de áudio/vídeo tem sido o "Santo Graal" do desenvolvimento da Web há muito tempo. Por muitos anos, tivemos que usar plug-ins do navegador (Flash ou Silverlight) para fazer isso. Vamos lá!

HTML5 ao resgate. Pode não ser aparente, mas o surgimento do HTML5 trouxe uma onda de acesso ao hardware do dispositivo. Geolocalização (GPS), a API de orientação (acelerômetro), WebGL (GPU) e a API Web Audio (hardware de áudio) são exemplos perfeitos. Esses recursos são incrivelmente poderosos, expondo APIs JavaScript de alto nível que se sobrepõem às capacidades 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 do usuário.

Como chegar a getUserMedia()

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

Várias variantes das "APIs de captura de mídia" evoluíram nos últimos anos. Muitas pessoas reconheceram a necessidade de conseguir acessar dispositivos nativos na Web, mas isso levou todos e sua mãe a elaborar uma nova especificação. As coisas ficaram tão confusas que o W3C finalmente decidiu formar um grupo de trabalho. Seu único propósito? Entenda a loucura! O grupo de trabalho de políticas de APIs do dispositivo (DAP, na sigla em inglês) foi encarregado de consolidar e padronizar a variedade de propostas.

Vou tentar resumir o que aconteceu em 2011...

Round 1: captura de mídia HTML

A captura de mídia HTML foi a primeira ação do DAP na padronização da captura de mídia na Web. Ele sobrecarrega o <input type="file"> e adiciona novos valores para o parâmetro accept.

Se você quiser permitir que os usuários tirem um snapshot 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">

Muito bom, não é? Particularmente, gosto da reutilização de uma entrada de arquivo. Semanticamente, isso faz muito sentido. O ponto fraco dessa "API" específica é a capacidade de criar efeitos em tempo real, por exemplo, renderizar dados ao vivo da webcam em um <canvas> e aplicar filtros WebGL. A captura de mídia HTML só permite gravar um arquivo de mídia ou tirar um instantâneo.

Suporte:

  • Navegador Android 3.0 - uma das primeiras implementações. Confira este vídeo para ver como isso funciona.
  • Chrome para Android (0.16)
  • Firefox 10.0 para dispositivos móveis
  • Safari e Chrome para iOS6 (suporte parcial)

Round 2: elemento device

Muitos pensavam que o HTML Media Capture era muito limitante, então surgiu uma nova especificação que oferece suporte a qualquer tipo de dispositivo (futuro). Como seria de se esperar, o design exigia um novo elemento, o elemento <device>, que se tornou o predecessor de 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, o WhatWG decidiu descartar a tag <device> em favor de outra, 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 para o grupo lançando um Laboratório para o IE9 com suporte à nova especificação.

Confira como <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 para se preocupar, eu acho :) A <device> tinha duas grandes vantagens: 1) ela era semântica e 2.) era facilmente extensível para oferecer suporte a mais do que apenas dispositivos de áudio/vídeo.

Respire fundo. Tudo passa rápido!

Rodada 3: WebRTC

O elemento <device> acabou se tornando o método Dodo.

O ritmo para encontrar uma API de captura adequada foi acelerado graças ao esforço maior de comunicações em tempo real da Web (WebRTC). Essa especificação é supervisionada pelo grupo de trabalho WebRTC do W3C (em inglês). Google, Opera, 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 stream de câmera/microfone local do usuário.

Suporte:

O getUserMedia() é compatível desde o Chrome 21, Opera 18 e Firefox 17.

Como começar

Com 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 de distância, não a uma instalação de distância. Ele ocorre diretamente no navegador. Já 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");
}

Obtenção de acesso a um dispositivo de entrada

Para usar a webcam ou o microfone, precisamos solicitar permissão. O primeiro parâmetro para navigator.mediaDevices.getUserMedia() é um objeto que especifica os detalhes e requisitos para cada tipo de mídia que você quer acessar. Por exemplo, se você quiser acessar a webcam, o primeiro parâmetro precisará 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 funcionando juntas. Ele funciona em conjunto com nossos outros elementos HTML5, <audio> e <video>. Observe que não estamos definindo um atributo src nem incluindo elementos <source> no elemento <video>. Em vez de enviar o vídeo com um URL para um arquivo de mídia, estamos definindo srcObject como o objeto LocalMediaStream que representa a webcam.

Também estou informando o <video> para autoplay. Caso contrário, ele seria congelado no primeiro frame. A adição de controls também funciona conforme o esperado.

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 apenas indicar que você quer acesso básico ao vídeo (por exemplo, {video: true}), é possível exigir que o stream seja 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 mais configurações, consulte a API de restrições.

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 estã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 excelente 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(), que oferece aos usuários a opção de conceder ou negar o acesso à câmera/ao microfone. Por exemplo, esta é a caixa de diálogo de permissão do Chrome:

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

Como fornecer substituto

Para usuários que não têm suporte a navigator.mediaDevices.getUserMedia(), uma opção é substituir por um arquivo de vídeo já existente se a API não for compatível 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;
}