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
1 change: 1 addition & 0 deletions packages/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export {
resolveFinalFrameExtractionWindow,
resolveVideoExtractionDuration,
resolvePlayableVideoDuration,
extractionFrameCountForDuration,
resolveProjectRelativeSrc,
getFrameAtTime,
createFrameLookupTable,
Expand Down
26 changes: 22 additions & 4 deletions packages/engine/src/services/extractionCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const keyFor = (videoPath: string, overrides: Partial<CacheKeyInput> = {}): Cach
size: stat.size,
mediaStart: 0,
duration: 3,
fps: 30,
fps: "30",
format: "jpg",
...overrides,
};
Expand All @@ -63,8 +63,8 @@ function seedPartialDir(entry: { dir: string; keyHash: string }, frameContent: s
}

describe("extractionCache constants", () => {
it("exposes the v2 schema prefix", () => {
expect(SCHEMA_PREFIX).toBe("hfcache-v3-");
it("exposes the v4 schema prefix", () => {
expect(SCHEMA_PREFIX).toBe("hfcache-v4-");
});

it("exposes the frame filename prefix shared with the extractor", () => {
Expand Down Expand Up @@ -123,10 +123,16 @@ describe("computeCacheKey", () => {

it("changes when fps changes (different frame count invalidates key)", () => {
const a = computeCacheKey(base(sourceFile));
const b = computeCacheKey({ ...base(sourceFile), fps: 60 });
const b = computeCacheKey({ ...base(sourceFile), fps: "60" });
expect(a).not.toBe(b);
});

it("keeps exact rational rates distinct from their JavaScript decimal", () => {
const rational = computeCacheKey({ ...base(sourceFile), fps: "30000/1001" });
const decimal = computeCacheKey({ ...base(sourceFile), fps: String(30000 / 1001) });
expect(rational).not.toBe(decimal);
});

it("changes when format changes", () => {
const a = computeCacheKey(base(sourceFile));
const b = computeCacheKey({ ...base(sourceFile), format: "png" });
Expand Down Expand Up @@ -200,6 +206,18 @@ describe("lookupCacheEntry / markCacheEntryComplete", () => {

const base = (videoPath: string): CacheKeyInput => keyFor(videoPath);

it("does not reuse a complete entry from the v3 numeric-fps namespace", () => {
const input = base(sourceFile);
const keyHash = computeCacheKey(input);
const staleV3Dir = join(tmpRoot, `hfcache-v3-${keyHash.slice(0, 16)}`);
mkdirSync(staleV3Dir, { recursive: true });
writeFileSync(join(staleV3Dir, COMPLETE_SENTINEL), "", "utf-8");

const lookup = lookupCacheEntry(tmpRoot, input);
expect(lookup.hit).toBe(false);
expect(lookup.entry.dir).toBe(join(tmpRoot, `${SCHEMA_PREFIX}${keyHash.slice(0, 16)}`));
});

it("misses on an empty cache root", () => {
const lookup = lookupCacheEntry(tmpRoot, base(sourceFile));
expect(lookup.hit).toBe(false);
Expand Down
11 changes: 7 additions & 4 deletions packages/engine/src/services/extractionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ export const GC_MARKER = ".hf-last-gc";
* VFR-to-CFR re-encode, changing frame contents for VFR sources under
* identical key tuples. Without the bump, warm v2 entries (two-pass frames)
* would keep being served across the deploy boundary.
* v3 -> v4: the target fps identity is the exact FFmpeg argument instead of
* a JavaScript number. This invalidates entries created after rational NTSC
* rates had already been rounded to a decimal.
*/
export const SCHEMA_PREFIX = "hfcache-v3-";
export const SCHEMA_PREFIX = "hfcache-v4-";

/** Truncated hex chars of SHA-256 used for the entry directory name. */
const KEY_HEX_CHARS = 16;
Expand All @@ -84,8 +87,8 @@ export interface CacheKeyInput {
* so callers that pass an unresolved "natural duration" still produce a
* stable key across invocations. */
duration: number;
/** Target output frames-per-second. */
fps: number;
/** Exact target output frame-rate argument (for example `30000/1001`). */
fps: string;
/** Output image format. */
format: CacheFrameFormat;
/** Optional source transform applied during extraction. */
Expand Down Expand Up @@ -136,7 +139,7 @@ function canonicalKeyBlob(input: CacheKeyInput): string {
s: number;
ms: number;
d: number;
f: number;
f: string;
fmt: CacheFrameFormat;
t?: string;
} = {
Expand Down
Loading
Loading