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 de navegador (Flash ou Silverlight) para fazer isso. Vamos lá!

O HTML5 está aqui para nos ajudar. Pode não parecer, mas o surgimento do HTML5 trouxe um aumento no acesso ao hardware do dispositivo. Geolocalização (GPS), a API Orientation (acelerômetro), WebGL (GPU) e a API Web Audio (hardware de áudio) são exemplos perfeitos. Esses recursos são extremamente avançados e expõem APIs JavaScript de alto nível que ficam sobre os recursos de hardware subjacentes do sistema.

Este tutorial apresenta uma nova API, GetUserMedia, que permite que 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 forma como chegamos à API getUserMedia() é interessante.

Várias variantes de "APIs Media Capture" evoluíram nos últimos anos. Muitas pessoas reconheceram a necessidade de acessar dispositivos nativos na Web, mas isso levou todo mundo 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? Entenda a loucura! O Grupo de Trabalho da Política de APIs de Dispositivo (DAP, na sigla em inglês) foi encarregado de consolidar e padronizar as várias propostas.

Vou tentar resumir o que aconteceu em 2011…

Rodada 1: captura de mídia HTML

A captura de mídia HTML foi a primeira tentativa do DAP de 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 especialmente do fato de que ele reutiliza uma entrada de arquivo. Semanticamente, isso faz muito sentido. O que falta nessa "API" é a capacidade de criar efeitos em tempo real, como renderizar dados de webcam ao vivo em um <canvas> e aplicar filtros WebGL. A captura de mídia HTML só permite gravar um arquivo de mídia ou tirar uma foto em um momento específico.

Suporte:

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

Rodada 2: elemento do dispositivo

Muitos achavam que a captura de mídia HTML era muito limitada, então surgiu uma nova especificação que aceitava qualquer tipo de dispositivo (futuro). Não é de surpreender que o design exigisse um novo elemento, o <device>, que se tornou o predecessor 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 mais preciso), o WhatWG decidiu descartar a tag <device> em favor de outra novidade, desta vez uma API JavaScript chamada navigator.getUserMedia(). Uma semana depois, o Opera lançou novas versões que incluíam suporte para a especificação getUserMedia() atualizada. No final daquele ano, a Microsoft se juntou à festa lançando um Lab para o IE9 com suporte para a nova especificação.

Veja como o <device> seria:

<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 incluiu <device>. Uma API a menos para se preocupar, eu acho :) No entanto, <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 rápido!

Rodada 3: WebRTC

O elemento <device> acabou sendo descontinuado.

O ritmo para encontrar uma API de captura adequada acelerou graças ao esforço maior do WebRTC (Web Real Time Communications). Essa especificação é supervisionada pelo grupo de trabalho do WebRTC do W3C. O Google, o Opera, o 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 da câmera/microfone local do usuário.

Suporte:

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

Primeiros passos

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

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 acessar um dispositivo de entrada

Para usar a webcam ou o microfone, precisamos pedir permissão. O primeiro parâmetro para 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 precisa 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 amigos do HTML5, <audio> e <video>. Não estamos definindo um atributo src nem incluindo elementos <source> no elemento <video>. Em vez de fornecer ao vídeo um URL para um arquivo de mídia, estamos definindo srcObject como o objeto LocalMediaStream que representa a webcam.

Também estou dizendo ao <video> para autoplay. Caso contrário, ele ficaria congelado no primeiro frame. Adicionar controls também funciona como esperado.

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

O primeiro parâmetro para getUserMedia() também pode ser usado para especificar mais requisitos (ou restrições) no stream 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 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 mais configurações, consulte a API de restrições.

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 Promise retornada é resolvida com uma matriz de objetos MediaDeviceInfo que descrevem os dispositivos.

Neste exemplo, o último microfone e a última 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 origem de mídia.

Segurança

Os navegadores mostram uma caixa de diálogo de permissão ao chamar navigator.mediaDevices.getUserMedia(), 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ão do Chrome:

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

Como fornecer substitutos

Para usuários que não têm suporte para navigator.mediaDevices.getUserMedia(), uma opção é usar um arquivo de vídeo 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;
}