Skip to content

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Nov 17, 2025

Summary by CodeRabbit

  • Bug Fixes

    • More reliable and consistent project config saving (debounced, serialized, and flushed on exit)
    • Improved macOS screen-recording exclusions to avoid missing/incorrectly excluded windows
    • More robust camera frame handling with clearer runtime logging
  • Performance Improvements

    • Optimized timeline waveform rendering for smoother, more responsive scrubbing and drawing

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 17, 2025

Walkthrough

Refactors project-config saves to an explicit, stateful debounce/schedule+flush flow; adds macOS ScreenCaptureKit handling for excluded window IDs with pruning and retry logic; and replaces per-frame canvas waveform drawing with Path2D-based, memoized waveform rendering and precomputed segment offsets.

Changes

Cohort / File(s) Summary
Project Config Save Scheduling
apps/desktop/src/routes/editor/context.ts
Replaces inline debounce with explicit scheduling state: PROJECT_SAVE_DEBOUNCE_MS, projectSaveTimeout, saveInFlight, shouldResave, hasPendingProjectSave; adds scheduleProjectConfigSave, flushProjectConfig, and onCleanup to serialize saves, coalesce rapid changes, retry when a save is in flight, and flush on teardown. Logic applied in two places in the file.
macOS Excluded Window Handling
apps/desktop/src-tauri/src/recording.rs
Threads excluded_windows into shareable-content acquisition, adds shareable_content_missing_windows and prune_excluded_windows_without_shareable_content, updates acquire_shareable_content_for_target to accept exclusions and refresh when target/windows missing, prunes exclusions before passing to macOS builders, and retries acquisition on shareable-content errors; adjusts error handling and logging.
Canvas Waveform Rendering Optimization
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
Reworks waveform rendering to use constants (CANVAS_HEIGHT, scaling, padding), createWaveformPath and gainToScale, memoized Path2D waveform paths, segmentOffsets precomputation, and a unified drawPath flow replacing per-frame calculation and removing secsPerPixel prop.
Camera Forwarding Logging & Reattach
crates/recording/src/sources/camera.rs
Adds frame counting, enhanced logs including frame counts on errors, uses cloned transmitters for registration and reattachment on disconnect, explicit cleanup and info logging of final loop result.
Permissions Config
.claude/settings.local.json
Adds "Bash(cargo build:*)" to allowed Bash commands alongside existing "Bash(cargo check:*)".

Sequence Diagram(s)

sequenceDiagram
    participant Component as Editor Component
    participant State as Save State
    participant Timer as Debounce Timer
    participant Commands as setProjectConfig

    Component->>State: change detected
    alt saveInFlight == true
        State->>State: set shouldResave
    else
        State->>Timer: schedule (250ms)
    end

    Timer->>State: timer fires
    State->>State: saveInFlight = true
    State->>Commands: execute setProjectConfig
    Commands-->>State: returns
    State->>State: saveInFlight = false
    alt shouldResave
        State->>Timer: schedule next save
    end
    Component->>State: onCleanup (unmount)
    State->>Timer: clear timer
    State->>Commands: flush if pending
Loading
sequenceDiagram
    participant Flow as start_recording
    participant Settings as General Settings
    participant Acquire as acquire_shareable_content_for_target
    participant Prune as prune_excluded_windows
    participant Builders as macOS Builders

    Flow->>Settings: load excluded_windows
    Flow->>Acquire: acquire(content, excluded_windows)
    Acquire->>Acquire: check missing target display?
    Acquire->>Acquire: check missing excluded windows?
    alt missing display or windows
        Acquire-->>Flow: request refresh / retry
    else
        Acquire-->>Flow: return content
    end
    Flow->>Prune: prune excluded_windows by content
    Prune-->>Flow: updated excluded_windows
    Flow->>Builders: pass shareable_content + excluded_windows
    alt builder error (shareable content)
        Flow->>Acquire: re-acquire with updated exclusions
        Flow->>Prune: re-prune
        Flow->>Builders: retry
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • apps/desktop/src-tauri/src/recording.rs: correctness of missing-target/windows detection, pruning logic, threading of shareable_content and excluded_windows into actor/builders, and retry/error paths.
    • apps/desktop/src/routes/editor/context.ts: concurrency/state correctness of saveInFlight/shouldResave transitions and cleanup behavior; duplicated logic locations.
    • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx: correctness and performance of Path2D memoization, coordinate transforms, and equivalence with prior waveform rendering.

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • Brendonovich

Poem

🐰 I hop on keys and nudge the save,

Schedules tidy, no frantic wave.
Waveforms drawn with path and grace,
macOS windows find their place —
Hooray, the code has found its pace! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title uses vague generic terminology ('Misc performance and UX fixes') that does not clearly convey the specific technical changes. Replace with a more specific title describing the primary change, such as 'feat: Improve project config save reliability and recording window exclusion handling'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 editor-perf

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

🧹 Nitpick comments (3)
apps/desktop/src-tauri/src/recording.rs (3)

34-35: Pruning helper is sound; consider documenting semantics

prune_excluded_windows_without_shareable_content is implemented safely: using mem::take avoids in-place mutation pitfalls, and dropping any IDs that can’t be resolved or don’t appear in the current ShareableContent (with a removed counter) is exactly what you want for keeping the exclusion list in sync.

As this will be subtle to reason about later (it removes both “never-resolvable” and “no longer present” windows), a short doc comment explaining that behavior would help future maintainers.

Also applies to: 68-69, 191-230


695-715: macOS excluded-window initialization and pruning fit well into the actor setup

Initializing excluded_windows from general settings, resolving them via resolve_window_ids, and immediately calling prune_excluded_windows_without_shareable_content against the freshly acquired shareable_content ensures the pipeline starts with a minimal, valid exclusion set. That should reduce the chance of later ScreenCaptureKit filter errors and keep the occlusion behavior aligned with the UI settings.

One thing to keep in mind for future performance work: all of this currently runs while the state write lock is held inside the actor-spawn path. If lock contention around recording startup ever shows up in profiling, this block (resolving/pruning exclusions and acquiring shareable content) would be a good candidate to move outside the critical section.

Also applies to: 717-721


840-851: Guard against unbounded retries on shareable-content errors

On macOS, is_shareable_content_error triggers a re-acquire of shareable_content and re-pruning of excluded_windows, then continue-s the loop. If SourceError::AsContentFilter keeps occurring even after refresh (for example due to a persistent OS-level issue), this loop can theoretically retry forever.

Consider adding a small attempt counter or time-based cutoff for this specific retry path, and after exceeding it, surface a more explicit error to the user instead of looping indefinitely. This keeps the behavior robust even when ScreenCaptureKit misbehaves.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd1fc73 and 5d95f24.

📒 Files selected for processing (1)
  • apps/desktop/src-tauri/src/recording.rs (8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.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/recording.rs
🧠 Learnings (1)
📚 Learning: 2025-10-28T08:39:42.230Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1305
File: crates/recording/src/output_pipeline/macos.rs:80-90
Timestamp: 2025-10-28T08:39:42.230Z
Learning: In `crates/recording/src/output_pipeline/macos.rs`, the `AVFoundationMp4Muxer` intentionally holds the `Mutex<MP4Encoder>` lock during retry attempts in `send_video_frame()` and `send_audio_frame()`. This blocking behavior is correct because frame processing must happen sequentially to prevent audio and video frames from being interleaved incorrectly in the encoder.

Applied to files:

  • apps/desktop/src-tauri/src/recording.rs
🧬 Code graph analysis (1)
apps/desktop/src-tauri/src/recording.rs (3)
crates/recording/src/sources/screen_capture/mod.rs (1)
  • window (74-79)
crates/scap-targets/src/platform/macos.rs (1)
  • from_id (48-51)
crates/scap-targets/src/lib.rs (2)
  • from_id (32-34)
  • from_id (122-124)
⏰ 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). (2)
  • GitHub Check: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (1)
apps/desktop/src-tauri/src/recording.rs (1)

111-147: Shareable content acquisition and missing-window handling look correct

The new acquire_shareable_content_for_target logic plus shareable_content_missing_windows gives a clear, bounded retry: one refresh on stale ScreenCaptureKit content, with DisplayMissing surfaced cleanly and excluded-window mismatches tolerated after a refresh. The distinction between display-missing and window-missing cases, and only treating known-but-unresolved windows as “missing”, matches the later pruning behavior and should avoid spurious failures.

Also applies to: 164-189

@richiemcilroy richiemcilroy changed the title feat: Editor performance improvements feat: Misc performance and UX fixes Nov 17, 2025
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: 0

🧹 Nitpick comments (1)
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (1)

46-94: Consider defensive bounds checking and add documentation.

The waveform path generation logic is sophisticated but could be clearer:

  1. Line 82: amplitudeAt(index - 1) on the first iteration may access waveform[-1] when segment.start is 0. While safely handled by the amplitudeAt function's type checks, consider clamping: Math.max(index - 1, 0).

  2. The bezier curve construction with control points and the coordinate normalization could benefit from inline comments explaining the approach.

Apply this diff to add bounds checking:

 		const prevY = 1 - amplitudeAt(index - 1);
+		const prevY = 1 - amplitudeAt(Math.max(index - 1, 0));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d95f24 and dbe8ae1.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{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/routes/editor/Timeline/ClipTrack.tsx
**/*.{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/routes/editor/Timeline/ClipTrack.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (3)
crates/editor/src/audio.rs (1)
  • gain (63-65)
crates/project/src/configuration.rs (2)
  • duration (526-528)
  • duration (591-593)
crates/editor/src/segments.rs (1)
  • segments (7-36)
⏰ 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). (2)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (5)
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (5)

33-44: LGTM! Good constants and scaling logic.

The waveform constants are well-defined and the gainToScale function correctly converts decibel gain to a linear scale factor for visualization purposes.


110-171: Excellent Path2D-based rendering optimization!

The refactor to use memoized Path2D objects with transform-based scaling is a solid performance improvement over per-frame waveform calculations. The reactive dependencies are correctly tracked.

Minor note: Line 136's canvas.width = canvasWidth already clears the canvas, making line 138's clearRect redundant (though harmless).


196-205: LGTM! Excellent performance optimization.

Precomputing segment offsets eliminates redundant calculations across multiple segments. The cumulative offset logic correctly accounts for timescale and provides O(1) lookups.


233-233: LGTM! Clean integration with segmentOffsets.

Using the precomputed offsets is more efficient and the nullish coalescing provides good safety.


489-495: LGTM! Appropriate conditional rendering.

Limiting waveform rendering to segments with timescale === 1 is sensible, as time-stretched segments would require different waveform handling.

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

🧹 Nitpick comments (1)
crates/recording/src/sources/camera.rs (1)

53-66: Consider adding logging when reattachment occurs.

The reattachment logic provides good resilience when the camera sender disconnects, but there's no info-level log when reattachment succeeds. This makes it difficult to diagnose intermittent disconnection issues in production.

Consider adding an info log after successful reattachment:

                        Err(_) => {
                            let (new_tx, new_rx) = flume::bounded(8);

                            if let Err(err) = feed_lock.ask(camera::AddSender(new_tx)).await {
                                warn!(
                                    ?err,
                                    "Camera sender disconnected and could not be reattached"
                                );
                                break Err(err);
                            }

+                           info!(
+                               frame_count,
+                               "Camera sender disconnected; successfully reattached"
+                           );
                            receiver = new_rx;
                        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbe8ae1 and 37eb7c8.

📒 Files selected for processing (2)
  • .claude/settings.local.json (1 hunks)
  • crates/recording/src/sources/camera.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/sources/camera.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/camera.rs
🧠 Learnings (1)
📚 Learning: 2025-10-28T08:39:42.230Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1305
File: crates/recording/src/output_pipeline/macos.rs:80-90
Timestamp: 2025-10-28T08:39:42.230Z
Learning: In `crates/recording/src/output_pipeline/macos.rs`, the `AVFoundationMp4Muxer` intentionally holds the `Mutex<MP4Encoder>` lock during retry attempts in `send_video_frame()` and `send_audio_frame()`. This blocking behavior is correct because frame processing must happen sequentially to prevent audio and video frames from being interleaved incorrectly in the encoder.

Applied to files:

  • crates/recording/src/sources/camera.rs
🧬 Code graph analysis (1)
crates/recording/src/sources/camera.rs (1)
crates/recording/src/feeds/camera.rs (1)
  • drop (175-179)
⏰ 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). (1)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (3)
.claude/settings.local.json (1)

7-8: Approve: Configuration update is consistent.

The addition of "Bash(cargo build:*)" follows the established pattern of wildcard permissions for build tool operations and complements the existing "Bash(cargo check:*)" entry.

crates/recording/src/sources/camera.rs (2)

37-37: LGTM! Good observability improvement.

The frame counting and enhanced error logging provide valuable diagnostics for tracking camera forwarding behavior. Including the frame count in error logs will help identify whether issues occur at startup or during sustained operation.

Also applies to: 42-50


68-76: LGTM! Explicit cleanup with observable logging.

The explicit drop statements and final info log provide clear lifecycle boundaries and telemetry. This makes it easier to diagnose camera forwarding issues and ensures resources are cleaned up promptly.


feed_lock
.ask(camera::AddSender(tx))
.ask(camera::AddSender(tx.clone()))
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 | 🟡 Minor

🧩 Analysis chain

Verify the inconsistent sender ownership pattern between initial setup and reattachment.

There's an inconsistency in how senders are passed to AddSender:

  • Line 29: tx.clone() is passed, keeping the original for explicit cleanup at line 69
  • Line 55: new_tx is passed directly, transferring ownership to the camera feed

After reattachment, the forwarding task no longer holds a reference to the active sender (new_tx), only to the original tx which may already be disconnected. When the task exits:

  • The original tx is explicitly dropped (line 69), but it may no longer be connected
  • The new_tx is owned by the camera feed and will only be implicitly disconnected when receiver (now new_rx) is dropped at line 70

This creates an asymmetry in cleanup behavior depending on whether reattachment occurred.

Run the following script to check how AddSender is used elsewhere and whether cloning is the expected pattern:

Consider whether line 55 should use new_tx.clone() to maintain consistency, and track the current sender in a mutable variable that gets updated during reattachment and dropped at line 69.

Also applies to: 55-55, 69-69


🏁 Script executed:

#!/bin/bash
# Search for other AddSender usages to verify the expected ownership pattern
rg -nP 'AddSender\(' --type rust -A 2 -B 2

Length of output: 4000


Remove the .clone() at line 29 for consistency with standard AddSender pattern.

Across the codebase, 7 of 9 AddSender usages pass senders directly without cloning, establishing a clear ownership transfer pattern. Line 29's tx.clone() breaks this convention. The cloned sender is never used to send frames; keeping the original tx alive until line 69 merely prevents premature channel closure—a role the clone already fulfills.

Change line 29 to:

.ask(camera::AddSender(tx))

This ensures consistency and eliminates the unnecessary local reference.

🤖 Prompt for AI Agents
In crates/recording/src/sources/camera.rs around line 29, the call currently
uses .ask(camera::AddSender(tx.clone())), which unnecessarily clones the sender
and breaks the established ownership-transfer pattern used elsewhere; replace it
with .ask(camera::AddSender(tx)) so the original tx is moved into AddSender
(remove the .clone()), aligning with the other 7/9 usages and eliminating the
redundant local reference.

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.

2 participants