\n
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.
example.com
local.example.com
(SSL certificates, signed by certificate authority, and DNS resolution points to a private IP like 192.168.0.4
)If I leave my home with an audio file currently streaming, here's what happens
\nlocal.example.com
fails because the IP is not reachable anymorecatch
the error, and start a new request to example.com
Response
from the service-worker as if nothing happenedYou can see this happen in the devtools:\n\nThe 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.
\nTo diagnose this issue:
\nthere is nothing specific on the client, just
\n<audio src="example.com/api/file/12345678" hidden playsinline autoplay></audio>\n
\nthere is nothing fantastic on the server either. It streams totally fine when reached at local.example.com
or at example.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) {\n try {\n const url = request.url.replace(location.origin, 'https://local.example.com')\n const response = await fetch(url, {\n headers: new Headers(request.headers),\n })\n if (response.ok) {\n return response\n }\n } catch { }\n return fetch(request)\n}\n\nasync function fetchFromServer (event: FetchEvent, request: Request) {\n const response = await fetchLocalOrRemote(request)\n if (response.status === 206) {\n response.clone()\n .arrayBuffer()\n .then((buffer) => {\n // do something unrelated to cache the response\n })\n } else if (response.status === 200) {\n const cacheResponse = response.clone()\n // do something unrelated to cache the response\n }\n return response\n}\n\nasync function fetchFromCache (event: FetchEvent, request: Request) {\n const response = await caches.match(event.request.url, {\n ignoreVary: true,\n ignoreSearch: true,\n cacheName: CACHES.media,\n })\n if (!response) {\n return fetchFromServer(event, request)\n }\n // something unrelated to respond from cache\n}\n\nfunction onFetch (event: FetchEvent) {\n const request = event.request\n if (request.method === "GET") {\n const url = new URL(request.url)\n if (url.pathname.startsWith("/api/file")) {\n event.respondWith(fetchFromCache(event, request))\n }\n }\n}\n\nself.addEventListener("fetch", onFetch)\n
\nThe 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\nRequest Method: GET\nStatus Code: 206 (from service worker)\nReferrer Policy: strict-origin-when-cross-origin\n\naccept-ch: Sec-CH-DPR, Sec-CH-Viewport-Width, Downlink\naccess-control-allow-origin: https://example.com\naccess-control-expose-headers: Content-Range\ncache-control: public, max-age=31536000\nContent-Length: 524288\nContent-Range: bytes 3145728-3670015/6044646\ncontent-type: audio/MPEG\ndate: Thu, 04 May 2023 14:15:14 GMT\nserver: Apache\nservice-worker-allowed: /\n
\n","author":{"@type":"Person","name":"Sheraff"},"upvoteCount":3,"answerCount":1,"acceptedAnswer":null}}Reputation: 6772
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.
example.com
local.example.com
(SSL certificates, signed by certificate authority, and DNS resolution points to a private IP like 192.168.0.4
)If I leave my home with an audio file currently streaming, here's what happens
local.example.com
fails because the IP is not reachable anymorecatch
the error, and start a new request to example.com
Response
from the service-worker as if nothing happenedYou 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 at example.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)
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: /
Upvotes: 3
Views: 119
Reputation: 6772
The answer is that for the <audio>
to be able to receive chunks from multiple origins when requesting a single source, you need to give it the crossorigin
attribute.
Upvotes: 1