fix(editor): a crop of a video with wide pixels keeps what its frame showed
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.
This commit is contained in:
@@ -60,6 +60,31 @@ class EditAborted(Exception):
|
||||
_MAX_SEGMENTS = 100
|
||||
|
||||
|
||||
def _crop_frame(crop: dict) -> dict | None:
|
||||
"""The picture size a crop was measured on, or None when it has none it could be scaled by (a
|
||||
spec from before it was sent, which `crop_filter` cuts in the file's own pixels)."""
|
||||
try:
|
||||
w, h = int(crop["frame_w"]), int(crop["frame_h"])
|
||||
# silent-ok: no frame is a crop's older form, and it is still the crop that was asked for.
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return None
|
||||
return {"frame_w": w, "frame_h": h} if w > 0 and h > 0 else None
|
||||
|
||||
|
||||
def crop_filter(crop: dict) -> str:
|
||||
"""The ffmpeg `crop` of an edit. Its numbers count the picture as the browser reports it, which
|
||||
is its SHOWN size: a video with wide pixels (320 stored, 640 shown) would be cut off to one side
|
||||
in ffmpeg's stored ones, so they are scaled to `iw` / `ih` by the size they were measured on. A
|
||||
rotated file needs nothing more: both the browser and ffmpeg's filters see it upright."""
|
||||
frame_w, frame_h = crop.get("frame_w"), crop.get("frame_h")
|
||||
if not frame_w or not frame_h:
|
||||
return f"crop={crop['w']}:{crop['h']}:{crop['x']}:{crop['y']}"
|
||||
return (
|
||||
f"crop=iw*{crop['w']}/{frame_w}:ih*{crop['h']}/{frame_h}"
|
||||
f":iw*{crop['x']}/{frame_w}:ih*{crop['y']}/{frame_h}"
|
||||
)
|
||||
|
||||
|
||||
def normalize_edit_spec(spec: dict | None) -> dict:
|
||||
"""Canonicalize a raw edit spec: clamp/round trim, coerce crop to ints, drop empties.
|
||||
|
||||
@@ -81,7 +106,8 @@ def normalize_edit_spec(spec: dict | None) -> dict:
|
||||
)
|
||||
c = None
|
||||
if c and c["w"] > 0 and c["h"] > 0 and c["x"] >= 0 and c["y"] >= 0:
|
||||
out["crop"] = c
|
||||
frame = _crop_frame(crop)
|
||||
out["crop"] = {**c, **frame} if frame else c
|
||||
|
||||
# Multi-segment JOIN (cut-list → one concatenated file). Takes precedence over `trim`.
|
||||
segs = spec.get("segments")
|
||||
@@ -268,7 +294,7 @@ def build_edit_cmd(src: Path, dest: Path, spec: dict, out_ext: str) -> list[str]
|
||||
if reencode:
|
||||
# Input -ss + re-encode is frame-accurate (decodes from the prior keyframe, discards up to
|
||||
# the exact cut). A crop adds the filter; an accurate trim only evens the frame.
|
||||
cropf = f"crop={crop['w']}:{crop['h']}:{crop['x']}:{crop['y']}," if crop else ""
|
||||
cropf = f"{crop_filter(crop)}," if crop else ""
|
||||
cmd += ["-vf", cropf + EVEN_FRAME, *X264_ARGS, *AAC_ARGS]
|
||||
else:
|
||||
cmd += ["-c", "copy", "-avoid_negative_ts", "make_zero"]
|
||||
@@ -313,7 +339,7 @@ def build_concat_plan(src: Path, dest: Path, spec: dict, out_ext: str, staging:
|
||||
]
|
||||
|
||||
if needs_reencode(spec):
|
||||
cropf = f",crop={crop['w']}:{crop['h']}:{crop['x']}:{crop['y']}" if crop else ""
|
||||
cropf = f",{crop_filter(crop)}" if crop else ""
|
||||
parts, labels = [], []
|
||||
for i, s in enumerate(segs):
|
||||
st, en = s["start_s"], s["end_s"]
|
||||
|
||||
@@ -16,7 +16,8 @@ That is a per-job ceiling, not an aggregate ledger: concurrent jobs of one user
|
||||
remainder (in-flight bytes are not `ready`, so `footprint` cannot see them), so a wave of
|
||||
max_concurrent downloads can still land over the cap — the deliberate trade, since a download's size
|
||||
is unknown until it finishes. CLIP jobs consult it before their fetch or encode and again with
|
||||
their output's real size before it is placed. Edit jobs do not consult it at all (tracked as SEC-19).
|
||||
their output's real size before it is placed. Edit jobs consult it with their output's real size
|
||||
before it is placed (`test_edit_cap.py`).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
Reference in New Issue
Block a user