mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(cli): align video output boundaries (#2490)
* fix(cli): align video output boundaries * test(cli): honor CI ffmpeg fixture path * test(cli): decouple duration precedence from ffmpeg * test(cli): pin half-open video boundaries * test(producer): refresh style 7 boundary golden
This commit is contained in:
@@ -4,7 +4,11 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { applyResolutionPreset, injectTailwindBrowserScript } from "./init.js";
|
||||
import {
|
||||
applyResolutionPreset,
|
||||
injectTailwindBrowserScript,
|
||||
resolveVideoDurationSeconds,
|
||||
} from "./init.js";
|
||||
|
||||
const cliEntry = resolve(fileURLToPath(import.meta.url), "..", "..", "cli.ts");
|
||||
const tailwindScript =
|
||||
@@ -177,6 +181,26 @@ describe("hyperframes init flag rename", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the video stream duration when audio outlasts the final video frame", () => {
|
||||
expect(
|
||||
resolveVideoDurationSeconds({
|
||||
streamDuration: 1,
|
||||
frameDuration: 1,
|
||||
formatDuration: 1.2,
|
||||
}),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("falls through unusable stream durations before using the container duration", () => {
|
||||
expect(
|
||||
resolveVideoDurationSeconds({
|
||||
streamDuration: 0,
|
||||
frameDuration: Number.NaN,
|
||||
formatDuration: 1.2,
|
||||
}),
|
||||
).toBe(1.2);
|
||||
});
|
||||
|
||||
it("--audio with a missing file fails without creating the project directory", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-init-test-"));
|
||||
const target = join(dir, "proj");
|
||||
|
||||
@@ -81,6 +81,22 @@ const TAILWIND_BROWSER_SRC = `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@
|
||||
const TAILWIND_BROWSER_INTEGRITY =
|
||||
"sha384-v5YF9xS+gLRWdvrQ0u/WRbCkjSIH0NjHIPe8tBL1ZRrmI7PiSH6LLdzs0aAIMCuh";
|
||||
|
||||
export function resolveVideoDurationSeconds({
|
||||
streamDuration,
|
||||
frameDuration,
|
||||
formatDuration,
|
||||
}: {
|
||||
streamDuration: number;
|
||||
frameDuration: number;
|
||||
formatDuration: number;
|
||||
}): number {
|
||||
return (
|
||||
[streamDuration, frameDuration, formatDuration].find(
|
||||
(duration) => Number.isFinite(duration) && duration > 0,
|
||||
) ?? DEFAULT_META.durationSeconds
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ffprobe helper — shells out to ffprobe to avoid engine dependency
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -103,6 +119,8 @@ function probeVideo(filePath: string): VideoMeta | undefined {
|
||||
height?: number;
|
||||
r_frame_rate?: string;
|
||||
avg_frame_rate?: string;
|
||||
duration?: string;
|
||||
nb_frames?: string;
|
||||
}[];
|
||||
format?: { duration?: string };
|
||||
} = JSON.parse(raw);
|
||||
@@ -124,11 +142,18 @@ function probeVideo(filePath: string): VideoMeta | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
const durationStr = parsed.format?.duration;
|
||||
const durationSeconds = durationStr !== undefined ? parseFloat(durationStr) : 5;
|
||||
const streamDuration = parseFloat(videoStream.duration ?? "");
|
||||
const frameCount = parseInt(videoStream.nb_frames ?? "", 10);
|
||||
const frameDuration = Number.isFinite(frameCount) && fps > 0 ? frameCount / fps : NaN;
|
||||
const formatDuration = parseFloat(parsed.format?.duration ?? "");
|
||||
const durationSeconds = resolveVideoDurationSeconds({
|
||||
streamDuration,
|
||||
frameDuration,
|
||||
formatDuration,
|
||||
});
|
||||
|
||||
return {
|
||||
durationSeconds: Number.isNaN(durationSeconds) ? 5 : durationSeconds,
|
||||
durationSeconds,
|
||||
width: videoStream.width ?? 1920,
|
||||
height: videoStream.height ?? 1080,
|
||||
fps,
|
||||
|
||||
@@ -325,6 +325,33 @@ describe("initSandboxRuntimeModular", () => {
|
||||
expect(child.style.visibility).toBe("hidden");
|
||||
});
|
||||
|
||||
it("uses a half-open interval around a timed element's end boundary", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
root.setAttribute("data-start", "0");
|
||||
root.setAttribute("data-width", "1920");
|
||||
root.setAttribute("data-height", "1080");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const clip = document.createElement("div");
|
||||
clip.setAttribute("data-start", "0");
|
||||
clip.setAttribute("data-duration", "2.5");
|
||||
root.appendChild(clip);
|
||||
|
||||
window.__timelines = { main: createMockTimeline(5) };
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
window.__player?.renderSeek(2.5 - 1e-9);
|
||||
expect(clip.style.visibility).toBe("visible");
|
||||
|
||||
window.__player?.renderSeek(2.5);
|
||||
expect(clip.style.visibility).toBe("hidden");
|
||||
|
||||
window.__player?.renderSeek(2.5 + 1e-9);
|
||||
expect(clip.style.visibility).toBe("hidden");
|
||||
});
|
||||
|
||||
it("keeps external composition hosts visible through their authored duration", async () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
|
||||
@@ -583,7 +583,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
const computedEnd =
|
||||
duration != null && duration > 0 ? start + duration : Number.POSITIVE_INFINITY;
|
||||
return (
|
||||
currentTime >= start && (Number.isFinite(computedEnd) ? currentTime <= computedEnd : true)
|
||||
currentTime >= start && (Number.isFinite(computedEnd) ? currentTime < computedEnd : true)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2827,7 +2827,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
const mediaStart =
|
||||
Number.parseFloat(rawEl.dataset.playbackStart ?? rawEl.dataset.mediaStart ?? "0") ||
|
||||
0;
|
||||
if (Number.isFinite(start) && state.currentTime >= start && state.currentTime <= end) {
|
||||
if (Number.isFinite(start) && state.currentTime >= start && state.currentTime < end) {
|
||||
if (!rawEl.paused) {
|
||||
clock.attachAudioSource({ el: rawEl, compositionStart: start, mediaStart });
|
||||
foundActive = true;
|
||||
|
||||
@@ -250,6 +250,28 @@ describe("syncRuntimeMedia", () => {
|
||||
expect(clip.el.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses a half-open interval around a clip's end boundary", () => {
|
||||
const clip = createMockClip({ start: 0, end: 2.5 });
|
||||
Object.defineProperty(clip.el, "readyState", { value: 4, writable: true });
|
||||
|
||||
syncRuntimeMedia({
|
||||
clips: [clip],
|
||||
timeSeconds: 2.5 - 1e-9,
|
||||
playing: true,
|
||||
playbackRate: 1,
|
||||
});
|
||||
expect(clip.el.play).toHaveBeenCalledTimes(1);
|
||||
|
||||
syncRuntimeMedia({ clips: [clip], timeSeconds: 2.5, playing: true, playbackRate: 1 });
|
||||
syncRuntimeMedia({
|
||||
clips: [clip],
|
||||
timeSeconds: 2.5 + 1e-9,
|
||||
playing: true,
|
||||
playbackRate: 1,
|
||||
});
|
||||
expect(clip.el.play).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("plays synchronously even when media is unbuffered (preserves user gesture)", () => {
|
||||
// Calling play() synchronously inside the user-gesture call chain lets the
|
||||
// browser queue playback until data buffers, while consuming the transient
|
||||
|
||||
@@ -173,7 +173,7 @@ export function syncRuntimeMedia(params: {
|
||||
// (el.ended resets to false when the user scrubs back, so seeks work.)
|
||||
const isActive =
|
||||
params.timeSeconds >= clip.start &&
|
||||
params.timeSeconds <= clip.end &&
|
||||
params.timeSeconds < clip.end &&
|
||||
relTime >= 0 &&
|
||||
(!el.ended || clip.loop);
|
||||
if (isActive) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:07aebceb20c5963e7a2feb392a564e2f592c39490dba1f6af07bb08b6a3c63d8
|
||||
size 12267006
|
||||
oid sha256:74c710f61ae3400ecc5477e9ab42fa78c25340e6947b8dc5e0a916da166620e4
|
||||
size 12282546
|
||||
|
||||
Reference in New Issue
Block a user