I used the following steps to accomplish offline video on the first page load without first watching the entire video(s).
- 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.
- 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 => {
...
});
- 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: