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:
@@ -70,7 +70,9 @@ export interface ClipRequest {
|
||||
export interface EditSpec {
|
||||
trim?: { start_s: number; end_s?: number };
|
||||
segments?: { start_s: number; end_s: number }[];
|
||||
crop?: { x: number; y: number; w: number; h: number };
|
||||
// In the picture's size as the browser reports it, which `frame_w` x `frame_h` says: the server
|
||||
// scales them to the file's own pixels (a video's stored and shown sizes can differ).
|
||||
crop?: { x: number; y: number; w: number; h: number; frame_w?: number; frame_h?: number };
|
||||
accurate?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A crop's numbers count the picture as the browser reports it, its shown size, which differs from the
|
||||
// file's own on a video with wide pixels. The size goes along, or the server cuts the wrong part.
|
||||
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }),
|
||||
}));
|
||||
|
||||
import VideoEditor from "./VideoEditor";
|
||||
import { api, type DownloadJob } from "../../lib/api";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver;
|
||||
globalThis.IntersectionObserver ??= Stub as unknown as typeof IntersectionObserver;
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const job = {
|
||||
id: 7,
|
||||
display_name: "Talk",
|
||||
asset: { duration_s: 100, width: 1920, height: 1080, title: "Talk" },
|
||||
} as unknown as DownloadJob;
|
||||
|
||||
describe("VideoEditor crop", () => {
|
||||
it("sends the size the crop was measured on with it", async () => {
|
||||
const enqueue = vi
|
||||
.spyOn(api, "enqueueEdit")
|
||||
.mockResolvedValue({ id: 8 } as unknown as DownloadJob);
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<VideoEditor job={job} onClose={() => {}} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "editor.cropOff" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "editor.create" }));
|
||||
await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(1));
|
||||
const crop = enqueue.mock.calls[0]?.[0].edit_spec.crop;
|
||||
expect(crop).toMatchObject({ frame_w: 1920, frame_h: 1080, w: 1536, h: 864 });
|
||||
});
|
||||
});
|
||||
@@ -226,14 +226,16 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
const willJoin = output === "join" && kept.length > 1;
|
||||
const reencode = cropOn || accurate;
|
||||
|
||||
const cropPx = (): { x: number; y: number; w: number; h: number } | undefined => {
|
||||
const cropPx = (): EditSpec["crop"] => {
|
||||
const v = videoRef.current;
|
||||
const vw = v?.videoWidth || vdim.w;
|
||||
const vh = v?.videoHeight || vdim.h;
|
||||
if (!cropOn) return undefined;
|
||||
// crop fractions are relative to the picture (the overlay is anchored to it), so this is a
|
||||
// straight fraction→source-px scale; see cropGeometry for the even-rounding + in-bounds clamp.
|
||||
return cropToSourcePx(crop, vw, vh) ?? undefined;
|
||||
const px = cropToSourcePx(crop, vw, vh);
|
||||
// The size they count in goes along: it is the shown one, and the file's own can differ.
|
||||
return px ? { ...px, frame_w: vw, frame_h: vh } : undefined;
|
||||
};
|
||||
|
||||
const create = useMutation({
|
||||
|
||||
Reference in New Issue
Block a user