9

Background

I am writing a service to serve static audio files in chunks to support a fluent experience for users with lower bandwidth. Once the file is fully streamed (the chunk sequence is linear, users won't be able to "jump" the range), I want to save the file into a local cache (using localforage, but that's not part of this question) to later load cached files from there and to save bandwidth.

Problem

It is with my current state of knowledge / code / tools only possible to do one of the following:

A) Stream the audio with HTMLAudioElement

const audio = new Audio()
audio.src = url
audio.preload = 'auto'
audio.load()

The HTML5 audio takes internally care of the partial response, working fine itself but I am not able to access the underlying buffer to save the file, once fully loaded. Thus, I won't be able to cache the file locally without downloading it separately (in another request).

B) Download / fetch the file as a whole, then play it

fetch(url, options) // set content header to array buffer
  .then((response) => {
    var blob = new Blob([response.value], { type: 'audio/mp3' })
    var url = window.URL.createObjectURL(blob)
    const audio = new Audio()
    audio.src = url
    audio.play()
  })

This gives me the access to the data so I can cache it for offline reuse. But I loose the streaming option, which makes it nearly impossible to play larger files without ages of waiting.

C) Using a custom loader and play each chunk using WebAudio API

Since A and B were not sufficient, I wrote a custom loader, that loads the chunks (which is working fine), and dispatches an event with the current chunk (as ArrayBuffer) as data. It also dispatches another event on end, that returns all the chunks, so I can create a blob from it:

const chunkSize = 2048
const audioContext = new AudioContext()
const loader = new StreamLoader(url,  {step: chunkSize})

loader.on(StreamLoader.event.response, (data) => {
  // data.response is of type ArrayBuffer
  const floats = new Float32Array(data.response)
  const audioBuffer = audioContext.createBuffer(1, chunkSize, 4410)
  audioBuffer.getChannelData(0).set(floats)
  const sourceNode = new AudioBufferSourceNode(audioContext, {buffer: audioBuffer})
  sourceNode.connect(audioContext.destination)
  sourceNode.start(0)
})

loader.once(StreamLoader.event.complete, (data) => {
  const blob = new Blob(Object.values(data), {type: fileType})
  const objectURL = URL.createObjectURL(blob)
  const audio = new Audio();
  audio.src = url;
  audio.play();
})

loader.load()

Problem here is that it produces only weird loud noise. However, playing from the blob, once completed, works fine.

I am aware of the issue, that the header needs to be present in order to decode the audio correctly, so I am curious how the HTMLAudioElement or MediaStream actually internally resolve this.

D) Using MediaStream or MediaRecorder with custom loader

Since I am not recording from a local device, I did not got any of these to work with streaming the chunks, that I loaded using xhr. Are they restricted to be used with local media sources?

E) Using tools like Howler, SoundManager, SoundJS.

I found the above mentioned tools, but unfortunately came back to issue A or B using them.

Expected Solution

What I expect to find is a solution, that allows me to play the audio from stream (partial response) and to access the buffered data in order to cache it.

Related SO resources:

How to play audio stream chunks recorded with WebRTC?

HTML5 record audio to file

HTML Audio element source from cache

Jankapunkt
  • 8,128
  • 4
  • 30
  • 59
  • Think you should take a look at using Service Worker and the [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) api. it's possible to download a resource, clone it and pipe it to both the cache and the response (Audio) at the same time. [sw-toolbox](https://github.com/GoogleChromeLabs/sw-toolbox/) is also pretty cool – Endless Oct 06 '18 at 19:04
  • if service worker is not an option (wish is sad cuz option A + service worker are meant for this), then you need to use fetch api and get a readableStream from `response.body` – Endless Oct 06 '18 at 19:08
  • Hi thank you for the response. Serviceworker is unfortunately not an option but the fetch solution would be interesting. Could you elaborate on that? How would you pass the readableStream to the `HtmlAudioElement` so that it can start playing as soon as possible / after the first N chunks? – Jankapunkt Oct 06 '18 at 20:22
  • Thanks for this code: var blob = new Blob([response.value], { type: 'audio/mp3' }) var url = window.URL.createObjectURL(blob) – Ievgen Martynov Jul 20 '19 at 22:43
  • Why this code http://jsfiddle.net/o2L3n5az/ gives me this error: Uncaught RangeError: byte length of Float32Array should be a multiple of 4 at new Float32Array () at StreamLoader. – apwnn Jun 09 '20 at 13:46

0 Answers0