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
getUserMediaen contextos seguros, es decir, desde páginas servidas porhttps://o desdelocalhost. Si abres tu archivo directamente con el protocolofile://, 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.
- Los navegadores modernos solo permiten usar
- 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.
- Cuando llamas a
- Usa siempre la API moderna
- Hoy todo se hace con
navigator.mediaDevices.getUserMedia().
Las versiones con prefijos de vendedor comonavigator.webkitGetUserMedia,navigator.mozGetUserMediaonavigator.msGetUserMediaestán oficialmente obsoletas y eliminadas de los navegadores actuales. No las uses en código nuevo.
- Hoy todo se hace con
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ófonoTambié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 tipoTypeError.successCallback(API antigua): función que se ejecuta si el usuario aprueba el permiso y el stream se carga correctamente. Recibe un objetoLocalMediaStreamcon 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:
| Error | Descripción |
|---|---|
PERMISSION_DENIED / NotAllowedError | El usuario denegó el permiso para acceder al dispositivo multimedia requerido. |
NOT_SUPPORTED_ERROR / NotSupportedError | Un constraint especificado no es soportado por el navegador o el dispositivo. |
MANDATORY_UNSATISFIED_ERROR | No se encontraron fuentes multimedia del tipo especificado en los constraints. |
NO_DEVICES_FOUND / NotFoundError | No 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
getUserMediaen páginas cuya URL empiece porfile://. Necesitas siempre un servidor local ohttps://. - 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(antesPERMISSION_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(antesNOT_SUPPORTED_ERROR)- El
constraintsolicitado no existe en ese dispositivo o navegador. - Ejemplo clásico: pedir
video: trueen un equipo de escritorio sin cámara.
- El
-
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á. Usalocalhosto un servidor conhttps://. - 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—concretamentenavigator.permissions.query({ name: "camera" })— para saber si el usuario ya concedió o denegó acceso antes de llamar agetUserMedia. - Si necesitas grabar video, añade la
MediaRecorder APIa tu flujo. Es el siguiente paso natural una vez dominasgetUserMedia. - 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
- ¿Por qué no funciona mi cámara si abro el archivo HTML directamente?
- Porque
file://no es un contexto seguro (secure context). Debes usar un servidor local comolocalhosto publicar tu app bajohttps://.
- Porque
- ¿Puedo capturar fotos sin plugins ni librerías externas?
- Sí. Solo necesitas un elemento
<video>para el stream y un<canvas>para capturar el frame. Todo con JavaScript nativo.
- Sí. Solo necesitas un elemento
- ¿Cómo capturo también el micrófono al mismo tiempo?
- Incluye
{ audio: true }en tu objetoconstraintsjunto avideo: true. El navegador pedirá permiso para ambos dispositivos en un solo diálogo.
- Incluye
- ¿Es obligatorio usar
srcObject?- Sí, es el método moderno y recomendado para asignar un
MediaStreama un elemento<video>. El uso deURL.createObjectURL(stream)está deprecado y eliminado en navegadores actuales.
- Sí, es el método moderno y recomendado para asignar un
- ¿Qué pasa si el usuario niega los permisos de cámara?
- La
Promisese rechaza con un errorNotAllowedError. 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.
- La
- ¿Funciona
getUserMediaen Safari?- Sí. Safari soporta
navigator.mediaDevices.getUserMediadesde Safari 11 en macOS y desde iOS 11 en iPhone/iPad. Recuerda añadir el atributoplaysinlineal elemento<video>para que reproduzca correctamente en iOS sin abrirse en pantalla completa.
- Sí. Safari soporta
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