Skip to content

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Sep 27, 2025

  • add a process-scoped ScreenCaptureKit cache with a single awaitable warm-up so later callers reuse in-flight work
  • trigger the prewarm when the main window appears and reuse the same state in the recording pipeline
  • raise the launch timeout to cover the slower first warm-up and keep logging tidy
  • updated tooling/types (tauri Specta event + utils) to wire the new prewarm trigger

Currently in production the current timeout of 5s is too slow - solely adjusting this is not enough currently on my machine, because every subsequent recording will still take my machine ~10s because there's no prewarming of SCK.

Before (production app):

Screen.Recording.2025-09-26.at.22.20.19.mov

After (with SCK warmup, fresh app startup):

Screen.Recording.2025-09-27.at.13.52.10.mov

Summary by CodeRabbit

  • New Features

    • macOS: ScreenCaptureKit prewarm integrated into app/window lifecycle to speed recording/preview startup.
    • Desktop: Thumbnail capture and collection for displays and windows (base64 PNG) on macOS and Windows.
  • Bug Fixes

    • macOS: Improved recording startup reliability via cached shareable content.
    • Reduced premature recording task failures by increasing per-task launch timeout on macOS.
  • Chores

    • CI: Rust workflow now installs clippy.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 27, 2025

Walkthrough

Adds macOS ScreenCaptureKit prewarming and a shareable-content cache with public async APIs; registers a RequestScreenCapturePrewarm event and prewarmer state; threads shareable content through recording builders and screen-capture sources; implements desktop thumbnail capture/collection and updates platform exports and macOS deps.

Changes

Cohort / File(s) Summary
App events & lifecycle (macOS prewarm wiring)
apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/src/windows.rs, apps/desktop/src-tauri/src/utils/tauri.ts
Add public RequestScreenCapturePrewarm { force: bool }; register/emit the Tauri event; add macOS-only ScreenCapturePrewarmer in app state; spawn listener/tasks to trigger prewarm on window show; update TS event typings and mapping.
macOS shareable content cache & API
apps/desktop/src-tauri/src/platform/macos/mod.rs, apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs
New sc_shareable_content module implementing a concurrency-safe global cache, deduplicated warmup coordination (Notify + in-flight task), WarmupTask/state machine, and public async APIs prewarm_shareable_content() and get_shareable_content().
Recording inputs & actor builders (threading shareable content)
apps/desktop/src-tauri/src/recording.rs, crates/recording/src/lib.rs, crates/recording/src/instant_recording.rs, crates/recording/src/studio_recording.rs, apps/cli/src/record.rs
Add macOS-only shareable_content field to RecordingBaseInputs; extend ActorBuilder::build signatures to accept and thread shareable_content into actor spawn/build calls; CLI now fetches current shareable content on macOS and passes it into build.
Screen capture source and capture flow (macOS)
crates/recording/src/sources/screen_capture/mod.rs, crates/recording/src/sources/screen_capture/macos.rs, crates/recording/src/capture_pipeline.rs
Add macOS-only shareable_content field and init parameter to ScreenCaptureSource; propagate retained shareable content into content-filter creation and into create_screen_capture calls; create_screen_capture signature extended under cfg(target_os = "macos").
Pipeline builder timeout tweak
crates/recording/src/pipeline/builder.rs
Per-task launch timeout set to 15s on macOS and 5s elsewhere via a launch_timeout variable.
SCAP targets & platform exposures
crates/scap-targets/src/platform/macos.rs, crates/scap-targets/src/platform/mod.rs, crates/scap-targets/Cargo.toml
Thread explicit ShareableContent into display/window lookup and content-filter construction; make macos module pub; add target-specific macOS tokio dependency with features ["sync","rt"].
Desktop thumbnails & collection
apps/desktop/src-tauri/src/thumbnails/mod.rs, apps/desktop/src-tauri/src/thumbnails/mac.rs, apps/desktop/src-tauri/src/thumbnails/windows.rs
Add thumbnail models CaptureDisplayWithThumbnail/CaptureWindowWithThumbnail, implement macOS and Windows thumbnail capture pipelines (pixel buffer handling, NV12/32-bit conversion, PNG encode, base64), and collectors to enumerate targets and fetch thumbnails concurrently.
Recording: remove old thumbnail helpers / integrate new flow
apps/desktop/src-tauri/src/recording.rs
Remove legacy thumbnail helper types and functions; switch list_displays_with_thumbnails/list_windows_with_thumbnails to new collectors; pass macOS shareable_content into actor construction paths.
CLI & app usage (deps)
apps/cli/Cargo.toml, apps/cli/src/record.rs
Add macOS-specific cidre dependency; CLI record path now awaits cidre::sc::ShareableContent::current() and passes it into actor builder on macOS.
CI tooling
.github/workflows/ci.yml
Ensure the clippy component is installed in the Rust toolchain step for the Clippy job.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Window
  participant App as Tauri App
  participant Prewarmer as ScreenCapturePrewarmer (macOS)
  participant SC as sc_shareable_content Cache
  participant Recorder
  participant ScreenCapture

  User->>Window: open/show
  Window->>App: emit "request-screen-capture-prewarm" (force=false)
  App->>Prewarmer: handle event
  Prewarmer->>SC: prewarm_shareable_content()
  alt cache present
    SC-->>Prewarmer: immediate OK
  else warmup needed
    SC->>SC: spawn/coordinate warmup (deduped)
    SC-->>Prewarmer: notify on completion
  end

  User->>Recorder: start recording
  Recorder->>SC: get_shareable_content()
  SC-->>Recorder: ShareableContent (retained)
  Recorder->>ScreenCapture: create_screen_capture(..., shareable_content)
  ScreenCapture-->>Recorder: initialized
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I prewarm the screen like a snug little den,
Cache up the pixels so capture's a friend.
Window emits, I hustle — warm, then retained,
Ready to stream when recording's ordained.
Hop, click, record — carrots for code! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately summarizes the main enhancement of the pull request by highlighting the macOS recording startup improvement through shared ScreenCaptureKit warm-up and is concise and specific to the changes implemented.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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 prewarming-sck

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: 2

🧹 Nitpick comments (6)
crates/recording/src/pipeline/builder.rs (1)

125-129: Consider OS-specific or configurable launch timeout

Raising to 15s helps macOS warm-up, but it may slow failure feedback on Windows. Consider gating by OS or making it configurable.

Apply this diff to keep 15s on macOS and 5s elsewhere:

-            tokio::time::timeout(Duration::from_secs(15), task.ready_signal.recv_async())
+            let launch_timeout = if cfg!(target_os = "macos") { 15 } else { 5 };
+            tokio::time::timeout(Duration::from_secs(launch_timeout), task.ready_signal.recv_async())
apps/desktop/src-tauri/src/windows.rs (1)

278-289: Avoid double-triggering prewarm

You both call prewarmer.request(false) and emit RequestScreenCapturePrewarm, which likely invokes the same code path again. The prewarmer dedupes, but this causes redundant work/logs. Prefer one path (local request is enough here).

Apply this diff to remove the redundant emit:

-                    if let Err(error) = (RequestScreenCapturePrewarm { force: false }).emit(app) {
-                        warn!(%error, "Failed to emit ScreenCaptureKit prewarm event");
-                    }

Based on learnings

crates/scap-targets/src/platform/macos.rs (1)

205-239: Minor: avoid redundant Vec re-collection.

windows is already a Vec; you can pass a slice directly to ns::Array.

Apply this diff:

-        let windows = windows.into_iter().collect::<Vec<_>>();
-        let excluded_windows = ns::Array::from_slice_retained(windows.as_slice());
+        let excluded_windows = ns::Array::from_slice_retained(&windows);
crates/scap-targets/src/platform/macos/cache.rs (3)

54-61: Simplify by returning the expression directly.

The let binding is unnecessary here.

Apply this diff to simplify:

-    let result = warmup
-        .result
-        .lock()
-        .await
-        .clone()
-        .expect("ScreenCaptureKit warmup task missing result");
-
-    result
+    warmup
+        .result
+        .lock()
+        .await
+        .clone()
+        .expect("ScreenCaptureKit warmup task missing result")

93-97: Collapse nested if statements.

These nested if statements can be collapsed into a single if-let expression with a guard.

Apply this diff to simplify:

-    if let Some(current) = guard.as_ref() {
-        if Arc::ptr_eq(&current.notify, &task.notify) {
-            *guard = None;
-        }
-    }
+    if let Some(current) = guard.as_ref()
+        && Arc::ptr_eq(&current.notify, &task.notify) {
+            *guard = None;
+    }

22-22: Consider simplifying the complex type with a type alias.

The nested generic type is complex and could benefit from a type alias for better readability.

Add a type alias at the module level:

type WarmupResult = Result<(), arc::R<ns::Error>>;

Then simplify the struct field:

 struct WarmupTask {
     notify: Arc<Notify>,
-    result: Arc<Mutex<Option<Result<(), arc::R<ns::Error>>>>>,
+    result: Arc<Mutex<Option<WarmupResult>>>,
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 273f2db and c3d130e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • apps/desktop/src-tauri/src/lib.rs (5 hunks)
  • apps/desktop/src-tauri/src/windows.rs (2 hunks)
  • apps/desktop/src/utils/tauri.ts (3 hunks)
  • crates/recording/src/capture_pipeline.rs (1 hunks)
  • crates/recording/src/pipeline/builder.rs (1 hunks)
  • crates/scap-targets/Cargo.toml (1 hunks)
  • crates/scap-targets/src/lib.rs (1 hunks)
  • crates/scap-targets/src/platform/macos.rs (4 hunks)
  • crates/scap-targets/src/platform/macos/cache.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
crates/**/src/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

For desktop IPC, use tauri_specta derive/macros on Rust types/events; do not hand-roll bindings

Files:

  • crates/scap-targets/src/lib.rs
  • crates/recording/src/pipeline/builder.rs
  • crates/scap-targets/src/platform/macos/cache.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/scap-targets/src/platform/macos.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add inline, block, or docstring comments in any language; code must be self-explanatory

Files:

  • crates/scap-targets/src/lib.rs
  • apps/desktop/src/utils/tauri.ts
  • crates/recording/src/pipeline/builder.rs
  • apps/desktop/src-tauri/src/windows.rs
  • crates/scap-targets/src/platform/macos/cache.rs
  • crates/recording/src/capture_pipeline.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/scap-targets/src/platform/macos.rs
**/*.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/scap-targets/src/lib.rs
  • crates/recording/src/pipeline/builder.rs
  • apps/desktop/src-tauri/src/windows.rs
  • crates/scap-targets/src/platform/macos/cache.rs
  • crates/recording/src/capture_pipeline.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/scap-targets/src/platform/macos.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/scap-targets/src/lib.rs
  • crates/recording/src/pipeline/builder.rs
  • crates/scap-targets/src/platform/macos/cache.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/scap-targets/src/platform/macos.rs
**/tauri.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Never edit auto-generated IPC bindings file tauri.ts

Do not edit auto-generated files named tauri.ts.

Files:

  • apps/desktop/src/utils/tauri.ts
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/**/*.{ts,tsx}: Do not manually import icons in the desktop app; rely on auto-imported icons
In the desktop app, use @tanstack/solid-query for server state management

Files:

  • apps/desktop/src/utils/tauri.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; leverage shared types from packages

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Files:

  • apps/desktop/src/utils/tauri.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/desktop/src/utils/tauri.ts
🧠 Learnings (1)
📚 Learning: 2025-09-22T14:17:47.407Z
Learnt from: CR
PR: CapSoftware/Cap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-22T14:17:47.407Z
Learning: Applies to crates/**/src/**/*.rs : For desktop IPC, use tauri_specta derive/macros on Rust types/events; do not hand-roll bindings

Applied to files:

  • crates/scap-targets/src/lib.rs
  • apps/desktop/src-tauri/src/windows.rs
🧬 Code graph analysis (6)
crates/scap-targets/src/lib.rs (2)
crates/scap-targets/src/platform/macos.rs (1)
  • prewarm_shareable_content (22-24)
crates/scap-targets/src/platform/macos/cache.rs (1)
  • prewarm_shareable_content (31-62)
apps/desktop/src-tauri/src/windows.rs (2)
apps/desktop/src-tauri/src/lib.rs (6)
  • tauri_specta (1929-2040)
  • app (1213-1214)
  • app (2295-2295)
  • app (2326-2326)
  • app (2363-2363)
  • app (2369-2369)
apps/desktop/src/utils/tauri.ts (1)
  • RequestScreenCapturePrewarm (437-437)
crates/scap-targets/src/platform/macos/cache.rs (1)
crates/scap-targets/src/platform/macos.rs (1)
  • prewarm_shareable_content (22-24)
crates/recording/src/capture_pipeline.rs (2)
crates/scap-targets/src/platform/macos.rs (1)
  • prewarm_shareable_content (22-24)
crates/scap-targets/src/platform/macos/cache.rs (1)
  • prewarm_shareable_content (31-62)
apps/desktop/src-tauri/src/lib.rs (3)
apps/desktop/src/utils/tauri.ts (1)
  • RequestScreenCapturePrewarm (437-437)
crates/scap-targets/src/platform/macos.rs (1)
  • prewarm_shareable_content (22-24)
crates/scap-targets/src/platform/macos/cache.rs (2)
  • state (27-29)
  • prewarm_shareable_content (31-62)
crates/scap-targets/src/platform/macos.rs (1)
crates/scap-targets/src/platform/macos/cache.rs (5)
  • prewarm_shareable_content (31-62)
  • get_display (100-135)
  • display (206-208)
  • get_window (137-172)
  • window (210-212)
🪛 GitHub Check: Clippy
crates/scap-targets/src/platform/macos/cache.rs

[warning] 93-97: this if statement can be collapsed
warning: this if statement can be collapsed
--> crates/scap-targets/src/platform/macos/cache.rs:93:5
|
93 | / if let Some(current) = guard.as_ref() {
94 | | if Arc::ptr_eq(&current.notify, &task.notify) {
95 | | *guard = None;
96 | | }
97 | | }
| |_____^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
= note: #[warn(clippy::collapsible_if)] on by default
help: collapse nested if block
|
93 ~ if let Some(current) = guard.as_ref()
94 ~ && Arc::ptr_eq(&current.notify, &task.notify) {
95 | *guard = None;
96 ~ }
|


[warning] 61-61: returning the result of a let binding from a block
warning: returning the result of a let binding from a block
--> crates/scap-targets/src/platform/macos/cache.rs:61:5
|
54 | / let result = warmup
55 | | .result
56 | | .lock()
57 | | .await
58 | | .clone()
59 | | .expect("ScreenCaptureKit warmup task missing result");
| |_______________________________________________________________- unnecessary let binding
60 |
61 | result
| ^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_and_return
= note: #[warn(clippy::let_and_return)] on by default
help: return the expression directly
|
54 ~
55 |
56 ~ warmup
57 + .result
58 + .lock()
59 + .await
60 + .clone()
61 + .expect("ScreenCaptureKit warmup task missing result")
|


[warning] 22-22: very complex type used. Consider factoring parts into type definitions
warning: very complex type used. Consider factoring parts into type definitions
--> crates/scap-targets/src/platform/macos/cache.rs:22:13
|
22 | result: Arc<Mutex<Option<Result<(), arc::Rns::Error>>>>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
= note: #[warn(clippy::type_complexity)] on by default

⏰ 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: Vercel Agent Review
  • 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 (22)
crates/scap-targets/src/lib.rs (1)

5-6: macOS-only re-export looks good

The conditional public re-export is clean and keeps non-macOS builds unaffected.

crates/recording/src/capture_pipeline.rs (1)

790-804: Prewarm before capture: LGTM

The macOS-only prewarm with trace/warn logging is correct and safely non-fatal.

apps/desktop/src-tauri/src/windows.rs (1)

19-24: Imports update is fine

Event trait import and expanded tracing are appropriate; unused-imports are already allowed at file top.

apps/desktop/src/utils/tauri.ts (2)

293-296: LGTM: event wiring added correctly (auto-generated file; do not edit manually).

requestScreenCapturePrewarm is mapped with the expected kebab-case name. No action needed. As per coding guidelines, this file is auto-generated and should not be edited manually.

Also applies to: 315-316


437-437: LGTM: TS payload matches Rust event shape.

force?: boolean aligns with #[serde(default)] pub force: bool. No action needed.

apps/desktop/src-tauri/src/lib.rs (7)

82-84: macOS‑gated import is correct.

tokio::sync::Mutex is only pulled on macOS. Good guard.


325-330: LGTM: New event payload is minimal and forward‑compatible.

#[serde(default)] on force matches the frontend’s optional field.


338-350: LGTM: Simple prewarm state machine and default initialization.

State enum + Mutex-wrapped holder are appropriate here.

Also applies to: 351-358


2023-2024: LGTM: Event registered with specta; keeps TS bindings in sync.


2163-2164: LGTM: Prewarmer added to app state on macOS only.


2293-2298: LGTM: Event handler invokes prewarm; executes off the UI thread.

listen_any_spawn already spawns; awaiting request here is fine.


360-411: Fix tracing field usage; current macro call likely doesn’t record elapsed_ms.

Use key=value (or %/? shorthand) in tracing! fields to avoid compile/log issues.

Apply this diff:

@@
-            Ok(()) => {
-                let elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0;
-                *state = PrewarmState::Warmed;
-                trace!(elapsed_ms, "ScreenCaptureKit cache warmed");
-            }
+            Ok(()) => {
+                let elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0;
+                *state = PrewarmState::Warmed;
+                trace!(elapsed_ms = elapsed_ms, "ScreenCaptureKit cache warmed");
+            }

Likely an incorrect or invalid review comment.

crates/scap-targets/src/platform/macos.rs (4)

18-24: LGTM: Introduces cache module and public prewarm entrypoint.

Good placement; logging via tracing is consistent with the rest of the crate.


184-195: LGTM: Display resolution now goes through warmed cache with error logging.

Gracefully degrades to None on cache errors.


229-236: LGTM: Content filter creation includes timing trace; helpful for perf telemetry.


490-501: LGTM: Window resolution now uses cache with warnings on failure.

Matches the display path; consistent behavior.

crates/scap-targets/src/platform/macos/cache.rs (6)

31-62: LGTM! Clean implementation of warmup orchestration.

The function correctly handles the deduplication of in-flight warmup tasks and properly awaits their completion. The trace logging provides good observability for debugging.


64-98: LGTM! Robust warmup execution with proper cleanup.

The warmup task correctly populates the cache, measures performance, and ensures cleanup of the warmup state. The error handling and logging are comprehensive.


100-135: LGTM! Efficient display lookup with fallback to warmup.

The function implements an efficient cache-first lookup pattern with automatic fallback to warmup on cache miss. The timing instrumentation is helpful for performance monitoring.


137-172: LGTM! Consistent window lookup implementation.

The window lookup function mirrors the display lookup pattern consistently, maintaining the same efficient cache-first approach with warmup fallback.


185-204: LGTM! Efficient cache structure with pre-built lookup maps.

The ShareableContentCache implementation efficiently pre-builds HashMap lookups during construction, avoiding repeated linear searches. The use of retained() ensures proper reference counting for the cached objects.


182-183: Verify thread safety of ShareableContentCache contents

Confirm that arc::R<sc::ShareableContent>, arc::R<sc::Display>, and arc::R<sc::Window> (and the underlying types) are all Send + Sync before unsafely impl-ing them on ShareableContentCache. I couldn’t find any Send/Sync impls for these in the code—please add explicit proofs or documentation.

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/recording/src/sources/screen_capture/macos.rs (1)

4-4: Import tracing macros used below

warn! and error! are used later but not imported; add them to avoid unresolved macro errors.

-use tracing::{debug, info, trace};
+use tracing::{debug, info, trace, warn, error};

As per coding guidelines.

crates/recording/src/lib.rs (1)

42-49: Gate macOS‐only shareable_content in RecordingBaseInputs

shareable_content references a macOS‐only type but isn’t cfg-gated in RecordingBaseInputs, leading to non-macOS build failures. Wrap it in #[cfg(target_os = "macos")] to align with downstream usage and avoid compilation errors.

Apply:

--- crates/recording/src/lib.rs
@@ pub struct RecordingBaseInputs {
-    pub shareable_content: cidre::arc::R<cidre::sc::ShareableContent>,
+    #[cfg(target_os = "macos")]
+    pub shareable_content: cidre::arc::R<cidre::sc::ShareableContent>,
 }
🧹 Nitpick comments (1)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1)

92-126: Consider panic safety for in-flight warmup.

If the async warmup block panics before line 114, task.result is never set and notify_waiters is never called, causing awaiting tasks to hang indefinitely. Consider wrapping the warmup logic with catch_unwind or ensuring that panics in ScreenCaptureKit APIs are handled gracefully.

Apply this pattern to ensure result is always set:

 async fn run_warmup(task: WarmupTask) {
-    let result = async {
+    let result = std::panic::AssertUnwindSafe(async {
         let warm_start = Instant::now();
         debug!("Populating ScreenCaptureKit shareable content cache");
 
         let content = sc::ShareableContent::current().await?;
         let cache = ShareableContentCache::new(content);
         let elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0;
 
         let mut guard = state().cache.write().unwrap();
         let replaced = guard.is_some();
         *guard = Some(cache);
 
         info!(
             elapsed_ms,
             replaced, "ScreenCaptureKit shareable content cache populated"
         );
         Ok::<(), arc::R<ns::Error>>(())
-    }
-    .await;
+    })
+    .catch_unwind()
+    .await
+    .unwrap_or_else(|_| Err(/* convert panic to error */));
 
     {
         let mut res_guard = task.result.lock().await;
         *res_guard = Some(result);
     }
 
     task.notify.notify_waiters();
 
     let mut guard = state().warmup.lock().await;
     if let Some(current) = guard.as_ref()
         && Arc::ptr_eq(&current.notify, &task.notify)
     {
         *guard = None;
     }
 }

Alternatively, document that panics in ScreenCaptureKit are not expected and accept the hang as an edge case.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cafb382 and b365307.

📒 Files selected for processing (12)
  • apps/desktop/src-tauri/src/lib.rs (5 hunks)
  • apps/desktop/src-tauri/src/platform/macos/mod.rs (1 hunks)
  • apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1 hunks)
  • apps/desktop/src-tauri/src/recording.rs (3 hunks)
  • crates/recording/src/capture_pipeline.rs (2 hunks)
  • crates/recording/src/instant_recording.rs (3 hunks)
  • crates/recording/src/lib.rs (1 hunks)
  • crates/recording/src/pipeline/builder.rs (1 hunks)
  • crates/recording/src/sources/screen_capture/macos.rs (1 hunks)
  • crates/recording/src/sources/screen_capture/mod.rs (4 hunks)
  • crates/recording/src/studio_recording.rs (6 hunks)
  • crates/scap-targets/src/platform/macos.rs (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • crates/recording/src/studio_recording.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/recording/src/pipeline/builder.rs
  • crates/recording/src/capture_pipeline.rs
🧰 Additional context used
📓 Path-based instructions (4)
crates/**/src/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

For desktop IPC, use tauri_specta derive/macros on Rust types/events; do not hand-roll bindings

Files:

  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/scap-targets/src/platform/macos.rs
  • crates/recording/src/sources/screen_capture/mod.rs
  • crates/recording/src/instant_recording.rs
  • crates/recording/src/lib.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add inline, block, or docstring comments in any language; code must be self-explanatory

Files:

  • crates/recording/src/sources/screen_capture/macos.rs
  • apps/desktop/src-tauri/src/platform/macos/mod.rs
  • apps/desktop/src-tauri/src/recording.rs
  • crates/scap-targets/src/platform/macos.rs
  • crates/recording/src/sources/screen_capture/mod.rs
  • crates/recording/src/instant_recording.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/recording/src/lib.rs
  • apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs
**/*.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/sources/screen_capture/macos.rs
  • apps/desktop/src-tauri/src/platform/macos/mod.rs
  • apps/desktop/src-tauri/src/recording.rs
  • crates/scap-targets/src/platform/macos.rs
  • crates/recording/src/sources/screen_capture/mod.rs
  • crates/recording/src/instant_recording.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/recording/src/lib.rs
  • apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.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/sources/screen_capture/macos.rs
  • crates/scap-targets/src/platform/macos.rs
  • crates/recording/src/sources/screen_capture/mod.rs
  • crates/recording/src/instant_recording.rs
  • crates/recording/src/lib.rs
🧬 Code graph analysis (6)
crates/recording/src/sources/screen_capture/macos.rs (2)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1)
  • display (160-162)
crates/recording/src/sources/screen_capture/mod.rs (1)
  • display (67-73)
apps/desktop/src-tauri/src/recording.rs (3)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1)
  • get_shareable_content (63-90)
crates/recording/src/instant_recording.rs (1)
  • builder (154-156)
crates/recording/src/studio_recording.rs (1)
  • builder (129-131)
crates/scap-targets/src/platform/macos.rs (2)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (2)
  • display (160-162)
  • window (164-166)
crates/scap-targets/src/lib.rs (1)
  • display (146-148)
crates/recording/src/instant_recording.rs (1)
apps/desktop/src-tauri/src/recording.rs (1)
  • inputs (66-71)
apps/desktop/src-tauri/src/lib.rs (2)
apps/desktop/src/utils/tauri.ts (1)
  • RequestScreenCapturePrewarm (437-437)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (2)
  • state (28-30)
  • prewarm_shareable_content (32-61)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (3)
crates/recording/src/sources/screen_capture/mod.rs (1)
  • display (67-73)
crates/scap-targets/src/platform/macos.rs (4)
  • display (466-486)
  • Self (279-288)
  • id (45-45)
  • id (295-297)
crates/scap-targets/src/lib.rs (3)
  • display (146-148)
  • id (28-30)
  • id (118-120)
⏰ 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: Vercel Agent Review
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (21)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (5)

1-11: LGTM!

Imports are appropriate for implementing a cached, async-aware ScreenCaptureKit prewarming layer.


12-30: LGTM!

The cache state and warmup coordination structures correctly enable single in-flight warmup with multiple awaiters via Notify, and safe concurrent cache access via RwLock.


139-167: LGTM!

The ShareableContentCache implementation correctly builds lookup tables for displays and windows, enabling efficient O(1) access by ID.


32-61: run_warmup sets result before notify
The code stores result in task.result under lock before calling task.notify.notify_waiters(), so the expect in prewarm_shareable_content cannot panic due to a missing result.


63-90: Remove stale concern about Ok(None) after prewarm
prewarm_shareable_content() only returns Ok(()) once run_warmup has populated state().cache via *guard = Some(cache);, so the second read in get_shareable_content() can never yield None after a successful prewarm.

Likely an incorrect or invalid review comment.

crates/scap-targets/src/platform/macos.rs (4)

177-183: LGTM!

Refactoring as_sc to accept explicit content parameter enables reuse of prewarmed shareable content and avoids redundant ScreenCaptureKit queries.


185-191: LGTM!

Delegating to as_content_filter_excluding_windows with an empty window list is correct and maintains backward compatibility.


193-208: LGTM!

The refactored as_content_filter_excluding_windows correctly resolves the display from the provided content and constructs the content filter with excluded windows.


458-464: LGTM!

Refactoring as_sc for WindowImpl to accept explicit content aligns with the display implementation and enables efficient reuse of cached shareable content.

apps/desktop/src-tauri/src/platform/macos/mod.rs (1)

14-16: LGTM!

The module declaration and public re-export correctly expose the shareable content prewarming API to the rest of the macOS platform codebase.

apps/desktop/src-tauri/src/recording.rs (2)

409-418: LGTM: Studio builder correctly forwards shareable_content under macOS

Passing the extra param behind cfg matches the builder signature change and keeps other platforms unaffected.


382-387: LGTM: macOS-only content fetch is correctly gated and error-mapped
Confirm CI passes for non-macOS builds after the lib.rs cfg update.

crates/recording/src/sources/screen_capture/mod.rs (1)

202-204: Approve macOS-only ShareableContent integration

The cfg-gated field is properly propagated across init, clone, and invocation sites and is consumed in src/sources/screen_capture/macos.rs via ShareableContent::current().await. No dead code remains after this refactor.

crates/recording/src/instant_recording.rs (2)

186-202: LGTM! Shareable content parameter correctly gated and propagated.

The macOS-only shareable_content parameter is properly added to the build signature with #[cfg(target_os = "macos")], forwarded into RecordingBaseInputs, and correctly threaded through the actor spawn path.


244-244: No action needed: the .retained() call returns cidre::arc::R<cidre::sc::ShareableContent>, matching the create_screen_capture parameter.

apps/desktop/src-tauri/src/lib.rs (6)

82-83: LGTM! Conditional Mutex import for macOS.

The Mutex import from tokio::sync is correctly gated with #[cfg(target_os = "macos")] for the prewarmer state machine.


325-329: LGTM! Event payload correctly defined with tauri_specta.

The RequestScreenCapturePrewarm struct uses proper derive macros for Tauri IPC and includes a force field with serde default, aligning with the coding guidelines for desktop IPC.


2162-2163: LGTM! ScreenCapturePrewarmer managed in app setup.

The ScreenCapturePrewarmer is correctly initialized and managed in the Tauri app state, gated with #[cfg(target_os = "macos")].


2023-2023: LGTM! Event registered in specta.

The RequestScreenCapturePrewarm event is correctly added to the collect_events! macro for TypeScript type generation.


2293-2297: LGTM! Event handler correctly wired.

The event handler retrieves the ScreenCapturePrewarmer from app state and invokes request(event.force).await, properly integrating the prewarm trigger into the app lifecycle.


338-411: Clarify force flag and concurrent request semantics

  • force:true resets only the local PrewarmState; prewarm_shareable_content uses a shared warmup mutex to join any in-flight warmup rather than abandoning it.
  • request(false) during Warming returns immediately (non-blocking) by design. Callers that need to await the warmed cache should call request(true) or synchronize explicitly on the shared cache.

Comment on lines +128 to +134
#[derive(Debug)]
struct ShareableContentCache {
#[allow(dead_code)]
content: arc::R<sc::ShareableContent>,
displays: HashMap<CGDirectDisplayID, arc::R<sc::Display>>,
windows: HashMap<CGWindowID, arc::R<sc::Window>>,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove incorrect #[allow(dead_code)] on content field.

The content field is used on line 72 (v.content.retained()), so the #[allow(dead_code)] attribute is incorrect.

Apply this diff:

 #[derive(Debug)]
 struct ShareableContentCache {
-    #[allow(dead_code)]
     content: arc::R<sc::ShareableContent>,
     displays: HashMap<CGDirectDisplayID, arc::R<sc::Display>>,
     windows: HashMap<CGWindowID, arc::R<sc::Window>>,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Debug)]
struct ShareableContentCache {
#[allow(dead_code)]
content: arc::R<sc::ShareableContent>,
displays: HashMap<CGDirectDisplayID, arc::R<sc::Display>>,
windows: HashMap<CGWindowID, arc::R<sc::Window>>,
}
#[derive(Debug)]
struct ShareableContentCache {
content: arc::R<sc::ShareableContent>,
displays: HashMap<CGDirectDisplayID, arc::R<sc::Display>>,
windows: HashMap<CGWindowID, arc::R<sc::Window>>,
}
🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs around
lines 128 to 134, the ShareableContentCache.content field is incorrectly
annotated with #[allow(dead_code)] despite being used elsewhere
(v.content.retained() at line 72); remove the #[allow(dead_code)] attribute from
the content field so the field's usage is correctly recognized by the compiler
and linter.

Comment on lines +136 to +137
unsafe impl Send for ShareableContentCache {}
unsafe impl Sync for ShareableContentCache {}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify arc::R is thread-safe before manual Send/Sync impls.

Manual unsafe impl Send and Sync for ShareableContentCache assume that arc::R<sc::ShareableContent>, arc::R<sc::Display>, and arc::R<sc::Window> are thread-safe. Verify that cidre::arc::R behaves like std::sync::Arc and is safe to send/share across threads.


🌐 Web query:

Is cidre::arc::R thread-safe and similar to std::sync::Arc in Rust?

💡 Result:

Short answer: No.

Details:

  • In the cidre crate arc module, R is a type alias for Retained, a thin wrapper around a &'static mut T that calls Objective‑C retain/release (the module defines Retained/Allocated and pub type R = Retained). This is an Obj‑C ownership helper, not an atomically reference‑counted, thread‑safe pointer. [1]
  • std::sync::Arc is an atomically reference‑counted, thread‑safe shared pointer (atomic refcounting; implements Send/Sync when T does). R in cidre does not provide that behavior. [2]

If you want thread‑safe shared ownership in Rust use std::sync::Arc (or wrap cidre types in synchronization primitives yourself).

Sources:
[1] cidre crate arc.rs (defines Retained/Allocated and pub type R = Retained). (docs.rs)
[2] std::sync::Arc documentation (thread‑safe, atomically reference counted). (doc.rust-lang.org)

Citations:


Remove manual unsafe impl Send/Sync for ShareableContentCache (apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs lines 136–137): cidre::arc::R is not thread-safe. Use std::sync::Arc or wrap the contents in proper synchronization primitives for cross-thread sharing.

🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs around
lines 136–137, remove the manual unsafe impl Send/Sync for ShareableContentCache
because cidre::arc::R is not thread-safe; instead make the cache use thread-safe
primitives (for example replace cidre::arc::R with std::sync::Arc if the inner
type is Send + Sync, or wrap the inner data in std::sync::Mutex/RwLock/Arc to
ensure proper synchronization), delete the unsafe impls, and adjust types/usages
so ShareableContentCache is only shared across threads via these safe, standard
synchronization primitives.

};

use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize};
use tracing::trace;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

❓ Verification inconclusive

Verify tracing::trace is used.

The trace import is added but no trace!() macro calls are visible in the provided code. Confirm this import is necessary or remove it if unused.


🏁 Script executed:

#!/bin/bash
# Description: Check for trace! macro usage in this file.

rg -n 'trace!' crates/scap-targets/src/platform/macos.rs

Length of output: 78


🏁 Script executed:

#!/bin/bash
# Check for any trace!() macro calls in the macOS platform file
rg -nF 'trace!(' crates/scap-targets/src/platform/macos.rs

Length of output: 0


Remove unused import
Drop use tracing::trace; from crates/scap-targets/src/platform/macos.rs (no trace!() calls present).

🤖 Prompt for AI Agents
In crates/scap-targets/src/platform/macos.rs around line 18, the file imports
tracing::trace but does not use trace!() anywhere; remove the unused import line
`use tracing::trace;` from the file to eliminate the dead import and any
unused-import lint warnings.

Comment on lines 140 to 142
let content = sc::ShareableContent::current()
.await
.map_err(|e| SourceError::CreateActor(e))?;
Copy link
Contributor

Choose a reason for hiding this comment

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

The screen capture source is still calling sc::ShareableContent::current() directly instead of using the cached shareable_content parameter, completely defeating the caching optimization.

View Details
📝 Patch Details
diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs
index 98131d4c..7c0d2110 100644
--- a/crates/recording/src/sources/screen_capture/macos.rs
+++ b/crates/recording/src/sources/screen_capture/macos.rs
@@ -128,6 +128,7 @@ impl PipelineSourceTask for ScreenCaptureSource<CMSampleBufferCapture> {
         let video_tx = self.video_tx.clone();
         let audio_tx = self.audio_tx.clone();
         let config = self.config.clone();
+        let shareable_content = self.shareable_content.clone();
 
         self.tokio_handle
             .block_on(async move {
@@ -137,9 +138,7 @@ impl PipelineSourceTask for ScreenCaptureSource<CMSampleBufferCapture> {
                 let display = Display::from_id(&config.display)
                     .ok_or_else(|| SourceError::NoDisplay(config.display))?;
 
-                let content = sc::ShareableContent::current()
-                    .await
-                    .map_err(|e| SourceError::CreateActor(e))?;
+                let content = shareable_content;
 
                 let content_filter = display
                     .raw_handle()

Analysis

Screen capture bypasses SCK caching system, causing 10s recording startup delays

What fails: ScreenCaptureSource::run() method in crates/recording/src/sources/screen_capture/macos.rs calls sc::ShareableContent::current().await (lines 140-142) instead of using the cached self.shareable_content field, completely defeating the SCK caching optimization.

How to reproduce:

  1. Initialize ScreenCaptureSource with a prewarmed shareable_content parameter
  2. Call PipelineSourceTask::run() to start recording
  3. Observe that the expensive SCK lookup happens every time instead of using the cached content

Result: Recording startup takes ~10 seconds each time due to ShareableContent::current().await call. The prewarmed cache in self.shareable_content is never used.

Expected: Should use the cached shareable_content field to achieve instant recording startup as intended by the caching system.

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

♻️ Duplicate comments (1)
crates/scap-targets/src/platform/macos.rs (1)

23-23: Remove unused import (duplicate).

use tracing::trace; is unused in this file. Drop it to satisfy clippy/rustc lints.

-use tracing::trace;
🧹 Nitpick comments (2)
crates/scap-targets/src/platform/macos.rs (1)

190-196: Avoid moving a Vec; accept a slice for windows.

Pass windows as a slice to avoid needless allocation/moves and make call sites cheaper.

-    pub async fn as_content_filter(
-        &self,
-        content: &sc::ShareableContent,
-    ) -> Option<arc::R<sc::ContentFilter>> {
-        self.as_content_filter_excluding_windows(content, vec![])
+    pub async fn as_content_filter(
+        &self,
+        content: &sc::ShareableContent,
+    ) -> Option<arc::R<sc::ContentFilter>> {
+        self.as_content_filter_excluding_windows(content, &[])
             .await
     }
 
-    pub async fn as_content_filter_excluding_windows(
+    pub async fn as_content_filter_excluding_windows(
         &self,
-        content: &sc::ShareableContent,
-        windows: Vec<arc::R<sc::Window>>,
+        content: &sc::ShareableContent,
+        windows: &[arc::R<sc::Window>],
     ) -> Option<arc::R<sc::ContentFilter>> {
         let display = content
             .displays()
             .iter()
             .find(|display| display.display_id().0 == self.0.id)
             .map(|display| display.retained())?;
 
         Some(sc::ContentFilter::with_display_excluding_windows(
             display.as_ref(),
-            &ns::Array::from_slice_retained(&windows),
+            &ns::Array::from_slice_retained(windows),
         ))
     }

Also applies to: 198-213

apps/desktop/src-tauri/src/windows.rs (1)

281-293: Avoid double prewarm; emit first, fallback to direct call on error.

Currently you both start prewarm and emit an event that does the same. Keep one path to prevent redundant work.

-                #[cfg(target_os = "macos")]
-                {
-                    let app_handle = app.clone();
-                    tauri::async_runtime::spawn(async move {
-                        let prewarmer = app_handle.state::<ScreenCapturePrewarmer>();
-                        prewarmer.request(false).await;
-                    });
-
-                    if let Err(error) = (RequestScreenCapturePrewarm { force: false }).emit(app) {
-                        warn!(%error, "Failed to emit ScreenCaptureKit prewarm event");
-                    }
-                }
+                #[cfg(target_os = "macos")]
+                {
+                    if let Err(error) = (RequestScreenCapturePrewarm { force: false }).emit(app) {
+                        warn!(%error, "Failed to emit ScreenCaptureKit prewarm event; falling back");
+                        let app_handle = app.clone();
+                        tauri::async_runtime::spawn(async move {
+                            let prewarmer = app_handle.state::<ScreenCapturePrewarmer>();
+                            prewarmer.request(false).await;
+                        });
+                    }
+                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8b34d1 and 66f59a8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • apps/desktop/src-tauri/src/lib.rs (5 hunks)
  • apps/desktop/src-tauri/src/recording.rs (3 hunks)
  • apps/desktop/src-tauri/src/windows.rs (2 hunks)
  • apps/desktop/src/utils/tauri.ts (3 hunks)
  • crates/scap-targets/Cargo.toml (1 hunks)
  • crates/scap-targets/src/platform/macos.rs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/scap-targets/Cargo.toml
  • apps/desktop/src-tauri/src/recording.rs
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add inline, block, or docstring comments in any language; code must be self-explanatory

Files:

  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/utils/tauri.ts
  • crates/scap-targets/src/platform/macos.rs
  • apps/desktop/src-tauri/src/windows.rs
**/*.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:

  • apps/desktop/src-tauri/src/lib.rs
  • crates/scap-targets/src/platform/macos.rs
  • apps/desktop/src-tauri/src/windows.rs
**/tauri.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Never edit auto-generated IPC bindings file tauri.ts

Do not edit auto-generated files named tauri.ts.

Files:

  • apps/desktop/src/utils/tauri.ts
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/**/*.{ts,tsx}: Do not manually import icons in the desktop app; rely on auto-imported icons
In the desktop app, use @tanstack/solid-query for server state management

Files:

  • apps/desktop/src/utils/tauri.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; leverage shared types from packages

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Files:

  • apps/desktop/src/utils/tauri.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/desktop/src/utils/tauri.ts
crates/**/src/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

For desktop IPC, use tauri_specta derive/macros on Rust types/events; do not hand-roll bindings

Files:

  • crates/scap-targets/src/platform/macos.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/scap-targets/src/platform/macos.rs
🧠 Learnings (1)
📚 Learning: 2025-09-22T14:17:47.407Z
Learnt from: CR
PR: CapSoftware/Cap#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-22T14:17:47.407Z
Learning: Applies to crates/**/src/**/*.rs : For desktop IPC, use tauri_specta derive/macros on Rust types/events; do not hand-roll bindings

Applied to files:

  • apps/desktop/src-tauri/src/windows.rs
🧬 Code graph analysis (3)
apps/desktop/src-tauri/src/lib.rs (2)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (2)
  • state (28-30)
  • prewarm_shareable_content (32-61)
apps/desktop/src-tauri/src/windows.rs (5)
  • app (215-215)
  • app (357-357)
  • app (447-447)
  • app (747-747)
  • app (936-936)
crates/scap-targets/src/platform/macos.rs (2)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (2)
  • display (160-162)
  • window (164-166)
crates/scap-targets/src/lib.rs (1)
  • display (146-148)
apps/desktop/src-tauri/src/windows.rs (1)
apps/desktop/src-tauri/src/lib.rs (6)
  • tauri_specta (1929-2043)
  • app (1213-1214)
  • app (2298-2298)
  • app (2329-2329)
  • app (2366-2366)
  • app (2372-2372)
⏰ 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 (3)
apps/desktop/src/utils/tauri.ts (1)

302-305: Event/type wiring looks correct; no action needed.

New RequestScreenCapturePrewarm event and payload are correctly exported and mapped. Note: this file is auto-generated; do not edit manually.

Also applies to: 324-326, 448-449

crates/scap-targets/src/platform/macos.rs (1)

181-188: LGTM: content-context threading.

Switching to content-parameterized lookups for displays/windows is clear and aligns with the shared ShareableContent cache.

Also applies to: 479-485

apps/desktop/src-tauri/src/lib.rs (1)

325-330: LGTM: event surface and wiring.

New RequestScreenCapturePrewarm event, managed state, and listener hookup look correct and are macOS-gated appropriately.

Also applies to: 2026-2026, 2165-2167, 2296-2301

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 66f59a8 and 5a56b31.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • apps/cli/Cargo.toml (1 hunks)
  • apps/cli/src/record.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add inline, block, or docstring comments in any language; code must be self-explanatory

Files:

  • apps/cli/src/record.rs
**/*.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:

  • apps/cli/src/record.rs
⏰ 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 (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (1)
apps/cli/Cargo.toml (1)

27-28: macOS-only dependency looks good.

Target-scoping cidre keeps non-mac builds clean while enabling the new ScreenCaptureKit path. No concerns on my end.

Comment on lines +64 to 69
.build(
#[cfg(target_os = "macos")]
cidre::sc::ShareableContent::current().await.unwrap(),
)
.await
.map_err(|e| e.to_string())?;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Propagate ShareableContent errors instead of panicking.

The unwrap() at line 66 will panic if ShareableContent::current() fails, crashing the CLI without user-friendly context. Since the outer await and map_err (lines 68-69) already handle build() errors, propagate the ShareableContent error through the same path.

Apply this diff:

             .build(
                 #[cfg(target_os = "macos")]
-                cidre::sc::ShareableContent::current().await.unwrap(),
+                cidre::sc::ShareableContent::current()
+                    .await
+                    .map_err(|e| format!("Failed to get shareable content: {}", e))?,
             )
             .await
             .map_err(|e| e.to_string())?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.build(
#[cfg(target_os = "macos")]
cidre::sc::ShareableContent::current().await.unwrap(),
)
.await
.map_err(|e| e.to_string())?;
.build(
#[cfg(target_os = "macos")]
cidre::sc::ShareableContent::current()
.await
.map_err(|e| format!("Failed to get shareable content: {}", e))?,
)
.await
.map_err(|e| e.to_string())?;
🤖 Prompt for AI Agents
In apps/cli/src/record.rs around lines 64 to 69, the code uses unwrap() on
ShareableContent::current() which can panic; replace the unwrap with error
propagation so the ShareableContent error flows through the existing map_err
path. Concretely, change the macOS branch to call
cidre::sc::ShareableContent::current().await.map_err(|e| e.to_string())? (i.e.
await, map the error to a string and use ?), so the failing ShareableContent
creation is returned instead of panicking.

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: 2

♻️ Duplicate comments (1)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1)

136-137: Revert the unsafe Send/Sync impl; it’s unsound.

cidre::arc::R intentionally does not implement Send/Sync, because the wrapped ScreenCaptureKit objects are only guaranteed to be safe on the thread they were created on. Forcing ShareableContentCache to be Send/Sync so it can survive inside a tokio::spawn moves those Objective‑C objects across the multi-threaded runtime and can trigger undefined behaviour at runtime. Please drop the unsafe impls and keep the warm-up future on the originating thread (e.g. run run_warmup directly in the first caller, or switch to a spawn_local/main-thread executor) so no cross-thread transfer is needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a56b31 and 1351f6c.

📒 Files selected for processing (9)
  • apps/desktop/src-tauri/src/lib.rs (6 hunks)
  • apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1 hunks)
  • apps/desktop/src-tauri/src/recording.rs (7 hunks)
  • apps/desktop/src-tauri/src/thumbnails/mac.rs (1 hunks)
  • apps/desktop/src-tauri/src/thumbnails/mod.rs (1 hunks)
  • apps/desktop/src-tauri/src/thumbnails/windows.rs (1 hunks)
  • apps/desktop/src-tauri/src/windows.rs (2 hunks)
  • crates/recording/src/sources/screen_capture/macos.rs (2 hunks)
  • crates/scap-targets/src/platform/macos.rs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src-tauri/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add inline, block, or docstring comments in any language; code must be self-explanatory

Files:

  • apps/desktop/src-tauri/src/thumbnails/windows.rs
  • apps/desktop/src-tauri/src/windows.rs
  • apps/desktop/src-tauri/src/thumbnails/mod.rs
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/scap-targets/src/platform/macos.rs
  • apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs
  • apps/desktop/src-tauri/src/recording.rs
  • apps/desktop/src-tauri/src/thumbnails/mac.rs
**/*.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:

  • apps/desktop/src-tauri/src/thumbnails/windows.rs
  • apps/desktop/src-tauri/src/windows.rs
  • apps/desktop/src-tauri/src/thumbnails/mod.rs
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/scap-targets/src/platform/macos.rs
  • apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs
  • apps/desktop/src-tauri/src/recording.rs
  • apps/desktop/src-tauri/src/thumbnails/mac.rs
crates/**/src/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

For desktop IPC, use tauri_specta derive/macros on Rust types/events; do not hand-roll bindings

Files:

  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/scap-targets/src/platform/macos.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/sources/screen_capture/macos.rs
  • crates/scap-targets/src/platform/macos.rs
🧬 Code graph analysis (7)
apps/desktop/src-tauri/src/thumbnails/windows.rs (3)
apps/desktop/src-tauri/src/thumbnails/mac.rs (2)
  • capture_display_thumbnail (7-12)
  • capture_window_thumbnail (14-20)
crates/scap-direct3d/src/lib.rs (1)
  • stride (543-545)
apps/desktop/src-tauri/src/thumbnails/mod.rs (1)
  • normalize_thumbnail_dimensions (38-81)
apps/desktop/src-tauri/src/windows.rs (2)
apps/desktop/src-tauri/src/lib.rs (6)
  • tauri_specta (1855-1969)
  • app (1139-1140)
  • app (2224-2224)
  • app (2255-2255)
  • app (2292-2292)
  • app (2298-2298)
apps/desktop/src/utils/tauri.ts (1)
  • RequestScreenCapturePrewarm (448-448)
apps/desktop/src-tauri/src/thumbnails/mod.rs (4)
crates/recording/src/sources/screen_capture/mod.rs (2)
  • list_displays (438-452)
  • list_windows (454-492)
apps/desktop/src/utils/tauri.ts (5)
  • CaptureDisplayWithThumbnail (365-365)
  • DisplayId (381-381)
  • CaptureWindowWithThumbnail (367-367)
  • WindowId (477-477)
  • LogicalBounds (410-410)
apps/desktop/src-tauri/src/thumbnails/mac.rs (3)
  • new (284-290)
  • capture_display_thumbnail (7-12)
  • capture_window_thumbnail (14-20)
apps/desktop/src-tauri/src/thumbnails/windows.rs (2)
  • capture_display_thumbnail (1-138)
  • capture_window_thumbnail (140-277)
crates/scap-targets/src/platform/macos.rs (1)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (2)
  • display (160-162)
  • window (164-166)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (2)
crates/scap-targets/src/platform/macos.rs (3)
  • display (490-510)
  • id (50-50)
  • id (303-305)
crates/scap-targets/src/lib.rs (3)
  • display (146-148)
  • id (28-30)
  • id (118-120)
apps/desktop/src-tauri/src/recording.rs (5)
apps/desktop/src/utils/tauri.ts (2)
  • CaptureDisplayWithThumbnail (365-365)
  • CaptureWindowWithThumbnail (367-367)
apps/desktop/src-tauri/src/thumbnails/mod.rs (2)
  • collect_displays_with_thumbnails (83-99)
  • collect_windows_with_thumbnails (101-149)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1)
  • get_shareable_content (63-90)
crates/recording/src/instant_recording.rs (1)
  • builder (154-156)
crates/recording/src/studio_recording.rs (1)
  • builder (129-131)
apps/desktop/src-tauri/src/thumbnails/mac.rs (2)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1)
  • get_shareable_content (63-90)
apps/desktop/src-tauri/src/thumbnails/mod.rs (1)
  • normalize_thumbnail_dimensions (38-81)
⏰ 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)

Comment on lines 197 to 205
pub fn list_displays_with_thumbnails() -> Result<Vec<CaptureDisplayWithThumbnail>, String> {
tauri::async_runtime::block_on(collect_displays_with_thumbnails())
}

#[tauri::command(async)]
#[specta::specta]
pub async fn list_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithThumbnail>, String> {
tokio::task::spawn_blocking(|| collect_windows_with_thumbnails())
.await
.map_err(|err| err.to_string())?
}

fn collect_displays_with_thumbnails() -> Result<Vec<CaptureDisplayWithThumbnail>, String> {
tauri::async_runtime::block_on(async move {
let displays = screen_capture::list_displays();

let mut results = Vec::new();
for (capture_display, display) in displays {
let thumbnail = capture_display_thumbnail(&display).await;
results.push(CaptureDisplayWithThumbnail {
id: capture_display.id,
name: capture_display.name,
refresh_rate: capture_display.refresh_rate,
thumbnail,
});
}

Ok(results)
})
}

fn collect_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithThumbnail>, String> {
tauri::async_runtime::block_on(async move {
let windows = screen_capture::list_windows();

debug!(window_count = windows.len(), "Collecting window thumbnails");
let mut results = Vec::new();
for (capture_window, window) in windows {
let thumbnail = capture_window_thumbnail(&window).await;
let app_icon = window.app_icon().and_then(|bytes| {
if bytes.is_empty() {
None
} else {
Some(base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
bytes,
))
}
});

if thumbnail.is_none() {
warn!(
window_id = ?capture_window.id,
window_name = %capture_window.name,
owner_name = %capture_window.owner_name,
"Window thumbnail capture returned None",
);
} else {
debug!(
window_id = ?capture_window.id,
window_name = %capture_window.name,
owner_name = %capture_window.owner_name,
"Captured window thumbnail",
);
}

results.push(CaptureWindowWithThumbnail {
id: capture_window.id,
name: capture_window.name,
owner_name: capture_window.owner_name,
bounds: capture_window.bounds,
refresh_rate: capture_window.refresh_rate,
thumbnail,
app_icon,
});
}

info!(windows = results.len(), "Collected window thumbnail data");

Ok(results)
})
tauri::async_runtime::block_on(collect_windows_with_thumbnails())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid blocking the Tauri async runtime in command handlers.

tauri::async_runtime::block_on is being invoked from within an async command, which risks panicking (“cannot block the current thread”) or stalling the runtime thread under Tokio. The command itself is already async, so we should just await the collectors directly.

-#[tauri::command(async)]
-pub fn list_displays_with_thumbnails() -> Result<Vec<CaptureDisplayWithThumbnail>, String> {
-    tauri::async_runtime::block_on(collect_displays_with_thumbnails())
-}
+#[tauri::command(async)]
+pub async fn list_displays_with_thumbnails() -> Result<Vec<CaptureDisplayWithThumbnail>, String> {
+    collect_displays_with_thumbnails().await
+}
@@
 pub async fn list_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithThumbnail>, String> {
-    tauri::async_runtime::block_on(collect_windows_with_thumbnails())
+    collect_windows_with_thumbnails().await
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/recording.rs around lines 197 to 205, the code
calls tauri::async_runtime::block_on inside command handlers (and a non-async
function), which can block or panic the runtime; change both functions to be
async and simply await the collector futures (e.g., make
list_displays_with_thumbnails async and remove block_on, and in
list_windows_with_thumbnails replace block_on with await), keep the existing
return types and attributes (add #[tauri::command(async)]/#[specta::specta] to
the displays function if needed), and remove the blocking call so the collectors
run on the async runtime without blocking the current thread.

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

♻️ Duplicate comments (1)
apps/desktop/src-tauri/src/recording.rs (1)

195-213: Avoid nested blocking in async command handlers.

Both list_displays_with_thumbnails and list_windows_with_thumbnails use spawn_blocking wrapping block_on, which is an anti-pattern. Since these commands are already async-capable and the collector functions are async, the handlers should simply await them directly.

This issue was previously flagged. Apply this diff to fix both functions:

-#[tauri::command]
+#[tauri::command(async)]
 #[specta::specta]
 pub async fn list_displays_with_thumbnails() -> Result<Vec<CaptureDisplayWithThumbnail>, String> {
-    tokio::task::spawn_blocking(|| {
-        tauri::async_runtime::block_on(collect_displays_with_thumbnails())
-    })
-    .await
-    .map_err(|e| e.to_string())?
+    collect_displays_with_thumbnails().await
 }
 
-#[tauri::command]
+#[tauri::command(async)]
 #[specta::specta]
 pub async fn list_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithThumbnail>, String> {
-    tokio::task::spawn_blocking(
-        || tauri::async_runtime::block_on(collect_windows_with_thumbnails()),
-    )
-    .await
-    .map_err(|e| e.to_string())?
+    collect_windows_with_thumbnails().await
 }
🧹 Nitpick comments (2)
apps/desktop/src-tauri/src/recording.rs (2)

29-29: Consider explicit tracing imports for clarity.

The wildcard use tracing::*; import brings all tracing macros and functions into scope, which can reduce clarity about which specific tracing facilities are used. While common in Rust codebases, explicit imports like use tracing::{debug, error, info, trace, warn}; improve code self-documentation.

Apply this diff to use explicit imports:

-use tracing::*;
+use tracing::{debug, error, info, trace, warn};

410-414: Consider more descriptive error messages for SCK failures.

The error messages "GetShareableContent: {e}" and "GetShareableContent/NotAvailable" are functional but could be more user-friendly for debugging. Consider adding context about why this matters (e.g., "Failed to initialize ScreenCaptureKit" or "ScreenCaptureKit not available on this system").

Apply this diff for clearer diagnostics:

                 #[cfg(target_os = "macos")]
                 let shareable_content = crate::platform::get_shareable_content()
                     .await
-                    .map_err(|e| format!("GetShareableContent: {e}"))?
-                    .ok_or_else(|| format!("GetShareableContent/NotAvailable"))?;
+                    .map_err(|e| format!("Failed to initialize ScreenCaptureKit: {e}"))?
+                    .ok_or_else(|| "ScreenCaptureKit content unavailable".to_string())?;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1351f6c and 1e6a697.

📒 Files selected for processing (2)
  • apps/desktop/src-tauri/src/recording.rs (7 hunks)
  • apps/desktop/src-tauri/src/thumbnails/windows.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add inline, block, or docstring comments in any language; code must be self-explanatory

Files:

  • apps/desktop/src-tauri/src/thumbnails/windows.rs
  • apps/desktop/src-tauri/src/recording.rs
**/*.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:

  • apps/desktop/src-tauri/src/thumbnails/windows.rs
  • apps/desktop/src-tauri/src/recording.rs
🧬 Code graph analysis (2)
apps/desktop/src-tauri/src/thumbnails/windows.rs (3)
apps/desktop/src-tauri/src/thumbnails/mac.rs (2)
  • capture_display_thumbnail (7-12)
  • capture_window_thumbnail (14-20)
crates/scap-direct3d/src/lib.rs (1)
  • stride (543-545)
apps/desktop/src-tauri/src/thumbnails/mod.rs (1)
  • normalize_thumbnail_dimensions (38-81)
apps/desktop/src-tauri/src/recording.rs (3)
apps/desktop/src/utils/tauri.ts (2)
  • CaptureDisplayWithThumbnail (365-365)
  • CaptureWindowWithThumbnail (367-367)
apps/desktop/src-tauri/src/thumbnails/mod.rs (2)
  • collect_displays_with_thumbnails (83-99)
  • collect_windows_with_thumbnails (101-149)
apps/desktop/src-tauri/src/platform/macos/sc_shareable_content.rs (1)
  • get_shareable_content (63-90)
⏰ 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 (3)
apps/desktop/src-tauri/src/thumbnails/windows.rs (1)

3-140: Function logic and error handling are sound.

The comprehensive overflow checks, dimension validation, and structured error handling with early returns are well-implemented. The progressive validation approach (dimensions → stride → offsets → capacity → reconstruction) is robust.

apps/desktop/src-tauri/src/recording.rs (2)

437-446: LGTM! Studio actor build correctly threads shareable_content.

The conditional macOS parameter passing is clean, and the error handling with descriptive logging is appropriate. This aligns with the PR's objective to prewarm and share SCK content across recording flows.


473-482: LGTM! Instant actor build correctly threads shareable_content.

The conditional macOS parameter passing matches the studio recording branch pattern, maintaining consistency across both recording modes. The error handling appropriately logs and propagates failures.

Comment on lines +3 to +279
pub async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option<String> {
use image::{ColorType, ImageEncoder, codecs::png::PngEncoder};
use scap_direct3d::{Capturer, Settings};
use std::io::Cursor;

let item = display.raw_handle().try_as_capture_item().ok()?;

let (tx, rx) = std::sync::mpsc::channel();

let settings = Settings {
is_cursor_capture_enabled: Some(false),
pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm,
..Default::default()
};

let mut capturer = Capturer::new(
item,
settings.clone(),
move |frame| {
let _ = tx.send(frame);
Ok(())
},
|| Ok(()),
None,
)
.ok()?;

capturer.start().ok()?;

let frame = rx.recv_timeout(std::time::Duration::from_secs(2)).ok()?;
let _ = capturer.stop();

let width = frame.width();
let height = frame.height();

if width == 0 || height == 0 {
return None;
}

let frame_buffer = frame.as_buffer().ok()?;
let data = frame_buffer.data();
let stride = frame_buffer.stride() as usize;

let width_usize = width as usize;
let height_usize = height as usize;

let Some(row_bytes) = width_usize.checked_mul(4) else {
warn!(
frame_width = width,
"Windows display thumbnail row size overflowed"
);
return None;
};

if stride < row_bytes {
warn!(
frame_width = width,
frame_height = height,
stride,
expected_row_bytes = row_bytes,
"Windows display thumbnail stride smaller than row size"
);
return None;
}

let rows_before_last = height_usize.saturating_sub(1);
let Some(last_row_start) = rows_before_last.checked_mul(stride) else {
warn!(
frame_width = width,
frame_height = height,
stride,
"Windows display thumbnail row offset overflowed"
);
return None;
};

let Some(required_len) = last_row_start.checked_add(row_bytes) else {
warn!(
frame_width = width,
frame_height = height,
stride,
required_row_bytes = row_bytes,
"Windows display thumbnail required length overflowed"
);
return None;
};

if data.len() < required_len {
warn!(
frame_width = width,
frame_height = height,
stride,
frame_data_len = data.len(),
expected_len = required_len,
"Windows display thumbnail frame buffer missing pixel data"
);
return None;
}

let Some(rgba_capacity) = height_usize.checked_mul(row_bytes) else {
warn!(
frame_width = width,
frame_height = height,
total_row_bytes = row_bytes,
"Windows display thumbnail RGBA capacity overflowed"
);
return None;
};

let mut rgba_data = Vec::with_capacity(rgba_capacity);
for y in 0..height_usize {
let row_start = y * stride;
let row_end = row_start + row_bytes;
rgba_data.extend_from_slice(&data[row_start..row_end]);
}

let Some(img) = image::RgbaImage::from_raw(width, height, rgba_data) else {
warn!("Windows display thumbnail failed to construct RGBA image");
return None;
};
let thumbnail = normalize_thumbnail_dimensions(&img);

let mut png_data = Cursor::new(Vec::new());
let encoder = PngEncoder::new(&mut png_data);
encoder
.write_image(
thumbnail.as_raw(),
thumbnail.width(),
thumbnail.height(),
ColorType::Rgba8.into(),
)
.ok()?;

Some(base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
png_data.into_inner(),
))
}

pub async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option<String> {
use image::{ColorType, ImageEncoder, codecs::png::PngEncoder};
use scap_direct3d::{Capturer, Settings};
use std::io::Cursor;

let item = window.raw_handle().try_as_capture_item().ok()?;

let (tx, rx) = std::sync::mpsc::channel();

let settings = Settings {
is_cursor_capture_enabled: Some(false),
pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm,
..Default::default()
};

let mut capturer = Capturer::new(
item,
settings.clone(),
move |frame| {
let _ = tx.send(frame);
Ok(())
},
|| Ok(()),
None,
)
.ok()?;

capturer.start().ok()?;

let frame = rx.recv_timeout(std::time::Duration::from_secs(2)).ok()?;
let _ = capturer.stop();

let width = frame.width();
let height = frame.height();

if width == 0 || height == 0 {
return None;
}

let frame_buffer = frame.as_buffer().ok()?;
let data = frame_buffer.data();
let stride = frame_buffer.stride() as usize;

let width_usize = width as usize;
let height_usize = height as usize;

let Some(row_bytes) = width_usize.checked_mul(4) else {
warn!(
frame_width = width,
"Windows window thumbnail row size overflowed"
);
return None;
};

if stride < row_bytes {
warn!(
frame_width = width,
frame_height = height,
stride,
expected_row_bytes = row_bytes,
"Windows window thumbnail stride smaller than row size"
);
return None;
}

let rows_before_last = height_usize.saturating_sub(1);
let Some(last_row_start) = rows_before_last.checked_mul(stride) else {
warn!(
frame_width = width,
frame_height = height,
stride,
"Windows window thumbnail row offset overflowed"
);
return None;
};

let Some(required_len) = last_row_start.checked_add(row_bytes) else {
warn!(
frame_width = width,
frame_height = height,
stride,
required_row_bytes = row_bytes,
"Windows window thumbnail required length overflowed"
);
return None;
};

if data.len() < required_len {
warn!(
frame_width = width,
frame_height = height,
stride,
frame_data_len = data.len(),
expected_len = required_len,
"Windows window thumbnail frame buffer missing pixel data"
);
return None;
}

let Some(rgba_capacity) = height_usize.checked_mul(row_bytes) else {
warn!(
frame_width = width,
frame_height = height,
total_row_bytes = row_bytes,
"Windows window thumbnail RGBA capacity overflowed"
);
return None;
};

let mut rgba_data = Vec::with_capacity(rgba_capacity);
for y in 0..height_usize {
let row_start = y * stride;
let row_end = row_start + row_bytes;
rgba_data.extend_from_slice(&data[row_start..row_end]);
}

let Some(img) = image::RgbaImage::from_raw(width, height, rgba_data) else {
warn!("Windows window thumbnail failed to construct RGBA image");
return None;
};
let thumbnail = normalize_thumbnail_dimensions(&img);

let mut png_data = Cursor::new(Vec::new());
let encoder = PngEncoder::new(&mut png_data);
encoder
.write_image(
thumbnail.as_raw(),
thumbnail.width(),
thumbnail.height(),
ColorType::Rgba8.into(),
)
.ok()?;

Some(base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
png_data.into_inner(),
))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract shared thumbnail capture logic to eliminate duplication.

Both capture_display_thumbnail and capture_window_thumbnail duplicate ~136 lines of identical logic. The only differences are the capture item source (line 8 vs 147) and warning message prefixes.

Consider extracting the shared pipeline (capturer setup, frame capture, validation, RGBA reconstruction, PNG encoding) into a helper function that accepts the capture item and a label for warnings.

Example refactor:

async fn capture_thumbnail_from_item(
    item: impl Into<scap_direct3d::CaptureItem>,
    label: &str,
) -> Option<String> {
    use image::{ColorType, ImageEncoder, codecs::png::PngEncoder};
    use scap_direct3d::{Capturer, Settings};
    use std::io::Cursor;

    let (tx, rx) = std::sync::mpsc::channel();

    let settings = Settings {
        is_cursor_capture_enabled: Some(false),
        pixel_format: scap_direct3d::PixelFormat::R8G8B8A8Unorm,
        ..Default::default()
    };

    let mut capturer = Capturer::new(
        item.into(),
        settings.clone(),
        move |frame| {
            let _ = tx.send(frame);
            Ok(())
        },
        || Ok(()),
        None,
    )
    .ok()?;

    capturer.start().ok()?;
    let frame = rx.recv_timeout(std::time::Duration::from_secs(2)).ok()?;
    let _ = capturer.stop();

    let width = frame.width();
    let height = frame.height();

    if width == 0 || height == 0 {
        return None;
    }

    let frame_buffer = frame.as_buffer().ok()?;
    let data = frame_buffer.data();
    let stride = frame_buffer.stride() as usize;

    let width_usize = width as usize;
    let height_usize = height as usize;

    let Some(row_bytes) = width_usize.checked_mul(4) else {
        warn!(frame_width = width, "{label} row size overflowed");
        return None;
    };

    if stride < row_bytes {
        warn!(
            frame_width = width,
            frame_height = height,
            stride,
            expected_row_bytes = row_bytes,
            "{label} stride smaller than row size"
        );
        return None;
    }

    let rows_before_last = height_usize.saturating_sub(1);
    let Some(last_row_start) = rows_before_last.checked_mul(stride) else {
        warn!(
            frame_width = width,
            frame_height = height,
            stride,
            "{label} row offset overflowed"
        );
        return None;
    };

    let Some(required_len) = last_row_start.checked_add(row_bytes) else {
        warn!(
            frame_width = width,
            frame_height = height,
            stride,
            required_row_bytes = row_bytes,
            "{label} required length overflowed"
        );
        return None;
    };

    if data.len() < required_len {
        warn!(
            frame_width = width,
            frame_height = height,
            stride,
            frame_data_len = data.len(),
            expected_len = required_len,
            "{label} frame buffer missing pixel data"
        );
        return None;
    }

    let Some(rgba_capacity) = height_usize.checked_mul(row_bytes) else {
        warn!(
            frame_width = width,
            frame_height = height,
            total_row_bytes = row_bytes,
            "{label} RGBA capacity overflowed"
        );
        return None;
    };

    let mut rgba_data = Vec::with_capacity(rgba_capacity);
    for y in 0..height_usize {
        let row_start = y * stride;
        let row_end = row_start + row_bytes;
        rgba_data.extend_from_slice(&data[row_start..row_end]);
    }

    let Some(img) = image::RgbaImage::from_raw(width, height, rgba_data) else {
        warn!("{label} failed to construct RGBA image");
        return None;
    };
    let thumbnail = normalize_thumbnail_dimensions(&img);

    let mut png_data = Cursor::new(Vec::new());
    let encoder = PngEncoder::new(&mut png_data);
    encoder
        .write_image(
            thumbnail.as_raw(),
            thumbnail.width(),
            thumbnail.height(),
            ColorType::Rgba8.into(),
        )
        .ok()?;

    Some(base64::Engine::encode(
        &base64::engine::general_purpose::STANDARD,
        png_data.into_inner(),
    ))
}

pub async fn capture_display_thumbnail(display: &scap_targets::Display) -> Option<String> {
    let item = display.raw_handle().try_as_capture_item().ok()?;
    capture_thumbnail_from_item(item, "Windows display thumbnail").await
}

pub async fn capture_window_thumbnail(window: &scap_targets::Window) -> Option<String> {
    let item = window.raw_handle().try_as_capture_item().ok()?;
    capture_thumbnail_from_item(item, "Windows window thumbnail").await
}
🤖 Prompt for AI Agents
apps/desktop/src-tauri/src/thumbnails/windows.rs lines 3-279: the two functions
capture_display_thumbnail and capture_window_thumbnail duplicate the entire
capture/validation/encoding pipeline; extract the shared logic into a single
async helper like capture_thumbnail_from_item(item: impl
Into<scap_direct3d::CaptureItem>, label: &str) -> Option<String> that performs
capturer setup, frame recv/stop, buffer validation, RGBA reconstruction,
normalization and PNG/base64 encoding, and replace both functions with thin
wrappers that convert their raw handle to a capture item and call the helper
with the appropriate label (e.g. "Windows display thumbnail" / "Windows window
thumbnail"); keep existing warning keys and messages but interpolate the label
so behavior and diagnostics remain identical.

@Brendonovich Brendonovich merged commit 14265ae into main Oct 3, 2025
15 checks passed
@Brendonovich Brendonovich deleted the prewarming-sck branch October 3, 2025 09:51
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.

3 participants