Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/cli/src/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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");
Expand Down
31 changes: 28 additions & 3 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand All @@ -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);
Expand All @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/runtime/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
};

Expand Down Expand Up @@ -2813,7 +2813,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;
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/runtime/media.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/runtime/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/producer/tests/style-7-prod/output/output.mp4
Git LFS file not shown
Loading