Search code examples
javascriptweb-audio-api

Is there any way to not freeze up the UI when creating multiple HTMLAudioElement DOM elements


The question in the title is pretty self-explanatory. I have this site that allows the user to upload multiple files at a time. And then it will go through and retrieve the duration for each one.

getDuration(file) {
    return new Promise(resolve => {
        let reader = new FileReader();
        reader.onload = e => {
            let audio = new Audio();
            audio.onloadeddata = () => {
                this.duration = audio.duration;
                resolve();
            };
            audio.src = e.target.result;
        };
        reader.readAsDataURL(file);
    });
}

But this, in Chrome at least, freezes up the UI pretty bad while it's doing all that processing. Is there anyway I could avoid this or go about this in a different way?


Solution

  • You can already start by not using a FileReader and data:// URIs

    The FileReader will read all the File from the disk into memory, with a lot of Files, that's a lot of IO and your disk (HDD or SSD) will not like it.

    Then creating a data:// URL from that file is not free operation, so it will take some CPU time.
    Finally, the browser will have to decode back that data: URL to raw binary before it's able to even try to decode that data as audio. Once again a few unnecessary steps here, and a lot of wasted memory.

    Instead, you could simply create a blob:// URL from the File your users sent and set your <audio>'s src to that. This will be a simple pointer to the actual File on the disk, so the browser will be able to stream-decode it and append only the chunks of data it needs in memory.

    To create such a blob:// URL, you have to use the URL.createObejctURL() method.

    Note that even though for Files from disk it's not that critical, you should get the good habit to revoke such blob:// URLs after you used it, because they'll blob the File from being Garbage Collected.

    function getDuration(file) {
      return new Promise(resolve => {
        const audio = new Audio();
        audio.onloadedmetadata = () => {
          URL.revokeObjectURL(file)
          resolve(audio.duration);
        };
        audio.src = URL.createObjectURL(file);
      });
    }
    document.querySelector("input").oninput = async (evt) => {
      console.log( await getDuration( evt.target.files[0] ) );
    }
    <input type="file">