Summary
Follow-up to #2514 / #2516. PR #2516 (shipped in 0.7.60) added opt-in hold-last-frame: a non-looping <video> with an explicit data-duration longer than its source now holds its final decoded frame for the rest of the slot. That part works correctly.
But render aborts by default on exactly these compositions, because the video-coverage gate counts only decoded source frames and does not credit the intentionally-held tail introduced by #2516.
Repro
-
Any non-looping video shorter than its slot — e.g. a 3s clip in a 10s slot:
<video class="clip" src="short-3s.mp4" data-start="0" data-duration="10" data-track-index="0" muted playsinline></video>
-
npx hyperframes@0.7.60 render -c comp.html
Actual
Video "hero-video" captured 90 of expected 300 frames (coverage 30.0%, threshold 95.0%).
check/snapshot may pass while the encoded MP4 renders this clip blank — aborting render
to prevent shipping a wrong MP4. Set HF_VIDEO_COVERAGE_THRESHOLD=0 to disable this gate.
(90 = 3s × 30fps decoded source frames; 300 = the 10s × 30fps authored slot. Coverage reads 30%.)
Expected
The render completes. The held tail (frames 90–300) is the intended output introduced by #2516 — the last frame repeated — not a blank or truncated clip. The gate should credit those held-tail frames toward coverage.
Why it's a false positive
The gate exists to catch genuinely truncated/blank video. With #2516, low source-frame coverage inside an explicit slot is now a supported, deliberate state, and the gate can't distinguish it from the failure it's meant to catch. So every held-video render is blocked unless the guard is disabled globally via HF_VIDEO_COVERAGE_THRESHOLD=0 — which also disables the guard for legitimately-broken videos in the same render.
Suggested fix
Where the held tail is already modeled (the isHeldVideoTail / final-frame-hold logic added in #2516, around packages/engine/src/services/videoFrameExtractor.ts), count the held-tail frames as covered — or compute coverage against min(slotFrames, sourceFrames) for a non-looping explicit-slot video — so an intentional hold reports 100% while a truly short/blank clip still trips the gate.
Workaround
HF_VIDEO_COVERAGE_THRESHOLD=0 (disables the gate for the whole render).
Verified
Confirmed the hold itself works on 0.7.60: a 3s clip in a 10s slot renders correctly with the override, and the video-card region holds a steady mid-tone (≈RGB 128,125,115) across t=1.5/2.9/6.0/9.5s — the held-tail frames are pixel-identical to the final source frame, not blank (≈255,255,255). Only the default coverage gate blocks it.
Environment
- hyperframes
0.7.60
- macOS (arm64), Node
v24.15.0
Summary
Follow-up to #2514 / #2516. PR #2516 (shipped in
0.7.60) added opt-in hold-last-frame: a non-looping<video>with an explicitdata-durationlonger than its source now holds its final decoded frame for the rest of the slot. That part works correctly.But
renderaborts by default on exactly these compositions, because the video-coverage gate counts only decoded source frames and does not credit the intentionally-held tail introduced by #2516.Repro
Any non-looping video shorter than its slot — e.g. a 3s clip in a 10s slot:
npx hyperframes@0.7.60 render -c comp.htmlActual
(90 = 3s × 30fps decoded source frames; 300 = the 10s × 30fps authored slot. Coverage reads 30%.)
Expected
The render completes. The held tail (frames 90–300) is the intended output introduced by #2516 — the last frame repeated — not a blank or truncated clip. The gate should credit those held-tail frames toward coverage.
Why it's a false positive
The gate exists to catch genuinely truncated/blank video. With #2516, low source-frame coverage inside an explicit slot is now a supported, deliberate state, and the gate can't distinguish it from the failure it's meant to catch. So every held-video render is blocked unless the guard is disabled globally via
HF_VIDEO_COVERAGE_THRESHOLD=0— which also disables the guard for legitimately-broken videos in the same render.Suggested fix
Where the held tail is already modeled (the
isHeldVideoTail/ final-frame-hold logic added in #2516, aroundpackages/engine/src/services/videoFrameExtractor.ts), count the held-tail frames as covered — or compute coverage againstmin(slotFrames, sourceFrames)for a non-looping explicit-slot video — so an intentional hold reports 100% while a truly short/blank clip still trips the gate.Workaround
HF_VIDEO_COVERAGE_THRESHOLD=0(disables the gate for the whole render).Verified
Confirmed the hold itself works on
0.7.60: a 3s clip in a 10s slot renders correctly with the override, and the video-card region holds a steady mid-tone (≈RGB 128,125,115) across t=1.5/2.9/6.0/9.5s — the held-tail frames are pixel-identical to the final source frame, not blank (≈255,255,255). Only the default coverage gate blocks it.Environment
0.7.60v24.15.0