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
46 changes: 46 additions & 0 deletions packages/cli/src/browser/ffmpeg.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("node:child_process", () => ({ execSync: vi.fn() }));
vi.mock("node:fs", () => ({ existsSync: vi.fn() }));

const mockExec = vi.mocked(execSync);
const mockExists = vi.mocked(existsSync);

// The common-dir fallback list is platform-gated (empty on win32), so pin the
// platform to a POSIX value to keep the test deterministic on Windows CI.
const originalPlatform = process.platform;
beforeEach(() => {
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
vi.resetModules();
});

afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
vi.clearAllMocks();
delete process.env.HYPERFRAMES_FFMPEG_PATH;
});

describe("findFFmpeg", () => {
it("falls back to a common install dir when `which` fails (GUI-launched PATH)", async () => {
// Simulate a process whose PATH lacks /opt/homebrew/bin: `which ffmpeg` throws.
mockExec.mockImplementation(() => {
throw new Error("which: no ffmpeg in PATH");
});
mockExists.mockImplementation((p) => p === "/opt/homebrew/bin/ffmpeg");

const { findFFmpeg } = await import("./ffmpeg.js");
expect(findFFmpeg()).toBe("/opt/homebrew/bin/ffmpeg");
});

it("returns undefined when ffmpeg is on neither PATH nor a common dir", async () => {
mockExec.mockImplementation(() => {
throw new Error("not found");
});
mockExists.mockReturnValue(false);

const { findFFmpeg } = await import("./ffmpeg.js");
expect(findFFmpeg()).toBeUndefined();
});
});
19 changes: 18 additions & 1 deletion packages/cli/src/browser/ffmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,30 @@ function findOnPath(name: "ffmpeg" | "ffprobe"): string | undefined {
}
}

// GUI/Dock/launchd-spawned processes on macOS don't inherit the shell PATH, so
// `which ffmpeg` fails even when ffmpeg is installed via Homebrew. Probe the
// well-known install dirs as a fallback. (No-op on Windows, where `where` and
// installer-added PATH entries cover it.)
const COMMON_BIN_DIRS =
process.platform === "win32"
? []
: ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/snap/bin"];

function findInCommonDirs(name: "ffmpeg" | "ffprobe"): string | undefined {
for (const dir of COMMON_BIN_DIRS) {
const candidate = `${dir}/${name}`;
if (existsSync(candidate)) return candidate;
}
return undefined;
}

function findConfiguredBinary(
envName: string,
binaryName: "ffmpeg" | "ffprobe",
): string | undefined {
const configured = process.env[envName]?.trim();
if (configured) return existsSync(configured) ? resolve(configured) : undefined;
return findOnPath(binaryName);
return findOnPath(binaryName) ?? findInCommonDirs(binaryName);
}

export function findFFmpeg(): string | undefined {
Expand Down
1 change: 1 addition & 0 deletions packages/player/src/runtime-message-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const makeCallbacks = (): MessageHandlerCallbacks => ({
updateControlsTime: vi.fn(),
updateControlsPlaying: vi.fn(),
dispatchEvent: vi.fn(),
onRuntimeReady: vi.fn(),
seek: vi.fn(),
play: vi.fn(),
getLoop: vi.fn(() => false),
Expand Down
27 changes: 0 additions & 27 deletions packages/studio/src/hooks/useGsapTweenCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,13 +366,6 @@ export function usePopulateKeyframeCacheForFile(
const { setKeyframeCache } = usePlayerStore.getState();
clearKeyframeCacheForFile(sf);
const { elements } = usePlayerStore.getState();
console.log(
"[kf:static] elements in store:",
elements
.map((e) => e.domId)
.filter(Boolean)
.join(", "),
);
const mergedByElement = new Map<string, GsapKeyframesData>();
for (const anim of parsed.animations) {
const id = extractIdFromSelector(anim.targetSelector);
Expand Down Expand Up @@ -415,12 +408,6 @@ export function usePopulateKeyframeCacheForFile(
mergedByElement.set(id, { ...kfData, keyframes: clipKeyframes });
}
}
console.log(
"[kf:static] merged elements:",
[...mergedByElement.keys()].join(", "),
"kf counts:",
[...mergedByElement.entries()].map(([k, v]) => `${k}:${v.keyframes.length}`).join(", "),
);
for (const [id, kfData] of mergedByElement) {
setKeyframeCache(`${sf}#${id}`, kfData);
setKeyframeCache(id, kfData);
Expand Down Expand Up @@ -454,12 +441,6 @@ export function usePopulateKeyframeCacheForFile(
if (el.domId) clipById.set(el.domId, { start: el.start, duration: el.duration });
}
const scanned = scanAllRuntimeKeyframes(iframe, clipById);
console.log(
"[kf:runtime] scanned",
scanned.size,
"elements:",
[...scanned.keys()].join(", "),
);
if (scanned.size === 0) return false;
const { setKeyframeCache, keyframeCache } = usePlayerStore.getState();
for (const [id, data] of scanned) {
Expand All @@ -470,14 +451,6 @@ export function usePopulateKeyframeCacheForFile(
if (alreadyCached) {
continue;
}
console.log(
"[kf:runtime] adding runtime entry:",
id,
"kfs:",
data.keyframes.length,
"arc:",
!!data.arcPath,
);
const entry = {
format: "percentage" as const,
keyframes: data.keyframes,
Expand Down
Loading