Skip to content

Sparse-keyframe check silently skips single-keyframe videos — the worst case goes unreported (0.8.10) #3460

Description

@skillet07

Summary

analyzeKeyframeIntervals bails out when a video has fewer than two keyframes and reports isProblematic: false. But a video with exactly one keyframe is the worst case for the very failure mode the warning exists to catch: every seek past 0 lands inside a single GOP that spans the whole file. The compiler warns about a 5-second GOP and stays silent about a 10-second one.

In analyzeKeyframeIntervalsUncached (bundled from engine/src/utils/ffprobe.ts):

const timestamps = stdout.split("\n").map(l => parseFloat(l.trim())).filter(Number.isFinite);
if (timestamps.length < 2) {
  return { avgIntervalSeconds: 0, maxIntervalSeconds: 0, keyframeCount: timestamps.length, isProblematic: false };
}

The early return is reasonable for still images and 1-frame assets, but it also swallows real single-GOP videos of arbitrary length.

Reproduction

Two 10-second clips, identical except for GOP size:

ffmpeg -y -f lavfi -i testsrc=size=640x360:rate=30:duration=10 \
  -c:v libx264 -pix_fmt yuv420p -g 1000 -keyint_min 1000 -sc_threshold 0 assets/single-gop.mp4
ffmpeg -y -f lavfi -i testsrc=size=640x360:rate=30:duration=10 \
  -c:v libx264 -pix_fmt yuv420p -g 150 -keyint_min 150 -sc_threshold 0 assets/five-second-gop.mp4

Keyframe timestamps:

single-gop.mp4        0.000000
five-second-gop.mp4   0.000000  5.000000

index.html — both clips, each seeking to 8s:

<div id="root" data-composition-id="gop-repro" data-start="0" data-duration="2" data-width="640" data-height="360">
  <video id="single-gop" class="clip layer" data-start="0" data-duration="1" data-track-index="0" data-media-start="8" src="assets/single-gop.mp4" muted></video>
  <video id="five-second-gop" class="clip layer" data-start="1" data-duration="1" data-track-index="0" data-media-start="8" src="assets/five-second-gop.mp4" muted></video>
</div>
npx hyperframes@0.8.10 render . -o out.mp4 2>&1 | grep Compiler

Observed

[WARN] [Compiler] WARNING: Video "five-second-gop" has sparse keyframes (max interval: 5s). ...

Nothing for single-gop, whose effective seek distance is 10s — twice as bad.

Expected

single-gop should warn at least as loudly as five-second-gop.

Suggested fix

When exactly one keyframe is found, the effective interval is the stream duration, not zero:

if (timestamps.length === 1) {
  const duration = await probeDurationSeconds(filePath); // already available via extractVideoMetadata
  return {
    avgIntervalSeconds: duration,
    maxIntervalSeconds: duration,
    keyframeCount: 1,
    isProblematic: duration > 2,
  };
}
if (timestamps.length === 0) {
  return { avgIntervalSeconds: 0, maxIntervalSeconds: 0, keyframeCount: 0, isProblematic: false };
}

Still images and single-frame assets keep their current behaviour, since their duration is 0 or below the threshold.

Why it matters in practice

This is not a synthetic corner case. In a 414-clip library downloaded straight from Pexels and Pixabay, 26 files are single-GOP with durations from 2.3s to 10s — exactly the ones the check skips. Overall that library sits well above the 2s threshold anyway (median max interval 3.04s, 82% above 3s), so in practice the check fires on nearly everything except the worst offenders.

Side note, offered as data rather than a complaint

While measuring the above we could not reproduce the failure the warning describes, on hyperframes render at least. Two stands cut deliberately mid-GOP — a stock clip with keyframes 10s apart, and an Archive.org master with 5.51s intervals, cut 3.5s past a keyframe — both rendered frame-accurate: mean per-pixel luma delta against ffmpeg-extracted ground truth was 1.4–1.8, versus 15–60 against any neighbouring moment. The video_extract phase resolves frames through ffmpeg (minVideoFrameCoverageRatio: 1), which decodes from the preceding keyframe correctly. Re-encoding per the suggested command changed render wall-clock by ~3% (95s vs 98s on the same stand), i.e. noise.

So the warning may be aimed at a different path (browser seeking in preview/studio?) than the one render takes. If that is the case, narrowing its wording or its scope might be worth considering alongside the fix above.

Environment: macOS 26.5.2 (arm64), Node 26.0.0, hyperframes 0.8.10 (also verified on 0.7.106).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions