如何将Blob URL转换为音频文件并保存到服务器

16
我已经成功地录制了音频并将其添加到HTML页面的audio标签中。 <audio controls="" src="blob:https://localhost:3000/494f62b9-0513-4d1c-9206-6569083a2661"></audio> 同时,我已经成功地使用这行代码从源标签获取了blob源URL: var source = document.getElementById("Audio").src; 这是我的blob URL:
blob:https://localhost:3000/494f62b9-0513-4d1c-9206-6569083a2661
现在,我该如何将blob源URL转换为音频文件并将其发送到我的服务器呢?
由于我正在使用此录音API来处理所有浏览器,因此我只有通过获取blob源,然后将其转换为音频文件并使用表单数据将音频文件发送到我的服务器的机会。
<!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>

我尝试了以下代码:

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

但这只是给我原始数据,如何发送和接收原始数据呢?


很难看到缩小变量的实际情况。你能否仅包含有关问题的相关部分并使用未经缩小的脚本?请澄清一下,您想要同时下载和解码以及编码和上传音频 blob 吗? - Emiel Zuurbier
@Emiel Zuurbier:缩小变量是我从GitHub链接https://github.com/ai/audio-recorder-polyfill得到的纯脚本。由于他要求我制作代码包,但我不懂如何制作代码包。因此,我直接从浏览器中获取了原始代码并将其实现在我的代码中。该代码在Safari和Edge中均可工作,但我必须将音频下载到我的服务器上。在这一点上,我已经卡了一个月。 - Aarwil
仅凭检查,async function sendto() 似乎缺少结尾的 } - Peter Mortensen
2个回答

19

首先需要一个适当的函数发送您的数据。 您最初的fetch方法接近,但并不完美。

考虑下面的函数。 它在file参数中输入Blob。 这个Blob将在答案后面创建。 在sendAudioFile函数中创建一个新的FormData对象。 将Blob附加到formData中。

现在使用POST方法将formData发送到您的服务器,并使用body属性进行formData

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

现在,要创建您的文件,您需要捕获记录的流。目前,您正在直接将录制设置为音频元素,但这对于获取记录的数据没有任何用处。

getUserMedia的回调函数中添加一个空数组,并称其为data。这个数组将捕获所有记录的数据并用它来创建一个Blob

dataavailable事件处理程序中,将e.data(即记录的数据)推送到data数组中。

添加另一个事件侦听器来侦听stop事件。每当录制停止并且收集了所有数据时,请在stop事件回调中创建一个Blob。您可以指定文件的MIME类型以告诉它的格式。

现在您有了包含记录数据的Blob,并且可以将其传递给sendAudioFile函数,该函数将向服务器发送您的Blob

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();
});

你的代码能在Edge、Safari和iOS移动版Safari上运行吗?如果可以,那么如何接收音频并将其存储在我的服务器后台页面中。我正在使用以下代码:const 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(); });});}); - Aarwil
感谢您的努力和及时回复,但是navigator.mediaDevices在safari和edge浏览器上没有支持。我的代码没有任何后端支持即可直接通过npm start运行。我已经使用了const blob = new Blob(data, { 'type': 'audio/mp3' });sendAudioFile(blob);但在“构造Blob失败:对象必须具有可调用的@@iterator属性”时显示错误。请帮我解决这个问题,在此提前致谢。 - Aarwil
根据 caniusenavigator.mediaDevices.getUserMedia() 应该得到支持。然而,在 Safari 中不支持 MediaRecorder,但你已经进行了 polyfill,对吧?我无法重现 Failed to construct 'Blob': The object must have a callable @@iterator property. 错误。当我运行它时,它可以工作。在 Chrome 和 Safari 中测试过。 - Emiel Zuurbier
太好了,@EmielZuurbier - 谢谢你! - Heckflosse_230
3
定义 Blob 类型为 audio/mp3 并不会自动将本机的 video/webm 转换成真正的 mp3 格式。 - Thanh Trung
显示剩余4条评论

2

补充一下Emiel Zuubier的好答案,如果您需要通过Base64编码的数据URI发送数据。在这种情况下,

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

然后像这样将其发送到服务器:
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())
    })

在Firefox和Safari中无法工作。 - Mukhilan Elangovan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接