Capturar audio y video en HTML5

Introducción

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

HTML5 al rescate. Puede que no sea evidente, pero el auge del HTML5 trajo consigo un aumento en el acceso al hardware del dispositivo. Geolocation (GPS), la API de orientación (acelerómetro), WebGL (GPU) y la API de Web Audio (hardware de audio) son ejemplos perfectos. Estas funciones son increíblemente potentes y exponen las APIs de JavaScript de alto nivel que se apoyan en 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 una historia interesante.

En los últimos años, evolucionaron varias variantes de las "APIs de captura de contenido multimedia". Muchas personas reconocieron la necesidad de tener acceso a dispositivos nativos en la Web, pero eso llevó a todos y a su madre a crear una nueva especificación. Todo se compuso tanto 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 Política de APIs de dispositivos (DAP) tiene la tarea de consolidar y estandarizar la gran cantidad de propuestas.

Probaré lo que pasó en 2011...

Ronda 1: Captura de medios HTML

La captura de medios HTML fue el primer intento del DAP en estandarizar la captura de medios en la Web. Funciona sobrecargando <input type="file"> y agregando valores nuevos para el parámetro accept.

Si quieres permitir que los usuarios tomen una instantánea de sí mismos con la cámara web, puedes hacerlo con capture=camera:

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

Grabar un video o audio es similar:

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

Muy lindo, ¿verdad? En particular, me gusta que reutiliza la entrada de un archivo. Semánticamente, tiene mucho sentido. Cuando esta "API" en particular falla, es la capacidad de generar 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 multimedia HTML solo te permite grabar un archivo multimedia o tomar una instantánea a tiempo.

Asistencia:

  • Navegador 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 iOS 6 (compatibilidad parcial)

Ronda 2: elemento de dispositivo

Muchos creían que la captura de medios HTML era demasiado limitante, por lo que surgió una nueva especificación que era compatible con cualquier tipo de dispositivo (en el 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 basada en el elemento <device>. Poco después (el mismo día, para ser precisos), WhatWG decidió descartar la etiqueta <device> en favor de otra persona interesada, 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 actualizada de getUserMedia(). Más tarde ese año, Microsoft se unió a la fiesta con el lanzamiento de un Lab para IE9 que respalda la especificación nueva.

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, ningún navegador lanzado incluyó <device>. Supongo que una API menos de la que preocuparse :). Sin embargo, <device> tenía dos excelentes características: 1.) era semántica y 2.) se podía extender con facilidad para admitir más que solo dispositivos de audio y video.

Respira con calma. ¡Esto va rápido!

Ronda 3: WebRTC

Finalmente, el elemento <device> siguió el camino del Dodo.

El ritmo para encontrar una API de captura adecuada se aceleró gracias a un esfuerzo mayor de WebRTC (Web Real Time Communications). El grupo de trabajo de WebRTC de W3C supervisa esa especificación. Google, Opera, Mozilla y algunos otros 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 del micrófono o la cámara del usuario.

Asistencia:

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

Primeros pasos

Con navigator.mediaDevices.getUserMedia(), finalmente podemos aprovechar la entrada de la cámara web y el micrófono sin un complemento. Ahora puedes acceder a la cámara con solo una llamada, no una instalación. Se preparan directamente en el navegador. ¿Emocionado?

Detección de funciones

La detección de características 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");
}

Obtén 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 requisitos para cada tipo de contenido multimedia al que deseas acceder. Por ejemplo, si deseas 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>

Bien. Entonces, ¿qué sucede aquí? La captura de contenido multimedia es un ejemplo perfecto de cómo las nuevas APIs de HTML5 funcionan en conjunto. Funciona en conjunto con nuestros otros compañeros 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 de un archivo multimedia, configuraremos srcObject en el objeto LocalMediaStream que representa la cámara web.

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

Cómo configurar restricciones de contenido multimedia (resolución, altura, ancho)

El primer parámetro de getUserMedia() también se puede usar para especificar más requisitos (o restricciones) en la transmisión multimedia que se muestra. Por ejemplo, en lugar de solo indicar que quieres tener 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 opciones 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 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);
}

Mira esta excelente demostración de Sam Dutton para permitir que los usuarios seleccionen la fuente multimedia.

Seguridad

Los navegadores muestran un diálogo de permiso cuando llaman a navigator.mediaDevices.getUserMedia(), 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 permiso de Chrome:

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

Cómo proporcionar resguardo

En el caso de 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 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;
}