b596c97faf
Deploy / deploy (push) Successful in 3m12s
Check shots on the Playlist page and "Play selection" plays them back to back without slate or handles. The cut comes from the version's handle and slate metadata (new cutSpan in lib/handles); the media is never changed. SequencePlayer double-buffers two videos: the next clip is parked a few frames before its cut-in and pre-rolled hidden, the outgoing clip pauses on its last cut frame, and the cut fires on the incoming clip's cut-in frame (requestVideoFrameCallback), with the start time calibrated at each cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { computeEditRange, cutSpan, frameRegion, isHandleFrame } from "./handles";
|
|
|
|
describe("computeEditRange", () => {
|
|
it("is null when no handles are marked", () => {
|
|
expect(computeEditRange(null, 100)).toBeNull();
|
|
expect(computeEditRange({}, 100)).toBeNull();
|
|
expect(computeEditRange({ handleHead: 0, handleTail: 0 }, 100)).toBeNull();
|
|
});
|
|
|
|
it("is null until the media length is known", () => {
|
|
expect(computeEditRange({ handleHead: 8, handleTail: 8 }, 0)).toBeNull();
|
|
});
|
|
|
|
it("marks 8+8 on media with no slate", () => {
|
|
expect(computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 100 }, 100)).toEqual({
|
|
pictureStart: 0,
|
|
cutIn: 8,
|
|
tailStart: 92,
|
|
pictureEnd: 100,
|
|
});
|
|
});
|
|
|
|
it("infers a 1-frame head slate from the media being one frame longer than the EXRs", () => {
|
|
// Pipeline preview: 100 EXR frames + 1 slate frame = 101 in the MP4
|
|
expect(computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 100 }, 101)).toEqual({
|
|
pictureStart: 1,
|
|
cutIn: 9,
|
|
tailStart: 93,
|
|
pictureEnd: 101,
|
|
});
|
|
});
|
|
|
|
it("uses a recorded picture start over inference", () => {
|
|
const r = computeEditRange(
|
|
{ handleHead: 8, handleTail: 8, frameCount: 100, pictureStartFrame: 2 },
|
|
102
|
|
);
|
|
expect(r).toEqual({ pictureStart: 2, cutIn: 10, tailStart: 94, pictureEnd: 102 });
|
|
});
|
|
|
|
it("anchors the tail on the picture when the container rounds a frame short", () => {
|
|
// Recorded 1-frame slate, 100 EXRs, but the MP4 duration floors to 100
|
|
const r = computeEditRange(
|
|
{ handleHead: 8, handleTail: 8, frameCount: 100, pictureStartFrame: 1 },
|
|
100
|
|
);
|
|
expect(r).toEqual({ pictureStart: 1, cutIn: 9, tailStart: 92, pictureEnd: 100 });
|
|
});
|
|
|
|
it("doesn't mistake a large unexplained length difference for a slate", () => {
|
|
const r = computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 50 }, 100);
|
|
expect(r?.pictureStart).toBe(0);
|
|
});
|
|
|
|
it("supports unequal and one-sided handles", () => {
|
|
expect(computeEditRange({ handleHead: 12, handleTail: 0, frameCount: 100 }, 100)).toEqual({
|
|
pictureStart: 0,
|
|
cutIn: 12,
|
|
tailStart: 100,
|
|
pictureEnd: 100,
|
|
});
|
|
});
|
|
|
|
it("never lets the handles overlap on a very short shot", () => {
|
|
const r = computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 10 }, 10)!;
|
|
expect(r.cutIn).toBe(8);
|
|
expect(r.tailStart).toBe(8);
|
|
});
|
|
});
|
|
|
|
describe("frameRegion", () => {
|
|
const range = computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 100 }, 101);
|
|
|
|
it("classifies slate, handles and edit", () => {
|
|
expect(frameRegion(range, 0)).toBe("slate");
|
|
expect(frameRegion(range, 1)).toBe("head");
|
|
expect(frameRegion(range, 8)).toBe("head");
|
|
expect(frameRegion(range, 9)).toBe("edit");
|
|
expect(frameRegion(range, 92)).toBe("edit");
|
|
expect(frameRegion(range, 93)).toBe("tail");
|
|
expect(frameRegion(range, 100)).toBe("tail");
|
|
});
|
|
|
|
it("treats everything as edit when handles are unknown", () => {
|
|
expect(frameRegion(null, 0)).toBe("edit");
|
|
expect(isHandleFrame(null, 0)).toBe(false);
|
|
});
|
|
|
|
it("does not count the slate as a handle", () => {
|
|
expect(isHandleFrame(range, 0)).toBe(false);
|
|
expect(isHandleFrame(range, 1)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("cutSpan", () => {
|
|
it("plays the cut when handles are marked, skipping slate and handles", () => {
|
|
// 1 slate + 8 head + 84 cut + 8 tail = 101 MP4 frames
|
|
expect(cutSpan({ handleHead: 8, handleTail: 8, frameCount: 100 }, 101)).toEqual({
|
|
start: 9,
|
|
end: 93,
|
|
});
|
|
});
|
|
|
|
it("plays the whole picture minus the slate when no handles are marked", () => {
|
|
expect(cutSpan({ frameCount: 100 }, 101)).toEqual({ start: 1, end: 101 });
|
|
expect(cutSpan({ pictureStartFrame: 2 }, 50)).toEqual({ start: 2, end: 50 });
|
|
});
|
|
|
|
it("plays the whole media when nothing is known about it", () => {
|
|
expect(cutSpan(null, 48)).toEqual({ start: 0, end: 48 });
|
|
expect(cutSpan({}, 48)).toEqual({ start: 0, end: 48 });
|
|
});
|
|
|
|
it("is null until the media length is known", () => {
|
|
expect(cutSpan({ handleHead: 8, handleTail: 8 }, 0)).toBeNull();
|
|
});
|
|
|
|
it("never returns an empty span", () => {
|
|
// Handles longer than the picture
|
|
expect(cutSpan({ handleHead: 30, handleTail: 30, frameCount: 40 }, 40)).toEqual({
|
|
start: 0,
|
|
end: 40,
|
|
});
|
|
});
|
|
});
|