cache_pause=no caused frame-wait-frame-wait on uncached videos because mpv kept playing through buffer underruns instead of pausing to refill. Flip to cache_pause=yes with a 2s resume threshold so playback is smooth after the initial buffer fill. Also: bump demuxer buffers (50→150MiB forward, add 75MiB back for backward seek without refetch), increase stream_buffer_size from default 128KiB to 4MiB to reduce syscall overhead, extend network timeout (10→30s) for slow CDNs, and set a browser-like user agent to avoid 403s from boorus that block mpv's default UA. behavior change
96 lines
3.8 KiB
Python
96 lines
3.8 KiB
Python
"""Pure helpers that build the kwargs dict passed to ``mpv.MPV`` and
|
|
the post-construction options dict applied via the property API.
|
|
|
|
Kept free of any Qt or mpv imports so the options can be audited from
|
|
a CI test that only installs the stdlib.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# FFmpeg ``protocol_whitelist`` value applied via mpv's
|
|
# ``demuxer-lavf-o`` option (audit finding #2). ``file`` must stay so
|
|
# cached local clips and ``.part`` files keep playing; ``http``/
|
|
# ``https``/``tls``/``tcp`` are needed for fresh network video.
|
|
# ``crypto`` is intentionally omitted — it's an FFmpeg pseudo-protocol
|
|
# for AES-decrypted streams that boorus do not legitimately serve.
|
|
LAVF_PROTOCOL_WHITELIST = "file,http,https,tls,tcp"
|
|
|
|
|
|
def lavf_options() -> dict[str, str]:
|
|
"""Return the FFmpeg lavf demuxer options to apply post-construction.
|
|
|
|
These cannot be set via ``mpv.MPV(**kwargs)`` because python-mpv's
|
|
init path uses ``mpv_set_option_string``, which routes through
|
|
mpv's keyvalue list parser. That parser splits on ``,`` to find
|
|
entries, so the comma-laden ``protocol_whitelist`` value gets
|
|
shredded into orphan tokens and mpv rejects the option with
|
|
-7 OPT_FORMAT. mpv's documented backslash escape (``\\,``) is
|
|
not unescaped on this code path either.
|
|
|
|
The post-construction property API DOES accept dict values for
|
|
keyvalue-list options via the node API, so we set them after
|
|
``mpv.MPV()`` returns. Caller pattern:
|
|
|
|
m = mpv.MPV(**build_mpv_kwargs(is_windows=...))
|
|
for k, v in lavf_options().items():
|
|
m["demuxer-lavf-o"] = {k: v}
|
|
"""
|
|
return {"protocol_whitelist": LAVF_PROTOCOL_WHITELIST}
|
|
|
|
|
|
def build_mpv_kwargs(is_windows: bool) -> dict[str, object]:
|
|
"""Return the kwargs dict for constructing ``mpv.MPV``.
|
|
|
|
The playback, audio, and network options are unchanged from
|
|
pre-audit v0.2.5. The security hardening added by SECURITY_AUDIT.md
|
|
finding #2 is:
|
|
|
|
- ``ytdl="no"``: refuse to delegate URL handling to yt-dlp. mpv's
|
|
default enables a yt-dlp hook script that matches ~1500 hosts
|
|
and shells out to ``yt-dlp`` on any URL it recognizes. A
|
|
compromised booru returning ``file_url: "https://youtube.com/..."``
|
|
would pull the user through whatever extractor CVE is current.
|
|
|
|
- ``load_scripts="no"``: do not auto-load Lua scripts from
|
|
``~/.config/mpv/scripts``. These scripts run in mpv's context
|
|
every time the widget is created.
|
|
|
|
- ``input_conf="/dev/null"`` (POSIX only): skip loading
|
|
``~/.config/mpv/input.conf``. The existing
|
|
``input_default_bindings=False`` + ``input_vo_keyboard=False``
|
|
are the primary lockdown; this is defense-in-depth. Windows
|
|
uses a different null-device path and the load behavior varies
|
|
by mpv build, so it is skipped there.
|
|
|
|
The ffmpeg protocol whitelist (also part of finding #2) is NOT
|
|
in this dict — see ``lavf_options`` for the explanation.
|
|
"""
|
|
kwargs: dict[str, object] = {
|
|
"vo": "libmpv",
|
|
"hwdec": "auto",
|
|
"keep_open": "yes",
|
|
"ao": "pulse,wasapi,",
|
|
"audio_client_name": "booru-viewer",
|
|
"input_default_bindings": False,
|
|
"input_vo_keyboard": False,
|
|
"osc": False,
|
|
"vd_lavc_fast": "yes",
|
|
"vd_lavc_skiploopfilter": "nonkey",
|
|
"cache": "yes",
|
|
"cache_pause": "yes",
|
|
"cache_pause_wait": "2",
|
|
"cache_pause_initial": "yes",
|
|
"cache_secs": "30",
|
|
"demuxer_max_bytes": "150MiB",
|
|
"demuxer_max_back_bytes": "75MiB",
|
|
"demuxer_readahead_secs": "20",
|
|
"stream_buffer_size": "4MiB",
|
|
"network_timeout": "30",
|
|
"user_agent": "Mozilla/5.0",
|
|
"ytdl": "no",
|
|
"load_scripts": "no",
|
|
}
|
|
if not is_windows:
|
|
kwargs["input_conf"] = "/dev/null"
|
|
return kwargs
|