22

I have an offline app that caches all static resources. Currently, only the first 15 seconds of video assets are cached.

Below shows basic implementations of the install and fetch event listeners.

Service Worker:

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/videos/one.mp4',
        '/videos/two.mp4'
      ]);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      if (response) return response;
      return fetch(event.request);
    });
  );
});

And in index.html

<video controls preload>
  <source src="/videos/one.mp4" type="video/mp4">
</video>
Raphael Rafatpanah
  • 19,082
  • 25
  • 92
  • 158
  • 1
    maybe you can convert the video to a blob and save the blob. [this](https://simpl.info/video/offline/) seems to have some relevant techniques – danyamachine Jan 29 '17 at 02:17
  • Cache API service workers use [stores request/response pairs](https://developer.mozilla.org/en-US/docs/Web/API/Cache/put#Parameters). So I doubt if there is any file type limitation. [Quote:](https://developer.mozilla.org/en-US/docs/Web/API/Cache) `...a storage mechanism for Request / Response object pairs that are cached...` – Umur Karagöz Jan 29 '17 at 07:49
  • 1
    In fact I have put it to test and it works great, as expected. [You can check it here](https://lazysheepherd.com/sw-test/offline/) – Umur Karagöz Jan 29 '17 at 20:33
  • @UmurKaragöz Thank you, I did too and it does work great. (I've just posted an answer including code) – Raphael Rafatpanah Jan 29 '17 at 20:41

1 Answers1

22

I used the following steps to accomplish offline video on the first page load without first watching the entire video(s).

  1. Register a service worker and cache all requests. Static assets are just '/' for this case. If you inspect the service worker's fetch event, you'll see that subsequent requests are also cached.
  2. Use the fetch API to request a video as a blob.

Example using fetch to request a video as a blob

const videoRequest = fetch('/path/to/video.mp4').then(response => response.blob());
videoRequest.then(blob => {
  ...
});
  1. Use the IndexedDB API to store the blob. (Use IndexedDB instead of LocalStorage to avoid blocking the main thread while storing.)

That's it! Now when in offline mode, the service worker will intercept requests and serve both the html and blob from cache.

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Test</title>
</head>
<body>

  <h1>Service Worker Test</h1>

  <p>Try reloading the page without an Internet connection.</p>

  <video controls></video>

  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js').then(registration => {
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(error => {
          console.log('ServiceWorker registration failed: ', error);
        });
      });
    } else {
      alert('serviceWorker is not in navigator');
    }
  </script>

  <script>
    const videos = {
      one: document.querySelector('video')
    };

    const videoRequest = fetch('/path/to/video.mp4').then(response => response.blob());
    videoRequest.then(blob => {
      const request = indexedDB.open('databaseNameHere', 1);

      request.onsuccess = event => {
        const db = event.target.result;

        const transaction = db.transaction(['videos']);
        const objectStore = transaction.objectStore('videos');

        const test = objectStore.get('test');

        test.onerror = event => {
          console.log('error');
        };

        test.onsuccess = event => {
          videos.one.src = window.URL.createObjectURL(test.result.blob);
        };
      }

      request.onupgradeneeded = event => {
        const db = event.target.result;
        const objectStore = db.createObjectStore('videos', { keyPath: 'name' });

        objectStore.transaction.oncomplete = event => {
          const videoObjectStore = db.transaction('videos', 'readwrite').objectStore('videos');
          videoObjectStore.add({name: 'test', blob: blob});
        };
      }
    });
  </script>
</body>
</html>

Service Worker

const latest = {
  cache: 'some-cache-name/v1'
};

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(latest.cache).then(cache => {
      return cache.addAll([
        '/'
      ]);
    })
  );
});

self.addEventListener('fetch', event => {
  // exclude requests that start with chrome-extension://
  if (event.request.url.startsWith('chrome-extension://')) return;
  event.respondWith(
    caches.open(latest.cache).then(cache => {
      return cache.match(event.request).then(response => {
        var fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        })
        return response || fetchPromise;
      })
    })
  );
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.filter(cacheName => {
          if (cacheName === latest.cache) {
            return false;
          }

          return true;
        }).map(cacheName => {
          return caches.delete(cacheName)
        })
      );
    })
  );
});

Resources:

Raphael Rafatpanah
  • 19,082
  • 25
  • 92
  • 158
  • Hi @Raphael Rafatpanah,I am trying to do this for mutliple videos,is there any way of achieving this,basically I want to save a total of 5 videos in indexeddb and be able to list the videos in the browser so that I can click and play them while offline. – BLESSING Apr 03 '19 at 13:14
  • As long as the device has enough disc space, it might be doable. You may run into issues on iOS since I believe there are stricter storage limits compared to Chrome Here's an example to get you started: https://github.com/persianturtle/offline-first-example – Raphael Rafatpanah Apr 03 '19 at 15:31