Service Worker Caching

Service workers provide a way to have a script run in the background even when the page is not loaded. It can respond to push events from other sites and can act as a transparent cache for the page.

This doc will show a basic example of how to create a transparent cache for Shaka that will not interfere with bandwidth estimation (for uncached segments). This is NOT a tutorial of service workers in general.

Cache Header

Shaka looks for a special header X-Shaka-From-Cache to indicate that a response was from a cache. This tells us to ignore the response for bandwidth estimation because the time is not accurate (i.e. it was loaded from disk). Simply adding this to the response object will ensure that cached segments will not interfere with bandwidth estimates.

Example Caching Service Worker

Registering code in the app:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service_worker.js').then(function() {
    console.log('Service worker registered successfully');
  }).catch(function(err) {
    console.error('Error registering service worker', err);
  });
} else {
  console.error('Browser doesn\'t support service workers');
}

Service worker code (/service_worker.js):

var CACHE_NAME = 'segment-cache-v1';

function shouldCache(url) {
  return url.endsWith('.mp4') || url.endsWith('.m4s');
}

function loadFromCacheOrFetch(request) {
  return caches.open(CACHE_NAME).then(function(cache) {
    return cache.match(request).then(function(response) {
      if (response) {
        // The custom header was added before putting it in the cache.
        console.log('Handling cached request', request.url);
        return response;
      }

      // Request not cached, make a real request for the file.
      return fetch(request.clone()).then(function(response) {

        // Cache any successfully request for an MP4 segment.  Service
        // workers cannot cache 206 (Partial Content).  This means that
        // content that uses range requests (e.g. SegmentBase) will require
        // more work.
        if (response.ok && response.status != 206 && shouldCache(request.url)) {
          console.log('Caching MP4 segment', request.url);
          cacheResponse(cache, request, response);
        }

        return response;
      });
    });
  })
}

function cacheResponse(cache, request, response) {
  // Response objects are read-only, so to add our custom header, we need to
  // recreate the object.
  var init = {
    status: response.status,
    statusText: response.statusText,
    headers: {'X-Shaka-From-Cache': true}
  };

  response.headers.forEach(function(value, key) {
    init.headers[key] = value;
  });

  // Response objects are single use.  This means we need to call clone() so
  // we can both store the ArrayBuffer and give the response to the page.
  return response.clone().arrayBuffer().then(function(ab) {
    cache.put(request, new Response(ab, init));
  });
}


self.addEventListener('fetch', function(event) {
  event.respondWith(loadFromCacheOrFetch(event.request));
});