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: