Hacer foto/s con Javascript y cámara/s para guardarla en servidor PHP
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.
- 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.
- 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.
- 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.>