14

I am currently implementing a web application and I want the users to record some audio and then I want a submit button to POST the mp3 file recorded to the server.

My server (Flask)'s main route '/' is waiting for the POST request:

@app.route('/', methods=['GET', 'POST'])
def index():
  if request.method == "GET":
    return render_template('index.html', request="GET")
  else:
    print request.files
    print request.form
    print request.form['file']
    if 'file' not in request.files:
      flash('No file part')
      return redirect(request.url)
    file = request.files['file']
    if file.filename == '':
      flash('No selected file')
      return redirect(request.url)
    if file and allowed_file(file.filename):
      handle_file(file)
    return render_template('index.html', request="POST")

Here is my JS code:

Jsfiddle

There are two main issues here:

1) When I download the mp3 file after recording, it cannot be opened by a media player. It seems like I am doing something wrong in just recording the audio.

2) When I print request.form in my server after getting the POST request, I only get this:

ImmutableMultiDict([('file', u'')])

And print request.form['file'] returns an empty line.

Why is this happening? Is there something wrong with the POST request.

Finally, I want to be able to decode the string I am posting to convert back to mp3. How do I do that?

Note: None of this has to stay the same. The task is to record audio and then POST it to the server. If there is a more efficient way to do that, any tips are welcome. Also, I don't care if the file will be wav or mp3.

Sagar V
  • 12,158
  • 7
  • 41
  • 68
pavlos163
  • 2,730
  • 4
  • 38
  • 82
  • 1) Have you tried setting `type` of `Blob` to `"audio/webm;codecs=opus"` or `"audio/ogg;codecs=opus"`?; 2) At `load` event of `XMLHttpRequest()` call the appropriate function. – guest271314 May 16 '17 at 23:54
  • 2
    Afaik no UA supports encoding to mp3 natively yet (may soon change with the end of royalties). The easiest is to not set it, let the browser handle that to its preferred MimeType. Similarly, you don't need to set the request headers when sending a formData. Checking your file's validity on its filename is a very bad idea. Now I guess it won't fix your issues but I though it was worth telling you. – Kaiido May 21 '17 at 23:34
  • I think it's no bueno to post audio files to server side. I wouldn't plain old js for this either. I suggest doing upload method, instead of post, to a file server. Then post a message to the server on success with filename. Now the backend API only has to process sound files. – Kevin Caravaggio May 22 '17 at 19:14
  • Why not convert the blob data to base64 and post the base64 string and decode the base64 string back to audio stream in the backend? – Nadir Laskar May 23 '17 at 06:54
  • you should check this repo https://github.com/coligo-io/file-uploader It works perfectly for your needs but it uses node as backend – Anurag May 23 '17 at 07:20
  • You are omitting the `handle_file` method. I reckon this is where your current problems are from. We need to see how you're saving the file. – Chibueze Opata May 23 '17 at 10:41
  • @Kaiido "Afaik no UA supports encoding to mp3 natively yet." Isn't that what the MediaRecorder API does? – 0xcaff Jun 24 '17 at 15:27
  • Which browser and media player are you using to test this? Your example works for me in chrome. – 0xcaff Jun 24 '17 at 15:28
  • Are you then able to play the downloaded mp3 file? – pavlos163 Jun 24 '17 at 16:25
  • I am using Chrome and VLC. – pavlos163 Jun 24 '17 at 16:26
  • @0xcaff nope. The MediaRecorder encodes in what the browser can encode, i.e free codecs for both FF and chrome like opus. Until last month mp3 wasn't free so they wouldn't pay the royalties. – Kaiido Jun 24 '17 at 21:41
  • I don't mind everything being in .wav here. Is that possible? – pavlos163 Jun 25 '17 at 01:12

2 Answers2

6

Note: this answer only treats current implementations in both chrome and Firefox. All this is subject to change any time soon.


I am not sure if anything is wrong in your server-side code, but don't send binary data as string. Instead, use an FormData to send it as multipart (you'll win 30% of data + integrity).

Also, it seems that in your MediaRecorder code, you are finalizing the file at every dataavailable event. It's generally not what you want.


Currently, no browser does support recording as mp3 natively.

var mimes = ['mpeg', 'mpeg3', 'x-mpeg3', 'mp3', 'x-mpeg']
console.log(mimes.some(m=>MediaRecorder.isTypeSupported('audio/'+m)));

So if you want to go the MediaRecorder way, you'd have to accommodate yourself with opus codec, encapsulated either in webm for chrome, or ogg for FF :

var enc = ['ogg', 'webm'];
var mime = "";
enc.forEach(e => {
  if (!mime && MediaRecorder.isTypeSupported(`audio/${e};codecs="opus"`)) {
    mime = `audio/${e};codecs="opus"`;
  }
});
console.log(mime);

So now we've got the correct mimeType, we can simply tell the browser to use it :

fiddle for chrome

var enc = ['ogg', 'webm'];
var extension = "",
  mime = '';
enc.forEach(e => !extension &&
  (mime = `audio/${e};codecs="opus"`) &&
  MediaRecorder.isTypeSupported(mime) &&
  (extension = e));
navigator.mediaDevices.getUserMedia({
    audio: true
  })
  .then(stream => {
    const chunks = [];
    const rec = new MediaRecorder(stream, {
      mimeType: mime // use the mimeType we've found
    });
    // this is not where we build the file, but where we store the chunks
    rec.ondataavailable = e => chunks.push(e.data);
    rec.onstop = e => {
      // stop our gUM stream
      stream.getTracks().forEach(t => t.stop());
      // NOW create the file
      let blob = new Blob(chunks, {
        type: mime
      });
      // we could now send this blob : 
      //   let form = new FormData();
      //   form.append('file', blob, 'filename.'+extension;
      //   ... declare xhr
      //   xhr.send(form);
      // but we'll just fetch it for the demo :
      let url = URL.createObjectURL(blob);
      let au = new Audio(url);
      au.controls = true;
      document.body.appendChild(au);
      au.play();
      // and create an downloadable link from it : 
      let a = document.createElement('a');
      a.href = url;
      a.download = 'filename.' + extension;
      a.innerHTML = 'download';
      document.body.appendChild(a);
    };
    rec.start();
    setTimeout(() => rec.stop(), 3000);
  });

Or we could also just let the browser do everything by default. This would be a problem only for the file extension...


Now, if you prefer wav over opus, you could let the MediaRecorder away, and simply use the WebAudioAPI, pass your gUM stream to it, and record the data from there. Here I'll use the recorder.js library for simplicity.

fiddle for chrome

navigator.mediaDevices.getUserMedia({
  audio: true
})
.then(stream => {
  const aCtx = new AudioContext();
  const streamSource = aCtx.createMediaStreamSource(stream);
  var rec = new Recorder(streamSource);
  rec.record();
  setTimeout(() => {
    stream.getTracks().forEach(t => t.stop());
    rec.stop()
    rec.exportWAV((blob) => {
      // now we could send this blob with an FormData too
      const url = URL.createObjectURL(blob);
      let au = new Audio(url);
      au.controls = true;
      document.body.appendChild(au);
      au.play();
      let a = document.createElement('a');
      a.href = url;
      a.innerHTML = 'download';
      a.download = 'filename.wav';
      document.body.appendChild(a);
    });
  }, 3000);
})
<script src="https://rawgit.com/mattdiamond/Recorderjs/master/dist/recorder.js"></script>

And if you really want an mp3, I guess you could use one of the javascript lame libraries available on the web.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thanks. I think the last one with .wav is the one I'll use. However, setting a timeout is not really wanted here, as I would like a `start` and `stop` button like in my example. How would you change this to fit with those buttons? – pavlos163 Jun 25 '17 at 14:09
  • @pk1914 simply move the function called on the timeout to be called where you want, like in a click event. – Kaiido Jun 25 '17 at 14:49
2

Try Converting the audio blob to Base64 and post the base64 string to the server.

function submit(blob) {
  var reader = new window.FileReader();
  reader.readAsDataURL(blob);
  reader.onloadend = function() {

    var fd = new FormData();
    base64data = reader.result;
    fd.append('file', base64data, 'audio.mp3');

    $.ajax({
      type: 'POST',
      url: '/',
      data: fd,
      cache: false,
      processData: false,
      contentType: false,
      enctype: 'multipart/form-data'
    }).done(function(data) {
      console.log(data);
    });
  }

}

Now, Convert the base64 String to a binary stream in your server.

Form more information on how to decode Base64 in python checkout this post. Python base64 data decode

Nadir Laskar
  • 4,012
  • 2
  • 16
  • 33
  • When I do this, the Flask `request.form` is `ImmutableMultiDict([('file', u'')])`. Shouldn't the `base64string` be the value of that dict entry? – pavlos163 Jun 24 '17 at 13:47
  • 2
    Base64 encoding bloats the request. You go from being able to store 1..265 per byte to only being able to store 1..64. Base64 is 4x worse than raw. – 0xcaff Jun 24 '17 at 14:07