feat(downloads): guarantee every download is browser-playable (mp4/H.264/AAC)

The Download module's purpose is download -> share -> play in-browser, so the
output must play in EVERY browser (iOS/Safari/WebKit included). The only
universally-safe combo is mp4 + H.264 + AAC/MP3.

- formats.normalize() now CLAMPS every spec to that profile (container->mp4,
  vcodec->None, audio_format opus->m4a) at the single point all paths flow
  through (picker, stored profile, raw API) — not just by hiding UI options. A
  source exposing no H.264 is still caught by the worker's browser-safe
  re-encode, so playability is guaranteed end-to-end.
- build_ydl_opts drops the now-unreachable non-compat branches (+ _VCODEC_SORT).
- _SIG_VERSION 3->4 so assets cached under an old non-compat spec — or a stale
  non-H.264 mp4 that slipped the v3 net (the HEVC Facebook dls) — re-derive.
- ProfileEditor drops the container/codec pickers and Opus; audio is M4A/MP3.
- i18n: remove the now-unused container/vcodec/any keys (HU+EN parity).
This commit is contained in:
2026-08-29 06:08:21 +02:00
parent fb2c843fa9
commit 94fcc40bde
5 changed files with 83 additions and 93 deletions
+32 -10
View File
@@ -1,6 +1,7 @@
"""build_ydl_opts: the mp4 compat profile must SELECT H.264 (not merely prefer it), so shared
downloads play on iOS/Safari, while custom vcodec / non-mp4 profiles opt out. See the iPad AV1
broken-player fix."""
"""build_ydl_opts: EVERY download is clamped to the mp4/H.264 compat profile (E7 browser-
playability guarantee) and must SELECT H.264 (not merely prefer it), so shared downloads play in
every browser incl. iOS/Safari. Non-mp4 containers and non-H.264 vcodecs are coerced, not honoured.
See the iPad AV1 broken-player fix + E7 format narrowing."""
from app.downloads import edit as editmod
from app.downloads import formats
@@ -56,16 +57,37 @@ def test_max_height_caps_every_branch():
assert fmt.count("[height<=?720]") >= 3 # applied to each avc1/fallback branch
def test_explicit_vcodec_opts_out_of_avc1_selector():
def test_explicit_vcodec_is_clamped_to_the_compat_selector():
# E7 browser-playability guarantee: a non-H.264 vcodec no longer opts out — normalize() coerces
# vcodec→None, so the avc1 selector (and mp4 merge) is used regardless of what was requested.
opts = _opts({"mode": "av", "container": "mp4", "vcodec": "vp9"})
assert "vcodec^=avc1" not in opts["format"]
assert opts["format"] == "bestvideo+bestaudio/best"
assert "vcodec:vp9" in opts["format_sort"]
assert opts["format"].startswith("bestvideo[vcodec^=avc1]")
assert opts["merge_output_format"] == "mp4"
assert "vcodec:vp9" not in opts["format_sort"]
def test_non_mp4_container_opts_out():
fmt = _opts({"mode": "av", "container": "webm"})["format"]
assert "vcodec^=avc1" not in fmt
def test_non_mp4_container_is_clamped_to_mp4():
# A webm/mkv request is coerced to the mp4/H.264 compat profile (nothing plays mkv in-browser;
# webm/VP9 fails iOS), so the avc1 selector is used and the merge target is mp4.
opts = _opts({"mode": "av", "container": "webm"})
assert opts["format"].startswith("bestvideo[vcodec^=avc1]")
assert opts["merge_output_format"] == "mp4"
def test_normalize_clamps_to_browser_playable():
n = formats.normalize({"container": "mkv", "vcodec": "av1", "audio_format": "opus"})
assert n["container"] == "mp4"
assert n["vcodec"] is None
assert n["audio_format"] == "m4a"
# A playable audio choice is preserved.
assert formats.normalize({"audio_format": "mp3"})["audio_format"] == "mp3"
def test_non_playable_specs_dedup_to_the_compat_identity():
# Two requests that differ only in (now-clamped) non-playable fields share ONE cache asset.
a = formats.format_sig({"mode": "av", "max_height": 720, "container": "webm", "vcodec": "av1"})
b = formats.format_sig({"mode": "av", "max_height": 720, "container": "mp4"})
assert a == b
def test_audio_only_unaffected():