"""A libx264 re-encode of an odd-sized frame: 4:2:0 halves the chroma, so libx264 refuses an odd width or height ("width not divisible by 2"). Measured: a source that is odd AND not 4:2:0 already (a 4:4:4 or RGB screen capture, 1365x767) failed its conversion outright; a 4:2:0 source never reaches the encoder odd (ffmpeg's crop rounds to its chroma grid), but the clip then stored the size the user dragged, not the one the file has. Every re-encode now ends its filter chain on `edit.EVEN_FRAME` (at most one pixel row or column), and a clip stores its probed frame. Real ffmpeg on both paths — the refusal is the encoder's, so only the encoder can show it is gone. """ import shutil import subprocess from pathlib import Path import pytest from app.config import settings from app.downloads import pipeline, playable, probe, service, storage from app.downloads import upload as uploadmod from app.downloads.models import DownloadJob, MediaAsset pytestmark = pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not available") def _ffmpeg(*args: str) -> None: subprocess.run(["ffmpeg", "-y", "-loglevel", "error", *args], check=True) def _frame(path: Path) -> tuple[int | None, int | None]: _, _, w, h, _ = probe.probe_media(path) return w, h def test_an_odd_sized_4_4_4_video_converts_to_the_even_frame_below_it(tmp_path): src = tmp_path / "in.mkv" _ffmpeg( "-f", "lavfi", "-i", "testsrc=size=161x121:rate=10:duration=1", "-c:v", "libx264", "-pix_fmt", "yuv444p", str(src), ) # fmt: skip assert _frame(src) == (161, 121) # odd for real (`testsrc2` rounds its own size) p = playable.plan(probe.probe_json(src), "mkv") assert p is not None and p.reencode_video dest = tmp_path / "out.mp4" subprocess.run(playable.build_cmd(src, dest, p), check=True, capture_output=True) assert _frame(dest) == (160, 120) def test_an_odd_crop_makes_a_clip_and_stores_the_frame_it_has(db, make_user, tmp_path, monkeypatch): monkeypatch.setattr(settings, "files_root", str(tmp_path)) src = tmp_path / "in.mp4" _ffmpeg( "-f", "lavfi", "-i", "testsrc2=size=160x120:rate=10:duration=2", "-f", "lavfi", "-i", "sine=duration=2", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", str(src), ) # fmt: skip u = make_user() source = uploadmod.ingest_upload( db, u.id, filename="clip.mp4", src_path=src, size=src.stat().st_size ) assert source.status == "done" # already phone-safe: the clip's source as it came job = service.enqueue_edit(db, u.id, source, {"crop": {"x": 3, "y": 5, "w": 101, "h": 51}}) asset = db.get(MediaAsset, job.asset_id) job.status, asset.status = "running", "downloading" # the worker's claim db.commit() pipeline._process_edit(job.id, asset.id) db.expire_all() job, asset = db.get(DownloadJob, job.id), db.get(MediaAsset, job.asset_id) assert (job.status, job.error, asset.status) == ("done", None, "ready") final = storage.abs_path(str(tmp_path), asset.rel_path) assert _frame(final) == (100, 50) assert (asset.width, asset.height) == (100, 50) # what the file is, not what was asked