How to convert a blob URL to a audio file and save it to the server

24,027

Solution 1

First you need a proper function to send your data. Your initial fetch approach was close, but not perfect.

Let's consider the function below. It takes in a Blob in the file parameter. This Blob will be created later in the answer. In the sendAudioFile function create a new FormData object. Append the Blob to the the formData.

Now send the formData with the POST method to your server and use the body property for the formData.

const sendAudioFile = file => {
  const formData = new FormData();
  formData.append('audio-file', file);
  return fetch('http://localhost:3000/audioUpload', {
    method: 'POST',
    body: formData
  });
};

Now to create your file you need to capture the recorded stream. Right now you are directly setting the recording to your audio element, but thats no use for you to get the recorded data.

Add an empty array inside the callback of getUserMedia and lets call it data. This array will capture all the recorded data and use it to create a Blob.

In the dataavailable event handler, push the e.data (which is the recorded data) to the data array.

Add another event listener that listens for the stop event. Whenever the recording has stopped and all data is collected, create a Blob in the stop event callback. You can specify what the MIME type of the file is to tell it's format.

Now you have your Blob with recorded data and can pass that to the sendAudioFile function which will send your Blob to the server.

navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
  // Collection for recorded data.
  let data = [];

  // Recorder instance using the stream.
  // Also set the stream as the src for the audio element.
  const recorder = new MediaRecorder(stream);
  audio.srcObject = stream;

  recorder.addEventListener('start', e => {
    // Empty the collection when starting recording.
    data.length = 0;
  });

  recorder.addEventListener('dataavailable', event => {
    // Push recorded data to collection.
    data.push(event.data);
  });

  // Create a Blob when recording has stopped.
  recorder.addEventListener('stop', () => {
    const blob = new Blob(data, { 
      'type': 'audio/mp3' 
    });
    sendAudioFile(blob);
  });

  // Start the recording.
  recorder.start();
});

Solution 2

Adding to @Emiel Zuubier great answer, if you need to send the data via base64 encoded data URI. In this case;

blobToBase64(blob) {
        const reader = new FileReader();
        reader.readAsDataURL(blob);
        return new Promise(resolve => {
            reader.onloadend = () => {
                resolve(reader.result);
            };
        });
    };

And then send to the server like so;

 blobToBase64(audioBlob)
        .then(base64Data => {
            const file = "data:audio/webm;base64," + base64Data;
            const formData = new FormData();
            formData.append('file', file);
            return fetch(url, {
                method: 'POST',
                body: formData
            }).then(res => res.json())
           })
Share:
24,027
Aarwil
Author by

Aarwil

Updated on March 21, 2021

Comments

  • Aarwil
    Aarwil about 3 years

    I have successfully recorded and added the recorded audio and placed it in the audio tag of my HTML page.

    <audio controls="" src="blob:https://localhost:3000/494f62b9-0513-4d1c-9206-6569083a2661"></audio>

    Also, I have successfully got the blob source url from the source tag using this line. var source = document.getElementById("Audio").src; and this is my blob url

    blob:https://localhost:3000/494f62b9-0513-4d1c-9206-6569083a2661

    Now how to convert the blob source url as an audio file and send it to my server. I have tried a lot, help to do this and I am new to this blob. Since I am using this recorder api to work on all browsers I have only this chance by getting the blob source then converting it into an audio file and sending the audio file to my server using form data.

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <title>FeedBack URL</title>
      <link rel="shortcut icon" href="./favicon.ico">
      <meta content="width=device-width" name="viewport">
      <meta name="theme-color" content="#00e5d2">
      <style>
        * {
          padding: 0;
          margin: 0
        }
    
        a {
          color: #009387;
          text-decoration: none
        }
    
        a:visited {
          color: #930087
        }
    
        body {
          margin: 1rem;
          font-family: sans-serif
        }
    
        main {
          max-width: 28rem;
          margin: 0 auto;
          position: relative
        }
    
        #controls {
          display: flex;
          margin-top: 2rem
        }
    
        button {
          flex-grow: 1;
          height: 2.5rem;
          min-width: 2rem;
          border: none;
          border-radius: .15rem;
          background: blue;
          margin-left: 2px;
          box-shadow: inset 0 -.15rem 0 rgba(0, 0, 0, .2);
          cursor: pointer;
          display: flex;
          justify-content: center;
          align-items: center
        }
    
        button:focus,
        button:hover {
          outline: none;
          background: blue;
        }
    
        button::-moz-focus-inner {
          border: 0
        }
    
        button:active {
          box-shadow: inset 0 1px 0 rgba(0, 0, 0, .2);
          line-height: 3rem
        }
    
        button:disabled {
          pointer-events: none;
          background: #d3d3d3
        }
    
        button:first-child {
          margin-left: 0
        }
    
        button svg {
          transform: translateY(-.05rem);
          fill: #000;
          width: 1.4rem
        }
    
        button:active svg {
          transform: translateY(0)
        }
    
        button:disabled svg {
          fill: #9a9a9a
        }
    
        button text {
          fill: #00e5d2
        }
    
        button:focus text,
        button:hover text {
          fill: #00ffe9
        }
    
        button:disabled text {
          fill: #d3d3d3
        }
    
        #formats,
        #mode {
          margin-top: .5rem;
          font-size: 80%
        }
    
        #mode {
          float: right
        }
    
        #support {
          display: none;
          margin-top: 2rem;
          color: red;
          font-weight: 700
        }
    
        #list {
          margin-top: 1.6rem
        }
    
        audio {
          display: block;
          width: 100%;
          margin-top: .2rem
        }
    
        li {
          list-style: none;
          margin-bottom: 1rem
        }
    
        .popup-position {
          display: none;
          position: fixed;
          top: 0;
          left: 0;
          background-color: rgba(0, 0, 0, 0.7);
          width: 100%;
          height: 100%;
    
          /* // The Modal Wrapper */
        }
    
        #popup-wrapper {
          text-align: left;
        }
    
        /* //The Modal Container */
        #popup-container {
    
          background-color: #fff;
          padding: 20px;
          border-radius: 10px;
          width: 300px;
          margin: 70px auto;
        }
    
        #closePopup {
          margin-left: 281px;
          margin-top: -18px;
        }
      </style>
    </head>
    
    <body>
      <a href="javascript:void(0)" onclick="toggle_visibility('contact-popup');">Open Popup</a>
      <div class="popup-position" id="contact-popup">
        <div class="popup-wrapper">
          <div id="popup-container">
            <h5>Feedback</h5>
            <p id="closePopup"><a href="javascript:void(0)" style="color: red;" title="Close"
                onclick="toggle_visibility('contact-popup');">X</a></p>
            <main>
              <div id="controls">
                <button id="record" disabled="" autocomplete="off" title="Record">
                  <svg viewBox="0 0 100 100" id="recordButton">
                    <circle cx="50" cy="50" r="46"></circle>
                  </svg>
                </button>
                <button id="pause" disabled="" autocomplete="off" title="Pause">
                  <svg viewBox="0 0 100 100">
                    <rect x="14" y="10" width="25" height="80"></rect>
                    <rect x="62" y="10" width="25" height="80"></rect>
                  </svg>
                </button><button id="resume" disabled="" autocomplete="off" title="Resume">
                  <svg viewBox="0 0 100 100">
                    <polygon points="10,10 90,50 10,90"></polygon>
                  </svg>
                </button><button id="stop" autocomplete="off" disabled="" title="Stop">
                  <svg viewBox="0 0 100 100">
                    <rect x="12" y="12" width="76" height="76"></rect>
                  </svg>
                </button>
              </div>
              <div id="mode">
                Native support,<a href="?polyfill">force polyfill</a>
              </div>
              <div id="formats"></div>
              <div id="support">
                Your browser doesn’t support MediaRecorder
                So please use chrome or edge or mozilla
              </div>
              <ul id="list"></ul>
              <form enctype="multipart/form-data"></form>
                <input id="image-file" type="file" hidden />
                <button type="button" id="formSubmit" onclick="sendto();">Submit</button>
              </form>
            </main>
            <div class="modal-footer">
            </div>
          </div>
        </div>
      </div>
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
      <script>
        (function () {
          var a, i, b, d, f, g, l = ["start", "stop", "pause", "resume"],
            m = ["audio/webm", "audio/ogg", "audio/wav"],
            j = 1024,
            k = 1 << 20;
    
          function n(e) {
            var r, $ = Math.abs(e);
            return $ >= k ? (r = "MB", e /= k) : $ >= j ? (r = "KB", e /= j) : r = "B", e.toFixed(0).replace(
              /(?:\.0*|(\.[^0]+)0+)$/, "$1") + " " + r;
          }
    
          function e(e) {
            i.innerHTML = "", navigator.mediaDevices.getUserMedia({
              audio: !0
            }).then(function (r) {
              a = new MediaRecorder(r), l.forEach(function (e) {
                a.addEventListener(e, t.bind(null, e));
              }), a.addEventListener("dataavailable", s), "full" === e ? a.start() : a.start(1e3);
            }), b.blur(), setTimeout(myFunction, 16000);
          }
    
          function o() {
            a.stop(), a.stream.getTracks()[0].stop(), g.blur();
          }
    
          function p() {
            a.pause(), d.blur();
          }
    
          function q() {
            a.resume(), f.blur();
          }
    
          function s(e) {
            var r = document.createElement("li"),
              $ = document.createElement("strong");
            $.innerText = "dataavailable: ", r.appendChild($);
            var a = document.createElement("span");
            a.innerText = e.data.type + ", " + n(e.data.size), r.appendChild(a), a.setAttribute("id", "span");
            var o = document.createElement("audio");
            o.controls = !0, o.src = URL.createObjectURL(e.data), o.setAttribute("id", "Audio"), r.appendChild(o), i
              .appendChild(r);
    
          }
    
          function t(e) {
    
            var r = document.createElement("li");
            r.innerHTML = "<strong>" + e + ": </strong>" + a.state, "start" === e && (r.innerHTML += ", " + a
                .mimeType), i.appendChild(r), "recording" === a.state ? (b.disabled = !0,
                f.disabled = !0, d.disabled = !1, g.disabled = !1) : "paused" === a.state ? (b
                .disabled = !0, f.disabled = !1, d.disabled = !0, g.disabled = !1) : "inactive" === a
              .state && (b.disabled = !1, f.disabled = !0, d.disabled = !0, g
                .disabled = !0);
          }
          i = document.getElementById("list"),
            b = document.getElementById("record"),
            f = document.getElementById("resume"),
            d = document.getElementById("pause"),
            g = document.getElementById("stop"),
            MediaRecorder.notSupported ? (i.style.display = "none",
              document.getElementById("controls").style.display = "none",
              document.getElementById("formats").style.display = "none",
              document.getElementById("mode").style.display = "none",
              document.getElementById("support").style.display = "block") : (document.getElementById("formats")
              .innerText = "Format: " + m
              .filter(function (e) {
                return MediaRecorder.isTypeSupported(e);
              }).join(", "), b.addEventListener("click", e.bind(null,
                "full")), f.addEventListener("click", q), d.addEventListener("click", p),
              g.addEventListener("click", o), b.disabled = !1);
        })();
    
        function myFunction() {
          document.getElementById("stop").click();
        }
    
        function toggle_visibility(id) {
          var element = document.getElementById(id);
    
          if (element.style.display == 'block')
            element.style.display = 'none';
          else
            element.style.display = 'block';
        }
    
        async function sendto() {
          var source = document.getElementById("Audio").src;
    
    
    
          $.ajax({
          type: 'POST',
          url: "http://localhost:3000/audioUpload",
          data: data,
          cache: false,
          processData: false,
          contentType: false,
          success: function(result) {
    
          }
        })
    
    
      </script>
    
    </body>
    
    </html>
    

    I have tried the fetch code that is

    let file = await fetch(source).then(r => r.blob()).then(blobFile => new File([blobFile], fileName, {
            type: res[0]
          })); 
    

    But it gives me the raw data I don't know how to send and receive the raw data.

    • Emiel Zuurbier
      Emiel Zuurbier about 4 years
      It is really hard to see what is going on with the minified variables. Could you include only the relevant parts for your question and use your unminified script? To clarify, you want to both download and decode, and encode and upload your audio blobs?
    • Aarwil
      Aarwil about 4 years
      @Emiel Zuurbier: The minified variables is plain script that I got from a github link github.com/ai/audio-recorder-polyfill, Since he asked me make a parcel of the code, but I am new to make parcels. So I took the raw code from the browsers directly and Implemented in my code. The code works in both safari and edge but I have to download the audio to my server. At this point that I got struck for a month.
  • Aarwil
    Aarwil about 4 years
    Is that, your code will work in edge, safari, and ios mobile safari? If yes then how to receive the audio and store in my server storage in my back page. I am using thisconst downloadFile = (async (file, fileLocation) => { const res = mainFile; const fileStream = fs.createWriteStream(fileLocation); await new Promise((resolve, reject) => { res.body.pipe(fileStream); res.body.on("error", (err) => { reject(err); }); fileStream.on("finish", function () { resolve(); });});});
  • Emiel Zuurbier
    Emiel Zuurbier about 4 years
    That seems like a new question. I'd suggest that you create a new question and ask again before making this thread is too broad. And this code should work in all the latest browsers, including the latest Edge, but not pre-chromium. If in doubt I'd recommend that you use a tool like Babel tool transpile your code to ES5 to support older browsers.
  • Aarwil
    Aarwil about 4 years
    Thanks for your effort and immediate reply, but the navigator.mediaDevices has no browser support on safari and edge. My code works without any back page support so you can directly run via npm start. I have used the const blob = new Blob(data, { 'type': 'audio/mp3' });sendAudioFile(blob); but shows error on Failed to construct 'Blob': The object must have a callable @@iterator property. Help me with that and thanks in advance
  • Emiel Zuurbier
    Emiel Zuurbier about 4 years
    navigator.mediaDevices.getUserMedia() should have support according to caniuse in both. MediaRecorder however is not supported in safari, but you got that polyfilled, right? I can't reproduce the Failed to construct 'Blob': The object must have a callable @@iterator property. error. When I run it, it works. Tested in Chrome and Safari.
  • Aarwil
    Aarwil about 4 years
    Ok, Thanks I got a constructed blob, I have a console of the blob in browser and is that a blob looks like this Blob {size: 95819, type: "audio/webm"} size: 95819 type: "audio/webm" __proto__: Blob if this is correct then I how do I receive it from the form data of my back page?
  • Emiel Zuurbier
    Emiel Zuurbier about 4 years
    That blob seems correct. I suggest that you open a new question on how to read a blob sent in FormData. And do some research on how to store a file in your chosen database.
  • Kaiido
    Kaiido about 4 years
    Where did you get that audio.src = URL.createObjectURL(e.data); That woudl work only for the first ondataavailable event, all subsequents ones would fail, and anyway, if you wish to monitor the MediaStream in your <audio>, then just set its .srcObject = stream;
  • led
    led over 3 years
    This is great, @EmielZuurbier - thank you!
  • Thanh Trung
    Thanh Trung over 2 years
    Defining the type of the Blob audio/mp3 doesn't automatically convert the native video/webm to the real mp3 format