javascript

JavaScript: tomar foto con cualquier cámara (frontal y trasera)

Introducción

Ya estamos aquí con un tercer post sobre tomar fotos con JavaScript. Este tutorial ofrece una nueva característica y es la de dar al usuario elegir cuál cámara usar para tomar la foto. En los posts anteriores únicamente tomaba de la cámara por defecto, y en algunos navegadores (Chrome, cof cof) no permite cambiar la cámara con la que se toma.

Afortunadamente eso no importa, porque podemos obtener la lista de dispositivos y cambiarlos como se nos dé la gana.

Por cierto, este post está más actualizado; usa constantes, querySelector y otras cosas que los anteriores no. Si bien esto no afecta el funcionamiento, nos da un código más bonito y entendible.

Pero basta de hablar, que hablar es de mal gusto; vamos a ver el código.

Versiones anteriores

En caso de que se me haya pasado explicar algo, puedes revisar las versiones anteriores de este post.

Tomar foto con cámara y JavaScript

Tomar foto con cámara y JavaScript v2

Compatibilidad

Compatible y probado con Edge, Firefox y Chrome en la fecha 22 de octubre del 2018. Puedes probarlo tú mismo y dejar en los comentarios si sigue funcionando (debería hacerlo por mucho tiempo).

En las siguientes imágenes he probado este código en el navegador, cubriendo obviamente mi cámara. Pruébalo tú si quieres y verás que funciona como un encanto.

 

Tomar foto con cámara trasera en Android usando Chrome
Tomar foto con JavaScript en Firefox

 

Tomar foto con JavaScript en Edge

 

Tomar foto con JavaScript en Chrome

Código fuente

Si lo deseas, puedes explorar el código de ejemplo en GitHub. Si más tarde hago actualizaciones podrás verlas directamente ahí. El repositorio está aquí.

Aquí está el código del script:

/*
    Tomar una fotografía y guardarla en un archivo v3
    @date 2018-10-22
    @author parzibyte
    @web parzibyte.me/blog
*/function tieneSoporteUserMedia() {
 return !!(navigator.getUserMedia || (navigator.mozGetUserMedia || navigator.mediaDevices.getUserMedia) || navigator.webkitGetUserMedia || navigator.msGetUserMedia)
}
function _getUserMedia() {
 return (navigator.getUserMedia || (navigator.mozGetUserMedia || navigator.mediaDevices.getUserMedia) || navigator.webkitGetUserMedia || navigator.msGetUserMedia).apply(navigator, arguments);
}
 
// Declaramos elementos del DOM
const $video = document.querySelector("#video"),
 $canvas = document.querySelector("#canvas"),
 $boton = document.querySelector("#boton"),
 $estado = document.querySelector("#estado"),
 $listaDeDispositivos = document.querySelector("#listaDeDispositivos");
 
// La función que es llamada después de que ya se dieron los permisos
// Lo que hace es llenar el select con los dispositivos obtenidos
const llenarSelectConDispositivosDisponibles = () => {
 
 navigator
  .mediaDevices
  .enumerateDevices()
  .then(function (dispositivos) {
   const dispositivosDeVideo = [];
   dispositivos.forEach(function (dispositivo) {
    const tipo = dispositivo.kind;
    if (tipo === "videoinput") {
     dispositivosDeVideo.push(dispositivo);
    }
   });
 
   // Vemos si encontramos algún dispositivo, y en caso de que si, entonces llamamos a la función
   if (dispositivosDeVideo.length > 0) {
    // Llenar el select
    dispositivosDeVideo.forEach(dispositivo => {
     const option = document.createElement('option');
     option.value = dispositivo.deviceId;
     option.text = dispositivo.label;
     $listaDeDispositivos.appendChild(option);
     console.log("$listaDeDispositivos => ", $listaDeDispositivos)
    });
   }
  });
}
 
(function () {
 // Comenzamos viendo si tiene soporte, si no, nos detenemos
 if (!tieneSoporteUserMedia()) {
  alert("Lo siento. Tu navegador no soporta esta característica");
  $estado.innerHTML = "Parece que tu navegador no soporta esta característica. Intenta actualizarlo.";
  return;
 }
 //Aquí guardaremos el stream globalmente
 let stream;
 
 
 // Comenzamos pidiendo los dispositivos
 navigator
  .mediaDevices
  .enumerateDevices()
  .then(function (dispositivos) {
   // Vamos a filtrarlos y guardar aquí los de vídeo
   const dispositivosDeVideo = [];
 
   // Recorrer y filtrar
   dispositivos.forEach(function (dispositivo) {
    const tipo = dispositivo.kind;
    if (tipo === "videoinput") {
     dispositivosDeVideo.push(dispositivo);
    }
   });
 
   // Vemos si encontramos algún dispositivo, y en caso de que si, entonces llamamos a la función
   // y le pasamos el id de dispositivo
   if (dispositivosDeVideo.length > 0) {
    // Mostrar stream con el ID del primer dispositivo, luego el usuario puede cambiar
    mostrarStream(dispositivosDeVideo[0].deviceId);
   }
  });
 
 
 
 const mostrarStream = idDeDispositivo => {
  _getUserMedia(
   {
    video: {
     // Justo aquí indicamos cuál dispositivo usar
     deviceId: idDeDispositivo,
    }
   },
   function (streamObtenido) {
    // Aquí ya tenemos permisos, ahora sí llenamos el select,
    // pues si no, no nos daría el nombre de los dispositivos
    llenarSelectConDispositivosDisponibles();
 
    // Escuchar cuando seleccionen otra opción y entonces llamar a esta función
    $listaDeDispositivos.onchange = () => {
     // Detener el stream
     if (stream) {
      stream.getTracks().forEach(function (track) {
       track.stop();
      });
     }
     // Mostrar el nuevo stream con el dispositivo seleccionado
     mostrarStream($listaDeDispositivos.value);
    }
 
    // Simple asignación
    stream = streamObtenido;
 
    // Mandamos el stream de la cámara al elemento de vídeo
    $video.srcObject = stream;
    $video.play();
 
    //Escuchar el click del botón para tomar la foto
    $boton.addEventListener("click", function () {
 
     //Pausar reproducción
     $video.pause();
 
     //Obtener contexto del canvas y dibujar sobre él
     let contexto = $canvas.getContext("2d");
     $canvas.width = $video.videoWidth;
     $canvas.height = $video.videoHeight;
     contexto.drawImage($video, 0, 0, $canvas.width, $canvas.height);
 
     let foto = $canvas.toDataURL(); //Esta es la foto, en base 64
     $estado.innerHTML = "Enviando foto. Por favor, espera...";
     fetch("./guardar_foto.php", {
      method: "POST",
      body: encodeURIComponent(foto),
      headers: {
       "Content-type": "application/x-www-form-urlencoded",
      }
     })
      .then(resultado => {
       // A los datos los decodificamos como texto plano
       return resultado.text()
      })
      .then(nombreDeLaFoto => {
       // nombreDeLaFoto trae el nombre de la imagen que le dio PHP
       console.log("La foto fue enviada correctamente");
       $estado.innerHTML = `Foto guardada con éxito. Puedes verla <a target='_blank' href='./${nombreDeLaFoto}'> aquí</a>`;
      })
 
     //Reanudar reproducción
     $video.play();
    });
   }, function (error) {
    console.log("Permiso denegado o error: ", error);
    $estado.innerHTML = "No se puede acceder a la cámara, o no diste permiso.";
   });
 }
})();

Es casi igual que el anterior, la lógica no cambia mucho.

Listar dispositivos y poder elegir cámara

Para poder listar los dispositivos llamamos a navigator.mediaDevices.enumerateDevices, lo que devuelve una lista de los mismos, que pueden ser micrófonos y otras cosas.

Si te fijas, estamos llamando dos veces a ese método, ¿por qué? bueno, primero lo llamamos para ver si hay algún dispositivo para comenzar el stream, pero para ese tiempo el usuario no ha dado permisos, por lo que este método únicamente devuelve el id de los dispositivos, más no el nombre. Si no nos da el nombre, no podremos llenar el select de una buena manera.

Una vez que el usuario da permisos, volvemos a llamar a ese método para que nos  dé los dispositivos. Como en este caso ya tenemos permisos, también devolverá el nombre de los mismos. Esto es más que nada por la seguridad.

Cuando hay más de un dispositivo, el select los muestra y al cambiarlos se usa esa cámara.

Seleccionar cámara para tomar foto

Fetch en lugar de XMLHttpRequest

XMLHttpRequest es una API que ya tiene años. Claro, es el estándar y los frameworks que usamos para hacer peticiones AJAX simplemente son una capa que nos permite interactuar con esta API.

Sin embargo, nosotros no estamos usando ningún framework, así que teníamos que usar XMLHttpRequest, pero ya no más, ahora usamos fetch (pronto escribiré sobre ello), ya que estamos explotando las nuevas características de JS.

Silenciar vídeo

En el código HTML también se hicieron cambios. Se movieron algunos elementos pero lo más importante es que se agregó el atributo muted al elemento <video> por las políticas del navegador Chrome (y tal vez todos lo hagan así). Esto no afecta en nada porque nosotros únicamente mostramos vídeo y no audio, pero igual se tiene que especificar.

El código queda así:

<!DOCTYPE html>
<html lang="es">
 
<head>
 <!--
  Tomar una fotografía y guardarla en un archivo v3
     @date 2018-10-22
     @author parzibyte
     @web parzibyte.me/blog
 -->
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
 <title>Tomar foto con Javascript y PHP v3.0</title>
 <style>
  @media only screen and (max-width: 700px) {
   video {
    max-width: 100%;
   }
  }
 </style>
</head>
 
<body>
 <h1>Tomar foto con JavaScript v3.0</h1>
 <p>
  Programado por Luis Cabrera Benito a.k.a. <a target="_blank" href="https://parzibyte.me/blog">parzibyte</a>
 </p>
 <h1>Selecciona un dispositivo</h1>
 <div>
  <select name="listaDeDispositivos" id="listaDeDispositivos"></select>
  <button id="boton">Tomar foto</button>
  <p id="estado"></p>
 </div>
 <br>
 <video muted="muted" id="video"></video>
 <canvas id="canvas" style="display: none;"></canvas>
</body>
<script src="script.js"></script>
 
</html>

Por cierto, puse unos estilos para que en móviles el vídeo ocupe el ancho de la pantalla pero no se pase más allá.

Código PHP

El código se queda intacto, pero aquí lo pego igualmente.

<?php
/*
    Tomar una fotografía y guardarla en un archivo
    @date @date 2018-10-22
    @author parzibyte
    @web parzibyte.me/blog
*/ 
$imagenCodificada = file_get_contents("php://input"); //Obtener la imagen
if(strlen($imagenCodificada) <= 0) exit("No se recibió ninguna imagen");
//La imagen traerá al inicio data:image/png;base64, cosa que debemos remover
$imagenCodificadaLimpia = str_replace("data:image/png;base64,", "", urldecode($imagenCodificada));
 
//Venía en base64 pero sólo la codificamos así para que viajara por la red, ahora la decodificamos y
//todo el contenido lo guardamos en un archivo
$imagenDecodificada = base64_decode($imagenCodificadaLimpia);
 
//Calcular un nombre único
$nombreImagenGuardada = "foto_" . uniqid() . ".png";
 
//Escribir el archivo
file_put_contents($nombreImagenGuardada, $imagenDecodificada);
 
//Terminar y regresar el nombre de la foto
exit($nombreImagenGuardada);
?>

Conclusión

El código se encuentra comentado para explicarse por sí mismo. Si tienes una duda, comentario o sugerencia puedes usar los comentarios.

Te invito a explorar el código en GitHub, recuerda que dejé el link arriba.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

  • Excelente !!!!Muchas gracias mi estimado amigo, le agradezco por esta gran aporte en el desarrollo web!!!

  • Muy bueno, ya me funciona bien para capturar cámara y guardar, pero al usarlo en celular, a pesar que selecciona ambas cámaras, frontal y facing. No me funciona tomar foto con cámara frontal, solo funciona con cámara de selfies. Hay alguna linea de codigo para que funcionen ambas cámaras con celular?

  • hola tengo una consulta el escrp funciona muy bien pero cuando lo trabajo de forma local pero cuando lo subo y lo pruebo ya en linea me da un error que es el siguiente:

    Uncaught TypeError: Cannot read property 'getUserMedia' of undefined
    at tieneSoporteUserMedia (script.js:8)
    at script.js:58
    at script.js:163

    pense que solo era en un navegador pero es en todos y en los dispositivos que puede ser me puedes ayudar.

  • Como puedo hacer para que me muestre o se recorte la imagen de la camara, para que se vea apaisado, como si fuera una foto para un id, la version 3 funciona a la perfeccion, pero me gustaria modificarlo para hacer fotos para id

  • Hola buen día, super bueno tu aporte, pero no me funciona al momento de ver la imagen tomada y al revisar en consola me sale un error 405 (Method Not Allowed), te pasó alguna vez? Saludos

  • Buenas tardes, esta muy bueno el codigo. lo descargue y lo estoy probando intacto como lo tienes en mi servidor. abre la pagina y apatece el boton tomar foto, pero el resto de cosas no aparece. me pide el permiso de usar la camara y se lo doy y no pasa nada. no sale el cuadro donde se ve la camara ni nada. que falto por hacer ?

  • Los mensajes son de éxito en la subida de la imagen pero no se guarda la imagen el server y me gustaría saber como guardar la imagen con un texto que provenga de una caja de texto.

    Gracias por tu ayuda

    Saludos

  • Hola, seria posible que en lugar de escuchar el click del botón "Tomar foto" se guardara la foto si se hace click en la camara? (en caso de utilizar una camara conectada que disponga de boton para tomar la foto)

    • Hola. Por el momento no cuento con una cámara para probar, puede probar usted mismo y comentar su resultado
      Saludos :)

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

1 semana hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 semanas hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 semanas hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 semanas hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 semanas hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 semanas hace

Esta web usa cookies.