Librerie ffmpeg in Javascript: conversione audio e video via web senza installare nulla sul server!

Il problema

Quando si vuole fornire un servizio di conversione audio e/o video nel proprio sito web (ad esempio i siti stile YouTube o quelli che forniscono un’applicazione web di qualche tipo) si incorre in varie problematiche. Occupazione eccessiva di banda e importante carico computazionale lato server sono spesso i primi ostacoli da tenere in considerazione. Problema essenziale sorge quando il dominio web non permette proprio l’installazione delle librerie ffmpeg, necessarie per potere effettuare qualsiasi conversione audio/video (è il caso, ad esempio, di Altervista), oppure ti permette di farlo, ma a pagamento.

La soluzione qui proposta è quella di demandare tutta l’elaborazione al computer lato client (quello del visitatore del sito), in maniera tale da sollevare il server da qualunque carico computazionale.

La soluzione: librerie ffmpeg in Javascript

Per fortuna ci viene in aiuto videoconverter.js: le librerie ffmpeg completamente convertite in linguaggio Javascript!

Non c’è nulla da installare, tutto quello che dovete fare è scaricare i sorgenti, che comprenderanno i file Javascript necessari per il funzionamento e vari esempi con cui esercitarsi.

Il sorgente lo trovate qui: https://github.com/bgrins/videoconverter.js

Oppure qui trovate la versione in WebAssembly (più leggero e performante) https://github.com/jonasof/videoconverter.js-wasm-demo

Scaricatelo e ricaricatelo in una apposita cartella nel server del vostro sito. Farete riferimento a questa cartella per richiamare il codice.

Nel sito ufficiale http://bgrins.github.io/videoconverter.js/ trovate le istruzioni che spiegano come si utilizza (in inglese) che qui di seguito riassumeremo:

0. Inizializzare il Worker e creare le variabili globali che ci servono:

Queste librerie Javascript funzionano attraverso un Worker. Per prima cosa quindi creiamo una variabile globale “worker” (ci servirà per interagire con lui) e inizializziamo il Worker richiamando la funzione “initWorker()” che analizzeremo più avanti.

Creiamo anche la o le variabili globali dove vogliamo che vengano memorizzati i dati dei file audio o video che vogliamo processare. Nell’esempio che segue utilizzeremo una variabile globale chiamata “sampleAudioData”.

1. Caricare i file audio e video

Per prima cosa bisogna caricare i file audio o video che si vogliono processare per l’esportazione del file finale che vogliamo creare.

I dati dei file devono essere caricati nel formato Uint8Array. Per farlo possiamo passare il nome del file caricato sul server alla seguente funzione:

function acquisisciAudio(NomeFileAudio) {
	var oReq = new XMLHttpRequest();
	oReq.open("GET", "percorso del file audio" + NomeFileAudio, true);
	oReq.responseType = "arraybuffer";
	oReq.onload = function (e) {
		var arrayBuffer = oReq.response;
		if (arrayBuffer) {
			sampleAudioData = new Uint8Array(arrayBuffer);
		}
	};
	oReq.send(null);
}

Dove sampleAudioData è la variabile globale usata in questo esempio (chiamata cosi perché nel mio caso carico file audio) dove verrà memorizzato il contenuto del file nel formato Uint8Array. Possiamo anche trasformarla in un array per caricare tutti i file in questa variabile.

2. Impartire i comandi ffmpeg con le librerie Javascript

Una volta assicurati che tutti i file interessati sono stati caricati (è sufficiente verificare che le variabili globali dove vengono memorizzati i dati Uint8Array non sono vuote) si può passare alla funzione parseArguments la stringa dei comandi ffmpeg che si vogliono impartire. Sono gli stessi identici comandi ffmpeg che avreste usato normalmente per realizzare il risultato desiderato.

La funzione parseArguments spezzetta la stringa in un array di stringhe che verrà dato in pasto al Worker:

function parseArguments(comando) {
  comando = comando.replace(/\s+/g, ' ');
  var args = [];
  comando.split('"').forEach(function(t, i) {
    t = t.trim();
    if ((i % 2) === 1) {
      args.push(t);
    } else {
      args = args.concat(t.split(" "));
    }
  });
  return args;
}

Una volta memorizzato il risultato della funzione parseArguments in una variabile (ad esempio ‘args’), si può avviare il processo richiamando il metodo worker.postMessage così strutturato:

worker.postMessage({
    type: "command",
    arguments: args,
    files: [
        {
          data: Uint8Array,
          name: string
        }
      ]
});
  • arguments: stringa passata alla funzione parseArguments contenente i comandi ffmpeg che vogliamo impartire per creare il file output desiderato.
  • files: array contenente i dati di ogni file da processare. Ogni elemento dell’array deve contenere:
    • data: la variabile contenente i dati in precedenza caricati nel formato Uint8Array;
    • name: il nome a cui fare riferimento nella stringa ‘arguments’.

Il metodo richiamerà il Worker Javascript che si occuperà di processare i comandi ffmpeg impartiti. Sarà possibile interagire con i vari stadi del processo attraverso la gestione degli appositi eventi dichiarati all’interno della funzione “initWorker()” richiamata all’inizio:

function initWorker() {
    worker = new Worker("percorso/worker.js");
    worker.onmessage = function (event) {
        var message = event.data;
        switch(message.type) {
            case "ready":
				/* il messaggio "ready" indica che il worker è pronto per ricevere un comando *
				 * si può utilizzare una variabile globale per memorizzare tale stato */
                isWorkerLoaded = true;
                break;

            case "start": 
                /* il messaggio "start" indica che il worker ha ricevuto il comando e sta cominciando a processare il file */
                outputElement.innerText = "Worker ha ricevuto il comando\n";
                break;            

            case "stdout":
                /* il messaggio "stdout" indica che il worker sta memorizzando nella variabile 'data' una stringa con il log delle azioni che sta compiendo (normalmente è lo stesso output di ffmpeg)
                 * si può tenere a disposizione un elemento dell'HTML per stampare a video tale stringa */
                outputElement.innerText += message.data + "\n";
                break;
            
            case "done":
                /* il messaggio "done" indica che il worker ha terminato l'elaborazione.
                 * il contenuto del file si trova all'interno della variabile 'data' in formato blob
                 * se non contiene nulla vuol dire che c'è stato qualche errore */
                var buffers = message.data;
                if (buffers.length) {
                    /* tutto ok, il file si trova all'interno della variabile 'message.data' (ora 'buffers') */
                } else {
                    /* qualcosa è andato storto, leggere il log inviato durante gli eventi di "stdout" per avere maggiori informazioni */
                }
                
                buffers.forEach(function(file) {
                    /** il risultato può essere trasformato in un link scaricabile attraverso la funzione window.URL.createObjectURL() **/
                    var a = document.createElement('a');
                    var blob = new Blob([file.data]);
                    var src = window.URL.createObjectURL(blob);
                    a.download = file.name;
                    a.href = src;
                    a.innerText = strCliccaDownloadFile;
                    a.className = "btn btn-success";
                    filesElement.appendChild(a);
                });
        }
    };
    worker.onerror = function (e) {
        console.log("Intercettato errore worker", e);
    };
}

Conclusioni

Grande utilità

Le librerie videoconverter.js si rivelano molto utili ed efficaci per demandare l’elaborazione di uno o più file audio o video al pc client del visitatore del vostro sito, senza dovere in alcun modo appesantire l’elaborazione lato server!

Velocità

Essendo in sostanza un porting javascript delle librerie ffmpeg, il caricamento (per quanto esso avvenga in background, quindi non dovrebbe incidere sulle performance del sito) potrebbe essere molto lento e variare da computer a computer e da browser a browser.

Limiti e workaround

Altro elemento importante da tenere in considerazione è che non si conosce la memoria RAM a disposizione del pc del visitatore del sito, senza contare che i browser notoriamente non assegnano una memoria ‘infinita’ al javascript di una pagina. Il risultato di tutto ciò si potrebbe tradurre nell’impossibilità di processare file di grandi dimensioni o effettuare elaborazioni troppo complesse che richiederebbero una memoria RAM superiore a quella dal browser assegnata ai processi javascript.

Prestare quindi molta attenzione al messaggio: “Cannot enlarge memory arrays” all’interno dei messaggi log presenti nella variabile ‘data’ (vedi sopra). Poco si può fare in questo caso. Personalmente questo problema si è presentato quando ho cercato di unire tanti file audio in uno solo con vari effetti. Sono riuscito ad ovviare a questo problema facendo processare pochi file per volta (ad es. processando 3 file alla volta) e poi unendo i file così ottenuti in un unico grande file lanciando un ultimo comando ffmpeg finale.

Esempi e utilizzi pratici

Se siete interessati posso fornire il sorgente del codice utilizzato nel mio sito: https://wedub.altervista.org

Il sito si propone di essere un editor audio/video online per chi ama fare doppiaggio, soprattutto amatoriale. In pratica è una web application che vorrei rendere open-source che consente, tra le altre cose, appunto di esportare tutte le clip audio del proprio doppiaggio in un unico file audio complessivo, contenente anche tutti gli effetti applicati (ad esempio il volume delle clip, ecc.) con anche la possibilità di scegliere il formato del file finale!

Come provarle

Per terminare questo articolo vi esorto a fare una prova delle potenzialità delle librerie ffmpeg in javascript nella pagina demo del sito ufficiale di videoconverter.js: http://bgrins.github.io/videoconverter.js/demo/

Vostri dubbi e commenti

Spero che la guida sia stata di vostro gradimento, per qualunque dubbio o chiarimento utilizzate pure i commenti qua sotto!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.