mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
* fix(core): a video after a cut starts early by its measured start delay, so it plays in sync * test(player): playbench reports how far the picture after a cut runs behind the playhead * fix(core): a looping video no longer skews the learned video start delay * fix(core): the video start delay is learned in less code and capped at 100 ms under test
179 lines
6.1 KiB
TypeScript
179 lines
6.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { analyze, type Dump, type Run } from "./analyze.ts";
|
|
import { buildEdit, compositionHtml } from "./compositions.ts";
|
|
|
|
const empty = (kind: Dump["kind"]): Dump => ({
|
|
kind,
|
|
events: [],
|
|
frames: [],
|
|
samples: [],
|
|
params: [],
|
|
edges: [],
|
|
sources: [],
|
|
tap: [],
|
|
ctxs: [],
|
|
rejects: 0,
|
|
});
|
|
|
|
describe("the two edits", () => {
|
|
it("the 3-take edit is 24.8 s at 30 fps with six media elements and audio windows that overlap about 300 ms", () => {
|
|
const edit = buildEdit("primary");
|
|
expect(edit.takes).toHaveLength(3);
|
|
expect(edit.takes.reduce((a, t) => a + t.dur, 0)).toBeCloseTo(24.8, 2);
|
|
expect(edit.takes[0]!.dur).toBeCloseTo(89 / 30, 3);
|
|
for (const [a, b] of [
|
|
[0, 1],
|
|
[1, 2],
|
|
] as const) {
|
|
const overlap =
|
|
edit.takes[a]!.audio.start + edit.takes[a]!.audio.dur - edit.takes[b]!.audio.start;
|
|
expect(overlap).toBeGreaterThan(0.27);
|
|
expect(overlap).toBeLessThan(0.31);
|
|
}
|
|
const html = compositionHtml(edit, "/media");
|
|
expect(html.match(/<(video|audio) /g)).toHaveLength(6);
|
|
expect(html).toContain("data-automation=");
|
|
});
|
|
|
|
it("the 10-take edit is 53.08 s at 25 fps with twenty media elements and take 1 cut from 208.05 s", () => {
|
|
const edit = buildEdit("tentake");
|
|
expect(edit.takes).toHaveLength(10);
|
|
expect(edit.takes.reduce((a, t) => a + t.dur, 0)).toBeCloseTo(53.08, 2);
|
|
expect(edit.takes[0]!.inPoint).toBeCloseTo(208.04, 1);
|
|
expect(compositionHtml(edit, "/media").match(/<(video|audio) /g)).toHaveLength(20);
|
|
expect(buildEdit("tentake")).toEqual(edit);
|
|
});
|
|
});
|
|
|
|
describe("analyze", () => {
|
|
const edit = buildEdit("primary");
|
|
const base = (): Run => ({
|
|
edit,
|
|
comp: empty("comp"),
|
|
host: empty("host"),
|
|
playAt: 1000,
|
|
endedAt: null,
|
|
ownership: [],
|
|
});
|
|
|
|
it("counts the frames a visible outgoing video presents past its out-point, and ignores hidden ones", () => {
|
|
const run = base();
|
|
const out = edit.takes[0]!.outFrame;
|
|
run.comp.frames = [
|
|
[1100, "v0", (out - 1) / 30, 1, 1],
|
|
[1133, "v0", out / 30, 2, 1],
|
|
[1166, "v0", (out + 1) / 30, 3, 1],
|
|
[1200, "v0", (out + 2) / 30, 4, 0],
|
|
];
|
|
expect(analyze(run).firstTakeOverrunFrames).toBe(2);
|
|
});
|
|
|
|
it("measures the jump of a seek in the first 600 ms of a join and the second-frame gap of the incoming video", () => {
|
|
const run = base();
|
|
const t = edit.takes[1]!;
|
|
// The playhead runs 1 s per second from the start; the audio window of take 1 opens at t.audio.start.
|
|
for (let ms = 0; ms <= 4000; ms += 10)
|
|
run.comp.samples.push([1000 + ms, "v0", 0, 0, 0, 1, 1, ms / 1000]);
|
|
const open = 1000 + t.audio.start * 1000;
|
|
// a1 plays, is seeked forward 80 ms at open + 200 ms
|
|
for (let ms = 0; ms <= 600; ms += 10)
|
|
run.comp.samples.push([
|
|
open + ms,
|
|
"a1",
|
|
t.audio.mediaStart + ms / 1000 + (ms >= 200 ? 0.08 : 0),
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
null,
|
|
]);
|
|
run.comp.samples.sort((a, b) => a[0] - b[0]);
|
|
run.comp.events = [
|
|
[open + 5, "a1", "playing", 0],
|
|
[open + 200, "a1", "seeking", 0],
|
|
];
|
|
run.comp.frames = [
|
|
[1000 + t.start * 1000, "v1", t.inPoint, 1, 1],
|
|
[1000 + t.start * 1000 + 66.6, "v1", t.inPoint + 1 / 30, 2, 1],
|
|
];
|
|
const m = analyze(run);
|
|
expect(m.joinsWithMidPlaySeek).toBe(1);
|
|
expect(m.jumpMaxMs).toBeGreaterThan(70);
|
|
expect(m.jumpMaxMs).toBeLessThan(90);
|
|
expect(m.secondFrameGapMedianMs).toBeCloseTo(66.6, 1);
|
|
});
|
|
|
|
it("reports a playhead that held 30 ms across a cut", () => {
|
|
const run = base();
|
|
const cut = edit.takes[1]!.start;
|
|
let wall = 1000,
|
|
ph = 0,
|
|
held = false;
|
|
for (let i = 0; i < 400; i++) {
|
|
run.comp.samples.push([wall, "v0", 0, 0, 0, 1, 1, ph]);
|
|
wall += 10;
|
|
if (!held && ph >= cut) {
|
|
wall += 30;
|
|
held = true;
|
|
}
|
|
ph += 0.01;
|
|
}
|
|
expect(analyze(run).playheadHoldMedianMs).toBeCloseTo(30, 0);
|
|
});
|
|
|
|
it("reports an incoming picture that runs 50 ms behind the playhead after its cut", () => {
|
|
const run = base();
|
|
for (const t of edit.takes.slice(1))
|
|
for (let i = 0; i <= 50; i++) {
|
|
const ph = t.start + i * 0.01;
|
|
run.comp.samples.push([
|
|
1000 + ph * 1000,
|
|
`v${t.index}`,
|
|
t.inPoint + ph - t.start - 0.05,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
ph,
|
|
]);
|
|
}
|
|
const m = analyze(run);
|
|
expect(m.cutPictureLagMedianMs).toBeCloseTo(50, 0);
|
|
expect(m.cutPictureLagMaxMs).toBeCloseTo(50, 0);
|
|
});
|
|
|
|
it("counts a take as heard only when the audio reaching the output comes from that take's stretch of the file", () => {
|
|
const run = base();
|
|
run.ownership = [[1000, "parent"]];
|
|
const t = edit.takes[1]!;
|
|
expect(t.inPoint).toBeGreaterThan(5);
|
|
// The playhead starts late and the host page has no __player, so its rows carry no playhead.
|
|
const delay = Math.round(t.dur * 600);
|
|
const open = 1000 + delay + t.start * 1000;
|
|
for (let ms = 0; ms <= (t.start + t.dur) * 1000 + delay + 1000; ms += 10) {
|
|
run.comp.samples.push([1000 + ms, "v0", 0, 0, 0, 1, 1, Math.max(0, ms - delay) / 1000]);
|
|
run.host.samples.push([1000 + ms, "x", 0, 0, 0, 1, 1, null]);
|
|
}
|
|
const row = (at: number, pos: number): Dump["tap"][number] => [
|
|
0,
|
|
48000,
|
|
0.1,
|
|
0,
|
|
Math.floor((pos % 16) / 0.5),
|
|
Math.floor(pos / 16),
|
|
at,
|
|
];
|
|
// the previous take is still audible while the delayed playhead has not reached this take
|
|
for (let ms = 0; ms < delay; ms += 100)
|
|
run.host.tap.push(row(1000 + t.start * 1000 + ms, (t.inPoint + 25) % 50));
|
|
for (let ms = 0; ms < t.dur * 1000; ms += 100)
|
|
run.host.tap.push(row(open + ms, t.inPoint + ms / 1000));
|
|
expect(analyze(run).takesHeard).toBe(1);
|
|
// audio from another stretch of the file (the proxy playing from the wrong offset) is not heard
|
|
run.host.tap = [];
|
|
for (let ms = 0; ms < t.dur * 1000; ms += 100)
|
|
run.host.tap.push(row(open + ms, (t.inPoint + 25 + ms / 1000) % 50));
|
|
expect(analyze(run).takesHeard).toBe(0);
|
|
});
|
|
});
|