Captura de áudio e vídeo em HTML5

Introdução

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

O HTML5 está aqui para ajudar. Pode não ser óbvio, mas o aumento 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 poderosos, expondo APIs JavaScript de alto nível que ficam acima das capacidades 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 maneira como chegamos à API getUserMedia() é 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 fez com que todo mundo criasse uma nova especificação. As coisas ficaram tão confusas que o W3C decidiu formar um grupo de trabalho. O único propósito deles? Entenda a loucura! O Grupo de trabalho da política de APIs de dispositivo (DAP) tem a tarefa de consolidar e padronizar a grande quantidade de 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 da 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 com a webcam, isso é possível com capture=camera:

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

Gravar um vídeo ou áudio é semelhante:

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

Legal, né? Gosto especialmente que ele reutilize uma entrada de arquivo. Semanticamente, faz muito sentido. Onde essa "API" específica fica aquém é na capacidade de fazer efeitos em tempo real (por exemplo, renderizar dados de webcam ao vivo em um <canvas> e aplicar filtros WebGL). A captura de mídia HTML permite apenas gravar um arquivo de mídia ou tirar uma foto instantânea.

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 no iOS6 (suporte parcial)

Rodada 2: elemento de dispositivo

Muitos achavam que o HTML Media Capture era muito limitado, então surgiu uma nova especificação que oferecia suporte a qualquer tipo de dispositivo (futuro). Não é surpreendente que o design tenha exigido 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, para ser mais preciso), o WhatWG decidiu descartar a tag <device> em favor de outra promessa, desta vez uma API JavaScript chamada navigator.getUserMedia(). Uma semana depois, o Opera lançou novas versões com suporte à especificação getUserMedia() atualizada. No final daquele ano, a Microsoft também lançou um laboratório para o IE9 com suporte à nova especificação.

Veja 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 incluiu <device>. Uma API a menos para se preocupar, eu acho :) O <device> tinha duas coisas ótimas: 1) era semântico e 2) era facilmente extensível para oferecer suporte a mais do que apenas dispositivos de áudio/vídeo.

Respire fundo. Essas coisas mudam muito rápido!

Rodada 3: WebRTC

O elemento <device> acabou desaparecendo.

O ritmo para encontrar uma API de captura adequada acelerou graças ao maior esforço do WebRTC (Web Real Time Communications). Essa especificação é supervisionada pelo Grupo de trabalho do WebRTC do W3C. 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() é compatível com o Chrome 21, o Opera 18 e o Firefox 17.

Primeiros passos

Com navigator.mediaDevices.getUserMedia(), 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 é integrado diretamente ao navegador. Gostou?

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 conseguir acesso a um dispositivo de entrada

Para usar a webcam ou o microfone, precisamos pedir permissão. O primeiro parâmetro de navigator.mediaDevices.getUserMedia() é um objeto que especifica os detalhes e 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. Então, o que está acontecendo aqui? A captura de mídia é um exemplo perfeito de novas APIs HTML5 trabalhando 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.

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 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}), você também 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 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 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 fluxo 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 da 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 o 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

Fornecer substituto

Para usuários que não têm suporte para navigator.mediaDevices.getUserMedia(), uma opção é fazer o fallback para 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;
}