"""The editor's crop lands on the part of the picture the frame was drawn on, also when a video's pixels are not square. The browser reports such a video by its shown size (320 stored, 640 shown), and ffmpeg's `crop` counts stored pixels: measured on a real clip, a frame over the middle half kept half the frame's red surroundings. So a crop carries the size it was measured on. Real ffmpeg: the pixels of the output are the oracle. """ import shutil import subprocess from pathlib import Path import pytest from app.config import settings from app.downloads import edit, pipeline, 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 _source(path: Path, size: str, box: str, sar: int, rotate: int = 0) -> None: """Red, with a green box: a crop of the box holds no red. `rotate` tags the file to be shown turned by that many degrees counter-clockwise, as a phone records a portrait video.""" tag = ["-display_rotation:v:0", str(rotate)] if rotate else [] subprocess.run( [ "ffmpeg", "-y", "-loglevel", "error", "-f", "lavfi", "-i", f"color=c=red:s={size}:d=2:r=10", "-f", "lavfi", "-i", "sine=duration=2", "-vf", f"drawbox={box}:c=lime:t=fill,setsar={sar}", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", str(path), ], check=True, ) # fmt: skip if tag: tagged = path.with_name("tagged-" + path.name) subprocess.run( ["ffmpeg", "-y", "-loglevel", "error", *tag, "-i", str(path), "-c", "copy", str(tagged)], check=True, ) # fmt: skip tagged.replace(path) def _red_and_green(path: Path) -> tuple[int, int]: raw = subprocess.run( ["ffmpeg", "-loglevel", "error", "-i", str(path), "-frames:v", "1", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], check=True, capture_output=True, ).stdout # fmt: skip red = green = 0 for i in range(0, len(raw) - 2, 3): if raw[i] > 180 and raw[i + 1] < 80: red += 1 elif raw[i + 1] > 180 and raw[i] < 80: green += 1 return red, green def _cut_asset(db, make_user, src: Path, spec: dict, then=lambda: None) -> MediaAsset: user = make_user() source = uploadmod.ingest_upload( db, user.id, filename="clip.mp4", src_path=src, size=src.stat().st_size ) assert source.status == "done" then() job = service.enqueue_edit(db, user.id, source, spec) 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") return asset def _cut(db, make_user, tmp_path, src: Path, crop: dict, **spec) -> Path: asset = _cut_asset(db, make_user, src, {"crop": crop, **spec}) return storage.abs_path(str(tmp_path), asset.rel_path) def test_a_crop_of_a_video_with_wide_pixels_keeps_what_the_frame_showed( db, make_user, tmp_path, monkeypatch ): monkeypatch.setattr(settings, "files_root", str(tmp_path)) src = tmp_path / "in.mp4" _source(src, "320x240", "x=80:y=60:w=160:h=120", sar=2) # What the editor sends for the middle half of this video: the browser shows it at 640 x 240. final = _cut( db, make_user, tmp_path, src, {"x": 160, "y": 60, "w": 320, "h": 120, "frame_w": 640, "frame_h": 240}, ) # fmt: skip red, green = _red_and_green(final) assert (red, green) == (0, 160 * 120) def test_a_crop_of_square_pixels_is_cut_where_it_was_drawn(db, make_user, tmp_path, monkeypatch): monkeypatch.setattr(settings, "files_root", str(tmp_path)) src = tmp_path / "in.mp4" _source(src, "480x640", "x=120:y=160:w=240:h=320", sar=1) final = _cut( db, make_user, tmp_path, src, {"x": 120, "y": 160, "w": 240, "h": 320, "frame_w": 480, "frame_h": 640}, ) # fmt: skip assert _red_and_green(final) == (0, 240 * 320) def test_a_crop_stored_before_it_carried_its_frame_is_cut_as_it_always_was(): spec = edit.normalize_edit_spec({"crop": {"x": 4, "y": 6, "w": 100, "h": 50}}) assert spec["crop"] == {"x": 4, "y": 6, "w": 100, "h": 50} cmd = edit.build_edit_cmd(Path("in.mp4"), Path("out.mp4"), spec, "mp4") assert cmd[cmd.index("-vf") + 1].startswith("crop=100:50:4:6,") @pytest.mark.parametrize("frame", [{"frame_w": 0, "frame_h": 240}, {"frame_w": "x", "frame_h": 2}]) def test_a_frame_that_cannot_be_read_is_not_divided_by(frame): spec = edit.normalize_edit_spec({"crop": {"x": 4, "y": 6, "w": 100, "h": 50, **frame}}) assert spec["crop"] == {"x": 4, "y": 6, "w": 100, "h": 50} def test_a_crop_of_a_join_keeps_what_the_frame_showed(db, make_user, tmp_path, monkeypatch): """A cut-list joins through `filter_complex`, a crop path of its own.""" monkeypatch.setattr(settings, "files_root", str(tmp_path)) src = tmp_path / "in.mp4" _source(src, "320x240", "x=80:y=60:w=160:h=120", sar=2) final = _cut( db, make_user, tmp_path, src, {"x": 160, "y": 60, "w": 320, "h": 120, "frame_w": 640, "frame_h": 240}, segments=[{"start_s": 0, "end_s": 0.5}, {"start_s": 1, "end_s": 1.5}], accurate=True, ) # fmt: skip assert _red_and_green(final) == (0, 160 * 120) def test_a_crop_of_a_rotated_video_is_cut_where_it_was_drawn(db, make_user, tmp_path, monkeypatch): """The browser shows a rotated file upright, so the frame was drawn on the upright picture.""" monkeypatch.setattr(settings, "files_root", str(tmp_path)) src = tmp_path / "in.mp4" # Stored 320 x 240, green on its left half; shown turned a quarter, 240 x 320. _source(src, "320x240", "x=0:y=0:w=160:h=240", sar=1, rotate=90) # Turned counter-clockwise, the stored left half is the shown bottom half. final = _cut( db, make_user, tmp_path, src, {"x": 0, "y": 160, "w": 240, "h": 160, "frame_w": 240, "frame_h": 320}, ) # fmt: skip assert _red_and_green(final) == (0, 240 * 160) def test_a_clip_ffprobe_cannot_size_gets_the_size_its_crop_cut( db, make_user, tmp_path, monkeypatch ): monkeypatch.setattr(settings, "files_root", str(tmp_path)) src = tmp_path / "in.mp4" _source(src, "320x240", "x=80:y=60:w=160:h=120", sar=2) def probe_fails(): monkeypatch.setattr(pipeline.probe, "probe_media", lambda _p: (None,) * 5) asset = _cut_asset( db, make_user, src, {"crop": {"x": 160, "y": 60, "w": 320, "h": 120, "frame_w": 640, "frame_h": 240}}, then=probe_fails, ) # fmt: skip # Half the shown width of a 320-wide file: 160 stored, not the 320 the frame counted. assert (asset.width, asset.height) == (160, 120) def test_a_rotated_clip_ffprobe_cannot_size_gets_the_upright_size_its_crop_cut( db, make_user, tmp_path, monkeypatch ): """The frame was drawn on the upright picture, which is the stored one turned: so is its cut.""" monkeypatch.setattr(settings, "files_root", str(tmp_path)) src = tmp_path / "in.mp4" _source(src, "320x240", "x=0:y=0:w=160:h=240", sar=1, rotate=90) def probe_fails(): monkeypatch.setattr(pipeline.probe, "probe_media", lambda _p: (None,) * 5) asset = _cut_asset( db, make_user, src, {"crop": {"x": 0, "y": 160, "w": 240, "h": 160, "frame_w": 240, "frame_h": 320}}, then=probe_fails, ) # fmt: skip # What ffmpeg cut (the rotated test above): 240 x 160, not the stored 320 x 120. assert (asset.width, asset.height) == (240, 160) @pytest.mark.parametrize("key", ["frame_w", "x"]) def test_an_infinite_number_in_a_crop_is_dropped_not_a_crash(key): crop = {"x": 4, "y": 6, "w": 100, "h": 50, "frame_w": 640, "frame_h": 240} spec = edit.normalize_edit_spec({"crop": {**crop, key: float("inf")}}) if key == "frame_w": assert spec["crop"] == {"x": 4, "y": 6, "w": 100, "h": 50} else: assert "crop" not in spec