A través del tiempo hemos visto cómo tomar fotos con JavaScript (así como enviarlas a un servidor con PHP) y cómo grabar el audio del micrófono (o guardarlo en un servidor).
En este post vamos a ver la “unión” de esas operaciones para grabar un vídeo con JavaScript.
El vídeo que vamos a grabar será tomado de la cámara web en caso de que sea una portátil, o de la cámara de un teléfono o tableta.
También podremos capturar vídeo de cualquier otra cámara, al igual que el audio. Por ejemplo, en los móviles vamos a elegir entre la cámara trasera o la frontal.
Lo que haremos será ver cómo acceder a la cámara y al micrófono, grabar el audio y el vídeo para descargar finalmente el vídeo que grabamos.
Aplicación final y código fuente completo
En el post voy a explicar los puntos más importantes del código, pero una demostración que puedes probar dentro del navegador se encuentra en este enlace. Por otro lado, el código fuente completo está en mi perfil de GitHub.
Ah, dentro del código utilizo funciones flecha y plantillas de cadena. No es la gran cosa pero te recomiendo leer sobre las mismas, harán que aprendas JavaScript moderno y no te llevará mucho tiempo entenderlas.
Permisos y limitaciones
Para acceder a la cámara y al micrófono para grabar el vídeo se necesita servir la app web en localhost o en un servidor con https.
No todos los navegadores son compatibles con MediaRecorder o getUserMedia, por eso es que existe esta función para comprobar si tiene permiso:
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 decente como Firefox o Google Chrome");
Obtener lista de micrófonos y cámaras web
Necesitamos llenar dos elementos select
: el de la lista de micrófonos y el de la lista de vídeos. Para eso llamamos a enumerateDevices
que traerá la lista.
Recorremos el arreglo y filtramos: si es un elemento de tipo audioinput lo ponemos en el select de micrófonos y en caso de que sea videoinput en el select de cámaras.
// Consulta la lista de dispositivos de entrada de audio y llena el select
const llenarLista = () => {
navigator
.mediaDevices
.enumerateDevices()
.then(dispositivos => {
limpiarSelect($dispositivosDeAudio);
limpiarSelect($dispositivosDeVideo);
dispositivos.forEach((dispositivo, indice) => {
if (dispositivo.kind === "audioinput") {
const $opcion = document.createElement("option");
// Firefox no trae nada con label, que viva la privacidad
// y que muera la compatibilidad
$opcion.text = dispositivo.label || `Micrófono ${indice + 1}`;
$opcion.value = dispositivo.deviceId;
$dispositivosDeAudio.appendChild($opcion);
} else 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);
}
})
})
};
Comenzar grabación
Cuando la lista está llena y el botón sea presionado, comenzamos la grabación. Llamamos a getUserMedia
(que va a pedir permiso de acceso al micrófono y a la cámara web) pasándole un objeto con el id del dispositivo de audio y el id del dispositivo de vídeo.
En caso que todo salga bien (el usuario cuente con una cámara y un micrófono, y se conceda el permiso) entonces la promesa que devuelve getUserMedia
se resuelve y trae consigo un stream.
// Comienza a grabar el audio con el dispositivo seleccionado
const comenzarAGrabar = () => {
if (!$dispositivosDeAudio.options.length) return alert("No hay micrófono");
if (!$dispositivosDeVideo.options.length) return alert("No hay cámara");
// No permitir que se grabe doblemente
if (mediaRecorder) return alert("Ya se está grabando");
navigator.mediaDevices.getUserMedia({
audio: {
deviceId: $dispositivosDeAudio.value, // Indicar dispositivo de audio
},
video: {
deviceId: $dispositivosDeAudio.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();
comenzarAContar();
// En el arreglo pondremos los datos que traiga el evento dataavailable
const fragmentosDeAudio = [];
// Escuchar cuando haya datos disponibles
mediaRecorder.addEventListener("dataavailable", evento => {
// Y agregarlos a los fragmentos
fragmentosDeAudio.push(evento.data);
});
})
.catch(error => {
// Aquí maneja el error, tal vez no dieron permiso
console.log(error)
});
};
Ese stream lo ponemos como fuente de un vídeo, pero además de ello, creamos un objeto de tipo MediaRecorder
con el mismo stream y comenzamos un conteo de tiempo.
Un paso muy importante es llamar a la función start
del objeto MediaRecorder
que hemos creado, pues eso comienza la grabación del audio y vídeo.
El objeto de MediaRecorder
va a emitir el evento “dataavailable
” que, cuando se desencadene, traerá datos de la grabación que colocamos en un arreglo.
El vídeo no es obligatorio
Mostramos el stream en un elemento de tipo video
pero no es necesario, solo lo hacemos para que el usuario vea una “previsualización”, pero bien podríamos quitarlo, ya que la grabación se hace con MediaRecorder.
Detener grabación
Para detener la grabación llamamos al método stop
. Eso desencadenará el evento stop
del objeto de MediaRecorder, al que debemos suscribirnos.
Dentro del mismo pausamos el vídeo, convertimos los fragmentos de la grabación en un blob y descargamos el vídeo grabado en formato webm.
// Cuando se detenga (haciendo click en el botón) se ejecuta esto
mediaRecorder.addEventListener("stop", () => {
// Pausar vídeo
$video.pause();
// Detener el stream
stream.getTracks().forEach(track => track.stop());
// Detener la cuenta regresiva
detenerConteo();
// Convertir los fragmentos a un objeto binario
const blobVideo = new Blob(fragmentosDeAudio);
// Crear una URL o enlace para descargar
const urlParaDescargar = URL.createObjectURL(blobVideo);
// Crear un elemento <a> invisible para descargar el audio
let a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = urlParaDescargar;
a.download = "grabacion_parzibyte.me.webm";
// Hacer click en el enlace
a.click();
// Y remover el objeto
window.URL.revokeObjectURL(urlParaDescargar);
});
El vídeo debe tener el formato webm por eso de los códecs libres y los formatos; además de que no todos los formatos son compatibles con todos los navegadores.
Otras funciones útiles
Tenemos algunas utilidades. Por ejemplo, una de ellas limpia un select, otra convierte el tiempo transcurrido a una cadena legible y finalmente otra se encarga de la cuenta de la duración del vídeo:
// Algunas funciones útiles
const limpiarSelect = elemento => {
for (let x = elemento.options.length - 1; x >= 0; x--) {
elemento.options.remove(x);
}
}
const segundosATiempo = numeroDeSegundos => {
let horas = Math.floor(numeroDeSegundos / 60 / 60);
numeroDeSegundos -= horas * 60 * 60;
let minutos = Math.floor(numeroDeSegundos / 60);
numeroDeSegundos -= minutos * 60;
numeroDeSegundos = parseInt(numeroDeSegundos);
if (horas < 10) horas = "0" + horas;
if (minutos < 10) minutos = "0" + minutos;
if (numeroDeSegundos < 10) numeroDeSegundos = "0" + numeroDeSegundos;
return `${horas}:${minutos}:${numeroDeSegundos}`;
};
// Ayudante para la duración; no ayuda en nada pero muestra algo informativo
const comenzarAContar = () => {
tiempoInicio = Date.now();
idIntervalo = setInterval(refrescar, 500);
};
Poniendo todo junto
El script de JavaScript y la vista HTML actualizados se encuentran en mi GitHub. Para probar la app visita este enlace.
Conclusiones
La grabación de vídeo con JavaScript y getUserMedia es compatible con los navegadores más populares: Chrome y Firefox; lo he probado en Android y Windows 10.
Gracias a estas APIs podemos agregar más funcionalidades a nuestras webapps accediendo al hardware que tiene el dispositivo.
Excelente, me fue de muchisima utilidad tu explicación y código.
Muchas Gracias.
Recuerda compartir y seguirme, así me motivas a escribir más tutoriales.
Saludos
Que excelente post!! te felicito por el aporte, tengo una duda, si quisiera que el video no despliegue la ventana para guardar sino que automáticamente se aloje en el servidor para guardar su enlace en una Bd se podría hacer?
Gracias por tus comentarios. Aquí tienes:
https://parzibyte.me/blog/2019/06/04/grabar-video-javascript-enviarlo-php/
No olvides seguirme y compartir
Pingback: Grabar vídeo con JavaScript y enviarlo a servidor con PHP - Parzibyte's blog