Capturar audio y video en HTML5

Introducción

La captura de audio y video ha sido el "Santo Grial" del desarrollo web durante mucho tiempo. Durante muchos años, tuvimos que depender de complementos del navegador (Flash o Silverlight) para realizar el trabajo. ¡Vamos!

HTML5 al rescate. Tal vez no sea evidente, pero el auge de HTML5 trajo consigo un aumento en el acceso al hardware de los dispositivos. La geolocalización (GPS), la API de Orientation (acelerómetro), WebGL (GPU) y la API de Web Audio (hardware de audio) son ejemplos perfectos. Estas funciones son increíblemente potentes, ya que exponen APIs de JavaScript de alto nivel que se encuentran sobre las capacidades de hardware subyacentes del sistema.

En este instructivo, se presenta una nueva API, GetUserMedia, que permite que las apps web accedan a la cámara y al micrófono de un usuario.

El camino hacia getUserMedia()

Si no conoces su historia, la forma en que llegamos a la API de getUserMedia() es un relato interesante.

En los últimos años, evolucionaron varias variantes de las "APIs de Media Capture". Muchas personas reconocieron la necesidad de poder acceder a dispositivos nativos en la Web, pero eso llevó a que todo el mundo creara una especificación nueva. Las cosas se volvieron tan caóticas que el W3C finalmente decidió formar un grupo de trabajo. ¿Su único propósito? ¡Interpreta la locura! Se asignó al grupo de trabajo de la política de APIs de dispositivos (DAP) la tarea de consolidar y estandarizar la gran cantidad de propuestas.

Intentaré resumir lo que sucedió en 2011…

Ronda 1: Captura de contenido multimedia HTML

La captura de contenido multimedia HTML fue el primer intento del DAP de estandarizar la captura de contenido multimedia en la Web. Para ello, sobrecarga <input type="file"> y agrega valores nuevos para el parámetro accept.

Si quieres permitir que los usuarios se tomen una foto con la cámara web, puedes hacerlo con capture=camera:

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

Grabar un video o un audio es similar:

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

Bastante bien, ¿no? Me gusta especialmente 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 realizar efectos en tiempo real (p.ej., renderizar datos de la cámara web en vivo en un <canvas> y aplicar filtros de WebGL). La captura de contenido multimedia HTML solo te permite grabar un archivo multimedia o tomar una instantánea en el momento.

Asistencia:

  • Navegador de Android 3.0: una de las primeras implementaciones. Mira este video para ver cómo funciona.
  • Chrome para Android (0.16)
  • Firefox Mobile 10.0
  • Safari y Chrome para iOS6 (compatibilidad parcial)

Ronda 2: Elemento del dispositivo

Muchos pensaron que la captura de contenido multimedia HTML era demasiado limitada, por lo que surgió una nueva especificación que admitía cualquier tipo de dispositivo (futuro). Como era de esperar, el diseño requería un elemento nuevo, el elemento <device>, que se convirtió en el predecesor de getUserMedia().

Opera fue uno de los primeros navegadores en crear implementaciones iniciales de captura de video basadas en el elemento <device>. Poco después (el mismo día, para ser precisos), WhatWG decidió descartar la etiqueta <device> en favor de otra que estaba en auge, esta vez una API de JavaScript llamada navigator.getUserMedia(). Una semana después, Opera lanzó nuevas compilaciones que incluían compatibilidad con la especificación getUserMedia() actualizada. Más adelante ese año, Microsoft se unió a la fiesta con el lanzamiento de un Lab para IE9 compatible con la nueva especificación.

Así es como se vería <device>:

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

Asistencia:

Lamentablemente, ningún navegador lanzado incluyó <device>. Supongo que es una API menos de la que preocuparse :) Sin embargo, <device> tenía dos grandes ventajas: 1) era semántica y 2) se podía extender fácilmente para admitir más que solo dispositivos de audio y video.

No te compliques. ¡Todo avanza muy rápido!

Ronda 3: WebRTC

El elemento <device> finalmente se extinguió.

El ritmo para encontrar una API de captura adecuada se aceleró gracias al mayor esfuerzo de WebRTC (Web Real Time Communications). Esa especificación la supervisa el grupo de trabajo de WebRTC del W3C. Google, Opera, Mozilla y algunos otros tienen implementaciones.

getUserMedia() se relaciona con WebRTC porque es la puerta de enlace a ese conjunto de APIs. Proporciona los medios para acceder a la transmisión local de la cámara o el micrófono del usuario.

Asistencia:

getUserMedia() es compatible desde Chrome 21, Opera 18 y Firefox 17.

Cómo comenzar

Con navigator.mediaDevices.getUserMedia(), por fin podemos acceder a la entrada de la cámara web y el micrófono sin un complemento. Ahora, el acceso a la cámara está a una llamada de distancia, no a una instalación. Está integrada directamente en el navegador. ¿Te entusiasma?

Detección de atributos

La detección de funciones es una verificación simple de la existencia de navigator.mediaDevices.getUserMedia:

if (navigator.mediaDevices?.getUserMedia) {
  // Good to go!
} else {
  alert("navigator.mediaDevices.getUserMedia() is not supported");
}

Cómo obtener acceso a un dispositivo de entrada

Para usar la cámara web o el micrófono, debemos solicitar permiso. El primer parámetro de navigator.mediaDevices.getUserMedia() es un objeto que especifica los detalles y los requisitos de cada tipo de contenido multimedia al que deseas acceder. Por ejemplo, si quieres acceder a la cámara web, el primer parámetro debe ser {video: true}. Para usar el micrófono y la cámara, pasa {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>

Muy bien. ¿Qué está pasando aquí? La captura de contenido multimedia es un ejemplo perfecto de las nuevas APIs de HTML5 que funcionan juntas. Funciona junto con nuestros otros amigos de HTML5, <audio> y <video>. Ten en cuenta que no estamos configurando un atributo src ni incluyendo elementos <source> en el elemento <video>. En lugar de proporcionar al video una URL a un archivo multimedia, configuramos srcObject en el objeto LocalMediaStream que representa la cámara web.

También le indico a <video> que use autoplay; de lo contrario, se congelaría en el primer fotograma. Agregar controls también funciona como se espera.

Establecer restricciones de contenido multimedia (resolución, altura y ancho)

El primer parámetro de getUserMedia() también se puede usar para especificar más requisitos (o restricciones) en la transmisión de contenido multimedia que se devuelve. Por ejemplo, en lugar de solo indicar que quieres acceso básico al video (p.ej., {video: true}), puedes solicitar que la transmisión sea en 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 obtener más información sobre las opciones de configuración, consulta la API de restricciones.

Selecciona una fuente de contenido multimedia

El método enumerateDevices() de la interfaz MediaDevices solicita una lista de los dispositivos de entrada y salida de contenido multimedia disponibles, como micrófonos, cámaras, auriculares, etcétera. La promesa que se muestra se resuelve con un array de objetos MediaDeviceInfo que describen los dispositivos.

En este ejemplo, el último micrófono y la última cámara que se encuentran se seleccionan como la fuente de transmisión de contenido multimedia:

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

Consulta la excelente demostración de Sam Dutton sobre cómo permitir que los usuarios seleccionen la fuente de contenido multimedia.

Seguridad

Los navegadores muestran un diálogo de permiso cuando se llama a navigator.mediaDevices.getUserMedia(), lo que les da a los usuarios la opción de otorgar o denegar el acceso a su cámara o micrófono. Por ejemplo, este es el diálogo de permisos de Chrome:

Diálogo de permisos en Chrome
Diálogo de permisos en Chrome

Proporciona un resguardo

Para los usuarios que no tienen compatibilidad con navigator.mediaDevices.getUserMedia(), una opción es recurrir a un archivo de video existente si la API no es compatible o si la llamada falla por algún motivo:

if (!navigator.mediaDevices?.getUserMedia) {
  video.src = "fallbackvideo.webm";
} else {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  video.srcObject = stream;
}