Offline video in a PWA: handling large media files the right way
Progressive Web Apps have made offline support a realistic expectation for web applications. While the Cache API is the natural first choice for storing documents, stylesheets, and images for offline use, it becomes impractical when the assets in question are large video files. The Cache API does not support pausing or resuming downloads, tracking download progress, or responding to HTTP range requests—all of which are critical for any video application.
For media-heavy PWAs, a more robust approach is needed. The demo PWA Kino (source code available on GitHub) demonstrates a practical implementation of offline streaming without using any functional or presentational frameworks. While educational in nature, the techniques shown are foundational for building custom solutions when existing media frameworks are not an option.
Downloading and storing large video data
The Fetch API offers a cross-browser way to access remote files as a readable stream. Combined with HTTP range requests, you can download large video files incrementally, storing each chunk as it arrives. A basic download loop looks like this:
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
// process the chunk of data
}
Because a media file is typically just one property of a larger structured object—which also includes metadata like name, description, and runtime—the IndexedDB API is the right choice for storage. It handles substantial amounts of binary data easily and supports indexes for fast lookups.
Pausing and resuming downloads
When a download is interrupted, the chunks that have already arrived need to remain safely stored so the download can be resumed later. Since the Kino server supports HTTP range requests, resuming is a matter of requesting only the remaining portion of the file:
const getDownloadResponse = (url, from) => fetch(url, {
headers: {
Range: `bytes=${from}-`,
},
});
A write buffer for IndexedDB
Storing each incoming chunk in IndexedDB as a separate transaction creates significant overhead. Network streams can emit many small chunks in rapid succession, which means the number of IndexedDB transactions quickly becomes a bottleneck.
The Kino demo solves this with an intermediary write buffer. Incoming data chunks are appended to the buffer first. If the buffer is full, it is flushed to the database and cleared before processing the rest of the incoming data. This reduces the frequency of writes and substantially improves performance.
Serving media files from offline storage
Once a video is stored, the service worker should serve it from IndexedDB rather than hitting the network. The event.respondWith() method requires a Response object, which can be constructed from several types, including a Blob, BufferSource, or ReadableStream. For large files, a ReadableStream is preferable because it does not hold all data in memory at once.
To further support efficient playback, a basic implementation of HTTP range requests is necessary so browsers can request only the portion of the file they currently need:
if (request.headers.has("range")) {
// Return partial content based on the requested byte range
}
The complete service worker implementation in the Kino source shows how to read file data from IndexedDB and construct a stream for a real application.
Additional features for a complete offline experience
With downloading, storage, and serving addressed, a video PWA can still benefit from a few polish touches, many of which are demonstrated in Kino:
- Media Session API integration: Lets users control playback from hardware media keys or notification popups.
- Caching supplementary assets: Subtitles and poster images can be stored via the Cache API without issue.
- Adaptive stream downloads: For DASH or HLS content, manifests may declare multiple bitrate sources. In that case, the manifest should be transformed to keep only one selected representation before storing it for offline viewing.



