admin管理员组

文章数量:1410717

I have a POST endpoint that returns an MP3 stream, and I want to play it in JavaScript using the native browser fetch API. The goal is to implement a cross-browser solution, but so far, nothing works in Firefox. Since I need to use a POST endpoint, using new Audio(url) isn’t an option. To make this easier to reproduce, I will use a GET endpoint for a random radio station that streams MP3, but the issue remains the same.

What I’ve tried:

  1. Using MediaSource
const playMP3Stream = async () => {
  const audio = new Audio();
  const mediaSource = new MediaSource();
  audio.src = URL.createObjectURL(mediaSource);

  mediaSource.onsourceopen = async () => {
    const sourceBuffer = mediaSource.addSourceBuffer("audio/mpeg");

    const response = await fetch(":8040/mp3");

    if (!response.ok) {
      return;
    }

    const reader = response.body?.getReader();

    if (!reader) return;

    while (mediaSource.readyState === "open") {
      const { value, done } = await reader.read();

      if (done) {
        mediaSource.endOfStream();
        break;
      }
      sourceBuffer.appendBuffer(value);
    }
  };

  await audio.play();
};
  1. Using AudioContext
const playMP3Stream = async () => {
  const audioContext = new (window.AudioContext || window.webkitAudioContext)();

  const response = await fetch(':8040/mp3');
  const reader = response.body.getReader();

  let streamBuffer = new Uint8Array();
  let startTime = audioContext.currentTime;

  async function processChunk() {
    const { done, value } = await reader.read();

    if (done) {
      return;
    }

    try {
      const newBuffer = new Uint8Array(streamBuffer.length + value.length);
      newBuffer.set(streamBuffer);
      newBuffer.set(value, streamBuffer.length);
      streamBuffer = newBuffer;

      if (streamBuffer.length > 10000) {
        const audioBuffer = await audioContext.decodeAudioData(
          streamBuffer.buffer,
        );
        const source = audioContext.createBufferSource();
        source.buffer = audioBuffer;
        source.connect(audioContext.destination);
        source.start(startTime);
        startTime += audioBuffer.duration;

        streamBuffer = new Uint8Array();
      }
    } catch (error) {
      console.error('Error decoding audio chunk:', error);
    }

    processChunk();
  }

  processChunk();
}

MediaSource works well in Chromium browsers, but unfortunately, the "audio/mpeg" MIME type is not supported in Firefox. The AudioContext approach works to some extent, but there are gaps between chunks, and it still only functions in Chromium browsers. I’m not sure if this is particularly difficult, or if I’m just struggling with the implementation, but I can't figure out how to play an MP3 audio stream in Firefox.

I would greatly appreciate any help, whether it's a working example of gapless MP3 streaming in Firefox or anything else that could help me make this work.

Thanks in advance!

I have a POST endpoint that returns an MP3 stream, and I want to play it in JavaScript using the native browser fetch API. The goal is to implement a cross-browser solution, but so far, nothing works in Firefox. Since I need to use a POST endpoint, using new Audio(url) isn’t an option. To make this easier to reproduce, I will use a GET endpoint for a random radio station that streams MP3, but the issue remains the same.

What I’ve tried:

  1. Using MediaSource
const playMP3Stream = async () => {
  const audio = new Audio();
  const mediaSource = new MediaSource();
  audio.src = URL.createObjectURL(mediaSource);

  mediaSource.onsourceopen = async () => {
    const sourceBuffer = mediaSource.addSourceBuffer("audio/mpeg");

    const response = await fetch("http://sc6.radiocaroline:8040/mp3");

    if (!response.ok) {
      return;
    }

    const reader = response.body?.getReader();

    if (!reader) return;

    while (mediaSource.readyState === "open") {
      const { value, done } = await reader.read();

      if (done) {
        mediaSource.endOfStream();
        break;
      }
      sourceBuffer.appendBuffer(value);
    }
  };

  await audio.play();
};
  1. Using AudioContext
const playMP3Stream = async () => {
  const audioContext = new (window.AudioContext || window.webkitAudioContext)();

  const response = await fetch('http://sc6.radiocaroline:8040/mp3');
  const reader = response.body.getReader();

  let streamBuffer = new Uint8Array();
  let startTime = audioContext.currentTime;

  async function processChunk() {
    const { done, value } = await reader.read();

    if (done) {
      return;
    }

    try {
      const newBuffer = new Uint8Array(streamBuffer.length + value.length);
      newBuffer.set(streamBuffer);
      newBuffer.set(value, streamBuffer.length);
      streamBuffer = newBuffer;

      if (streamBuffer.length > 10000) {
        const audioBuffer = await audioContext.decodeAudioData(
          streamBuffer.buffer,
        );
        const source = audioContext.createBufferSource();
        source.buffer = audioBuffer;
        source.connect(audioContext.destination);
        source.start(startTime);
        startTime += audioBuffer.duration;

        streamBuffer = new Uint8Array();
      }
    } catch (error) {
      console.error('Error decoding audio chunk:', error);
    }

    processChunk();
  }

  processChunk();
}

MediaSource works well in Chromium browsers, but unfortunately, the "audio/mpeg" MIME type is not supported in Firefox. The AudioContext approach works to some extent, but there are gaps between chunks, and it still only functions in Chromium browsers. I’m not sure if this is particularly difficult, or if I’m just struggling with the implementation, but I can't figure out how to play an MP3 audio stream in Firefox.

I would greatly appreciate any help, whether it's a working example of gapless MP3 streaming in Firefox or anything else that could help me make this work.

Thanks in advance!

Share Improve this question edited Mar 10 at 19:26 LeCarteloo asked Mar 10 at 18:25 LeCartelooLeCarteloo 415 bronze badges 4
  • @Yogi I used Radio Caroline as a simple reproduction example (as mentioned in the post). What I actually need is a way to handle the POST request. – LeCarteloo Commented Mar 10 at 22:30
  • 1 Maybe, using a ServiceWorker you could proxy your POST request to a GET one in the browser itself, but that's a big maybe, I never tried it myself and it also may not work at all. – Kaiido Commented Mar 11 at 5:43
  • As for the issue with the AudioBuffer version, it's probably because the network chunks don't correspond to the proper MPEG frame. IIRC it's not trivial to find the proper boundaries of these frames, so using a standalone decoder might be a better alternative. – Kaiido Commented Mar 11 at 5:50
  • @Kaido is correct, see answer below – anthumchris Commented Mar 13 at 19:15
Add a comment  | 

2 Answers 2

Reset to default 2

MediaSource does not support MP3 in Firefox, and AudioContext is not suitable for continuous MP3 streaming.

The best solution to this issue could be an instance of the Audio class in JS. A POST request complicates playback because browser-based players (<audio>, new Audio()) only work with GET — they expect a simple, directly accessible URL. Even if the response includes Content-Type: audio/mpeg, the browser player does not allow direct streaming via POST.

Therefore, the best approach is for the server to handle the POST request, then make a GET request to the actual stream and forward it to the client. This way, <audio> can receive the stream via GET, ensuring a cross-browser solution.

in html:

<audio controls autoplay>
  <source src="http://localhost:3000/proxy-stream" type="audio/mpeg">
  ohh hell no, your browser does not support the audio element #sad
</audio>

or in js:

const audio = new Audio("your-site/proxy-stream");
audio.play();

"your-site/proxy-stream" is an API endpoint that acts as a proxy server. It is not a direct audio file but a server route that receives a request and returns an MP3 stream. On the client side, we can access this endpoint using <audio> or new Audio(), while the server fetches the actual MP3 stream (http://mp3.some-real-stream:8040/mp3) and forwards it back to the client side.

As Kaido stated, this can be achieved with a Service Worker that transforms requests from GET to POST.

Example: https://cøder/static/79498844/


index.htm

<audio hidden controls />

<script type="module">
  const reg = await navigator.serviceWorker.register('./sw.js')
  const newSw = reg.installing || reg.waiting
  if (newSw) { 
    newSw.addEventListener('statechange', event => {
      if ('activated' === newSw.state) {
        location.reload()
      }
    })
  } else if (reg.active) {
    init()
  }

  function init() {
    const audio = document.querySelector('audio')
    audio.hidden = false
    audio.src = 'https://fetch-stream-audio.anthum/audio/house-64kbs.mp3'
  }
</script>

sw.js

self.addEventListener('install', event => {
  self.skipWaiting()
})

self.addEventListener('fetch', event => {
  const { method, url } = event.request
  if (url.endsWith('.mp3') && method === 'GET') {
    event.respondWith(fetch(url, { method: 'POST' }))
  } else {
    event.respondWith(fetch(event.request))
  }
})

本文标签: htmlPlaying MP3 Stream in JavascriptStack Overflow