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.

Introducción

La captura de audio y vídeo ha sido el "Santo Grial" del desarrollo web durante mucho tiempo. Durante muchos años, hemos dependido de los complementos de los navegadores (Flash o Silverlight) para hacer el trabajo. ¡Vamos!

¡HTML5 al rescate! Puede que no resulte evidente, pero la llegada del HTML5 ha ocasionado un repentino aumento del acceso a dispositivos de hardware. La geolocalización (GPS), el API de orientación (acelerómetro), WebGL (GPU) y el API de audio web (hardware de audio) son ejemplos perfectos. Estas funciones son terriblemente potentes, dejando al descubierto las API JavaScript de alto nivel que hay encima de las funciones de hardware subyacentes del sistema.

En este tutorial presentamos una nueva API, navigator.getUserMedia(), que permite a las aplicaciones acceder al micrófono y a la cámara del usuario.

El camino hacia getUserMedia()

La historia sobre cómo llegamos hasta el API getUserMedia() es muy interesante.

Durante los últimos años, han evolucionado varias variantes de las "API de captura multimedia". Muchos compañeros reconocieron la necesidad de poder acceder a dispositivos nativos en la Web, pero eso llevó a muchos a crear nuevas especificaciones. Las cosas se enredaron tanto que al final el W3C decidió formar un grupo de trabajo. ¿Y cuál era su único objetivo? ¡Poner un poco de cordura en toda la locura que se formó! El grupo de trabajo de la Política de API de dispositivos recibió la tarea de consolidar y estandarizar ese gran número de propuestas.

Intentaré resumir qué ocurrió en 2011...

Asalto 1: captura multimedia HTML

La captura multimedia HTML fue el primer intento de la política de API de dispositivos de estandarizar la captura multimedia en Internet. Funciona sobrecargando <input type="file"> y añadiendo nuevos valores para el parámetro accept.

capture=camera permite a los usuarios tomar una a instantánea de sí mismos con la cámara web:

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

Para grabar audio o vídeo, el procedimiento es similar:

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

No está mal, ¿verdad? Particularmente me gusta que reutilice una entrada de archivo. Semánticamente, tiene mucho sentido. En lo que esta "API" en particular se queda corta es en la capacidad de aplicar efectos en tiempo real (por ejemplo, transferir los datos de la cámara web en directo a <canvas> y aplicar filtros WebGL). La captura multimedia HTML solo permite grabar un archivo multimedia o tomar una instantánea en tiempo real.

Compatibilidad:

Mi recomendación es que te mantengas alejado de esta API, a menos que utilices uno de estos navegadores. Los proveedores se están decantando por getUserMedia(). Es poco probable que nadie más implemente la captura multimedia HTML a largo plazo.

Asalto 2: elemento de dispositivo

Muchos pensaron que la captura multimedia HTML estaba demasiado limitada, así que surgió una nueva especificación compatible con cualquier tipo de dispositivo (futuro). No es de extrañar que el diseño necesitara un nuevo elemento, <device>, que se convirtió en el predecesor de getUserMedia().

Opera fue uno de los primeros navegadores en implementar capturas de vídeo basadas en el elemento <device>. Poco después (el mismo día para ser exactos), el WhatWG decidió desechar la etiqueta <device> en favor de otra promesa, esta vez un API JavaScript llamada navigator.getUserMedia(). Una semana después, Opera sacó nuevas compilaciones compatibles con la especificación getUserMedia() actualizada. Más tarde ese año, Microsoft se unió a la fiesta con el lanzamiento de una implementación para Internet Explorer 9 compatible con la nueva especificación.

Este es el aspecto que hubiera tenido <device>:

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

Compatibilidad:

Lamentablemente, ninguno de los navegadores del mercado incluyó nunca <device>. Un API menos de la que preocuparse, supongo :) Pero <device> sí tenía dos grandes cualidades para el éxito: era semántico y se podía ampliar fácilmente para admitir más que solo dispositivos de audio y vídeo.

Respira. ¡Esto va rápido!

Asalto 3: WebRTC

El elemento <device> terminó quedándose obsoleto.

La carrera por encontrar un API de captura adecuado se ha acelerado en los últimos meses gracias a un proyecto de mayor envergadura llamado WebRTC (comunicaciones web en tiempo real). La especificación está supervisada por el grupo de trabajo WebRTC de W3C. Google, Opera, Mozilla y unos pocos más están trabajando actualmente para incorporar implementaciones en sus navegadores.

getUserMedia() está relacionado con WebRTC porque es la puerta de entrada a ese conjunto de API. Proporciona la forma de acceder al flujo de datos de la cámara y del micrófono del usuario.

Compatibilidad:

WebRTC se puede habilitar en Chrome 18.0.1008 y versiones posteriores en la página about:flags.

Cómo empezar

Con navigator.getUserMedia(), al fin podemos conectarnos a la entrada del micrófono y de la cámara web sin necesidad de ningún complemento. Ahora se puede acceder a la cámara con solo realizar una llamada, sin tener que instalar nada. Los datos se procesan y se envían directamente al navegador. Emocionante, ¿verdad?

Dónde se puede habilitar

El API getUserMedia() aún es muy reciente y se incluye en algunas compilaciones para desarrolladores de Google y Opera. En Chrome 18 y versiones posteriores, el API se puede habilitar en la página about:flags.

Opción para habilitar getUserMedia() en la página about:flags de Chrome

En Opera, descarga una de las compilaciones experimentales para escritorio y para Android.

Detección de funciones

La detección de funciones es simplemente una comprobación de la existencia 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');
}

Acceso a un dispositivo de entrada

Para utilizar una cámara web o un micrófono, necesitamos solicitar permiso. El primer parámetro de getUserMedia() es para especificar el tipo de medio al que se quiere acceder. Por ejemplo, si quieres solicitar la cámara web, el primer parámetro debe ser video. Para usar tanto el micrófono como la cámara, utiliza 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>

De acuerdo. Entonces, ¿qué está pasando aquí? La captura multimedia es un ejemplo perfecto de nuevas API HTML5 en colaboración. Funciona en combinación con otros miembros de la familia HTML5: <audio> y <video>. Ten en cuenta que no se establece un atributo src ni se incluyen elementos <source> en el elemento <video>. En lugar de dar al vídeo una URL a un archivo multimedia, le estamos dando una URL Blob obtenida a partir de un objeto LocalMediaStream que representa a la cámara web.

También le estoy dando al elemento <video> la orden autoplay (reproducción automática), ya que de lo contrario se quedaría congelado en el primer fotograma. Añadir controls también funciona como cabría esperar.

Nota: un error en Chrome hace que no se pueda transmitir solo "audio": crbug.com/112367. Tampoco pude hacer que <audio> funcionara en Opera.

Tanto Opera como Chrome implementan diferentes versiones de la especificación. Esto hace que el uso práctico resulte un poco más "desafiante" de lo que será en realidad.

En Chrome:

Este fragmento funciona en Chrome 18 y en versiones posteriores (se habilita en la página about:flags):

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

En Opera:

Las compilaciones para desarrolladores de Opera funcionan con una versión actualizada de la especificación. Este fragmento funciona en Opera:

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

Las diferencias clave son:

  • getUserMedia() no tiene prefijo.
  • Un objeto se transmite como el primer argumento en lugar de como una lista de cadenas.
  • video.src se establece directamente en el objeto LocalMediaStream en lugar de en la URL Blob. Me han dicho que Opera terminará por actualizarlo para que requiera una URL Blob.

En ambos:

Si quieres algo que funcione en ambos navegadores (pero es muy delicado), prueba esto:

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.
}

No olvides consultar el gUM Shieldr de Mike Robinson y Mike Taylor. Realiza un gran trabajo a la hora de "normalizar" las inconsistencias entre las implementaciones de los navegadores.

Seguridad

En el futuro, los navegadores podrían mostrar una barra de información al ejecutar getUserMedia(), lo que daría a los usuarios la opción de otorgar o denegar el permiso a su cámara y su micrófono. Lamentablemente, la especificación es muy reservada en lo referente a seguridad. En estos momentos nadie implementa una barra de permiso.

Opción alternativa

Para aquellos usuarios que no pueden utilizar getUserMedia(), una opción es reproducir un archivo de vídeo existente si el API no es compatible o si la llamada falla por algún 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);
}

Demo básica

Capturas de pantalla

El método ctx.drawImage(video, 0, 0) del API de <canvas> hace que resulte muy fácil dibujar fotogramas de <video> en <canvas>. Por supuesto, al obtener la entrada de vídeo a través de getUserMedia(), es igual de fácil crear una aplicación tipo Photo Booth con vídeo en tiempo 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);

Aplicación de efectos

Filtros CSS

Actualmente pueden utilizar filtros CSS las últimas versiones de WebKit y Chrome 18 y versiones posteriores.

Con los filtros CSS podemos aplicar algunos efectos asombrosos en <video> mientras se está capturando el vídeo:

<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>

Haz clic en el vídeo para ver todos los filtros CSS.

Texturas WebGL

Uno de los usos más increíbles de la captura de vídeo es transferir la entrada de vídeo en directo a la textura WebGL. Como yo no sé absolutamente nada de WebGL, voy a sugerirte que le eches un vistazo al tutorial y a la demo de Jerome Etienne. Trata sobre cómo utilizar getUserMedia() y Three.js para transferir vídeo en directo a WebGL.

Uso de getUserMedia con el API de audio web

En esta sección trataremos posibles mejoras y extensiones del API en el futuro.

Uno de mis sueños es incorporar AutoTune al navegador únicamente con tecnología web de código abierto. En realidad, no estamos muy lejos de poder hacerlo. Ya tenemos getUserMedia() para la entrada de micrófono. Aplica los efectos en tiempo real del API de audio web y ya está. Lo que falta es integrar ambos (crbug.com/112404), aunque se está trabajando en una propuesta preliminar para conseguirlo.

Conectar la entrada de micrófono al API de audio web podría ser algo así algún día:

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);

Si quieres ver a getUserMedia() conectado al API de audio web, vota por crbug.com/112404.

Conclusión

En general, el acceso a dispositivos en la Web ha sido un hueso duro de roer. Muchos lo han intentado, pero pocos lo han conseguido. La mayoría de las primeras ideas nunca han prosperado fuera del entorno del creador ni se han adoptado ampliamente.

El verdadero problema es que el modelo de seguridad de Internet es muy diferente al entorno nativo. Por ejemplo, probablemente no quiero que el sitio web de cualquiera tenga acceso aleatorio a mi cámara de vídeo. Es muy difícil acertar.

Vincular marcos de trabajo como PhoneGap ha ayudado a superar barreras, pero es solo un comienzo y una solución temporal a un problema subyacente. Para que las aplicaciones web sean competitivas respecto a las aplicaciones de escritorio, necesitamos acceder a los dispositivos nativos.

getUserMedia() solo es el primer intento de acceder a nuevos tipos de dispositivos. Espero que esto siga desarrollándose en un futuro próximo.

Recursos adicionales

Demos

Comments

0