Skip to content

Conversation

Brendonovich
Copy link
Member

@Brendonovich Brendonovich commented Oct 17, 2025

On macOS, ScreenCaptureKit sometimes won't continue to output frames if there's only static content on screen. This accounts for that in the encoder by reusing the last received frame if the end time is > 0.5s greater than the most recent frame's timestamp. I'm not sure if this will fix issues of recordings being cut short but it will account for an edge case we're not handling.

Summary by CodeRabbit

  • Bug Fixes
    • Improved timestamp handling across the recording pipeline for more accurate frame sync on pause/resume and when finalizing recordings.
    • Preserves the most recent frame together with its timestamp so end-of-session timing is more precise and the final frame is queued when needed.
    • Added logging for failed final-frame queuing to improve reliability and diagnostics.

Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

Walkthrough

MP4Encoder now stores the most recent video frame alongside its timestamp and accepts owned frame values. queue_video_frame and finish signatures changed; finish can take an optional timestamp and may queue a final frame. The Muxer trait and its implementations were updated to accept a timestamp passed through the recording pipeline.

Changes

Cohort / File(s) Summary
MP4Encoder frame/timestamp changes
crates/enc-avfoundation/src/mp4.rs
Replaced most_recent_timestamp: Option<Duration> with most_recent_frame: Option<(arc::R<cm::SampleBuf>, Duration)>. queue_video_frame now takes arc::R<cm::SampleBuf> (owned/cloned) and updates most_recent_frame. finish(&mut self, timestamp: Option<Duration>) may queue a final frame if the provided timestamp differs >500ms from the stored frame timestamp; uses stored frame timestamp for end time. drop calls finish(None). Added error logging for final-frame queuing failures.
Muxer trait and callsite
crates/recording/src/output_pipeline/core.rs
Muxer::finish signature changed from fn finish(&mut self) -> anyhow::Result<()> to fn finish(&mut self, timestamp: Duration) -> anyhow::Result<()>. Call sites now compute and pass timestamps.instant().elapsed() into finish.
FFmpeg Muxer implementations
crates/recording/src/output_pipeline/ffmpeg.rs
Mp4Muxer::setup parameter renamed to unused placeholder (_: &mut TaskPool). Mp4Muxer::finish and OggMuxer::finish now accept a Duration parameter (fn finish(&mut self, _: Duration)), which is ignored internally; existing cleanup/trailer logic unchanged.
macOS AVFoundation Muxer
crates/recording/src/output_pipeline/macos.rs
AVFoundationMp4Muxer::finish now accepts timestamp: Duration and calls encoder.finish(Some(timestamp)). queue_video_frame now passes frame buffer by value (frame.sample_buf) instead of by reference. Errors still propagated.
Windows Muxer
crates/recording/src/output_pipeline/win.rs
WindowsMuxer::finish signature updated to accept an ignored Duration parameter (fn finish(&mut self, _: Duration)); internal finalization logic unchanged.

Sequence Diagram(s)

sequenceDiagram
    participant Recorder
    participant Pipeline
    participant Muxer
    participant Encoder as MP4Encoder

    Recorder->>Pipeline: queue_video_frame(frame)
    activate Pipeline
    Pipeline->>Muxer: queue_video_frame(frame)
    activate Muxer
    Muxer->>Encoder: queue_video_frame(frame: arc::R<SampleBuf>, timestamp)
    Encoder->>Encoder: most_recent_frame = Some((frame.clone(), timestamp))
    deactivate Muxer
    deactivate Pipeline

    Recorder->>Pipeline: finish()
    activate Pipeline
    Note over Pipeline: elapsed = timestamps.instant().elapsed()
    Pipeline->>Muxer: finish(elapsed)
    activate Muxer
    Muxer->>Encoder: finish(Some(elapsed))
    activate Encoder
    Note over Encoder: diff = elapsed - most_recent_frame.timestamp
    alt diff > 500ms
        Encoder->>Encoder: attempt to queue final frame at elapsed
        alt queue success
            Encoder->>Encoder: update most_recent_frame with queued timestamp
        else queue error
            Note right of Encoder: log error
        end
    end
    Encoder->>Encoder: compute end_time = most_recent_frame.1 - offset
    deactivate Encoder
    deactivate Muxer
    deactivate Pipeline
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I clutch each frame with tweaked delight,
Timestamps snugged close through day and night.
A final hop to place in time,
No orphaned frames — the end’s sublime.
Hoppity-hop, the recorder's right! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "desktop: account for screen capture not providing frames" accurately describes the core issue addressed in this changeset. The changes implement mechanisms to handle the scenario where ScreenCaptureKit on macOS stops outputting frames during static content by reusing the last received frame when a time gap exceeds 500ms. The title is specific, clear, and directly relates to the primary objective of the PR—addressing an edge case where frame delivery is interrupted. The prefix "desktop:" appropriately scopes the change to the desktop platform.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch account-for-no-frames

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 223fa48 and d48fed9.

📒 Files selected for processing (1)
  • crates/recording/src/output_pipeline/win.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Format Rust code using rustfmt and ensure all Rust code passes workspace-level clippy lints.
Rust modules should be named with snake_case, and crate directories should be in kebab-case.

Files:

  • crates/recording/src/output_pipeline/win.rs
crates/*/src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Rust crates should place tests within the src/ and/or a sibling tests/ directory for each crate inside crates/*.

Files:

  • crates/recording/src/output_pipeline/win.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: Brendonovich
PR: CapSoftware/Cap#1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.552Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.
🧬 Code graph analysis (1)
crates/recording/src/output_pipeline/win.rs (4)
crates/recording/src/output_pipeline/macos.rs (1)
  • finish (52-58)
crates/recording/src/output_pipeline/ffmpeg.rs (2)
  • finish (68-80)
  • finish (134-137)
crates/enc-avfoundation/src/mp4.rs (1)
  • finish (350-394)
crates/recording/src/output_pipeline/core.rs (1)
  • finish (769-769)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (1)
crates/recording/src/output_pipeline/win.rs (1)

212-218: LGTM: Signature updated to match trait requirement.

The finish method now accepts a Duration parameter to satisfy the updated Muxer trait. The parameter is intentionally unused (indicated by the _: prefix), which is appropriate since the frame extension logic is specific to macOS ScreenCaptureKit's behavior during static content. This approach is consistent with the FFmpeg implementation, which also ignores the timestamp.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
crates/enc-avfoundation/src/mp4.rs (1)

377-379: End time based on most_recent_frame.1 minus offset — consistent

Matches the timebase used at session start (ms). Consider documenting the 1000 time scale choice for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0416b18 and 3e9d7bc.

📒 Files selected for processing (4)
  • crates/enc-avfoundation/src/mp4.rs (8 hunks)
  • crates/recording/src/output_pipeline/core.rs (2 hunks)
  • crates/recording/src/output_pipeline/ffmpeg.rs (2 hunks)
  • crates/recording/src/output_pipeline/macos.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Format Rust code using rustfmt and ensure all Rust code passes workspace-level clippy lints.
Rust modules should be named with snake_case, and crate directories should be in kebab-case.

Files:

  • crates/recording/src/output_pipeline/ffmpeg.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/recording/src/output_pipeline/macos.rs
  • crates/enc-avfoundation/src/mp4.rs
crates/*/src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Rust crates should place tests within the src/ and/or a sibling tests/ directory for each crate inside crates/*.

Files:

  • crates/recording/src/output_pipeline/ffmpeg.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/recording/src/output_pipeline/macos.rs
  • crates/enc-avfoundation/src/mp4.rs
🧬 Code graph analysis (4)
crates/recording/src/output_pipeline/ffmpeg.rs (6)
crates/enc-avfoundation/src/mp4.rs (1)
  • finish (350-394)
crates/recording/src/output_pipeline/core.rs (1)
  • finish (769-769)
crates/recording/src/output_pipeline/macos.rs (1)
  • finish (52-58)
crates/enc-ffmpeg/src/mux/mp4.rs (1)
  • finish (98-118)
crates/recording/src/output_pipeline/win.rs (1)
  • finish (212-218)
crates/enc-ffmpeg/src/mux/ogg.rs (1)
  • finish (40-46)
crates/recording/src/output_pipeline/core.rs (4)
crates/enc-avfoundation/src/mp4.rs (1)
  • finish (350-394)
crates/recording/src/output_pipeline/ffmpeg.rs (3)
  • finish (68-80)
  • finish (134-137)
  • timestamp (22-24)
crates/recording/src/output_pipeline/macos.rs (1)
  • finish (52-58)
crates/recording/src/output_pipeline/win.rs (1)
  • finish (212-218)
crates/recording/src/output_pipeline/macos.rs (3)
crates/enc-avfoundation/src/mp4.rs (1)
  • finish (350-394)
crates/recording/src/output_pipeline/core.rs (2)
  • finish (769-769)
  • timestamp (750-750)
crates/recording/src/output_pipeline/ffmpeg.rs (3)
  • finish (68-80)
  • finish (134-137)
  • timestamp (22-24)
crates/enc-avfoundation/src/mp4.rs (2)
crates/recording/src/output_pipeline/core.rs (2)
  • timestamp (750-750)
  • finish (769-769)
crates/recording/src/output_pipeline/macos.rs (1)
  • finish (52-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Clippy
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (9)
crates/recording/src/output_pipeline/ffmpeg.rs (2)

68-80: OK to ignore timestamp in FFmpeg MP4 path

Accepting and ignoring the Duration is fine since the encoder already flushes and trailer writes finalize duration. No change requested.

Please confirm that cap_enc_ffmpeg cannot benefit from duplicating the last frame when input stalls (unlike ScreenCaptureKit). If needed later, the timestamp is now available to implement parity.


134-137: OggMuxer finish signature updated — behavior unchanged

Signature aligned; finish delegates to encoder.finish() and writes trailer as before. Looks good.

crates/recording/src/output_pipeline/macos.rs (1)

77-79: Pass-by-value is correct and aligns with new encoder API

Sending frame.sample_buf by value matches MP4Encoder::queue_video_frame(arc::R<...>). This avoids extra borrows and fits the (frame, timestamp) caching.

crates/enc-avfoundation/src/mp4.rs (5)

19-19: Cache (frame, timestamp) — good foundation for tail extension

Storing the last frame alongside its timestamp enables safe duplication on finish. Looks good.


214-216: API change: take owned SampleBuf

Taking arc::Rcm::SampleBuf is appropriate; internal code clones once to store for reuse. Call sites updated accordingly.


227-227: Update most_recent_frame on every enqueue

Correctly tracks the last frame to allow duplication later.


334-340: Pause stores timestamp from last frame — OK

Deriving pause point from most_recent_frame ensures accurate offset accounting across pauses.


399-400: Drop calls finish(None) — safe, idempotent cleanup

Prevents duplicate work if user forgets to finish; good.

crates/recording/src/output_pipeline/core.rs (1)

352-352: Timestamp origins are consistent; no inconsistency found

Verification confirms both code paths use the same timestamp reference. Line 352 calls timestamps.instant().elapsed() (measuring from the stored Instant to now), while lines 427 and 502 call duration_since(timestamps) (measuring from the same stored Instant to the current frame/timestamp). Both reference the same timestamps.instant as their origin, so there is no over/under-extension risk from clock mismatch.

The secondary consideration about handling pause-near-shutdown scenarios is an architectural design choice (whether to use elapsed time vs. last frame timestamp for finish), not a timestamp consistency problem.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e9d7bc and 223fa48.

📒 Files selected for processing (1)
  • crates/recording/src/output_pipeline/ffmpeg.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Format Rust code using rustfmt and ensure all Rust code passes workspace-level clippy lints.
Rust modules should be named with snake_case, and crate directories should be in kebab-case.

Files:

  • crates/recording/src/output_pipeline/ffmpeg.rs
crates/*/src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Rust crates should place tests within the src/ and/or a sibling tests/ directory for each crate inside crates/*.

Files:

  • crates/recording/src/output_pipeline/ffmpeg.rs
🧬 Code graph analysis (1)
crates/recording/src/output_pipeline/ffmpeg.rs (6)
crates/recording/src/output_pipeline/macos.rs (1)
  • finish (52-58)
crates/enc-avfoundation/src/mp4.rs (1)
  • finish (350-394)
crates/recording/src/output_pipeline/core.rs (1)
  • finish (769-769)
crates/enc-ffmpeg/src/mux/mp4.rs (1)
  • finish (98-118)
crates/recording/src/output_pipeline/win.rs (1)
  • finish (212-218)
crates/enc-ffmpeg/src/mux/ogg.rs (1)
  • finish (40-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (2)
crates/recording/src/output_pipeline/ffmpeg.rs (2)

42-42: LGTM: Idiomatic unused parameter naming.

Renaming the TaskPool parameter to _ clearly indicates it's unused in this implementation while still satisfying the trait signature.


68-80: LGTM: Correctly implements updated trait signature.

The Duration parameter is appropriately ignored here since the underlying FFmpeg encoders don't support the timestamp-based frame extension feature implemented in the AVFoundation encoder. The frame extension to handle ScreenCaptureKit gaps is macOS-specific.

@Brendonovich Brendonovich merged commit 90a36b2 into main Oct 17, 2025
15 checks passed
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