The browser reports such a video by its shown size and ffmpeg's crop counts stored pixels, so a frame over the middle half of a 320 stored, 640 shown video kept the whole width. A crop now carries the size it was measured on and is scaled to the file's own pixels. A spec from before it did is cut as it always was. Also drops a stale note in downloads/quota.py: edit jobs are held to the cap.
113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
"""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) -> None:
|
|
"""Red, with the picture's middle half green: a crop of that middle holds no red."""
|
|
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
|
|
|
|
|
|
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(db, make_user, tmp_path, src: Path, crop: dict) -> Path:
|
|
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"
|
|
job = service.enqueue_edit(db, user.id, source, {"crop": crop})
|
|
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 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}
|