Capturar audio y video en HTML5

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 de navegador (Flash o Silverlight) para realizar el trabajo. ¡Vamos!

HTML5 al rescate. Es posible que no sea evidente, pero el auge de HTML5 generó un aumento en el acceso al hardware de los dispositivos. La ubicación geográfica (GPS), la API de Orientation (acelerómetro), WebGL (GPU) y la API de Web Audio (hardware de audio) son ejemplos perfectos. Estas funciones son ridículamente potentes y exponen APIs de JavaScript de alto nivel que se suman a las capacidades del hardware subyacente 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 manera en la que llegamos a la API de getUserMedia() es interesante.

Varias variantes de las "APIs de captura de contenido multimedia" evolucionaron en los últimos años. Muchas personas reconocieron la necesidad de poder acceder a dispositivos nativos en la Web, pero eso llevó a que todos y sus madres crearan una nueva especificación. La situación se volvió tan desordenada que el W3C finalmente decidió formar un grupo de trabajo. ¿Cuál es su único propósito? Comprende la locura. El Grupo de trabajo de la Política de APIs de dispositivos (DAP) tiene la tarea de consolidar y estandarizar la gran cantidad de propuestas.

Voy a tratar de resumir lo que sucedió en 2011...

Ronda 1: Captura de contenido multimedia HTML

La captura de medios HTML fue la primera vez que el DAP estandarizó la captura de contenido multimedia en la Web. Funciona mediante la sobrecarga de <input type="file"> y la adición de 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">

La grabación de audio o video es similar:

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

Es bastante bueno, ¿no? En particular, me gusta que reutiliza una entrada de archivo. Semánticamente, tiene mucho sentido. En lo que esta "API" en particular no es suficiente, es en la capacidad de realizar efectos en tiempo real (p. ej., renderizar datos de cámara web en vivo en un <canvas> y aplicar filtros WebGL). La Captura de contenido multimedia HTML solo te permite grabar un archivo multimedia o tomar una instantánea en un momento determinado.

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 para celulares 10.0
  • Safari y Chrome para iOS6 (compatibilidad parcial)

Ronda 2: elemento del dispositivo

Muchos pensaban que la captura de medios HTML era demasiado limitante, por lo que surgió una nueva especificación que admitía cualquier tipo de dispositivo (futuro). No es de extrañar que el diseño requiera un nuevo elemento, 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 prometedora, esta vez una API de JavaScript llamada navigator.getUserMedia(). Una semana después, Opera lanzó compilaciones nuevas que incluían compatibilidad con la especificación getUserMedia() actualizada. Más adelante ese año, Microsoft se unió a la fiesta y lanzó un Lab para IE9 que admitía la nueva especificación.

Así 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, nunca ningún navegador publicado incluyó <device>. Supongo que es una API menos por la que preocuparse :). Sin embargo, <device> tenía dos aspectos positivos: 1.) era semántica y 2.) se podía extender fácilmente para admitir más que solo dispositivos de audio y video.

Respira. ¡Esto se mueve rápido!

Ronda 3: WebRTC

El elemento <device> finalmente siguió el camino del Dodo.

El ritmo para encontrar una API de captura adecuada aceleró gracias a una iniciativa más amplia de WebRTC (Web Real Time Communications). El grupo de trabajo de WebRTC del W3C supervisa esa especificación. Google, Opera, Mozilla y algunas más tienen implementaciones.

getUserMedia() está relacionado 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 del micrófono sin un complemento. Ahora puedes acceder a la cámara con una llamada, no con una instalación. Se integra directamente en el navegador. ¿No es genial?

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

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>

De acuerdo. Entonces, ¿qué sucede aquí? La captura de contenido multimedia es un ejemplo perfecto de cómo trabajan juntas las nuevas APIs de HTML5. Funciona en conjunto 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 proporcionarle al video una URL a un archivo multimedia, configuramos srcObject en el objeto LocalMediaStream que representa la cámara web.

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

Cómo 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 muestra. Por ejemplo, en lugar de solo indicar que deseas acceso básico al video (p. ej., {video: true}), puedes exigir que la transmisión sea 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 parámetros de configuración, consulta la API de restricciones.

Cómo seleccionar una fuente 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 demostración fantástica de Sam Dutton sobre cómo permitir que los usuarios seleccionen la fuente de contenido multimedia.

Seguridad

Los navegadores muestran un diálogo de permisos cuando se llama a navigator.mediaDevices.getUserMedia(), que brinda a los usuarios la opción de otorgar o rechazar 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 una opción alternativa

Para los usuarios que no admiten 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;
}