mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
fix(engine): hold the last video frame at the inclusive clip end (#1564)
* fix(engine): hold the last video frame at the inclusive clip end The frame-lookup active set deactivated a video on an exclusive end-bound (globalTime < end), while the runtime keeps an element visible through currentTime <= end (core/runtime init.ts). The rendered frame landing exactly on a clip's end went blank even though the runtime still showed the element on its final frame: one blank frame at the end of every clip whose end lands on a frame boundary. Make the active window inclusive of the end to match the runtime, and at t === end serve the last extracted frame (the runtime holds the element's final frame there too). Mid-clip source exhaustion (t < end) stays blank, unchanged. * fix(engine): align getFrame boundary to match refreshActiveSet Make getFrame's end-bound inclusive (> instead of >=) for consistency with the refreshActiveSet changes. getFrame is currently unused externally but should match the same contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
Carlos Alcaraz
parent
1bab79ef4c
commit
fdb8f33fc0
@@ -328,6 +328,71 @@ describe("FrameLookupTable", () => {
|
||||
expect(table.getActiveFramePayloads(0.5).has("hero")).toBe(true);
|
||||
expect(table.getActiveFramePayloads(1.5).has("hero")).toBe(false);
|
||||
});
|
||||
|
||||
it("holds the last frame at the inclusive clip end (t === end)", () => {
|
||||
// clip [1,3] with exactly 2s of source frames (60 @ 30fps). The frame
|
||||
// landing on t === end used to deactivate one frame early and render blank,
|
||||
// while the runtime keeps the element visible on its last frame.
|
||||
const table = createFrameLookupTable(
|
||||
[
|
||||
{
|
||||
id: "hero",
|
||||
src: "clip.webm",
|
||||
start: 1,
|
||||
end: 3,
|
||||
mediaStart: 0,
|
||||
loop: false,
|
||||
hasAudio: false,
|
||||
},
|
||||
],
|
||||
[fakeExtracted(60, 30)],
|
||||
);
|
||||
const atEnd = table.getActiveFramePayloads(3.0).get("hero");
|
||||
expect(atEnd?.frameIndex).toBe(59);
|
||||
// mid-clip is unaffected
|
||||
expect(table.getActiveFramePayloads(2.5).get("hero")?.frameIndex).toBe(45);
|
||||
});
|
||||
|
||||
it("holds the last frame at the clip end even when the source is shorter than the window", () => {
|
||||
// clip [0,5] with only 1s of source (30 @ 30fps). The mid-clip tail stays
|
||||
// blank (source exhausted), but t === end still holds the last frame to
|
||||
// match the runtime's inclusive visibility.
|
||||
const table = createFrameLookupTable(
|
||||
[
|
||||
{
|
||||
id: "hero",
|
||||
src: "clip.webm",
|
||||
start: 0,
|
||||
end: 5,
|
||||
mediaStart: 0,
|
||||
loop: false,
|
||||
hasAudio: false,
|
||||
},
|
||||
],
|
||||
[fakeExtracted(30, 30)],
|
||||
);
|
||||
expect(table.getActiveFramePayloads(1.5).has("hero")).toBe(false);
|
||||
expect(table.getActiveFramePayloads(5.0).get("hero")?.frameIndex).toBe(29);
|
||||
});
|
||||
|
||||
it("keeps both clips active at a shared adjacent boundary, matching the runtime", () => {
|
||||
// clip A ends at 3.0, clip B starts at 3.0. The runtime shows both at the
|
||||
// shared instant; the active set must too.
|
||||
const table = createFrameLookupTable(
|
||||
[
|
||||
{ id: "a", src: "a.webm", start: 0, end: 3, mediaStart: 0, loop: false, hasAudio: false },
|
||||
{ id: "b", src: "b.webm", start: 3, end: 6, mediaStart: 0, loop: false, hasAudio: false },
|
||||
],
|
||||
// createFrameLookupTable maps each clip to extracted frames by id.
|
||||
[
|
||||
{ ...fakeExtracted(90, 30), videoId: "a" },
|
||||
{ ...fakeExtracted(90, 30), videoId: "b" },
|
||||
],
|
||||
);
|
||||
const payloads = table.getActiveFramePayloads(3.0);
|
||||
expect(payloads.has("a")).toBe(true);
|
||||
expect(payloads.has("b")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseImageElements", () => {
|
||||
|
||||
Reference in New Issue
Block a user