Record to an Audio File using HTML5 and JS

Pure Native HTML5 Audio Recording is here and has been for a while, no more flash needed! Don't get too excited though, it still requires a bit of work to support it in a consistent manner.

This post will focus on getting the recorded audio into the same file format across browsers. And yes, I spent a long time debugging in the browser.

At time of writing the MediaRecorder API is in editors draft.

Example

Here is the "simplest" way to record, play and download audio in Chrome (this also works in Firefox but ogg format is used instead of webm).

Optional: Use polyfill from WebRTC project to do some cross-browser support of the API such as the navigator.mediaDevices.getUserMedia function on older Firefox and Chrome browsers.

// appends an audio element to playback and download recording
function createAudioElement(blobUrl) {
    const downloadEl = document.createElement('a');
    downloadEl.style = 'display: block';
    downloadEl.innerHTML = 'download';
    downloadEl.download = 'audio.webm';
    downloadEl.href = blobUrl;
    const audioEl = document.createElement('audio');
    audioEl.controls = true;
    const sourceEl = document.createElement('source');
    sourceEl.src = blobUrl;
    sourceEl.type = 'audio/webm';
    audioEl.appendChild(sourceEl);
    document.body.appendChild(audioEl);
    document.body.appendChild(downloadEl);
}

// request permission to access audio stream
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
    // store streaming data chunks in array
    const chunks = [];
    // create media recorder instance to initialize recording
    const recorder = new MediaRecorder(stream);
    // function to be called when data is received
    recorder.ondataavailable = e => {
      // add stream data to chunks
      chunks.push(e.data);
      // if recorder is 'inactive' then recording has finished
      if (recorder.state == 'inactive') {
          // convert stream data chunks to a 'webm' audio format as a blob
          const blob = new Blob(chunks, { type: 'audio/webm' });
          // convert blob to URL so it can be assigned to a audio src attribute
          createAudioElement(URL.createObjectURL(blob));
      }
    };
    // start recording with 1 second time between receiving 'ondataavailable' events
    recorder.start(1000);
    // setTimeout to stop recording after 4 seconds
    setTimeout(() => {
        // this will trigger one final 'ondataavailable' event and set recorder state to 'inactive'
        recorder.stop();
    }, 4000);
  }).catch(console.error);

When you paste this example in the developer console it will ask for permission to record audio, record audio for 4 seconds and then append a HTML5 input audio element to the body which can playback the audio. You can also then save the audio to a file from this input element.

Native Support

Support for MediaRecorder is still not on Safari, IE or Edge. Chrome and Firefox also have different audio recording formats but the same codec.

At time of writing these are the only supported native audio recording formats.

Browser Format Codec Mime Type
Chrome
webm
opus "audio/webm;codecs=opus"
Firefox ogg
opus "audio/ogg;codecs=opus"

You can test the supported recording formats in the browser by using the MediaRecorder function isTypeSupported.

// true on chrome, false on firefox
MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
// false on chrome, true on firefox
MediaRecorder.isTypeSupported('audio/ogg;codecs=opus') 

Remember: MediaRecorder !== MediaSource. Playing formats are much better supported than recording formats. See this excellent support media formats page.

The Format Problem

Recording audio in the same format across browsers is annoying, especially if you want the audio files sent to a backend.

Converting to a consistent audio format on the frontend before sending to the backend is a good solution. This is how Facebook Messenger and WhatsApp do their voice recording on the web.

They use a WebWorker on the client for the possible heavy audio conversion process before sending it to the backend.

The project web-audio-recorder-js has some great examples of how to encode to WAV, OGG and MP3 in the browser. Including a demonstration of how to do the encoding in a WebWorker.

#1 Facebook Solution

Facebook messenger records the audio streaming data as before but then converts it in a WebWorker to a WAV format. They then send that format to the backend.

They do something very similar to this project I found by a Googler who seems to specialise in Web Audio.

POST request made to https://upload.facebook.com/ajax/mercury/upload.php with this payload which includes the "audio/wav" format.

#2 WhatsApp Solution

WhatsApp on Web convert the recorded data to an OGG format from inside a WebWorker. They use a library similar to opus.js-sample to do it - this means both Chrome, Firefox and other browsers can then use ogg.

They also access the microphone raw data similary to this article by Google developers. This uses the AudioContext API which is part of the Web Audio API, this means they can also add effects and filters before creating the output. I noticed they added a Butterworth Filter which I believe reduces background noise from the recording.

Interestingly they don't also use navigator.mediaDevices.getUserMedia like Facebook do - they use navigator.getUserMedia which is a an older deprecated API but it checks out.

JS Object before the POST request is sent, it contains the "audio/ogg; codecs=opus" mimeType.

POST request is then made to https://mmi664.whatsapp.net.

#3 Convert to MP3 Solution

Use a JS port of Lame like LameJS or libmp3lame-js to do the conversion to MP3 inside a WebWorker. Once converted, send the MP3 audio to the backend.

Here is a good explanation of how its possible to convert recorded audio to MP3.

#4 Backend Solution

Another alternative solution would be convert those ogg and webm formats to a consistent format after receiving them in the backend. Avoiding any kind of client-side work to convert them.

FFmpeg for example can do the conversion quite easily.

ffmpeg -i input.webm output.m4a
ffmpeg -i input.ogg output.m4a

Issues

Recording in Chrome produces a webm file without any meta data about the duration of the recording. This means when Chrome plays back the recording it has no idea how long it is! Ogg format on Firefox does not suffer from this.

There is a bug in Chromium open for this issue.

Update: This bug's status is now "WontFix".

Conclusion

Recording Audio in the browser with the new HTML5 API is really nice. The downside is that the API is not yet supported in all browsers and recording formats are not consistent. There are solutions that can be used for both frontend and backend to tackle the problem though.

Happy recording!

References