3

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 like 192.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 to example.com
  • the client receives its Response from the service-worker as if nothing happened

You can see this happen in the devtools: enter image description here 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)
    

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: /
Sheraff
  • 5,730
  • 3
  • 28
  • 53

1 Answers1

1

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.

Sheraff
  • 5,730
  • 3
  • 28
  • 53