fix(downloads): prefer H.264+AAC for mp4 so shared files play on iOS/Safari

mp4 downloads used yt-dlp's default best video+audio, which on YouTube is
VP9+Opus at 1080p. Chromium plays that, but iOS/Safari WebKit (which every
iOS browser is forced to use) decodes only H.264/H.265+AAC inside mp4 — so a
shared /watch link showed a broken player on iPhone/iPad while the download
still worked. Rank vcodec:h264 + acodec:aac above resolution for mp4 output
(unless a custom profile pins a vcodec), keeping VP9/AV1 as a graceful
fallback. Bump the format signature (v2) so an identical spec gets a fresh
cache identity instead of hitting a stale VP9 asset.
This commit is contained in:
npeter83 2026-07-04 17:29:05 +02:00
parent 524507ce9b
commit ea1df66dc3

View file

@ -44,6 +44,11 @@ _DEFAULTS = {
"sponsorblock": False,
}
# Bump when the spec→output-bytes mapping changes so an identical spec gets a fresh cache
# identity instead of hitting a stale asset produced by the old rule. v2: mp4 downloads now
# prefer H.264+AAC (browser/iOS-compatible) rather than yt-dlp's default VP9/Opus best.
_SIG_VERSION = 2
_SPONSORBLOCK_CATEGORIES = ["sponsor", "selfpromo", "interaction"]
_VCODEC_SORT = {"h264": "vcodec:h264", "vp9": "vcodec:vp9", "av1": "vcodec:av01"}
@ -69,7 +74,9 @@ def normalize(spec: dict) -> dict:
def format_sig(spec: dict) -> str:
"""Stable short hash of the output-affecting spec — the MediaAsset cache key component."""
s = normalize(spec)
payload = json.dumps({k: s[k] for k in _SIG_KEYS}, sort_keys=True, separators=(",", ":"))
data = {k: s[k] for k in _SIG_KEYS}
data["_v"] = _SIG_VERSION
payload = json.dumps(data, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode()).hexdigest()[:24]
@ -145,6 +152,16 @@ def build_ydl_opts(
if s["container"]:
opts["merge_output_format"] = s["container"]
sort = []
# Compatibility-first for mp4 (the universal container): prefer H.264 video + AAC
# audio so shared/downloaded files play in EVERY browser, including iOS/Safari.
# WebKit (which every iOS browser, Brave included, is forced to use) decodes only
# H.264/H.265 + AAC inside mp4 — a VP9/Opus-in-mp4 file shows a broken player there,
# even though Chromium plays it fine. These sort keys rank codec ABOVE resolution, so
# a 720p H.264 stream is chosen over a 1080p-VP9-only one; VP9/AV1 remain a graceful
# fallback only when no H.264 stream exists. A custom profile that explicitly sets
# `vcodec` opts out (its codec choice wins over the compatibility default).
if s["container"] == "mp4" and s["vcodec"] is None:
sort += ["vcodec:h264", "acodec:aac"]
if h:
sort.append(f"res:{h}")
if s["vcodec"] in _VCODEC_SORT: