"""The phone-safe rule: what a library file must be so it plays wherever a share link lands. A shared `/watch` link is opened by whoever got it, on whatever they hold: an iPhone's WebKit, an Android Chrome, a desktop Firefox. The one shape every one of them plays: * video → H.264, 8-bit 4:2:0 (`yuv420p`, or its full-range twin `yuvj420p`), AAC audio, in .mp4 * audio only → MP3 in .mp3, or AAC in .m4a Measured on the owner's iPhone + iPad (2026-10-05): AV1, Matroska, HEVC tagged `hev1` and MP3 inside an .mp4 all fail there. VP9/WebM, HEVC `hvc1` and 10-bit H.264 played on those devices, but Chrome and Firefox do not decode HEVC and browsers decode 10-bit / 4:4:4 H.264 only patchily, so the rule is the shape every browser plays, not the shape one device plays. Every file that becomes a ready library asset passes through here — a download, an upload (the worker converts it, see `upload`), and an editor clip — so a link can only ever serve this shape. `plan` says what (if anything) must change; `build_cmd` is the one ffmpeg command that changes it: re-encode only the stream that fails, copy the rest, or just remux when only the container is wrong. """ from dataclasses import dataclass from pathlib import Path from app.downloads import edit as editmod from app.downloads import probe _SAFE_VCODEC = "h264" _SAFE_PIX_FMTS = frozenset({"yuv420p", "yuvj420p"}) _VIDEO_EXT = "mp4" # An audio-only file: the codec, and the extension it plays in. Anything else becomes AAC in .m4a. _AUDIO_HOME = {"mp3": "mp3", "aac": "m4a"} _AUDIO_FALLBACK_EXT = "m4a" @dataclass(frozen=True) class Plan: """What makes one file phone-safe. `ext` is the extension the safe file has (no dot).""" ext: str has_video: bool reencode_video: bool reencode_audio: bool needs_change: bool def plan(data: dict | None, ext: str) -> Plan | None: """The phone-safe plan for a file whose `probe.probe_json` is `data` and whose extension is `ext`; None when it is not media at all (an unreadable probe, a still image, no audio or video stream) — such a file is a download-only 'file' and never plays inline, so there is nothing to make safe.""" if data is None: return None video, audio = probe.primary_streams(data) ext = ext.lower().lstrip(".") if video is not None: rv = (video.get("codec_name") or "").lower() != _SAFE_VCODEC or ( video.get("pix_fmt") or "" ).lower() not in _SAFE_PIX_FMTS ra = audio is not None and (audio.get("codec_name") or "").lower() != "aac" return Plan(_VIDEO_EXT, True, rv, ra, rv or ra or ext != _VIDEO_EXT) if audio is not None: home = _AUDIO_HOME.get((audio.get("codec_name") or "").lower()) target = home or _AUDIO_FALLBACK_EXT return Plan(target, False, False, home is None, home is None or ext != target) return None def build_cmd(src: Path, dest: Path, p: Plan, threads: int | None = None) -> list[str]: """The ffmpeg command that writes `src` to `dest` (whose suffix is `p.ext`) in the phone-safe shape: the failing stream(s) re-encoded with the package's shared encoder settings, the rest stream-copied. An audio-only result drops any cover-art stream. `-progress pipe:1` streams machine-readable progress; `threads` caps the encoder so a re-encode cannot claim every core on the shared box (see the worker-child de-prioritization).""" cmd = [ "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-nostdin", "-progress", "pipe:1", "-nostats", *editmod.INPUT_GUARD, "-i", str(src), ] # Exactly the two streams `plan` judged: `V` is a video stream that is not cover art. Nothing # else rides along — a subtitle or data track the .mp4 muxer cannot take would fail the run. if p.has_video: cmd += ["-map", "0:V:0", "-map", "0:a:0?"] if p.reencode_video: cmd += ["-vf", editmod.EVEN_FRAME, *editmod.X264_ARGS] else: cmd += ["-c:v", "copy"] else: cmd += ["-map", "0:a:0"] cmd += editmod.AAC_ARGS if p.reencode_audio else ["-c:a", "copy"] if threads and threads > 0: cmd += ["-threads", str(threads)] if p.ext in ("mp4", "m4a"): cmd += ["-movflags", "+faststart"] cmd += [str(dest)] return cmd