javascript

Tomar foto con Javascript y cámara para guardarla en servidor PHP

Nota: ya hay una versión 3 de este código. En ese nuevo post explico cómo dar la posibilidad de que el usuario cambie la cámara, además de que introduzco otras mejoras y actualizaciones. Míralo aquí.

Nunca imaginé que algún día se podría tomar una foto y guardarla en un servidor usando código nativo de Javascript y la cámara del dispositivo. Eso abre un mundo de posibilidades que permite a nuestras aplicaciones tener más características.

Hoy mostraré aquí un pequeño tutorial que nos permitirá tomar una simple foto y subirla a un servidor que tendrá PHP. No se usará ningún framework, ni de Javascript ni de PHP.

Nota: debido a que vamos a tomar una foto con la cámara, debemos servir nuestra app en localhost (para hacer pruebas locales) o en un servidor con https. Es decir, nuestro código debe estar en un servidor con un certificado SSL, o corriendo en nuestra máquina.

Si te gusta programar en Python, te invito a leer cómo tomar una foto de la cámara web, utilizando Python.

Probar proyecto terminado

Descargar

También puedes descargar el código de GitHub (https://github.com/parzibyte/fotos_js), extraerlo y pegarlo en la raíz de tu servidor local. Si usas XAMPP (en este tutorial explico cómo configurarlo), este podría ser C:\xampp\htdocs.

Programando la parte del cliente

Consiguiendo permiso

Para acceder a la cámara del dispositivo es necesario tener permiso del usuario (si no, imagina: cualquier usuario tendría acceso para tomarte fotografías y vídeos con sólo visitar una página web).

Antes de pedir acceso, debemos comprobar si el navegador del usuario tiene soporte. Como no sabemos cuál es el que usa, y como tampoco podemos imaginar un escenario perfecto en donde todos usen Chrome, debemos considerar todas las posibilidades.

El siguiente código comprueba si existe al menos una de las funciones. Primero prueba con la que debería ser de Chrome, después con Mozilla y así. Si no encuentra ninguna, entonces regresa false.

function tieneSoporteUserMedia(){
 return !!(navigator.getUserMedia || (navigator.mozGetUserMedia ||  navigator.mediaDevices.getUserMedia) || navigator.webkitGetUserMedia || navigator.msGetUserMedia)
}

Una vez que tenemos la función para verificar, debemos crear otra función que envuelva a todas las posibles opciones. Es decir, que devuelva la función para pedir permiso independientemente del navegador. Para ello, podemos crear una de la siguiente forma:

function _getUserMedia(){
 return (navigator.getUserMedia || (navigator.mozGetUserMedia ||  navigator.mediaDevices.getUserMedia) || navigator.webkitGetUserMedia || navigator.msGetUserMedia).apply(navigator, arguments);
}

Con todo esto, finalmente podremos pedir permiso sólo si el navegador tiene soporte. Es buen momento para describir los argumentos que toma la función getUserMedia.

  1. Restricciones: aquí pedimos lo que queremos obtener. Debemos indicar si queremos vídeo y audio con una variable booleana. En este caso sólo pondré video a true, porque el audio no es necesario.
  2. Función en caso de aprobación: si el usuario da permiso, se llamará a esta función que traerá el stream como parámetro.
  3. Función en caso de rechazo: si el usuario deniega el servicio o existe algún error, se llamará esta función que traerá el error como parámetro.

Una vez explicado esto, ahora sí podemos pedir permiso de la siguiente manera:

if (tieneSoporteUserMedia()) {
    _getUserMedia(
        {video: true},
        function (stream) {
            console.log("Permiso concedido");
        }, function (error) {
            console.log("Permiso denegado o error: ", error);
        });
} else {
    alert("Lo siento. Tu navegador no soporta esta característica");
}

Nota: por si no lo sabes, para abrir la consola y ver los mensajes puedes presionar F12 en Chrome, Mozilla y Edge. Y Ctrl + Shift + I en Opera.

Mostrando stream

Ya tenemos el stream, pero no lo estamos mostrando. Se supone que debemos mostrar al usuario lo que su cámara ve, y cuando él quiera, tomar la foto. Así que vamos a definir un elemento video:

<video id="video"></video>

En el código haremos referencia a él y mostraremos el stream:

var $video = document.getElementById("video");
$video.src = window.URL.createObjectURL(stream);
$video.play();

Con esto, mostraremos al usuario lo que su cámara ve.

Agregando botón y canvas para tomar foto

Abajo del vídeo agregaremos un botón. También hará falta un elemento canvas para pintar ahí la foto tomada. Ahora que lo pienso, necesitamos un elemento para mostrarle al usuario el estado; es decir, si la foto ya se guardó, si se está enviando, etcétera. Para ello pondré un párrafo vacío.

El HTML quedaría así:

<video id="video"></video>
<br>
<button id="boton">Tomar foto</button>
<p id="estado"></p>
<canvas id="canvas" style="display: none;"></canvas>

Notar por favor que el canvas está oculto, porque no sería necesario mostrarlo. Simplemente lo usaremos para poner ahí la imagen.

El código que define a los elementos está hasta ahora así:

var $video = document.getElementById("video"),
$canvas = document.getElementById("canvas"),
$boton = document.getElementById("boton"),
$estado = document.getElementById("estado");

Ahora es momento de escuchar el click del botón y al hacerlo tomar la foto. El código quedaría así:

$boton.addEventListener("click", function(){
 
    //Pausar reproducción
    $video.pause();
 
    //Obtener contexto del canvas y dibujar sobre él
    var contexto = $canvas.getContext("2d");
    $canvas.width = $video.videoWidth;
    $canvas.height = $video.videoHeight;
    contexto.drawImage($video, 0, 0, $canvas.width, $canvas.height);
 
    var foto = $canvas.toDataURL(); //Esta es la foto, en base 64
    $estado.innerHTML = "Enviando foto. Por favor, espera...";
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "./guardar_foto.php", true);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhr.send(encodeURIComponent(foto)); //Codificar y enviar
 
    xhr.onreadystatechange = function() {
        if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
            console.log("La foto fue enviada correctamente");
            $estado.innerHTML = "Foto guardada con éxito. Puedes verla <a target='_blank' href='./" + xhr.responseText + "'> aquí</a>";
        }
    }
 
    //Reanudar reproducción
    $video.play();
});

De esta forma hacemos una petición POST usando AJAX al servidor. Enviamos la foto que estaba en el canvas en base64. Se supone que el servidor nos debería responder con el nombre de la imagen guardada.

Recibirla y guardarla en el servidor es otra historia que veremos abajo. Hasta aquí termina la programación del lado del cliente.

Importante: Actualización abril 2018

Chrome genera una advertencia indicando que URL.createObjectURL es obsoleto, así que tenemos que actualizarnos.

Básicamente es cambiar esta línea:

$video.src = window.URL.createObjectURL(stream);

Por esto:

$video.srcObject = stream;

Más información aquí: Tomar foto de cámara web con Javascript, actualizado

Programando servidor

Ahora es el turno de PHP. Vamos a recibir la imagen, decodificarla y guardarla. Algo muy importante para tomar en cuenta es que al obtenerla desde el canvas, se agrega “data:image/png;base64,” al inicio de la cadena. Si la guardamos así, no será una imagen válida; tenemos que remover esa parte.

Recordemos también que la imagen se codifica para que viaje sin alterarse, y sin tener su forma original.

Dicho esto, aquí está el código PHP:

<?php
/*
    Tomar una fotografía y guardarla en un archivo
    @date 2017-11-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);
?>

Los datos enviados por ajax los obtenemos leyendo el stream de php://input. Comprobamos si se mandó algo y si es que sí, lo recuperamos.

Eso lo decodificamos y limpiamos para finalmente hacer uso de file_put_contents para escribir la imagen en un fichero. uniqid es usado para generar, como su nombre lo dice, una cadena única. Así, aunque se envíen muchas imágenes todas tendrán un nombre distinto.

Al terminar, salimos y mostramos los bytes que se escribieron. No es necesario capturar esa respuesta

¿Y en dónde veo las fotos que he tomado? muy fácil, si vamos al directorio en donde reside el archivo php, las imágenes estarán ahí mismo. Por ejemplo, mi carpeta luce así:

Si lo descargas y pruebas tú mismo, al tomar la foto saldrá un enlace para que la veas.

Referencias

Algunas de las cosas que aparecen en este post no las sabía, y las aprendí de algunos sitios. Aquí dejo los enlaces:

MDN – Navigator.getUserMedia

Ajax sin jQuery

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

  • Hola me parece extraordinario tu articulo y lo estoy implementando en una pagina, pero tengo duda en registrar el nombre de la foto en una BD, siguiendo tus ejemplos y tu comentario a, tengo en el index.php una variable $cuenta, esa variable qiero que sea el nombre de la foto como $noFot=$cuenta.uniqid() . ".png" y a ese nombre $noFot grabarlo en un campo de la BD, como puedo "acarrear" la variable $cuenta a script.js y a guardar_foto.php para que se registre a la BD? de antemano gracias por tu ayuda, saludos y gracias

  • Te agradezco mucho, muy interesante tu script para tomar fotos, si fuera posible, quisiera saber que puedo hacer para que funcione en un navegador Safari, debido a que, en IPHONE e IPAD no se activa la función de vídeo, es decir, sólo funciona en Chrome y en EDGE ¿debo comentar alguna parte del código Javascript o que puedo hacer? Gracias

    • Solo debe dejar de usar Safari y usar un navegador compatible, como Google Chrome. De otro modo no es posible
      Saludos!

  • probé tu código y funciona de maravilla, pero al querer usarlo en un ipad no me muestra ninguna cámara, ni me pide permisos de activar cámara, que puede ser? sera que hay que modificar algo? gracias

  • como podría guardar el nombre de la imagen y guardar la imagen en una carpeta. espero me puedas ayudar.

    • Hola, cuando se invoque a file_put_contents puede cambiar tanto el nombre como la ruta.
      Saludos :)

    • Hola. Aunque suene un poco obvio, usted debe convertir la imagen PNG a JPG con cualquier librería que le parezca, por ejemplo, ImageMagick
      Saludos :)

  • Gracias por compartir tu conocimiento. Quisiera saber si hay manera de mejorar la calidad de las fotos ya que al hacer zoom se pixelea demasiado.
    Saludos

    • Hola de nuevo, cuando tomo fotos con celular guarda dos veces la foto. ¿A qué se debe? probé con celular de un solo lente.

    • Me parece que se puede especificar la resolución de la foto o cámara con algunas constraints al invocar a getUserMedia, sería cuestión de investigar y probar.
      Saludos

  • Hola me interesa saber como se guarda la imagen con un nombre que sea ingresado desde una caja de texto. Agradezco tu ayuda.

  • hola , muy buen código , lo probé en local y bien , pero cuando lo subí al servidor , este no funciona , no encuentra los dispositivos de cámara para ser utilizados.

  • Muy buen código, gracias por compartirlo!
    Sólo una pregunta: ¿Sería posible tomar varias fotos sin subirlas y mediante un botón de 'Aceptar' se suban todas a la vez?

    • Me parece que es posible, podrías crear un FormData y agregarle cada foto, para enviarlo después cuando se haga click en el botón.
      Saludos.

  • Un excelente programa pero tengo un problema debo de generar un submit desde el .js para que me cargue el realize varios procedimientos pero no me funciona me podrias el nombre del formulario y .submit(); y no me funciona gracias por tu ayuda

Entradas recientes

Creador de credenciales web – Aplicación gratuita

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

5 días 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.