TL;DR When an <audio>
element receives 206 Content-Range chunks of data, switching the origin of these chunks mid-stream through a service-worker stops the playback, and it cannot be resumed. Why and how can I prevent it from stopping?
I have a server on my network and this server knows how to respond to Content-Range
audio streaming requests with the correct 206 chunks.
On the client, there is an <audio src="...">
tag and a service-worker that intercepts the fetch requests emitted by that audio element.
- if the service-worker knows that it's on a network outside of that of the server, it will make a "normal request" through the internet to
example.com
- if the service-worker knows that it's on the same network as the server, it will fetch from a "local URL"
local.example.com
(SSL certificates, signed by certificate authority, and DNS resolution points to a private IP like192.168.0.4
)
If I leave my home with an audio file currently streaming, here's what happens
- the request made by the service-worker to
local.example.com
fails because the IP is not reachable anymore - the service-worker can
catch
the error, and start a new request toexample.com
- the client receives its
Response
from the service-worker as if nothing happened
You can see this happen in the devtools:
The first request is that of the client, the second (red) the one to
local.example.com
, and the last one the one to example.com
Except... that the audio playback gets interrupted and cannot be resumed.
To diagnose this issue:
there is nothing specific on the client, just
<audio src="example.com/api/file/12345678" hidden playsinline autoplay></audio>
there is nothing fantastic on the server either. It streams totally fine when reached at
local.example.com
or atexample.com
. The issue only appears when a single<audio>
is streamed partially from both.here's the code from the relevant part of the service worker. As far as I can tell, there isn't much out of the ordinary here, except maybe that the responses are always cloned with
response.clone()
to allow the service-worker to work on caching them while responding to the fetch event.async function fetchLocalOrRemote (request: Request) { try { const url = request.url.replace(location.origin, 'https://local.example.com') const response = await fetch(url, { headers: new Headers(request.headers), }) if (response.ok) { return response } } catch { } return fetch(request) } async function fetchFromServer (event: FetchEvent, request: Request) { const response = await fetchLocalOrRemote(request) if (response.status === 206) { response.clone() .arrayBuffer() .then((buffer) => { // do something unrelated to cache the response }) } else if (response.status === 200) { const cacheResponse = response.clone() // do something unrelated to cache the response } return response } async function fetchFromCache (event: FetchEvent, request: Request) { const response = await caches.match(event.request.url, { ignoreVary: true, ignoreSearch: true, cacheName: CACHES.media, }) if (!response) { return fetchFromServer(event, request) } // something unrelated to respond from cache } function onFetch (event: FetchEvent) { const request = event.request if (request.method === "GET") { const url = new URL(request.url) if (url.pathname.startsWith("/api/file")) { event.respondWith(fetchFromCache(event, request)) } } } self.addEventListener("fetch", onFetch)
Can some explain why the audio playback gets interrupted when it started playing as a stream of 206 responses from an origin, and in the middle of streaming, starts receiving 206 responses from another origin ?
The response header received by the audio element are identical, whether it's from local.example.com
or from example.com
:
Request URL: https://example.com/api/file/clh2jwi0s030kyqjx2i45ct6w
Request Method: GET
Status Code: 206 (from service worker)
Referrer Policy: strict-origin-when-cross-origin
accept-ch: Sec-CH-DPR, Sec-CH-Viewport-Width, Downlink
access-control-allow-origin: https://example.com
access-control-expose-headers: Content-Range
cache-control: public, max-age=31536000
Content-Length: 524288
Content-Range: bytes 3145728-3670015/6044646
content-type: audio/MPEG
date: Thu, 04 May 2023 14:15:14 GMT
server: Apache
service-worker-allowed: /