fix(downloads): make shared files iOS-playable + default to 720p

Shared /watch links showed a "broken play button" on iPad (any iOS
browser — all forced onto WebKit) while playing fine on desktop Chrome.
Root cause: the stored mp4's video codec was AV1 (confirmed via ffprobe),
which WebKit can't decode. The existing H.264 guard was only a
format_sort *preference*, so a non-YouTube source (Facebook) that ships
AV1 as its best video-only stream slipped through.

- B1: for the mp4 compat profile, prefer H.264 at the SELECTION level
  with a progressive fallback (bestvideo[vcodec^=avc1]+bestaudio /
  best[vcodec^=avc1] / anything) so an avc1 progressive is chosen over
  an AV1-only video-only stream.
- B2: re-encode the rare AV1/VP9-only result to H.264+AAC (+faststart)
  in the worker before storing, gated on the compat profile; corrects
  the asset codec metadata to match.
- Bump _SIG_VERSION 2->3 so cached AV1 assets re-derive.
- Default download preset is now 720p (migration 0058 re-seeds builtins
  720p-first; ProfileEditor BLANK 1080->720).
- Tests for the selector, compat opt-outs, and the transcode predicate.
This commit is contained in:
2026-07-22 01:19:31 +02:00
parent 51781819db
commit 8fd0049dce
6 changed files with 236 additions and 16 deletions
+40
View File
@@ -268,6 +268,46 @@ def run_ffmpeg(cmd, total_s, on_progress, should_cancel) -> None:
raise RuntimeError(f"ffmpeg failed ({ret}): {err[:300]}")
# --- ffmpeg: iOS/Safari codec compatibility -------------------------------------------------
# Apple WebKit (every iOS browser, Brave included) decodes only H.264/H.265 in mp4 — an AV1/VP9
# file plays on desktop Chrome but shows a "broken player" on iPad. The mp4 compat profile now
# *selects* H.264, but a source may expose no H.264 stream at all, so this is the last-ditch net.
_IOS_SAFE_VCODECS = ("h264", "hevc")
def probe_vcodec(path: Path) -> str | None:
"""Return the file's primary video codec name (lowercased) via ffprobe, or None if unknown."""
try:
out = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=codec_name", "-of", "default=nw=1:nk=1", str(path)],
capture_output=True, text=True, timeout=30, check=True,
).stdout.strip().lower()
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, OSError):
return None
return out or None
def needs_ios_transcode(vcodec: str | None) -> bool:
"""True when an mp4's video codec won't play in Apple WebKit and should be re-encoded to H.264.
None (unknown/ffprobe failed) is treated as safe so we never re-encode on a bad probe."""
return bool(vcodec) and vcodec not in _IOS_SAFE_VCODECS and vcodec != "none"
def build_h264_transcode_cmd(src: Path, dest: Path) -> list[str]:
"""Re-encode `src` to an iOS-safe H.264 + AAC mp4 (faststart), preserving resolution.
`-progress pipe:1` streams machine-readable progress for run_ffmpeg."""
return [
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-nostdin",
"-progress", "pipe:1", "-nostats",
"-i", str(src),
"-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
"-c:a", "aac", "-b:a", "192k",
"-movflags", "+faststart",
str(dest),
]
# --- filmstrip (editor scrub track) ---------------------------------------------------------
_SB_COLS = 12