Capturing Audio & Video in HTML5

HTML5 Rocks

This article discusses APIs that are not yet fully standardized and still in flux. Be cautious when using experimental APIs in your own projects.

Introdução

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

HTML5 para nos resgatar. Talvez não seja aparente, mas o surgimento do HTML5 trouxe um pico de acesso ao hardware do dispositivo. localização geográfica (GPS), API de orientação (acelerômetro), WebGL (GPU) e API de áudio da web (hardware de áudio) são exemplos perfeitos. Esses recursos são incrivelmente poderosos, expondo APIs JavaScript de alto nível que se sobrepõem aos recursos de hardware subjacentes do sistema.

Este tutorial introduz uma nova API, navigator.getUserMedia(), que permite aos aplicativos da web acessar a câmera e o microfone do usuário.

O histórico de getUserMedia()

Se você não conhece a história, vale dizer que o modo como chegamos à API getUserMedia() é um conto interessante.

Nos últimos anos, foram desenvolvidas diversas variações de "APIs de captura de mídia". Muitos colegas reconheceram a necessidade de conseguir acessar dispositivos nativos na web, mas isso levou ao desenvolvimento de uma nova especificação. As coisas ficaram tão confusas que o W3C finalmente decidiu formar um grupo de trabalho. O objetivo? Arrumar a bagunça. O grupo de trabalho DAP (política de APIs de dispositivo) tinha a tarefa de consolidar e padronizar a variedade de propostas.

Tentarei resumir o que aconteceu em 2011...

Round 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. Ela funciona sobrepondo <input type="file"> e adicionando novos valores para o parâmetro accept.

Os usuários possam fazer uma captura de tela por conta própria com a webcam usando 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 está em sua capacidade de criar efeitos em tempo real (por exemplo, processar 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 fazer uma captura de tela por vez.

Suporte:

Minha recomendação é não utilizá-lo, a não ser que você esteja trabalhando com um dos navegadores para celular acima. Os fornecedores estão migrando para getUserMedia(). É muito improvável que mais alguém implemente a captura de mídia HTML em longo prazo.

Round 2: elemento device

Como muitos pensavam que a captura de mídia HTML era muito limitada, uma nova especificação surgiu para dar suporte a qualquer tipo de dispositivo (futuro). Como era de se esperar, o projeto precisava de um novo elemento, o elemento <device>, que se tornou o antecessor 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 exato), o WhatWG decidiu usar a tag <device> em outro projeto, dessa vez, uma API JavaScript chamada navigator.getUserMedia(). Uma semana depois, o Opera criou novas versões que incluíam suporte para a especificação getUserMedia() atualizada. Mais tarde nesse mesmo ano, a Microsoft se juntou ao projeto lançando um Lab for IE9 com suporte para a nova especificação.

Veja como seria o <device>:

<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 :) <device> tinha duas grandes vantagens: 1.) era semântico e 2.) podia ser facilmente estendido para oferecer suporte a mais do que simplesmente dispositivos de áudio/vídeo.

Respire fundo. As coisas mudam rapidamente.

Round 3: WebRTC

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

O ritmo para encontrar uma API de captura adequada acelerou nos últimos meses graças a uma iniciativa mais abrangente chamada WebRTC (Comunicação on-line em tempo real). A especificação é supervisionada pelo grupo de trabalho WebRTC do W3C. Google, Opera, Mozilla e alguns outros navegadores estão trabalhando no momento para trazer as implementações para seus navegadores.

getUserMedia() está relacionado ao WebRTC porque é a porta de acesso para esse conjunto de APIs. Ele fornece o meio para acessar o fluxo da câmera/microfone local do usuário.

Suporte:

WebRTC pode ser ativado no Google Chrome 18.0.1008+ em about:flags.

Primeiros passos

Com navigator.getUserMedia(), podemos finalmente trabalhar com a entrada da webcam e do microfone sem um plug-in. O acesso da câmera agora está a uma chamada, não a uma instalação de distância. Ele ocorre diretamente no navegador. Já se interessou?

Ativando

A API getUserMedia() ainda é muito recente, e somente Google e Opera têm versões de desenvolvedor que a incluem. No Google Chrome 18+, a API pode ser ativada em about:flags.

Ativação de getUserMedia() na página about:flags do Google Chrome.

Para o Opera, faça download das versões experimentais para Android e computador.

Detecção de recursos

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

function hasGetUserMedia() {
  // Note: Opera builds are unprefixed.
  return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia || navigator.msGetUserMedia);
}

if (hasGetUserMedia()) {
  // Good to go!
} else {
  alert('getUserMedia() is not supported in your browser');
}

Obtenção de acesso a um dispositivo de entrada

Para usar a webcam ou o microfone, precisamos solicitar permissão. O primeiro parâmetro de getUserMedia() serve para especificar o tipo de mídia que você deseja acessar. Por exemplo, para solicitar a webcam, o primeiro parâmetro deverá ser "video". Para usar o microfone e a câmera, transmita "video, audio":

<video autoplay></video>

<script>
  var onFailSoHard = function(e) {
    console.log('Reeeejected!', e);
  };

  // Not showing vendor prefixes.
  navigator.getUserMedia('video, audio', function(localMediaStream) {
    var video = document.querySelector('video');
    video.src = window.URL.createObjectURL(localMediaStream);

    // Note: onloadedmetadata doesn't fire in Chrome when using it with getUserMedia.
    // See crbug.com/110938.
    video.onloadedmetadata = function(e) {
      // Ready to go. Do some stuff.
    };
  }, onFailSoHard);
</script>

OK. O que está acontecendo aqui? A captura de mídia é um exemplo perfeito de novas APIs HTML5 funcionando juntas. Ela funciona junto com os 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 direcionar o vídeo para um URL de um arquivo de mídia, estamos direcionando-o para um URL de blob obtido a partir de um objeto LocalMediaStream que representa a webcam.

Também estou instruindo o <video> a ser reproduzido automaticamente; caso contrário, ele seria congelado no primeiro quadro. A adição de controles também funciona conforme esperado.

Observação: há um bug no Google Chrome porque a transmissão de apenas "audio" não funciona: crbug.com/112367. O elemento <audio> também não funcionaria no Opera.

O Opera e o Chrome implementam versões diferentes da especificação. Isso deixa o uso prático um pouco mais "desafiador" do que normalmente seria.

No Chrome:

Esse snippet funciona no Chrome 18+ (ative em about:flags):

navigator.webkitGetUserMedia('audio, video', function(localMediaStream) {
  var video = document.querySelector('video');
  video.src = window.webkitURL.createObjectURL(localMediaStream);
}, onFailSoHard);

No Opera:

As versões de desenvolvedor do Opera funcionam com uma versão atualizada da especificação. Esse snippet funciona no Opera:

navigator.getUserMedia({audio: true, video: true}, function(localMediaStream) {
  video.src = localMediaStream;
}, onFailSoHard);

As principais diferenças são:

  • getUserMedia() não tem prefixo.
  • Um objeto é transmitido como o primeiro argumento em vez de uma lista de strings.
  • video.src é definido diretamente como o objeto LocalMediaStream em vez de um URL de blob. Soube que o Opera acabará atualizando isso para exigir um URL de blob.

Nos dois:

Se desejar que algo funcione em vários navegadores (mas isso é muito delicado), tente fazer isso:

var video = document.querySelector('video');

if (navigator.getUserMedia) {
  navigator.getUserMedia({audio: true, video: true}, function(stream) {
    video.src = stream;
  }, onFailSoHard);
} else if (navigator.webkitGetUserMedia) {
  navigator.webkitGetUserMedia('audio, video', function(stream) {
    video.src = window.webkitURL.createObjectURL(stream);
  }, onFailSoHard);
} else {
  video.src = 'somevideo.webm'; // fallback.
}

Confira o trabalho de Mike Taylor e Mike Robinson gUM Shield. Eles fazem um trabalho excelente de "normalização" das inconsistências entre implementações de navegadores.

Segurança

No futuro, os navegadores podem lançar uma barra de informações chamando getUserMedia(), o que daria aos usuários a opção de conceder ou negar o acesso à câmera ou ao microfone. Infelizmente, a especificação não traz muitos detalhes sobre segurança. No momento, ninguém implementa uma barra de permissão.

Fornecimento de fallback

Para usuários que não têm suporte para getUserMedia(), uma opção seria executar fallback para um arquivo de vídeo já existente se a API não fosse compatível e/ou a chamada falhasse por algum motivo:

// Not showing vendor prefixes or code that works cross-browser:

function fallback(e) {
  video.src = 'fallbackvideo.webm';
}

function success(stream) {
  video.src = window.URL.createObjectURL(stream);
}

if (!navigator.getUserMedia) {
  fallback();
} else {
  navigator.getUserMedia({video: true}, success, fallback);
}

Demonstração básica

Como fazer capturas de tela

O método ctx.drawImage(video, 0, 0) da API <canvas> simplifica o desenho de quadros de <video> no <canvas>. Obviamente, agora que temos a entrada de vídeo via getUserMedia(), ficou fácil criar um aplicativo para tirar foto com vídeo em tempo real:

<video autoplay></video>
<img src="">
<canvas style="display:none;"></canvas>

var video = document.querySelector('video');
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var localMediaStream = null;

function snapshot() {
  if (localMediaStream) {
    ctx.drawImage(video, 0, 0);
    // "image/webp" works in Chrome 18. In other browsers, this will fall back to image/png.
    document.querySelector('img').src = canvas.toDataURL('image/webp');
  }
}

video.addEventListener('click', snapshot, false);

// Not showing vendor prefixes or code that works cross-browser.
navigator.getUserMedia({video: true}, function(stream) {
  video.src = window.URL.createObjectURL(stream);
  localMediaStream = stream;
}, onFailSoHard);

Aplicação de efeitos

Filtros do CSS

Atualmente, os filtros do CSS são aceitos no WebKit nightlies e no Chrome 18+.

Usando os filtros do CSS, podemos aplicar alguns efeitos de nós ao <video> conforme ele é capturado:

<style>
video {
  width: 307px;
  height: 250px;
  background: rgba(255,255,255,0.5);
  border: 1px solid #ccc;
}
.grayscale {
  +filter: grayscale(1);
}
.sepia {
  +filter: sepia(1);
}
.blur {
  +filter: blur(3px);
}
...
</style>

<video autoplay></video>

<script>
var idx = 0;
var filters = ['grayscale', 'sepia', 'blur', 'brightness', 'contrast', 'hue-rotate',
               'hue-rotate2', 'hue-rotate3', 'saturate', 'invert', ''];

function changeFilter(e) {
  var el = e.target;
  el.className = '';
  var effect = filters[idx++ % filters.length]; // loop through filters.
  if (effect) {
    el.classList.add(effect);
  }
}

document.querySelector('video').addEventListener('click', changeFilter, false);
</script>

Clique no vídeo para percorrer os filtros do CSS

Texturas de WebGL

Um caso de uso incrível para captura de vídeo é processar a entrada ao vivo como uma textura de WebGL. Como eu não sei absolutamente nada sobre WebGL (a não ser que é bom), vou sugerir que vocês confiram o tutorial e a demonstração de Jerome Etienne. Eles mostram como usar getUserMedia() e Three.js para processar vídeo ao vivo no WebGL.

Como usar getUserMedia com a API de áudio da web

Esta seção discute os possíveis aprimoramentos e extensões futuros para a API atual.

Um dos meus sonhos é criar o AutoTune no navegador tendo apenas apenas a tecnologia da web aberta. Não estamos muito longes dessa realidade. Já temos getUserMedia() para entrada de microfone. Use a API de áudio da web para obter efeitos em tempo real e pronto. Ainda falta a integração dos dois (crbug.com/112404), embora haja uma proposta preliminar nos trabalhos para que isso aconteça.

Algum dia, a execução da entrada de microfone na API de áudio da web poderia ser parecida com:

var context = new window.webkitAudioContext();

navigator.webkitGetUserMedia({audio: true}, function(stream) {
  var microphone = context.createMediaStreamSource(stream);
  var filter = context.createBiquadFilter();

  // microphone -> filter -> destination.
  microphone.connect(filter);
  filter.connect(context.destination);
}, onFailSoHard);

Se desejar ver getUserMedia() associado à API de áudio da web, marque crbug.com/112404 com uma estrela.

Conclusão

Em geral, o acesso do dispositivo na web tem sido um grande problema. Muitas pessoas tentaram, mas poucas conseguiram. A maioria das primeiras ideias nunca saiu de um ambiente reservado, nem foi adotada de modo abrangente.

O verdadeiro problema é que o modelo de segurança da web é muito diferente do mundo nativo. Por exemplo, eu provavelmente não quero que todos os sites de Joe Shmoe tenham acesso aleatório à minha câmera de vídeo. É um grande problema fazer isso.

A associação de estruturas como PhoneGap tem ajudado a ultrapassar a barreira, mas ela é apenas um ponto de partida e uma solução temporária para um problema subjacente. Para que os aplicativos da web sejam competitivos com seus correspondentes de computador, precisamos acessar os dispositivos nativos.

getUserMedia() é a primeira onda de acesso a novos tipos de dispositivo. Espero que haja outros em um futuro próximo.

Recursos adicionais

Demonstrações

Comments

0