From 8effbdb2b79c034dab08eafea7455b857113aeaa Mon Sep 17 00:00:00 2001 From: Abhishek-kumarsingh Date: Tue, 8 Sep 2026 11:18:22 +0530 Subject: [PATCH] fix(engine): flag single-keyframe videos as sparse instead of skipping them analyzeKeyframeIntervals bailed out with isProblematic: false whenever a video had fewer than two keyframes, treating a single-GOP video the same as a still image. But one keyframe is the worst case for the failure mode the check exists to catch: every seek past 0 lands inside a single GOP spanning the whole file. A 10s single-GOP video went unreported while a 5s-interval video triggered the warning. When exactly one keyframe is found, the effective interval is now the stream duration (via extractMediaMetadata), not zero. Still images and single-frame assets keep their current behaviour since their duration is at or below the 2s threshold. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LQXJUdfkq6CGWh6KNfVXyw --- packages/engine/src/utils/ffprobe.test.ts | 85 +++++++++++++++++++++++ packages/engine/src/utils/ffprobe.ts | 18 ++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/utils/ffprobe.test.ts b/packages/engine/src/utils/ffprobe.test.ts index 2030dc1054..1c93881ed5 100644 --- a/packages/engine/src/utils/ffprobe.test.ts +++ b/packages/engine/src/utils/ffprobe.test.ts @@ -931,6 +931,91 @@ describe("ffprobe option separator", () => { }); }); +describe("analyzeKeyframeIntervals — single-keyframe videos", () => { + afterEach(() => { + vi.resetModules(); + vi.doUnmock("child_process"); + }); + + it("treats a single keyframe spanning a long duration as problematic, not as a skip", async () => { + const { spawn } = createSpawnSpy([ + // keyframe timestamp probe: one keyframe at t=0 + { kind: "exit", code: 0, stdout: "0.000000\n" }, + // duration probe (extractMediaMetadata), used to size the effective interval + { + kind: "exit", + code: 0, + stdout: JSON.stringify({ + streams: [ + { + codec_type: "video", + codec_name: "h264", + width: 640, + height: 360, + r_frame_rate: "30/1", + avg_frame_rate: "30/1", + }, + ], + format: { duration: "10.0" }, + }), + }, + ]); + vi.resetModules(); + vi.doMock("child_process", () => ({ spawn })); + + const { analyzeKeyframeIntervals } = await import("./ffprobe.js"); + const result = await analyzeKeyframeIntervals("/tmp/single-gop.mp4"); + + expect(result.keyframeCount).toBe(1); + expect(result.maxIntervalSeconds).toBe(10); + expect(result.avgIntervalSeconds).toBe(10); + expect(result.isProblematic).toBe(true); + }); + + it("does not flag a single keyframe on a short (still-image-like) asset", async () => { + const { spawn } = createSpawnSpy([ + { kind: "exit", code: 0, stdout: "0.000000\n" }, + { + kind: "exit", + code: 0, + stdout: JSON.stringify({ + streams: [ + { + codec_type: "video", + codec_name: "h264", + width: 640, + height: 360, + r_frame_rate: "30/1", + avg_frame_rate: "30/1", + }, + ], + format: { duration: "1.0" }, + }), + }, + ]); + vi.resetModules(); + vi.doMock("child_process", () => ({ spawn })); + + const { analyzeKeyframeIntervals } = await import("./ffprobe.js"); + const result = await analyzeKeyframeIntervals("/tmp/short-single-gop.mp4"); + + expect(result.keyframeCount).toBe(1); + expect(result.isProblematic).toBe(false); + }); + + it("still reports zero keyframes as non-problematic", async () => { + const { spawn } = createSpawnSpy([{ kind: "exit", code: 0, stdout: "" }]); + vi.resetModules(); + vi.doMock("child_process", () => ({ spawn })); + + const { analyzeKeyframeIntervals } = await import("./ffprobe.js"); + const result = await analyzeKeyframeIntervals("/tmp/no-keyframes.mp4"); + + expect(result.keyframeCount).toBe(0); + expect(result.isProblematic).toBe(false); + }); +}); + describe("parseFrameRate", () => { // Direct against the exported function. The previous table drove this // through extractMediaMetadata behind a spawn mock, which cost a diff --git a/packages/engine/src/utils/ffprobe.ts b/packages/engine/src/utils/ffprobe.ts index c4358494e1..2c7d440340 100644 --- a/packages/engine/src/utils/ffprobe.ts +++ b/packages/engine/src/utils/ffprobe.ts @@ -1050,11 +1050,25 @@ async function analyzeKeyframeIntervalsUncached(filePath: string): Promise parseFloat(line.trim())) .filter((t) => Number.isFinite(t)); - if (timestamps.length < 2) { + if (timestamps.length === 1) { + // A single keyframe means every seek past 0 lands inside one GOP + // spanning the whole file — the effective interval is the stream + // duration, not zero. Still images and single-frame assets stay + // unproblematic because their duration is at or below the threshold. + const { durationSeconds } = await extractMediaMetadata(filePath); + return { + avgIntervalSeconds: durationSeconds, + maxIntervalSeconds: durationSeconds, + keyframeCount: 1, + isProblematic: durationSeconds > 2, + }; + } + + if (timestamps.length === 0) { return { avgIntervalSeconds: 0, maxIntervalSeconds: 0, - keyframeCount: timestamps.length, + keyframeCount: 0, isProblematic: false, }; }