How to stream audio chunks from the browser to the server?

Here you go (posted from my second account):

hooks.Recorder = {
  async mounted() {
    let firstBlob = null; // contains file header information that we need to store

    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    this.mediaRecorder = new MediaRecorder(stream, {
      mimeType: "audio/ogg; codecs=opus",
      audioBitsPerSecond: 128000,
    });

    // Create an audio context and connect the stream source to an analyzer node
    const audioContext = new AudioContext();
    const source = audioContext.createMediaStreamSource(stream);
    const analyzer = audioContext.createAnalyser();
    source.connect(analyzer);

    const array = new Uint8Array(analyzer.fftSize);

    function getPeakLevel() {
      analyzer.getByteTimeDomainData(array);
      return (
        array.reduce(
          (max, current) => Math.max(max, Math.abs(current - 127)),
          0
        ) / 128
      );
    }

    const reader = new FileReader();
    reader.onloadend = () => {
      const base64String = reader.result.split(",")[1];
      const payload = {
        chunk: base64String,
      };
      this.pushEvent("send_chunk", payload);
    };

    this.mediaRecorder.ondataavailable = (e) => {
      let data = null;
      if (firstBlob && e.data.size < 25000) {
        return;
      }
      if (!firstBlob) {
        firstBlob = e.data;
      }
      data = new Blob([firstBlob, e.data], { type: e.type }); // prepend the file information which is stored in the first blob

      reader.readAsDataURL(data);
    };

    let lastTick = 0;
    let now = 0;
    let peakLevels = [];
    const samplingFreq = 80;
    const sampingWindow = 3;

    const tick = () => {
      if (!this.mediaRecorder || this.mediaRecorder.state !== "recording") {
        return;
      }
      now = performance.now();

      if (now - lastTick > samplingFreq || !firstBlob) {
        const peakLevel = getPeakLevel();
        peakLevels = [peakLevel, ...peakLevels];
        if (peakLevels.length > sampingWindow) {
          peakLevels.pop();
        }

        if (
          (peakLevels.length === sampingWindow &&
            !peakLevels.find((level) => level > 0.01)) ||
          !firstBlob
        ) {
          // all levels have been less than 0.01 so we have a longer silence and can trigger subs
          this.mediaRecorder.requestData();
          peakLevels = [];
        }

        lastTick = performance.now();
      }
      setTimeout(() => {
        tick();
      }, samplingFreq);
    };

    const startBtn = this.el.querySelector("#start");
    startBtn.addEventListener("click", () => {
      this.mediaRecorder.start();
      now = performance.now();
      lastTick = performance.now();
      // This is a hack to generate the first chunk which can't be empty
      setTimeout(() => {
        tick();
      }, 300);
    });
    const stopBtn = this.el.querySelector("#stop");
    stopBtn.addEventListener("click", () => {
      this.mediaRecorder.stop();
    });
  },
};

As I said, disclaimer, this was just hacked together for a PoC.

2 Likes