Javascript

Hacer foto/s con Javascript y cámara/s para guardarla en servidor PHP

07/11/2020

Introducción

A medida que voy desarrollando nuevos proyectos, empiezan a complicarse de alguna manera. Hoy os traigo la última novedad que me han pedido: hacer una foto y guardarla en un servidor usando código nativo de Javascript y la cámara o cámaras del dispositivo. Eso abre un mundo de posibilidades que permite a nuestras aplicaciones tener más características únicas y sobretodo para cualquier dispositivo (ordenador, tablet, TV, smartphone, etc.).

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

Nota importante: debido a que vamos a hacer una foto con la cámara, debemos servir a 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 en nuestra ordenador.

Probar proyecto terminado

Si quieres ver el resultado en vivo, sin descargar nada, pues hacer click aquí.

Nota: no me hago responsable de las fotos que hagas. Es posible que no se borren al instante y pueden quedar en el servidor un periodo de tiempo.

Descargar proyecto

También puedes descargar este archivo, extraerlo y pegarlo en la raíz de tu servidor local. O descargar el que se adjunta en este post del blog.

Programado la parte del cliente

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.

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.

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. Reestriccioes: 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.

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 imagen

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

Agregando botón y canvas para tomar foto

Debajo del vídeo agregaremos un botón. También hará falta un elemento canvas para visualizar la cámara en tiempo real y un elemento para mostrarle al usuario el estado; es decir, si la foto ya se guardó, si se está enviando, etc.

Y finalmete, pondremos un botón para hacer la foto.

Código lado de cliente

HTML:






Javascript:

		/*
			Hacer una fotografia/s y guardarla/s en un archivo
			@date 2020-11-07
			@autor Alejandro Ruiz Lameiro
			@website https://www.alexruiz.eu/
		*/
		
		const init = () => {
        const tieneSoporteUserMedia = () =>
            !!(navigator.mediaDevices.getUserMedia)
    
        // Si no soporta...
        // Amable aviso para que el mundo comience a usar navegadores decentes ;)
        if (typeof MediaRecorder === "undefined" || !tieneSoporteUserMedia())
            return alert("Tu navegador web no cumple los requisitos; por favor, actualiza a un navegador como Firefox o Google Chrome");
    	
		
		function _getUserMedia() {
			return (navigator.getUserMedia || (navigator.mozGetUserMedia || navigator.mediaDevices.getUserMedia) || navigator.webkitGetUserMedia || navigator.msGetUserMedia).apply(navigator, arguments);
		}

    
        // Declaración de elementos del DOM
        const $dispositivosDeVideo = document.querySelector("#dispositivosDeVideo"),
			$canvas = document.querySelector("#canvas"),
            $duracion = document.querySelector("#duracion"),
            $video = document.querySelector("#video"),
            $btnDetenerGrabacion = document.querySelector("#btnDetenerGrabacion");
    
        // Algunas funciones útiles
        const limpiarSelect = elemento => {
            for (let x = elemento.options.length - 1; x >= 0; x--) {
                elemento.options.remove(x);
            }
        }
        // Variables "globales"
        let tiempoInicio, mediaRecorder, idIntervalo;
    
        // Consulta la lista de dispositivos de entrada de audio y llena el select
        const llenarLista = () => {
            navigator
                .mediaDevices
                .enumerateDevices()
                .then(dispositivos => {
                    limpiarSelect($dispositivosDeVideo);
                    dispositivos.forEach((dispositivo, indice) => {
                        if (dispositivo.kind === "videoinput") {
                            const $opcion = document.createElement("option");
                            // Firefox no trae nada con label, que viva la privacidad
                            // y que muera la compatibilidad
                            $opcion.text = dispositivo.label || `Cámara ${indice + 1}`;
                            $opcion.value = dispositivo.deviceId;
                            $dispositivosDeVideo.appendChild($opcion);
                        }
                    })
                })
        };
		
		
		$dispositivosDeVideo.addEventListener("change", function(){
			$video.pause();
			mediaRecorder.stop();
            mediaRecorder = null;
			inicializeVideo();
			
		});
    
        // Comienza a grabar el audio con el dispositivo seleccionado
		function inicializeVideo()
		{
			 _getUserMedia({
				video: true
			},
			function(stream) {
				if (!$dispositivosDeVideo.options.length) return alert("No hay cámara");
		
				navigator.mediaDevices.getUserMedia({
						video: {
							deviceId: $dispositivosDeVideo.value, // Indicar dispositivo de vídeo
						}
					})
					.then(stream => {
						// Poner stream en vídeo
						$video.srcObject = stream;
						$video.play();
						// Comenzar a grabar con el stream
						mediaRecorder = new MediaRecorder(stream);
						mediaRecorder.start();
						
						$btnDetenerGrabacion.addEventListener("click", function() {
	
							//Pausar reproducción
							$video.pause();
							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
							$duracion.innerHTML = "Enviando foto. Por favor, espera...";
							
							fetch("./foto_render.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");
									$duracion.innerHTML = `Foto guardada con éxito. Puedes verla  aquí`;
								})
		 
							//Reanudar reproducción
							$video.play();
							
						});
					})
					.catch(error => {
						// Aquí maneja el error, tal vez no dieron permiso
						console.log(error)
					});
			},
			function(error) {
				console.log("Permiso denegado o error: ", error);
				$estado.innerHTML = "No se puede acceder a la cámara, o no diste permiso.";
			});
		}
    
    
        /*const detenerConteo = () => {
            clearInterval(idIntervalo);
            tiempoInicio = null;
            $duracion.textContent = "";
        }
    
        const detenerGrabacion = () => {
            if (!mediaRecorder) return alert("No se está reproduciendo");
            mediaRecorder.stop();
            mediaRecorder = null;
        };*/
    
        $btnDetenerGrabacion.addEventListener("click", function(){
			detenerGrabacion();
			$video.pause();
			mediaRecorder.stop();
            mediaRecorder = null;
			inicializeVideo();
		});
    
        // Cuando ya hemos configurado lo necesario allá arriba llenamos la lista
    
        llenarLista();
		inicializeVideo();
    }
    
    // Esperar a que el documento está listo...
    document.addEventListener("DOMContentLoaded", init);

Programando lado del servidor

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

Recordemos también que la imagen se codifica para que el traspaso de datos no se aletere.

Código lado del servidor

PHP:

$imagenCodificada = file_get_contents("php://input"); //Obtener la imagen
if(strlen($imagenCodificada) <= 0)
{
	exit("No se ha recibido 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&oaccute;lo la codificamos así para que viaje por la red, ahora la decodificamos y
//todo el contenido lo guardamos en un archivo
$imagenDecodificada = base64_decode($imagenCodificadaLimpia);

//Calcular un nombre único
$nuevoNombre = "foto_" . uniqid() . ".png";
$nombreImagenGuardada  = $nuevoNombre;

//Escribir el archivo
file_put_contents($nombreImagenGuardada, $imagenDecodificada);

//Terminar y devolver 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

Conclusiones

Parece un código complicado, pero es fácil de seguir. Se ha tenido en cuenta el comportamiento de un smartphone, para que se arranque la cámara principal y se puedan capturar fotos indefinidamente.

Es importante destacar que, en este código podemos usar las cámaras disponibles en nuestro dispositivo, poudiendo cambiarlas al seleccionar el desplegable.

Referencias

  1. Parzibytes blog
  2. MDN - Navigator.getUserMedia
  3. Ajax sin jQuery

Más artículos