Skip to content

fix(enc-ffmpeg): force recording keyframes every 2 s of capture time - #2641

Closed
richiemcilroy wants to merge 3 commits into
mainfrom
fix/time-based-keyframes
Closed

richiemcilroy wants to merge 3 commits into
mainfrom
fix/time-based-keyframes

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Crash-recoverable recordings are written as 2 s fragments, but a fragment can only end at a keyframe, and every recording encoder places keyframes by frame count. When frames arrive slower than the nominal rate, a fragment covers far more than 2 s of capture, and a crash loses all of it.

flowchart LR
    A[ScreenCaptureKit / WGC<br/>delivers only changes] --> B[encoder<br/>g = 2 s x nominal fps frames]
    B --> C[DASH writer<br/>cuts at the first keyframe after 2 s]
    C --> D[fragment on disk]
    style B fill:#fdd
Loading
  • g is round(2 x nominal fps) frames for libx264, NVENC, QSV, AMF and Media Foundation (crates/enc-ffmpeg/src/video/h264.rs:1225); recording VideoToolbox keeps FFmpeg's default gop_size of 12 frames.
  • At 60 fps nominal, libx264 cuts every 120 delivered frames. On a mostly static screen at 3 frames per second that is 40 s per fragment; on a loaded machine delivering 33 fps it is 3.6 s.
  • KeyframeCadence (crates/enc-ffmpeg/src/video/keyframe_cadence.rs) sends a frame as a keyframe (pict_type = I) once 2 s of capture time passed since the last keyframe. The last keyframe is the newest one that was requested by the caller, forced here, or seen in the encoder's output packets; the encoder's own frame count (gop_size) is only a prediction until its packet confirms it, so a scene-cut keyframe or a skipped one cannot shift the timer. Frames at the nominal rate are keyframes exactly where the encoder puts them today.
  • Keyframes callers request (pict_type = I on the input frame) are kept and restart the interval, also when the frame goes through the converter.
  • Applied to recording encoders only (H264Encoder and H264PacketEncoder built without export settings); exports are unchanged. NVENC and QSV get forced-idr / forced_idr so a forced frame is an IDR the writer can cut at; libx264 (closed GOP) and VideoToolbox already turn a forced I frame into an IDR.
  • A forced type is restored right after the send, so a pooled or reused frame never carries it into the next one.
let reference = anchor.max(predicted); // requested, forced or seen in the output
let due = pts - reference >= self.interval;
let keyframe = requested || due;

Keyframes and crash loss

Keyframe times from the recording encoder (unit tests in h264_packet.rs), 30 fps nominal:

input encoder before after
5 fps for 13 s libx264 0 s, 12 s every 2.0 to 2.2 s
1 fps for 13 s VideoToolbox 0 s, 12 s at most 3 s apart (2 s + one frame)
30 fps for 9 s libx264 0, 2, 4, 6, 8 s 0, 2, 4, 6, 8 s (unchanged)

Engine-verify crash suite, static screen (3 frames per second at 30 fps nominal), recorder killed after 11 s, Blacksmith runners:

platform before: complete on disk before: lost after: longest fragment after: lost
macOS (libx264) nothing 11.03 s 2.00 s 1.03 s
Linux nothing 11.03 s 2.00 s 1.03 s
Windows nothing 11.03 s 2.00 s 1.03 s

Real capture on Blacksmith macOS (libx264 path, ScreenCaptureKit delivering about 33 fps): the Studio crash-recovery case lost 4.5 to 4.8 s of screen before (fragments of 3.68 s, keyframes every 120 frames). With this change and #2640 it stays inside its 3 s budget in 10 of 10 runs on macOS 15 and 26.

RetriggerView in GreptileConfidence Score: 5/5

No new blocking issue was established.

Summary

Recording H.264 encoders now request keyframes using capture time rather than frame count alone.

  • Recording H.264 keyframes now follow capture time.

Both earlier threads were manually resolved without explanation. They were unnumbered, so neither receives a previousFindings entry. The VideoToolbox test now checks a minimum keyframe count.

The full PR diff was used because the previous and current commits have different merge bases with the target commit. No tests were run.

Reviews (3) · Last reviewed commit: "test(enc-ffmpeg): require VideoToolbox k..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cap-status Ready Ready Preview Oct 16, 2026 10:42am UTC

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found 1 potential issue (1 medium).

Open in Capy

Comment thread crates/enc-ffmpeg/src/video/keyframe_cadence.rs Outdated
Comment thread crates/enc-ffmpeg/src/video/keyframe_cadence.rs Outdated
Comment thread crates/enc-ffmpeg/src/video/h264_packet.rs
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@richiemcilroy

Copy link
Copy Markdown
Member Author

Replaced by #2646 (merged): Studio now flushes a fragment every 2 s of capture time without forcing extra keyframes, so the encoded size of sparse recordings is unchanged.

This branch was successfully deployed

2 active deployments
Preview – cap-web — 94ace03c Deployed Oct 16, 2026 by vercel[bot]
Preview – cap-status — 94ace03c Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant