JavaScript Camera API: accede a la cámara y micrófono con getUserMedia

- Andrés Cruz - EN In english

JavaScript Camera API: accede a la cámara y micrófono con getUserMedia

Cuando descubrí por primera vez que HTML5 permitía abrir la cámara sin plugins, pensé exactamente lo que seguramente piensas tú ahora: "esto es fascinante". Poder acceder al video y al audio del usuario directamente desde JavaScript cambió radicalmente la forma en que construimos aplicaciones web: desde lectores de códigos QR hasta videollamadas o sistemas de captura de imágenes, todo esto acerca a las apps web al nivel de experiencia que antes solo ofrecían las apps nativas.

En esta guía te enseño, paso a paso, cómo acceder a la cámara y al micrófono con la JavaScript Camera API, cómo mostrar el stream en un elemento <video>, cómo capturar una foto con <canvas> y cómo resolver los errores típicos que todos hemos encontrado alguna vez.

Esta es una de las características más poderosas que trae la API de HTML5 con Canvas consigo: acceder a la cámara y al micrófono del usuario (con su previa aprobación) desde cualquier computadora o dispositivo móvil, sin necesidad de instalar ningún plugin. En este artículo aprenderemos cómo obtener ese flujo de datos (stream) y dirigirlo dentro de un elemento <video> para, finalmente, capturar imágenes a través de la cámara del usuario.

Requisitos, permisos y limitaciones: HTTPS, compatibilidad y errores comunes

Antes de escribir una sola línea de código, hay tres reglas fundamentales que debes conocer para evitar frustraciones:

  • Necesitas HTTPS (o localhost)
    • Los navegadores modernos solo permiten usar getUserMedia en contextos seguros, es decir, desde páginas servidas por https:// o desde localhost. Si abres tu archivo directamente con el protocolo file://, la API simplemente no estará disponible. La primera vez que lo intenté desde un archivo suelto me volví loco: el código era correcto, pero la cámara no aparecía por ningún lado. Esta restricción de secure context es obligatoria según el estándar del W3C y la respetan Chrome, Firefox, Safari y Edge.
  • El usuario siempre debe aprobar el permiso
    • Cuando llamas a getUserMedia(), el navegador muestra automáticamente un popup pidiendo acceso a la cámara y/o al micrófono. El usuario puede aceptar o denegar. Si deniega —o si el navegador recuerda una denegación anterior— se disparará un error que debes capturar y gestionar de forma amigable.
  • Usa siempre la API moderna
    • Hoy todo se hace con navigator.mediaDevices.getUserMedia().
      Las versiones con prefijos de vendedor como navigator.webkitGetUserMedia, navigator.mozGetUserMedia o navigator.msGetUserMedia están oficialmente obsoletas y eliminadas de los navegadores actuales. No las uses en código nuevo.

Cómo acceder a la cámara con JavaScript: la JavaScript Camera API

La base de todo es navigator.mediaDevices.getUserMedia(). Este método devuelve una Promise y recibe un objeto llamado constraints que especifica qué deseas activar: video, audio o ambos. Si el usuario aprueba el permiso, la promesa resuelve con un objeto MediaStream listo para usar.

El objeto constraints de getUserMedia

Las configuraciones típicas son las siguientes. Debes especificar al menos una en true; de lo contrario, la llamada lanzará un error de tipo TypeError:

{ video: true, audio: false } // Solo cámara (webcam)
{ video: false, audio: true } // Solo micrófono
{ video: true, audio: true }  // Cámara + micrófono

También puedes pasar configuraciones avanzadas dentro de video para controlar resolución, cámara frontal/trasera en móviles o fotogramas por segundo:

{ video: { width: 1280, height: 720, facingMode: "user" }, audio: false }

La propiedad facingMode: "user" activa la cámara frontal en dispositivos móviles, mientras que facingMode: "environment" activa la cámara trasera. Muy útil si estás construyendo una app para móvil.

A continuación se describen los tres parámetros clave de la llamada a getUserMedia:

  • constraints: objeto de configuración que indica qué flujo de datos activar. Casos posibles:
    • { video: true, audio: true }: Habilita ambos; el stream contendrá video y audio.
    • { video: true, audio: false }: Solo video.
    • { video: false, audio: true }: Solo audio/micrófono.
    • { video: false, audio: false }: Esta combinación NO es válida y el navegador lanzará un error de tipo TypeError.
      • successCallback (API antigua): función que se ejecuta si el usuario aprueba el permiso y el stream se carga correctamente. Recibe un objeto LocalMediaStream con el flujo de datos listo para asignar al elemento <video>.
      • errorCallback (opcional, API antigua): función que se ejecuta si el usuario deniega el acceso o si ocurre cualquier otro fallo al cargar el stream. Recibe uno de los siguientes códigos de error:
ErrorDescripción
PERMISSION_DENIED / NotAllowedErrorEl usuario denegó el permiso para acceder al dispositivo multimedia requerido.
NOT_SUPPORTED_ERROR / NotSupportedErrorUn constraint especificado no es soportado por el navegador o el dispositivo.
MANDATORY_UNSATISFIED_ERRORNo se encontraron fuentes multimedia del tipo especificado en los constraints.
NO_DEVICES_FOUND / NotFoundErrorNo se encontró ninguna webcam ni micrófono disponible en el sistema.

Referencia completa disponible en la documentación oficial de MDN: MediaDevices.getUserMedia().

Ejemplo completo: accediendo a la cámara y el micrófono con JavaScript

A continuación verás el ejemplo completo. La primera versión usa la API antigua con prefijos de compatibilidad (hoy obsoleta) y la segunda es la forma moderna y recomendada con Promise:

Versión legacy (solo referencia histórica, no usar en producción):

// Compatibilidad con navegadores antiguos (obsoleto)
navigator.getUserMedia = ( navigator.getUserMedia ||
                       navigator.webkitGetUserMedia ||
                       navigator.mozGetUserMedia ||
                       navigator.msGetUserMedia);

navigator.getUserMedia(

   // constraints
   {
      video: true,
      audio: false
   },

   // successCallback
   function(localMediaStream) {
      var video = document.querySelector("video");
      video.src = window.URL.createObjectURL(localMediaStream);
   },

   // errorCallback
   function(err) {
    console.log("Ocurrió el siguiente error: " + err);
   }

);

En este ejemplo, primero asignamos el constraints indicando que solo nos interesa el video:

// constraints
{
   video: true,
   audio: false
},

Si no ocurrió ningún error, asignamos el flujo de datos devuelto en el objeto LocalMediaStream directamente al elemento <video>:

// successCallback
function(localMediaStream) {
   var video = document.querySelector("video");
   video.src = window.URL.createObjectURL(localMediaStream);
},

Y por último, gestionamos cualquier error que pueda ocurrir:

// errorCallback
function(err) {
 console.log("Ocurrió el siguiente error: " + err);
}

Dos limitaciones importantes a recordar:

  • No puedes usar getUserMedia en páginas cuya URL empiece por file://. Necesitas siempre un servidor local o https://.
  • La API solicita permiso explícito al usuario antes de acceder a la cámara o al micrófono. Nunca tienes acceso silencioso.

Versión moderna con Promises (recomendada):

navigator.mediaDevices.getUserMedia({ video: true })
  .then(stream => {
    const video = document.querySelector("video");
    video.srcObject = stream;
    video.play();
  })
  .catch(error => {
    console.error("Ocurrió un error al acceder a la cámara:", error.name, error.message);
  });

Nótese el uso de video.srcObject en lugar del obsoleto window.URL.createObjectURL(). Esta es la forma estándar actual recomendada por la MDN y el W3C.

▶️ Mostrar el video en un elemento <video>

Tu HTML puede ser tan simple como esto:

<video autoplay playsinline></video>

El atributo autoplay hace que el video arranque en cuanto el stream esté disponible, y playsinline es fundamental en iOS para evitar que Safari lo abra en pantalla completa. En cuanto el usuario apruebe el permiso, el video aparecerá como por arte de magia, exactamente como en una app nativa.

️ Cómo acceder también al micrófono con JavaScript

Acceder al micrófono es exactamente igual que a la cámara: solo cambia el constraint que activas. Esto es especialmente útil si construyes grabadoras de voz, transcriptores o sistemas de reconocimiento de audio en el navegador:

navigator.mediaDevices.getUserMedia({ audio: true })
 .then(stream => {
   console.log("Micrófono listo:", stream);
 })
 .catch(error => {
   console.error("Error al acceder al micrófono:", error.name);
 });

️ Cámara y micrófono en el mismo stream

Si necesitas capturar audio y video simultáneamente —por ejemplo para una videollamada o una grabación—, basta con combinar ambos constraints en una sola llamada. El navegador solicitará permiso para los dos dispositivos a la vez:

navigator.mediaDevices.getUserMedia({
 video: true,
 audio: true
})
.then(stream => {
 const video = document.querySelector("video");
 video.srcObject = stream;
 video.play();
})
.catch(error => {
 console.error("Error al acceder a la cámara o el micrófono:", error.name);
});

Tomar fotos desde la webcam con <canvas>

Este es uno de mis usos favoritos de esta API.
Para capturar un fotograma estático desde el elemento <video> en tiempo real, solo necesitas un elemento <canvas> y el método drawImage(). El resultado es inmediato: un snapshot exacto de lo que muestra la cámara en ese instante.

️ Vincular el <canvas> con el stream de video

Primero defines la estructura HTML básica:

<video autoplay playsinline></video>
<canvas id="canvas" width="640" height="480"></canvas>
<button id="capture">Tomar foto</button>

Luego, en JavaScript, escuchas el clic del botón y dibujas el frame actual del video sobre el canvas con drawImage():

const video = document.querySelector("video");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

document.getElementById("capture").addEventListener("click", () => {
 ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
});

Cuando implementé esto en un proyecto real, ver la imagen congelada en el canvas fue como tener Photoshop integrado directamente en el navegador. El método drawImage() recibe el elemento <video> como fuente y lo "fotografía" en ese preciso instante.

Guardar o descargar la imagen capturada

Una vez que tienes la imagen en el canvas, puedes convertirla a base64 con toDataURL() y tratarla como cualquier imagen: mostrarla, subirla a un servidor o forzar su descarga:

const dataURL = canvas.toDataURL("image/png");

// Mostrar en un elemento <img>
document.getElementById("preview").src = dataURL;

// Forzar descarga
const link = document.createElement("a");
link.download = "captura.png";
link.href = dataURL;
link.click();

❌ Manejo de errores y permisos denegados en getUserMedia

Aquí se concentran los errores más comunes. En la API moderna, todos los errores llegan al bloque .catch() como un objeto de tipo DOMException con una propiedad name que identifica el tipo de fallo:

  • NotAllowedError (antes PERMISSION_DENIED)
    • Ocurre cuando el usuario rechaza el acceso o cuando el navegador ya tiene bloqueados los permisos de cámara para ese sitio. Suele pasarles a quienes pulsan "Bloquear" por accidente la primera vez. Solución: indicar al usuario cómo restablecer los permisos desde la configuración del navegador.
  • NotSupportedError (antes NOT_SUPPORTED_ERROR)
    • El constraint solicitado no existe en ese dispositivo o navegador.
    • Ejemplo clásico: pedir video: true en un equipo de escritorio sin cámara.
  • NotFoundError / NO_DEVICES_FOUND
    • No se encontró ninguna cámara ni micrófono disponible en el sistema. Ocurre también cuando el dispositivo está en uso por otra aplicación.

Un manejador de errores robusto quedaría así:

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    document.querySelector("video").srcObject = stream;
  })
  .catch(error => {
    switch (error.name) {
      case "NotAllowedError":
        alert("Permiso denegado. Autoriza el acceso a la cámara en la configuración del navegador.");
        break;
      case "NotFoundError":
        alert("No se encontró ninguna cámara en este dispositivo.");
        break;
      case "NotSupportedError":
        alert("Tu navegador o dispositivo no soporta esta funcionalidad.");
        break;
      default:
        console.error("Error inesperado:", error.name, error.message);
    }
  });

Consejos prácticos tras implementarlo en proyectos reales

  • Nunca pruebes desde file://: la API simplemente no funcionará. Usa localhost o un servidor con https://.
  • Evita las APIs con prefijos (webkitGetUserMedia, mozGetUserMedia): están obsoletas y eliminadas en los navegadores modernos.
  • Captura siempre los errores, especialmente en móviles donde la casuística de permisos es más variada.
  • Comprueba los permisos previos: usa la Permissions API —concretamente navigator.permissions.query({ name: "camera" })— para saber si el usuario ya concedió o denegó acceso antes de llamar a getUserMedia.
  • Si necesitas grabar video, añade la MediaRecorder API a tu flujo. Es el siguiente paso natural una vez dominas getUserMedia.
  • Detén siempre el stream cuando ya no lo necesites para liberar la cámara y el LED indicador: stream.getTracks().forEach(track => track.stop()).

❓ Preguntas Frecuentes sobre la JavaScript Camera API

  1. ¿Por qué no funciona mi cámara si abro el archivo HTML directamente?
    1. Porque file:// no es un contexto seguro (secure context). Debes usar un servidor local como localhost o publicar tu app bajo https://.
  2. ¿Puedo capturar fotos sin plugins ni librerías externas?
    1. Sí. Solo necesitas un elemento <video> para el stream y un <canvas> para capturar el frame. Todo con JavaScript nativo.
  3. ¿Cómo capturo también el micrófono al mismo tiempo?
    1. Incluye { audio: true } en tu objeto constraints junto a video: true. El navegador pedirá permiso para ambos dispositivos en un solo diálogo.
  4. ¿Es obligatorio usar srcObject?
    1. Sí, es el método moderno y recomendado para asignar un MediaStream a un elemento <video>. El uso de URL.createObjectURL(stream) está deprecado y eliminado en navegadores actuales.
  5. ¿Qué pasa si el usuario niega los permisos de cámara?
    1. La Promise se rechaza con un error NotAllowedError. Debes capturarlo en el bloque .catch() y mostrar un mensaje claro indicando cómo puede el usuario restablecer el permiso desde la configuración del navegador.
  6. ¿Funciona getUserMedia en Safari?
    1. Sí. Safari soporta navigator.mediaDevices.getUserMedia desde Safari 11 en macOS y desde iOS 11 en iPhone/iPad. Recuerda añadir el atributo playsinline al elemento <video> para que reproduzca correctamente en iOS sin abrirse en pantalla completa.

Conclusión

Acceder a la cámara y al micrófono con JavaScript es más sencillo de lo que parece. Con la MediaDevices.getUserMedia API moderna puedes construir desde simples capturas de foto hasta aplicaciones completas de grabación, streaming en tiempo real y videollamadas, todo sin depender de ningún plugin externo.

La clave está en dominar el objeto constraints, asignar correctamente el stream con srcObject, manejar los errores reales que devuelve la API —especialmente NotAllowedError y NotFoundError— y entender cómo funcionan los permisos de secure context en los navegadores modernos. A partir de aquí, tienes todo lo que necesitas para construir cualquier herramienta multimedia basada en el navegador.

El siguiente experimento recomendado: Cómo crear anillos de círculos en JavaScript y Canvas

Aprende a usar navigator.mediaDevices.getUserMedia en JavaScript para acceder a la cámara y el micrófono, capturar fotos con canvas y manejar errores de permiso.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.