javascript

Grabar vídeo de cámara con JavaScript

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.

Grabar vídeo de cámara con JavaScript, MediaRecorder y getUserMedia

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.

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

Entradas recientes

Creador de credenciales web – Aplicación gratuita

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

6 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.