Watch once, scrub freely forever.
Persistent HTTP 206 byte-range disk cache & cross-URL resume for mpv.
Install • Why? • How It Works • Options • Commands • Technical Deep Dive • FAQ
| For Everyday Watching | For Technical / Power Users |
|---|---|
Never re-download what you already watched. Seeking backward, closing mpv, or rewatching tomorrow plays cached parts straight from your SSD/HDD at zero bandwidth cost. |
Disjoint HTTP 206 interval cache. Merges downloaded inclusive [start, end] intervals and splits new requests into ordered cache hits and net gap fetches with leading-error prefetching. |
| Scrub to the end without wasting disk space. Jumping 90 minutes into a 40 GB stream stores only the few megabytes you actually played. | Native NTFS / POSIX sparse files. Newly created cache.data files invoke FSCTL_SET_SPARSE (0x000900C4) via Win32 DeviceIoControl, so uncached holes occupy zero physical clusters. |
| Expiring links & new mirrors keep your cache. When a CDN link rotates or you switch hosts for the same file, playback and resume pick up right where you left off. | Content-addressable alias matching. Compares exact total_size plus a SHA-256 fingerprint of the first 64 KB (HEAD_HASH_BYTES = 65536), linking URL hashes cleanly via alias_keys. |
Clean, human-readable cache folders. Every movie/video gets its own named folder (Movie.2025.mkv/) instead of cryptic hash soup. |
Collision-safe subfolder store & atomic metadata. Uses sanitized path names (with __<key8> tie-breakers), atomic temp-file writes (os.replace), and automatic legacy flat-file migration. |
Works seamlessly with your existing VPN / HTTP proxy. mpv.conf proxy and TLS settings continue protecting your remote requests automatically. |
Loopback proxy bypass + header tunneling. Clears file-local-options/http-proxy for 127.0.0.1 traffic while forwarding upstream http-proxy, tls-verify, and network-timeout via X-Mpv-Cache-* headers. |
By default, mpv's built-in demuxer cache (--cache=yes, --demuxer-max-bytes=...) lives only in RAM and evaporates the instant you close the player—or as soon as older chunks get evicted during long streams.
With mpv-http-range-cache:
- Replay & backward seeks are instant even days later or offline.
- Slow or capped connections stay usable—you download every byte at most once.
- Signed / expiring CDN URLs don't waste gigabytes—matching by
ignore_query_paramsormatch_by_content=yesreuses existing bytes AND keeps yoursave-position-on-quittimestamp intact. - Your native
mpvsetup stays untouched—ytdl_hook,ffmpeg,osc/uosc, subtitles, andmpv.confnetwork options keep working normally.
- Zero-hassle transparent hooking: Intercepts
http://andhttps://streams aton_loadpriority50(afterytdl_hookresolves extractors at priority10), and cleanly restores originalstream-open-filename,http-proxy, and headers onon_load_fail(priority5). - Hybrid cache + network range planner: A single HTTP
Rangerequest spanning both cached and uncached gaps streams cached chunks straight from disk and fetches only missing upstream spans—no duplicate requests. - Cross-URL
watch_latersynchronization: Periodically snapshots playback timestamps (every 15s and via detached asyncend-filedispatch) intocache.watch_laterinside each video's subfolder and propagates state across mirror URL hashes so nativesave-position-on-quit=yesresumes seamlessly across URL changes. - Smart protocol & media exclusions: Leaves local files, loopback servers (
127.0.0.1,localhost,::1), and adaptive manifests (.m3u8,.mpd,.pls, HLS/DASH/HTML MIME types) untouched, while falling back cleanly on HTTP200-only upstreams that ignoreRange. - Single-instance daemon lifecycle: Spawns one background
proxy.pyprocess percache_dirguarded by an exclusiveproxy.lock, advertises its ephemeral loopback port inserver.json, flushes & closes data handles when idle (release_user) so OS file explorers display accurate sizes, and exits automatically afteridle_timeout. - Optional LRU size budget (
max_cache_size_mb): Automatically evicts least-recently-accessed cache folders (while protecting the currently streaming file) when a disk cap is configured. - OSD feedback & bindings: Displays cached percentages and resume notices via OSD, plus unbound commands for status inspection, clearing current/all entries, and toggling caching on the fly.
Think of mpv-http-range-cache as a tiny smart DVR sitting between mpv and the internet:
- You open an online video in
mpv. - Instead of
mpvasking the server directly for every seek, it asks the local DVR running on your computer. - Anything the DVR hasn't saved yet is downloaded once (through your proxy/VPN if configured) and saved into a clean folder named after the video.
- Next time you rewind, restart
mpv, or even open a different link for the same file, the DVR handsmpvthe saved parts immediately from disk.
mpv player
│
├─ on_load (priority 50, after ytdl_hook)
│ ├─ local file / 127.0.0.1 / .m3u8 / .mpd ──▶ direct playback
│ └─ http(s) media stream:
│ • moves mpv's http-proxy ─▶ X-Mpv-Cache-Proxy header
│ • sets file-local-options/http-proxy="" (bypass proxy on loopback)
│ • rewrites stream-open-filename ─▶ http://127.0.0.1:<port>/stream?url=...
▼
proxy.py (127.0.0.1:<ephemeral-port>, async aiohttp server + ClientSession pool, singleton via proxy.lock)
├─ GET /__watch_later ──▶ restores newest position across mirror URLs
├─ GET /stream ──▶ plan_segments(cached_intervals, req_start, req_end)
│ ├─ "cache" span ──▶ async thread pread from <cache_dir>/<file>/cache.data
│ └─ "net" span ──▶ sliding-window parallel Range workers (connections × part_size_kb)
│ └─ ordered stream ─▶ client + sparse file + merge_intervals
└─ POST /__watch_later ──▶ saves position on 15s tick & end-file (clears on EOF)
-
mpv 0.33 or newer
-
Python 3.8+ with
aiohttp:pip install aiohttp
-
Optional:
curlinPATH(used by Lua for sub-10ms control calls; automatically falls back topythonurllib.requestif absent).irm https://raw.githubusercontent.com/Arsks12/mpv-http-range-cache/HEAD/installers/windows.ps1 | iex
Installs into
MPV_CONFIG_DIR, nearbyportable_config, or%APPDATA%\mpv, backing up any previous version automatically before updating.
Requires curl and unzip.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Arsks12/mpv-http-range-cache/HEAD/installers/unix.sh)"Auto-detects
MPV_CONFIG_DIR, Flatpak (io.mpv.Mpv), Snap (mpv/mpv-wayland),~/.config/mpv($XDG_CONFIG_HOME), and macOS.
-
Clone or extract this repository as
http-range-cacheinside yourmpvscripts/folder (mpv config locations):git clone https://github.com/Arsks12/mpv-http-range-cache.git ~/.config/mpv/scripts/http-range-cache(On Windows:
%APPDATA%\mpv\scripts\http-range-cache.) -
Install Python dependencies:
pip install -r ~/.config/mpv/scripts/http-range-cache/requirements.txt -
Copy
http_range_cache.confto<mpv-config>/script-opts/http_range_cache.confand adjustcache_dirif you want a dedicated drive/partition (for examplecache_dir=D:/mpv-cache).
# Enables cross-session and cross-URL playback resume
save-position-on-quit=yes
# Keep a healthy in-memory demuxer cache alongside the persistent disk cache
cache=yes
demuxer-max-bytes=2GiB
# Optional upstream proxy (automatically forwarded for uncached requests)
# http-proxy=http://user:pass@host:portAll options live in <mpv-config>/script-opts/http_range_cache.conf (documented template: http_range_cache.conf).
| Option | Type / Values | Default | What It Controls |
|---|---|---|---|
enabled |
yes / no |
yes |
Master switch at mpv startup (can also be toggled live with toggle-cache). |
cache_dir |
Path (~~ supported) |
~~/cache/http-range-cache |
Root folder containing per-file cache directories, server.json, proxy.lock, and optional proxy.log. |
idle_timeout |
Seconds (0 = never) |
300 |
Auto-terminates proxy.py after N seconds with zero active HTTP requests. |
max_cache_size_mb |
Megabytes (0 = unlimited) |
0 |
Soft LRU disk cap; oldest-accessed entries are deleted while protecting the active stream. |
connections |
Integer | 4 |
Concurrent upstream Range connections per missed network segment (1–32). |
part_size_kb |
Kilobytes | 2048 |
Byte-range chunk size fetched per parallel worker (256–16384). |
chunk_size |
Bytes | 65536 |
Read/write block size (64 KB default, minimum enforced 8192 bytes). |
flush_interval_kb |
Kilobytes | 1024 |
Writes cache.meta.json every N KB downloaded (also flushed whenever a connection finishes). |
ignore_query_params |
Comma list | (empty) | URL query keys stripped before key hashing (e.g. token,sig,expires). |
match_by_content |
yes / no |
yes |
Deduplicates distinct URLs hosting identical files via total_size + 64 KB SHA-256 head hash. |
python_path |
Path / auto |
auto |
Explicit interpreter path or auto probe order (pythonw -> python -> python3 on Windows; python3 -> python elsewhere). |
show_osd |
yes / no |
yes |
Displays load cache percentage, cross-URL resume messages, and command notifications. |
debug_log |
yes / no |
no |
Redirects proxy stdout/stderr to <cache_dir>/proxy.log for troubleshooting. |
All commands are unbound by default. Bind them in input.conf using script-binding http-range-cache/{command} or script-message {message}:
# Bind keys in input.conf
alt+c script-binding http-range-cache/cache-status
alt+x script-binding http-range-cache/clear-current-cache
alt+X script-binding http-range-cache/clear-all-cache
alt+t script-binding http-range-cache/toggle-cacheBinding (script-binding) |
Script Message (script-message) |
Action |
|---|---|---|
http-range-cache/cache-status |
http-range-cache-status |
OSD readout of current file cached bytes / total / percentage / disjoint range count, plus total disk usage across all cached files. |
http-range-cache/clear-current-cache |
http-range-cache-clear-current |
Deletes the active (or last proxied) stream's cache subfolder and aliases cleanly. |
http-range-cache/clear-all-cache |
http-range-cache-clear-all |
Removes every cached stream folder in cache_dir (while protecting proxy control files). |
http-range-cache/toggle-cache |
http-range-cache-toggle |
Enables/disables caching for subsequent file loads without restarting mpv. |
uosc Context Menu Integration
Add #! comments in input.conf to expose cache controls directly inside uosc's right-click menu:
alt+c script-binding http-range-cache/cache-status #! Cache > Status
alt+t script-binding http-range-cache/toggle-cache #! Cache > Toggle range cache
# script-binding http-range-cache/clear-current-cache #! Cache > Clear current stream
# script-binding http-range-cache/clear-all-cache #! Cache > Clear all cacheon_loadruns at priority50, intentionally afterytdl_hook(priority10), so resolved direct media URLs fromyt-dlpget cached when they point to HTTP/HTTPS streams—while preservingmpv'sfile-local-options/http-header-fields.- Any incoming
X-Mpv-Cache-*headers are sanitized before settingX-Mpv-Cache-Proxy,X-Mpv-Cache-Tls-Verify, andX-Mpv-Cache-Timeout. on_load_failruns at priority5(earlier thanytdl_hookat10), restoringstream-open-filename,file-local-options/http-proxy, andhttp-header-fieldsif local proxy startup or upstream connection fails before playback begins.
- Cached spans are stored as sorted disjoint inclusive intervals
[[start, end], ...]. Every write merges touching/overlapping spans in$O(k)$ viamerge_intervals. - For an incoming client request
[req_start, req_end],plan_segmentsemits a minimal alternating sequence of("cache", s, e)and("net", s, e)segments. - Network spans run through an async
aiohttpsliding-window parallel downloader (_fetch_parallel_net_segment): splits[s, e]intopart_size_kbparts across up toconnectionsconcurrent workers sharing a pooledaiohttp.ClientSession(keep-alive + DNS cache), writes each part at its exact sparse offset immediately on arrival, and streams parts in order tompv. On cold start, the firstpart_size_kbprobe hands its already-open stream directly into part 0 without a duplicate request. - If
segments[0]requires network ("net"), the proxy opens the upstream probe before committing client response headers, allowing genuine HTTP errors (403,404,502) to propagate cleanly instead of truncating a fake200/206body. - Incoming client disconnects (
ConnectionResetError,ClientConnectionResetError,CancelledError,OSError) are trapped gracefully without corrupting already-written upstream chunks. - All disk/metadata I/O inside
proxy.py(read_chunk,write_chunk,save_meta,sync_watch_later,clear_url,clear_all, deduplication hashing, and LRU eviction) is offloaded viaasyncio.to_thread(...), and allmain.luaprobes/HTTP requests run viamp.command_native_async, keeping both theaiohttpevent loop andmpv's playback thread non-blocking end to end. - Upstream requests force
Accept-Encoding: identityand strip hop-by-hop headers (HOP_BY_HOP_HEADERS) + internalX-Mpv-Cache-*headers so byte offsets on disk always stay 1:1 with the wire stream.
- On a fresh
bytes=0-(offset0) fetch where another cached entry already has the sametotal_sizeand cached bytes covering[0, min(65536, total_size) - 1],proxy.pybuffers the first 64 KB from the upstream stream, hashes it with SHA-256 (head_hash), and scans matching size candidates. - On a match,
link_aliascloses the redundant upstream stream immediately, binds the new URL hash to the existing entry'salias_keys, records the URL inknown_urls(capped at 30 recent URLs), and begins serving cached segments from disk immediately. - For playback resume,
mpvnatively hashes the rewritten or original path differently across URLs;main.luaqueriesGET /__watch_lateronfile-loadedand posts updates toPOST /__watch_laterevery 15s and onend-file(clearingcache.watch_laterwhenreason == "eof"). If another mirror URL has awatch_laterfile >1s newer than the current one, the proxy writes the synced MD5watch_laterfile and returnsstart+sid/aid/sub-delay/audio-delayproperties to seek seamlessly.
| Endpoint | Method | Purpose |
|---|---|---|
/__health |
GET, HEAD |
Returns {"status": "ok", "pid": ..., "port": ...} for readiness probes. |
/__status?url=... |
GET |
Global stats (entries_count, total_cached_bytes, cache_dir) + current entry metadata. |
/__watch_later?url=...&wl_dir=... |
GET |
Resolves newest saved resume state across cache.watch_later and known_urls. |
/__watch_later?url=...&wl_dir=...&pos=...&reason=... |
POST |
Persists cache.watch_later and mirrors to existing watch_later files (deletes backup on eof). |
/__clear?url=... |
POST |
Invalidates & deletes the entry subfolder and all aliases for url. |
/__clear_all |
POST |
Purges all cached subfolders and legacy files in cache_dir. |
/stream?url=... |
GET, HEAD |
Serves cached/upstream byte ranges with full Content-Range / 416 / 200-fallback support. |
<cache_dir>/
├── Movie.Name.2025.1080p.mkv/
│ ├── cache.data # Sparse binary file containing only downloaded intervals
│ ├── cache.meta.json # Key, URLs, alias_keys, total_size, head_hash, intervals, timestamps
│ └── cache.watch_later # Latest synced watch_later snapshot for cross-URL resume
├── Another.Movie.mkv__a1b2c3d4/
│ └── ... # Auto-suffixed with 8-char SHA-256 prefix on filename collision
├── proxy.lock # OS file lock enforcing one proxy process per cache_dir
├── server.json # {"pid": ..., "port": ..., "started_at": ...}
└── proxy.log # Debug output when debug_log=yes
Example cache.meta.json schema
{
"key": "f09051468bef30c0d630df0bdaf29d3f4045cb2c115dab759f6f2d7f80b7ed68",
"url": "https://cdn-mirror-2.example.com/videos/Movie.Name.2025.1080p.mkv",
"total_size": 4831838208,
"accept_ranges": true,
"content_type": "video/x-matroska",
"etag": "\"66a1-4b2\"",
"last_modified": "Wed, 01 Oct 2025 12:00:00 GMT",
"head_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"known_urls": [
"https://cdn-1.example.com/Movie.Name.2025.1080p.mkv",
"https://cdn-mirror-2.example.com/videos/Movie.Name.2025.1080p.mkv"
],
"alias_keys": ["e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
"ranges": [[0, 67108863], [1073741824, 1140850687]],
"cached_bytes": 134217728,
"created_at": 1762880000.12,
"last_access": 1762881205.45
}Run the full test suite (unit helpers, legacy flat-file migration, mock HTTP range/dedup/watch_later server, plus end-to-end headless mpv online/offline/cross-URL tests):
python tests/test_range_cache.pySkip mpv integration tests when running in headless CI without mpv/ffmpeg:
python tests/test_range_cache.py --skip-mpvContributions are welcome:
- Follow formatting rules in
.editorconfig(UTF-8, LF, tabs in.lua/.sh/.ps1, 4 spaces in.py). - Ensure
python -m py_compile proxy.py tests/test_range_cache.pyandpython tests/test_range_cache.pypass before opening a pull request.
Two things happen on Windows:
- Windows Explorer caches directory entry sizes while a write handle remains open. Once
mpvcloses connections (_active_users == 0),proxy.pyflushes buffers and closescache.data, and Explorer immediately reflects the true byte span. cache.datais marked sparse (FSCTL_SET_SPARSE), so Size reflects the highest written byte offset while Size on disk reflects only actual cached chunks.
mpv's embedded Lua sandbox has no async HTTP server sockets, pooled multi-connection Range downloaders, or Win32 DeviceIoControl sparse-file calls. Offloading HTTP range splicing, parallel part fetching, and sparse disk I/O to a tiny localhost aiohttp process (proxy.py) lets mpv keep using its native ffmpeg demuxer cache and fast seeks untouched.
Everything keeps working as expected. main.lua copies your configured http-proxy into X-Mpv-Cache-Proxy, clears file-local-options/http-proxy so mpv talks directly to 127.0.0.1 (avoiding proxy loops or corporate proxy blocks on loopback), and proxy.py sends all uncached upstream requests through your proxy after stripping X-Mpv-Cache-*.
No:
- Playlist/adaptive manifests (
.m3u8,.m3u,.mpd,.pls, HLS/DASH/HTML responses) are automatically bypassed so live streams play normally. - Servers returning full HTTP
200withoutRangesupport still cache bytes sequentially from offset0(skip_leadinghandles offset requests while writing received chunks).