feat(clip): crop a clip to a frame drawn on the picture, and give the clip panel the room it needs
An optional crop in clip mode: a Crop toggle puts a frame on the picture (free, 16:9, 9:16 or 1:1; the whole picture stays the default). The frame is sent in fractions of the box it was drawn in, with that box's shape; the worker fits the real picture into the box by its display aspect (sample aspect and rotation aware), drops the part over the bars, and crops first in the filter chain. A frame covering the whole box is no crop at all, and one off the picture is refused by name. The crop frame is a ui primitive (CropFrame) shared with the video editor, keyboard-operable. YouTube: the wide side panel shuts while clipping and comes back after; maximised, the clip panel docks in the stage. Plex: the picture sits above the clip panel so the frame lies on it.
This commit is contained in:
@@ -983,4 +983,44 @@ describe("PlexPlayer — clip mode", () => {
|
||||
audio_track: 0,
|
||||
});
|
||||
});
|
||||
it("crops on the picture's own shape, and Escape turns the crop off before the panel", async () => {
|
||||
const enqueue = vi.spyOn(api, "enqueueClip").mockResolvedValue({ id: 9 } as never);
|
||||
// A 720×480 stream: the frame is drawn on a 3:2 picture, and the cut is told so.
|
||||
const proto = HTMLVideoElement.prototype;
|
||||
const own = ["videoWidth", "videoHeight"].map(
|
||||
(k) => [k, Object.getOwnPropertyDescriptor(proto, k)] as const,
|
||||
);
|
||||
Object.defineProperty(proto, "videoWidth", { configurable: true, get: () => 720 });
|
||||
Object.defineProperty(proto, "videoHeight", { configurable: true, get: () => 480 });
|
||||
try {
|
||||
mount({ details: { a: item("a") } });
|
||||
await playingAt("/media/a", 60);
|
||||
key("c");
|
||||
fireEvent.click(screen.getByRole("button", { name: "clip.crop" }));
|
||||
// The frame's layer is the video's box: both end above the panel, both at the player's top
|
||||
// (its flex box centred the shortened video, and the frame sat 155 px above the picture).
|
||||
const video = document.querySelector("video")!;
|
||||
const layer = screen.getByTestId("crop-frame").parentElement!;
|
||||
expect(video.style.alignSelf).toBe("flex-start");
|
||||
expect(layer.className).toContain("top-0");
|
||||
expect(layer.style.height).toBe(video.style.height);
|
||||
key("Escape");
|
||||
expect(screen.queryByTestId("crop-frame")).toBeNull();
|
||||
expect(screen.getByTestId("clip-panel")).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "clip.crop" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "clip.cut" }));
|
||||
await waitFor(() => expect(enqueue).toHaveBeenCalled());
|
||||
expect(enqueue.mock.calls[0]![0].crop).toEqual({
|
||||
x: 0.1,
|
||||
y: 0.1,
|
||||
w: 0.8,
|
||||
h: 0.8,
|
||||
aspect: 1.5,
|
||||
});
|
||||
} finally {
|
||||
for (const [k, d] of own)
|
||||
if (d) Object.defineProperty(proto, k, d);
|
||||
else delete (proto as unknown as Record<string, unknown>)[k];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user