Skip to content

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Dec 22, 2025

  • Replace segmented muxers with unified M4S muxer on macOS for improved recording reliability
  • Add async finalization for fragmented recordings to prevent UI blocking during recording completion
  • Improve audio-video synchronization and adjust playback buffer sizes in the editor
  • Enhance YUV conversion performance with optimized frame handling
  • Refactor macOS memory usage reporting to use libproc for more accurate metrics
  • Remove backward stale frame handling in AVAssetReaderDecoder (no longer needed)
  • Clean up deprecated fragmented_mp4 and segmented_stream encoders
  • Add new segmented_stream encoder with improved architecture

Summary by CodeRabbit

  • New Features

    • Export preview generation (fast and full) and a redesigned Export page with live previews and export workflows.
    • Editor skeleton UI for improved loading state and smoother perceived startup.
  • Performance Improvements

    • Smarter multi-position decoding for faster seeking and lower memory use.
    • Improved audio–video synchronization and reduced frame cache size.
  • Bug Fixes

    • More robust recovery for fragmented recordings, background finalization, pause-aware timing, and improved frame-drop tracking.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Warning

Rate limit exceeded

@richiemcilroy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between bbd80f3 and 9285e02.

📒 Files selected for processing (3)
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/recovery.rs
📝 Walkthrough

Walkthrough

Added thread-safe finalization tracking and wait-for-ready logic; moved fragmented recording remux/finalization to background tasks; replaced legacy fragmented muxers with new segmented/fragmented M4S implementations (FFmpeg + macOS); added multi-position decoder pool, audio playhead sync, pause-aware timestamps, export preview commands, and multiple rendering/muxer refactors.

Changes

Cohort / File(s) Summary
Finalization tracking & editor wiring
apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/src/recording.rs
Add public FinalizingRecordings (Mutex map + tokio::sync::watch channels). Add wait_for_recording_ready and wire finalization wait into editor creation/startup; start/finish notifications used by background finalizer.
Background finalization & remux/recovery
crates/enc-ffmpeg/src/remux.rs, crates/recording/src/recovery.rs
New remux utilities: remux_streams, probe_m4s_can_decode_with_init, concatenate_m4s_segments_with_init; recovery now handles optional init segments and delegates to new remux paths.
FFmpeg segmented muxer (new)
crates/enc-ffmpeg/src/mux/segmented_stream.rs, crates/enc-ffmpeg/src/mux/mod.rs
Add SegmentedVideoEncoder with manifest/segment tracking, public errors/types, atomic manifest writes; replace fragmented_mp4 export with segmented_stream.
Removed legacy fragmented exports
crates/enc-ffmpeg/src/mux/fragmented_mp4.rs (removed), crates/enc-avfoundation/src/segmented.rs (removed)
Remove previous fragmented MP4 modules and public re-exports.
macOS fragmented M4S muxer (new)
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs, crates/recording/src/output_pipeline/mod.rs
New MacOSFragmentedM4SMuxer and camera variant: encoder thread, pause-aware timestamps, frame pool, manifests; consolidate macOS fragmented exports.
Removed old macOS fragmented implementations
crates/recording/src/output_pipeline/fragmented.rs (removed), crates/recording/src/output_pipeline/macos_segmented_ffmpeg.rs (removed)
Delete AVFoundation/FFmpeg fragmented muxer implementations and their manifests.
Shared pause state & segmented audio
crates/recording/src/output_pipeline/core.rs, crates/recording/src/output_pipeline/ffmpeg.rs, crates/recording/src/capture_pipeline.rs
Add SharedPauseState (adjust semantics) and thread it into SegmentedAudioMuxer and capture pipeline; adjust timestamp handling on send/finish.
Recording finalization flow
apps/desktop/src-tauri/src/recording.rs
When remux required, spawn background finalize_studio_recording using FinalizingRecordings; immediate finish path skips remuxing.
Screen capture & drop tracking
crates/recording/src/sources/screen_capture/macos.rs
Change pixel format BGRA→NV12; add drop_counter: Arc<AtomicU64> and runtime buffer/queue env configurables; log drop rates and increment counter on send failures.
Decoder pool / multi-position decoding
crates/rendering/src/decoder/multi_position.rs, crates/rendering/src/decoder/avassetreader.rs, crates/rendering/src/decoder/mod.rs
New multi-position decoder pool, DecoderPoolManager, ScrubDetector, KeyframeIndex integration; AVAssetReaderDecoder constructors updated to use keyframe index and pool selection; Arc-based frame data.
Rendering & YUV conversion optimizations
crates/rendering/src/yuv_converter.rs, crates/rendering/src/layers/*
Add BindGroupCache to reuse NV12/YUV420P bind groups; update converters and invalidate on size changes; camera/display layers switch to time-based dedup and adjusted prepare signatures.
Audio/video playback sync
crates/editor/src/playback.rs, crates/editor/src/audio.rs
Publish per-frame playhead channel, propagate to AudioPlayback for drift correction (~0.15s); add AudioPlaybackBuffer::current_playhead(). Lower prefetch/buffer constants and adjust warmup timings.
H.264 reusable-frame path & logging
crates/enc-ffmpeg/src/video/h264.rs
Add queue_frame_reusable methods to builder and encoder; improve encoder/scaler logging and tracing.
Segmented audio finish fix
crates/enc-ffmpeg/src/mux/segmented_audio.rs
finish_with_timestamp now uses effective end timestamp (max of last frame, provided timestamp) to compute duration.
Timestamp helpers
crates/timestamp/src/lib.rs, .../macos.rs, .../win.rs
Add signed-duration helpers returning f64 seconds (signed_duration_since_secs / signed_duration_since).
Export preview & UI changes
apps/desktop/src-tauri/src/export.rs, apps/desktop/src/utils/tauri.ts, apps/desktop/src/routes/editor/ExportPage.tsx, apps/desktop/src/routes/editor/ExportDialog.tsx (removed)
Add ExportPreviewSettings/Result and Tauri commands generate_export_preview / generate_export_preview_fast; replace old ExportDialog with new ExportPage and preview/caching UI.
Editor UI & skeleton
apps/desktop/src/routes/editor/index.tsx, Editor.tsx, Header.tsx, editor-skeleton.tsx (new)
Replace Suspense loader with EditorSkeleton, add export-mode layout using ExportPage, change export button styling, add new skeleton components.
Export audio generation & tests
crates/export/src/mp4.rs
Use fixed video timestamping and cursor-driven audio sample batching; add unit test for audio/frame durations across FPS set.
Misc / tools / examples / deps
crates/recording/examples/*, crates/recording/Cargo.toml, crates/editor/Cargo.toml, .claude/settings.local.json, packages/ui-solid/src/auto-imports.d.ts, apps/desktop/tailwind.config.js
Memory detector uses libproc footprint; add macOS deps; add decode-benchmark example and Tailwind animations; add UI icon auto-imports and Claude allowlist entries.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as Desktop UI
    participant Tauri as Backend
    participant Recorder as Recording Layer
    participant Finalizer as Background Finalizer
    participant Muxer as M4S Muxer
    participant Recovery as Remux/Recovery

    User->>Frontend: request open editor / load recording
    Frontend->>Tauri: load_recording(path)
    Tauri->>Recorder: wait_for_recording_ready(path)
    alt recording is finalizing
        Recorder->>Recorder: subscribe via FinalizingRecordings (watch::Receiver)
        Recorder-->>Tauri: wait until notified
        Finalizer->>Recorder: send completion (watch::Sender)
    else not finalizing
        Recorder->>Recorder: load RecordingMeta
        alt fragmented and needs remux
            Recorder->>Finalizer: spawn finalize_studio_recording (blocking remux task)
            Recorder->>Tauri: return finalizing state
            Finalizer->>Muxer: perform remux/concatenate (probe/concatenate_with_init)
            Finalizer->>Recovery: update meta, write manifests, signal finish
        else ready
            Recorder->>Tauri: return ready
        end
    end
Loading
sequenceDiagram
    participant Player as Renderer
    participant DecoderPool as DecoderPoolManager
    participant KeyIndex as KeyframeIndex
    participant Decoder as DecoderInstance
    participant Audio as AudioPlayback

    Player->>DecoderPool: request_frame(time)
    DecoderPool->>KeyIndex: consult keyframes (if present)
    DecoderPool->>Decoder: select or reset best decoder
    Decoder->>Decoder: decode -> produce Arc frame
    Decoder-->>Player: deliver frame
    Player->>Audio: send_playhead(frame_time_fraction) via channel
    Audio->>Audio: adjust_playhead_if_drift()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Brendonovich

Poem

🐰
I hopped through fragments, stitched each part with care,
Keyframes, playheads, synced in morning air,
Decoders pooled, remux tasks hum low,
Previews shimmer, exports ready to show —
A rabbit's joy for pipelines everywhere!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.64% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objectives: replacing segmented muxers with M4S muxer and adding async finalization for macOS, with additional optimizations noted.

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

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src-tauri/src/recording.rs (1)

1995-2030: Crash‑recovery remux misses M4S fragments

The new macOS M4S muxer writes segments in the standard fragmented MP4 format: init.mp4 plus numbered media segments with .m4s extension. However, the crash-recovery fragment detection has a critical mismatch:

  • find_fragments_in_dir only accepts .mp4 or .m4a files
  • The muxer writes .m4s segments (per FFmpeg DASH output specification)

Result: when recording crashes with fragmented M4S output, the recovery logic sees empty directories and fails with "Could not find fragments to remux", even though valid .m4s segments exist on disk.

The fix is straightforward—extend find_fragments_in_dir to accept .m4s:

fn find_fragments_in_dir(dir: &Path) -> Vec<PathBuf> {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return Vec::new();
    };

    let mut fragments: Vec<_> = entries
        .filter_map(|e| e.ok())
        .map(|e| e.path())
-        .filter(|p| p.extension().is_some_and(|e| e == "mp4" || e == "m4a"))
+        .filter(|p| p.extension().is_some_and(|e| e == "mp4" || e == "m4a" || e == "m4s"))
         .collect();

     fragments.sort();
     fragments
 }
🧹 Nitpick comments (20)
crates/enc-avfoundation/src/mp4.rs (1)

134-153: LGTM! Solid encoder configuration improvements.

The keyframe interval calculation (one keyframe every 2 seconds) and the additional compression properties (disabling frame reordering, setting expected frame rate, and max keyframe interval) are appropriate choices for recording scenarios. These settings improve seek performance, crash recovery, and encoding latency.

Optional: Consider logging the computed keyframe_interval

For easier debugging, you could log the computed keyframe interval alongside the bitrate:

 let keyframe_interval = (fps * 2.0) as i32;

-            debug!("recording bitrate: {bitrate}");
+            debug!("recording bitrate: {bitrate}, keyframe_interval: {keyframe_interval}");
crates/rendering/src/decoder/avassetreader.rs (2)

286-289: Remove stale #[allow(unused)] annotation.

last_active_frame is used in the cache eviction logic (lines 403-411), making this allow attribute incorrect.

🔎 Proposed fix
-        #[allow(unused)]
-        let mut last_active_frame = None::<u32>;
+        let mut last_active_frame = None::<u32>;

378-381: Consider logging frame decode errors instead of silently continuing.

The error from frame.map_err() is discarded. While continuing is reasonable, logging would aid debugging decode failures.

🔎 Proposed fix
             for frame in &mut frames {
-                let Ok(frame) = frame.map_err(|e| format!("read frame / {e}")) else {
+                let Ok(frame) = frame else {
+                    debug!("frame decode error, skipping");
                     continue;
                 };
crates/recording/examples/memory-leak-detector.rs (1)

110-113: Consider adjusting separator width for precise alignment.

The separator line width (50 characters) doesn't precisely match the header column widths. For better visual alignment, consider calculating the exact width based on the column format widths (8 + 14 + 10 + 12 + spacing).

🔎 Proposed fix for precise alignment
-        println!("{:-<50}", "");
+        println!("{:-<48}", "");
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (3)

21-26: Environment variable fallback should be documented or made more discoverable.

The CAP_MUXER_BUFFER_SIZE environment variable provides a tuning knob but isn't documented. Consider making this a configuration option instead, or ensure it's documented in operational guides.


209-248: Consider thread parking instead of busy-wait for cleaner shutdown.

The 50ms polling loop works but is less efficient than parking/unparking or using a condition variable. Additionally, if the encoder thread is abandoned at timeout (line 233), calling finish_with_timestamp afterward (line 240) races with the still-running thread, though the mutex provides safety.


600-891: Consider reducing duplication between display and camera muxers.

MacOSFragmentedM4SCameraMuxer duplicates ~290 lines from MacOSFragmentedM4SMuxer with only minor differences (thread name, log prefixes, VideoFrame type). Consider extracting shared logic into a generic helper or inner struct to reduce maintenance burden.

crates/enc-ffmpeg/src/remux.rs (1)

440-509: Consider extracting shared remuxing logic.

The remux_to_regular_mp4 function shares significant code with concatenate_with_concat_demuxer (lines 106-181), particularly the stream mapping and DTS/PTS adjustment logic. This duplication could be reduced by extracting a common helper.

🔎 Example refactor approach

Extract a shared helper that takes an input context and handles the stream mapping and packet writing:

fn remux_packets(
    ictx: &mut avformat::context::Input,
    output: &Path,
) -> Result<(), RemuxError> {
    let mut octx = avformat::output(output)?;
    // ... shared stream mapping and packet writing logic ...
}

Then both functions could use this helper with their respective input contexts.

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

243-247: Extract manifest version to a shared constant instead of hardcoding.

The hardcoded max_supported_version of 4 for m4s_segments should reference the MANIFEST_VERSION constant from cap-enc-ffmpeg. Since the recording crate already depends on enc-ffmpeg, export MANIFEST_VERSION as pub and use it here to prevent version drift if the encoder's version ever changes.

crates/enc-ffmpeg/src/mux/segmented_stream.rs (6)

15-45: Consider extracting shared helper functions to avoid duplication.

atomic_write_json and sync_file are duplicated from segmented_audio.rs. Consider extracting them to a shared module (e.g., crates/enc-ffmpeg/src/mux/util.rs) to reduce maintenance burden and ensure consistency.

Additionally, sync_file (lines 40-44) uses nested if statements, which is inconsistent with both the coding guidelines and the && chained syntax already used in atomic_write_json (lines 26-28) within this same file.

🔎 Proposed fix for sync_file
 fn sync_file(path: &Path) {
-    if let Ok(file) = std::fs::File::open(path) {
-        if let Err(e) = file.sync_all() {
-            tracing::warn!("File fsync failed for {}: {e}", path.display());
-        }
+    if let Ok(file) = std::fs::File::open(path)
+        && let Err(e) = file.sync_all()
+    {
+        tracing::warn!("File fsync failed for {}: {e}", path.display());
     }
 }

232-241: Filesystem check per frame may impact performance.

detect_current_segment_index calls next_segment_path.exists() on every frame, resulting in a syscall per frame (30-60+ per second). Consider caching the detection or using a frame-count/timestamp heuristic to reduce filesystem overhead, checking only periodically (e.g., every N frames or when approaching the expected segment boundary).


243-284: Return type Result<(), QueueFrameError> is never Err.

This method always returns Ok(()). Consider simplifying the return type to () unless error paths are planned for future implementation.

🔎 Proposed simplification
     fn on_segment_completed(
         &mut self,
         completed_index: u32,
         timestamp: Duration,
-    ) -> Result<(), QueueFrameError> {
+    ) {
         // ... body unchanged ...
-
-        Ok(())
     }

And update the call site in queue_frame:

         if new_segment_index > prev_segment_index {
-            self.on_segment_completed(prev_segment_index, timestamp)?;
+            self.on_segment_completed(prev_segment_index, timestamp);
         }

291-372: Consider extracting shared SegmentEntry mapping logic.

The SegmentEntry construction from VideoSegmentInfo is duplicated across write_manifest, write_in_progress_manifest, and finalize_manifest. A small helper could reduce repetition.

🔎 Example helper
impl VideoSegmentInfo {
    fn to_segment_entry(&self) -> SegmentEntry {
        SegmentEntry {
            path: self.path.file_name()
                .unwrap_or_default()
                .to_string_lossy()
                .into_owned(),
            index: self.index,
            duration: self.duration.as_secs_f64(),
            is_complete: true,
            file_size: self.file_size,
        }
    }
}

429-457: Deep nesting reduces readability; consider guard clauses.

The nested if let chains create 5+ levels of indentation. Using early continue statements can flatten the logic.

🔎 Flattened approach
for entry in entries.flatten() {
    let path = entry.path();
    let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
        continue;
    };
    if !name.starts_with("segment_") || !name.ends_with(".m4s.tmp") {
        continue;
    }
    let final_name = name.trim_end_matches(".tmp");
    let final_path = self.base_path.join(final_name);
    let Ok(metadata) = std::fs::metadata(&path) else {
        continue;
    };
    if metadata.len() == 0 {
        continue;
    }
    // ... rename logic ...
}

580-586: Option wrapper is unnecessary when always returning Some.

current_encoder and current_encoder_mut always return Some(...). Unless this is intentional for API compatibility with other encoder types that may not have a current encoder, consider returning the reference directly.

🔎 Proposed simplification
-    pub fn current_encoder(&self) -> Option<&H264Encoder> {
-        Some(&self.encoder)
+    pub fn current_encoder(&self) -> &H264Encoder {
+        &self.encoder
     }

-    pub fn current_encoder_mut(&mut self) -> Option<&mut H264Encoder> {
-        Some(&mut self.encoder)
+    pub fn current_encoder_mut(&mut self) -> &mut H264Encoder {
+        &mut self.encoder
     }
crates/recording/src/cursor.rs (1)

155-161: Optional: Reduce string clones for better performance.

cursor_id and last_cursor_id are cloned multiple times (lines 155, 158, 161, 173, 195). Consider using Rc<str> or Arc<str> to reduce allocations, especially since cursor IDs are frequently copied into events.

Example refactor
+use std::sync::Arc;

-        let mut last_cursor_id = "default".to_string();
+        let mut last_cursor_id: Arc<str> = Arc::from("default");

             let cursor_id = if position_changed {
                 last_position = position;
                 if let Some(data) = get_cursor_data() {
                     // ...
-                    let cursor_id = existing_id.id.to_string();
+                    let cursor_id: Arc<str> = Arc::from(existing_id.id.to_string());
                     // ...
-                    last_cursor_id = cursor_id.clone();
-                    cursor_id
+                    last_cursor_id = Arc::clone(&cursor_id);
+                    Arc::clone(&cursor_id)
                 } else {
-                    last_cursor_id.clone()
+                    Arc::clone(&last_cursor_id)
                 }
             } else {
-                last_cursor_id.clone()
+                Arc::clone(&last_cursor_id)
             };

Note: Arc::clone only increments a reference count, avoiding string data copies.

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

93-135: FinalizingRecordings registry is sound; consider guarding against duplicate starts

The registry design (per‑path watch<bool> with cloned receivers) is straightforward and fits the async finalization flow.

One edge case to keep in mind: start_finalizing always inserts a fresh channel and overwrites any existing entry for the same PathBuf. If, in future, you ever call start_finalizing twice for a given path (e.g. overlapping retries), any code holding the old Receiver will never see the true signal. If that scenario is possible, consider returning the existing receiver when present instead of creating a new one.


3167-3235: Editor readiness gating is correct; you may want to deduplicate crash‑recovery remux work

Wiring create_editor_instance_impl through wait_for_recording_ready cleanly separates three cases:

  • Active async finalization (FinalizingRecordings present) → wait on watch<bool>.
  • No finalization, but fragmented recording → run remux_fragmented_recording synchronously in a blocking task.
  • Already‑final/instant recordings → return immediately.

One thing to consider: in the crash‑recovery branch you don’t mark the recording as “finalizing” in FinalizingRecordings, so two editor windows opened simultaneously on the same crashed project would each kick off an independent remux. If that’s a realistic scenario, you could reuse the same registry here (insert before spawning the remux task and reuse the watch channel) so concurrent callers coalesce onto a single remux effort.

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

1369-1448: Async studio finalization flow is well‑structured; consider surfacing failures into meta/status

The new needs_fragment_remux path plus finalize_studio_recording background task nicely decouple:

  • Immediate user‑visible behaviour (open editor or overlay, play stop sound).
  • Long‑running remux + project config + screenshot work in the background.
  • Coordination via FinalizingRecordings so later editor opens can wait instead of duplicating work.

One gap is error propagation: if finalize_studio_recording (or its internal remux_fragmented_recording) fails, you currently just log and still call finish_finalizing, leaving the on‑disk RecordingMeta status unchanged. From the user’s point of view this can look like a “completed” studio recording that may be missing media or config.

If you want failures here to be diagnosable and possibly block opening in the editor, consider updating the meta to a StudioRecordingStatus::Failed { error } (similar to the crash‑handling path in handle_recording_end) or emitting a dedicated event/notification when background finalization fails.

Also applies to: 1644-1708

crates/editor/src/playback.rs (1)

679-686: Audio/video sync correction works conceptually but may be biased by prefill and output latency

The new sync loop between video and audio is a solid idea:

  • Video publishes its playhead over playhead_rx.
  • The audio callback compares that to audio_renderer.current_playhead().
  • When either drift exceeds SYNC_THRESHOLD_SECS or video seeks by more than the threshold, you reseed the audio renderer to video_playhead + initial_compensation_secs.

However, AudioPlaybackBuffer::current_playhead() is based on elapsed_samples, which is incremented every time you render samples – including during the initial prefill and any subsequent top‑up of the ring buffer. It also doesn’t account for the device’s output latency beyond the static initial_compensation_secs you add when reseeding.

In practice this means:

  • Right after prefill, audio_playhead can already be ahead of the video playhead by roughly “prefill duration + output latency”.
  • The sync logic may treat that constant offset as drift and repeatedly reseed, especially around startup or after buffer refills, even though what the user hears is still aligned once latency is considered.

You might get a more stable and accurate sync signal by incorporating the latency estimate into the comparison, for example:

  • Use something like effective_audio_playhead = audio_renderer.current_playhead() - latency_corrector.current_secs().unwrap_or_default() when computing drift.
  • Or at least subtract an approximation derived from your headroom (headroom_for_stream / sample_rate) if LatencyCorrector doesn’t expose a suitable metric.

That would make drift closer to “what the user is actually hearing vs. the video playhead”, and should reduce unnecessary reseeks while still catching real desyncs and explicit scrubs.

Also applies to: 757-772, 891-903, 923-949

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 444df9e and 3bfdbbc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src-tauri/src/recording.rs
  • crates/editor/src/audio.rs
  • crates/editor/src/playback.rs
  • crates/enc-avfoundation/src/lib.rs
  • crates/enc-avfoundation/src/mp4.rs
  • crates/enc-avfoundation/src/segmented.rs
  • crates/enc-ffmpeg/src/lib.rs
  • crates/enc-ffmpeg/src/mux/fragmented_mp4.rs
  • crates/enc-ffmpeg/src/mux/mod.rs
  • crates/enc-ffmpeg/src/mux/segmented_audio.rs
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/recording/Cargo.toml
  • crates/recording/examples/memory-leak-detector.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/recording/src/cursor.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/recording/src/output_pipeline/fragmented.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/output_pipeline/macos_segmented_ffmpeg.rs
  • crates/recording/src/output_pipeline/mod.rs
  • crates/recording/src/recovery.rs
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/recording/src/studio_recording.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/ffmpeg.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/layers/camera.rs
  • crates/rendering/src/layers/display.rs
  • crates/rendering/src/lib.rs
  • crates/rendering/src/yuv_converter.rs
💤 Files with no reviewable changes (6)
  • crates/enc-avfoundation/src/segmented.rs
  • crates/enc-avfoundation/src/lib.rs
  • crates/enc-ffmpeg/src/mux/fragmented_mp4.rs
  • crates/enc-ffmpeg/src/lib.rs
  • crates/recording/src/output_pipeline/macos_segmented_ffmpeg.rs
  • crates/recording/src/output_pipeline/fragmented.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/editor/src/audio.rs
  • crates/recording/src/cursor.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/enc-ffmpeg/src/mux/mod.rs
  • crates/rendering/src/decoder/ffmpeg.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/enc-ffmpeg/src/mux/segmented_audio.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/rendering/src/lib.rs
  • crates/rendering/src/layers/camera.rs
  • crates/recording/src/output_pipeline/mod.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/editor/src/playback.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/enc-avfoundation/src/mp4.rs
  • crates/recording/src/recovery.rs
  • apps/desktop/src-tauri/src/recording.rs
  • crates/recording/examples/memory-leak-detector.rs
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/rendering/src/layers/display.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/recording/src/studio_recording.rs
  • crates/rendering/src/yuv_converter.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/editor/src/audio.rs
  • crates/recording/src/cursor.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/enc-ffmpeg/src/mux/mod.rs
  • crates/rendering/src/decoder/ffmpeg.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/enc-ffmpeg/src/mux/segmented_audio.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/rendering/src/lib.rs
  • crates/rendering/src/layers/camera.rs
  • crates/recording/src/output_pipeline/mod.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/editor/src/playback.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/enc-avfoundation/src/mp4.rs
  • crates/recording/src/recovery.rs
  • apps/desktop/src-tauri/src/recording.rs
  • crates/recording/examples/memory-leak-detector.rs
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/rendering/src/layers/display.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/recording/src/studio_recording.rs
  • crates/rendering/src/yuv_converter.rs
🧠 Learnings (3)
📓 Common learnings
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.
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.
📚 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/capture_pipeline.rs
  • crates/rendering/src/decoder/ffmpeg.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/rendering/src/layers/camera.rs
  • crates/recording/src/output_pipeline/mod.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/editor/src/playback.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/enc-avfoundation/src/mp4.rs
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/recording/src/studio_recording.rs
📚 Learning: 2025-10-17T05:58:22.586Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.

Applied to files:

  • crates/enc-ffmpeg/src/mux/segmented_audio.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/enc-avfoundation/src/mp4.rs
  • crates/recording/src/recovery.rs
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/sources/screen_capture/macos.rs
  • crates/recording/src/studio_recording.rs
🧬 Code graph analysis (8)
crates/recording/src/cursor.rs (1)
crates/cursor-capture/src/position.rs (1)
  • get (12-20)
crates/recording/src/capture_pipeline.rs (1)
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (2)
  • default (161-167)
  • default (617-623)
crates/rendering/src/decoder/avassetreader.rs (1)
crates/rendering/src/decoder/mod.rs (12)
  • new_with_arc (210-223)
  • data (454-456)
  • new_nv12_with_arc (240-259)
  • y_stride (532-534)
  • uv_stride (536-538)
  • new_yuv420p_with_arc (282-301)
  • format (466-468)
  • width (458-460)
  • height (462-464)
  • new (87-89)
  • new (111-118)
  • new (195-208)
crates/rendering/src/layers/camera.rs (1)
apps/desktop/src/utils/tauri.ts (1)
  • XY (518-518)
crates/editor/src/playback.rs (4)
crates/recording/src/studio_recording.rs (1)
  • watch (550-550)
crates/media-info/src/lib.rs (1)
  • fps (265-267)
crates/rendering/src/project_recordings.rs (1)
  • fps (50-52)
crates/audio/src/latency.rs (1)
  • initial_compensation_secs (141-143)
apps/desktop/src-tauri/src/lib.rs (1)
crates/project/src/meta.rs (5)
  • path (127-129)
  • path (312-314)
  • path (363-365)
  • load_for_project (131-137)
  • studio_meta (177-182)
crates/enc-ffmpeg/src/mux/segmented_stream.rs (2)
crates/enc-ffmpeg/src/mux/segmented_audio.rs (3)
  • atomic_write_json (11-33)
  • sync_file (35-41)
  • queue_frame (167-193)
crates/enc-ffmpeg/src/video/h264.rs (2)
  • new (52-60)
  • queue_frame (362-387)
crates/recording/src/sources/screen_capture/macos.rs (1)
crates/recording/src/sources/screen_capture/mod.rs (1)
  • pixel_format (237-237)
⏰ 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: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Clippy (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (78)
crates/recording/src/sources/screen_capture/macos.rs (7)

27-39: LGTM!

Clean implementation of runtime configurables with sensible defaults. The pattern of parsing environment variables with fallback defaults is idiomatic.


47-49: Pixel format change is consistent.

The NV12 format returned by pixel_format() aligns with the cv::PixelFormat::_420V setting on line 166. This is a more efficient format for video encoding pipelines compared to BGRA.

Also applies to: 166-166


85-88: LGTM!

Proper initialization of the drop counter with Arc<AtomicU64> for thread-safe sharing across the capture pipeline.


148-155: LGTM!

Queue depth calculation scales appropriately with FPS and uses idiomatic .clamp() for bounds checking. Good observability with structured logging.


211-219: Good addition of drop tracking.

Using try_send with drop counting provides visibility into backpressure situations without blocking the capture callback. This is a significant improvement over silently dropping frames.


366-381: LGTM!

The drop_counter field is consistently added to both VideoSourceConfig and VideoSource structs, properly propagating the tracking through the pipeline.


458-501: Well-implemented monitoring with proper safeguards.

Good implementation:

  • Uses saturating_sub for delta calculations per coding guidelines
  • Drop rate formula correctly handles the ratio of drops to total attempts
  • 5% threshold provides actionable warnings without noise
  • Cancellation token ensures cleanup on stop

The spawned task's JoinHandle being dropped is acceptable here since it's a fire-and-forget monitoring task tied to the cancellation token lifecycle.

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

82-90: LGTM! Useful diagnostic logging added.

The structured logging provides comprehensive encoder initialization information that will be valuable for debugging and monitoring.


548-550: The extern declarations for AVVideoAllowFrameReorderingKey, AVVideoExpectedSourceFrameRateKey, and AVVideoMaxKeyFrameIntervalKey are correctly typed as &'static ns::String. These AVFoundation keys have been available since macOS 10.10, which is well below any reasonable minimum deployment target for a modern macOS application. No issues detected.

crates/rendering/src/layers/display.rs (1)

92-94: LGTM - Practical threshold for frame timing comparison.

The change from f32::EPSILON to 0.001 (1ms) is a sensible tolerance for frame deduplication. This threshold is consistent with the same logic in CameraLayer::prepare.

crates/rendering/src/decoder/ffmpeg.rs (3)

190-190: LGTM - First-frame tracking for improved fallback behavior.

This complements the existing last_sent_frame pattern and provides a meaningful fallback when playback seeks to a position before any frames have been sent.


279-285: LGTM - Efficient first-frame capture.

Processing the first frame immediately and caching it avoids redundant decoding when used as a fallback. The pattern correctly converts the cache_frame to Processed to avoid re-processing later.


369-373: LGTM - Improved fallback avoids unnecessary black frames.

Using the first decoded frame as a fallback provides a better user experience than immediately showing a black frame when no prior frames have been sent.

crates/rendering/src/lib.rs (2)

1694-1705: LGTM - Recording time propagation to camera layer.

The recording_time is correctly propagated from segment_frames to the camera layer's prepare method, aligning with the updated signature in camera.rs.


1707-1718: LGTM - Consistent recording time propagation to camera_only layer.

Same pattern applied consistently to the camera_only layer.

crates/rendering/src/yuv_converter.rs (5)

136-158: LGTM - Bind group cache for YUV conversion optimization.

The BindGroupCache struct provides an efficient mechanism to avoid recreating bind groups on every frame. The invalidation logic correctly clears cached bind groups when dimensions change, and integration with ensure_texture_size (line 626) ensures cache coherence when textures are reallocated.


160-200: LGTM - NV12 bind group caching with dimension tracking.

The dimension check provides an additional safety layer beyond the explicit invalidation in ensure_texture_size. The unwrap() on line 199 is safe since the preceding block guarantees the slot is populated.


202-248: LGTM - YUV420P bind group caching follows consistent pattern.

The implementation mirrors the NV12 caching logic, maintaining consistency across both YUV formats.


694-716: LGTM - Cache integration in convert_nv12.

The cache is correctly used with allocated_width and allocated_height as the key dimensions, ensuring consistency with the texture allocation tracking.


853-876: LGTM - Cache integration in convert_yuv420p.

Consistent cache usage pattern applied to YUV420P conversion path.

crates/rendering/src/layers/camera.rs (4)

17-17: LGTM - Semantic improvement from pointer to time-based tracking.

Using last_recording_time is more semantically meaningful than pointer comparison for frame deduplication.


62-68: LGTM - Updated signature aligns with lib.rs changes.

The fourth parameter f32 for recording time is correctly extracted from the tuple and used for frame change detection.


73-77: LGTM - Consistent timing threshold with DisplayLayer.

The 0.001 (1ms) threshold matches the implementation in DisplayLayer::prepare, ensuring uniform frame deduplication behavior across layers.


124-150: LGTM - macOS IOSurface path with graceful fallback.

The macOS-specific path correctly:

  1. Attempts zero-copy IOSurface conversion first
  2. Falls back to plane-based NV12 conversion if IOSurface is unavailable
  3. Uses #[cfg(not(target_os = "macos"))] for non-macOS platforms
crates/rendering/src/decoder/avassetreader.rs (7)

23-28: LGTM - Clean encapsulation of frame data with Arc.

The FrameData struct consolidates ownership of frame bytes with stride metadata, enabling efficient sharing via Arc.


40-66: LGTM - Arc-based constructors used correctly.

The destructuring and use of Arc::clone to increment reference counts is appropriate for shared ownership.


294-297: LGTM - Clean request batching structure.

Simple struct to accumulate pending frame requests for batch processing.


348-348: Verify BACKWARD_SEEK_TOLERANCE is appropriate.

The hardcoded tolerance of 120 frames determines when a seek triggers a decoder reset. At 30fps this is 4 seconds, at 60fps it's 2 seconds. Consider whether this value should be derived from fps or made configurable.


350-351: LGTM - Correct use of saturating_sub.

Good adherence to the coding guidelines to prevent underflow panics. As per coding guidelines, saturating_sub is used instead of - for arithmetic on unsigned types.


455-462: Requests for uncached frames are silently dropped.

After the frame iteration loop completes, any remaining requests for frames not in cache have their senders dropped without a response. The caller will receive a channel error rather than an explicit "frame not found" or fallback frame. Verify this is intentional behavior.


289-289: Remove the unused first_ever_frame variable.

The first_ever_frame variable is declared at line 289 and populated at lines 392-393 but never actually used. Unlike the similar pattern in ffmpeg.rs which reads this value later, here it serves no purpose and should be removed to eliminate dead code.

crates/rendering/src/decoder/mod.rs (6)

210-223: LGTM - Consistent Arc-based constructor for RGBA format.

Follows the same pattern as new() but accepts pre-wrapped Arc<Vec<u8>>.


240-259: LGTM - Arc-based NV12 constructor.

Properly mirrors new_nv12 with Arc-wrapped data.


282-301: LGTM - Arc-based YUV420p constructor.

Properly mirrors new_yuv420p with Arc-wrapped data.


323-341: LGTM - Arc-based IOSurface constructor for macOS.

Combines Arc-wrapped data with IOSurface backing for efficient GPU access.


343-360: LGTM - Zero-copy constructor for IOSurface-backed frames.

Creates a frame with empty data buffer, relying entirely on iosurface_backing for pixel data. Callers should use iosurface_backing() rather than data() for these frames.


550-550: Verify impact of reducing FRAME_CACHE_SIZE from 750 to 60.

This is a 12.5x reduction in cache capacity. At 30fps, the cache now holds ~2 seconds of frames instead of ~25 seconds. This may impact seeking smoothness, especially for longer seeks or slower decode paths. Ensure this was tested with real-world playback scenarios.

crates/recording/Cargo.toml (2)

92-93: libproc placement is correct for example code usage. The libproc = "0.14" dependency in [target.'cfg(target_os = "macos")'.dev-dependencies] is appropriate because libproc is used only in crates/recording/examples/memory-leak-detector.rs, which is example code. Dev-dependencies are the standard location for dependencies required by examples, tests, and benchmarks but not part of the production library. No changes needed.

Likely an incorrect or invalid review comment.


65-65: The libc = "0.2" dependency is appropriately specified and will automatically receive security patches. The latest version is 0.2.178 (released December 2, 2025), and this version constraint allows all 0.2.x releases, ensuring automatic access to patch updates. No security vulnerabilities are known for the Rust libc crate.

crates/recording/examples/memory-leak-detector.rs (9)

27-29: LGTM!

The memory metric conversions from bytes to megabytes are correct, and using footprint_mb as the primary metric aligns with the PR's goal to improve memory reporting accuracy on macOS.


51-51: LGTM!

The use of footprint_mb as the primary metric with resident_mb as a fallback is appropriate. The .unwrap_or() usage follows coding guidelines for simple values.


85-89: LGTM!

The baseline output clearly shows both Footprint and RSS metrics, consistent with the new reporting approach.


125-129: LGTM!

The timeline row formatting matches the header columns correctly.


136-137: LGTM!

The summary labels correctly reflect the shift to Footprint as the primary metric.


258-258: LGTM!

The increased sampling frequency (1 second instead of 5 seconds) provides more granular memory tracking, which is beneficial for detecting leaks. This change will produce more frequent output but improves monitoring precision.


269-272: LGTM!

The runtime output correctly displays Footprint first, then RSS, consistent with the new metric priority.


341-344: LGTM!

The camera-only test output correctly shows both Footprint and RSS metrics along with frame and queue statistics.


21-24: Verify that libproc dependency is properly declared and API fields are available.

The libproc crate (version 0.14) is correctly declared in Cargo.toml at line 93. The RUsageInfoV4 struct provides the fields ri_resident_size and ri_phys_footprint used at lines 23-24. The std::process::id() as i32 cast is safe on POSIX systems where PID values remain well below i32::MAX. The code is properly guarded with #[cfg(target_os = "macos")], ensuring it only compiles on macOS where the pidrusage API is available.

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

8-9: LGTM!

The import and usage of MacOSFragmentedM4SMuxer with MacOSFragmentedM4SMuxerConfig::default() are consistent with the new M4S-based fragmented muxer implementation. The change is a straightforward type substitution for the fragmented capture path.

Also applies to: 86-90

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

4-5: LGTM!

The module consolidation from separate fragmented and macos_segmented_ffmpeg modules into a single macos_fragmented_m4s module is clean. The re-exports are correctly gated with #[cfg(target_os = "macos")].

Also applies to: 10-11

crates/recording/src/studio_recording.rs (3)

14-18: LGTM!

The updated imports correctly bring in the new MacOSFragmentedM4SCameraMuxer and MacOSFragmentedM4SCameraMuxerConfig types for the camera path, while retaining the non-fragmented AVFoundationCameraMuxer types.


846-852: Verify the intentional FPS reduction for fragmented mode.

The max_fps is set to 60 for fragmented mode versus 120 for non-fragmented. This may be intentional for performance or file size optimization in the segmented pipeline, but it's a notable behavioral change that could affect recording quality.


885-908: LGTM!

The camera pipeline correctly switches between MacOSFragmentedM4SCameraMuxer for fragmented mode and AVFoundationCameraMuxer for non-fragmented mode, consistent with the display pipeline changes.

crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (6)

28-75: LGTM!

PauseTracker correctly uses checked_sub and checked_add to prevent underflow/overflow panics, returning descriptive errors for timestamp regressions and offset overflows.


77-136: LGTM!

FrameDropTracker properly guards against division by zero and provides useful observability with threshold-based warnings.


457-490: LGTM!

FramePool properly manages frame reuse. The unwrap() calls are safe: get_frame always ensures the frame exists before returning, and take_frame has an unwrap_or_else fallback.


573-598: Verify unsafe slice construction relies on cidre API guarantees.

The plane_data method constructs a slice from a raw pointer using plane_bytes_per_row * plane_height for the length. This assumes the buffer is contiguous with no gaps at row ends. Verify that cidre's plane_base_address and dimension methods guarantee this layout.


492-563: LGTM!

The fill_frame_from_sample_buf function correctly handles pixel format conversion for _420V (4:2:0 with proper UV subsampling), _32_BGRA, and _2VUY formats, with appropriate plane copying and error handling for unsupported formats.


434-455: LGTM!

The copy_plane_data helper efficiently handles three cases: matching strides with row width, matching source/dest strides, and row-by-row copy for stride mismatches. This avoids unnecessary per-row copies when possible.

crates/enc-ffmpeg/src/mux/segmented_audio.rs (1)

370-390: LGTM - Improved segment duration calculation.

The effective_end_timestamp logic correctly ensures the final duration accounts for the latest frame timestamp, preventing underreporting when the last frame's timestamp exceeds the provided end timestamp. The use of saturating_sub is appropriate.

crates/enc-ffmpeg/src/mux/mod.rs (1)

5-5: LGTM - Module reorganization aligns with the PR objectives.

The replacement of fragmented_mp4 with segmented_stream correctly reflects the shift to the new segmented DASH muxing architecture.

crates/recording/src/recovery.rs (3)

45-49: LGTM - Clean struct for fragment info with init segment.

The FragmentsInfo struct provides a clear abstraction for returning fragments alongside an optional init segment.


300-345: LGTM - Comprehensive fragment validation with init-aware paths.

The validation correctly differentiates between M4S segments (requiring init segment for decoding) and standard video/media fragments, with appropriate error logging for each failure case.


481-532: LGTM - Init-aware recovery with proper cleanup.

The recovery logic correctly handles:

  1. Single fragment optimization only when no init segment exists (line 481)
  2. M4S concatenation with init segment when present (lines 494-514)
  3. Cleanup of both fragments and init segments after processing
crates/enc-ffmpeg/src/remux.rs (2)

368-398: LGTM - Proper temporary file handling for M4S probing.

The function correctly:

  1. Creates a temporary combined file from init + segment
  2. Probes decode capability using existing infrastructure
  3. Cleans up the temporary file regardless of outcome

400-438: LGTM - Correct M4S concatenation approach.

The function properly:

  1. Validates all inputs exist before processing
  2. Concatenates init segment with all media segments at the byte level
  3. Remuxes to a standard MP4 for broad compatibility
  4. Cleans up the intermediate combined file
crates/enc-ffmpeg/src/video/h264.rs (3)

123-139: LGTM - Helpful encoder selection logging.

The differentiated logging for hardware vs software encoders provides valuable diagnostic information. Using error! level for the software encoder fallback appropriately draws attention to the performance impact.


389-417: LGTM - Efficient frame reuse pattern.

The queue_frame_reusable method correctly:

  1. Lazily allocates the converted frame only when needed via get_or_insert_with
  2. Reuses the same buffer across multiple calls, reducing allocation pressure
  3. Maintains the same encoding semantics as queue_frame

This is a valuable optimization for high-frequency encoding pipelines.


178-185: LGTM - Comprehensive encoder configuration logging.

The structured debug logging provides clear visibility into pixel format decisions, which will be valuable for diagnosing encoding issues.

crates/enc-ffmpeg/src/mux/segmented_stream.rs (3)

47-119: LGTM!

The struct definitions, manifest types, and error handling hierarchy are well-designed. The use of thiserror with proper From conversions provides good ergonomics for error propagation.


374-422: LGTM!

The finish methods handle encoder flush and trailer writing gracefully, logging warnings rather than failing hard. The fallback logic for computing end timestamps is robust. Based on learnings, this approach aligns with the project's pattern of graceful degradation during finalization.


460-538: LGTM!

The orphan recovery logic is thorough: it handles incomplete segment files from crashes, skips corrupt tiny files, and provides reasonable duration estimates. Sorting at the end ensures consistent manifest ordering.

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

164-180: LGTM: Move events correctly gated by position changes.

The logic appropriately generates cursor move events only when the position actually changes, avoiding redundant events. The position transformation chain (relative→normalized→cropped) is correct.


106-106: No issues with this change. The 16ms sleep interval aligns with standard 60fps video frame rates and is appropriate for cursor recording. At this polling frequency (62.5Hz), motion is captured adequately while reducing CPU overhead compared to the previous 10ms (100Hz) interval. Since this is recording-based polling—not real-time display rendering—high-refresh-rate display responsiveness is not a concern.

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

485-531: Bounded drain on cancellation looks good; semantics are consistent with existing pipeline

The new drain_timeout, max_drain_frames, and skipped handling give you a bounded amount of post‑cancel mux work while still fully draining the channel, and the logging around drained vs skipped frames should be very helpful when diagnosing stalls. The behaviour around first_tx and muxer errors remains consistent with the pre‑change code (errors no longer abort the drain loop, which is preferable here).

No correctness issues stand out in this hunk.

crates/editor/src/audio.rs (1)

146-148: current_playhead accessor matches existing AudioRenderer semantics

elapsed_samples_to_playhead computes playhead time from elapsed_samples, and AudioPlaybackBuffer::current_playhead is a thin, correct wrapper around that. The method itself is fine; any nuances about what “elapsed” means for sync (e.g. including prefilled samples) are addressed in the playback logic that consumes this API.

Also applies to: 280-283

crates/editor/src/playback.rs (2)

33-38: Prefetch and frame‑cache tuning plus playback‑position tracking look consistent

The new constants and the introduction of playback_position_tx/playback_position_rx give the prefetcher a clear notion of “how far ahead/behind” to decode, with:

  • MAX_PREFETCH_AHEAD bounding forward prefetch relative to the current frame.
  • PREFETCH_BEHIND and prefetched_behind limiting backward prefetch work.
  • FRAME_CACHE_SIZE and PREFETCH_BUFFER_SIZE enforced via pruning logic.

The arithmetic around frame numbers and durations is safe (saturating where needed), and the watch channel wiring is coherent. No correctness issues spotted here.

Also applies to: 123-140, 191-205, 253-305


336-347: Video playhead publishing via watch channel is wired correctly

The audio_playhead_tx/audio_playhead_rx channel is set up cleanly:

  • Initial value matches the starting video playhead.
  • audio_playhead_tx.send(frame_number as f64 / fps_f64) is called both on normal frame advancement and when you skip ahead to catch up.
  • AudioPlayback receives a cloned playhead_rx, so the audio thread stays updated without additional synchronization primitives.

This gives the audio path a simple, monotonic view of the video timeline.

Also applies to: 424-437, 630-655

Comment on lines +278 to +283
let pixel_format = match video_config.pixel_format {
cap_media_info::Pixel::NV12 => ffmpeg::format::Pixel::NV12,
cap_media_info::Pixel::BGRA => ffmpeg::format::Pixel::BGRA,
cap_media_info::Pixel::UYVY422 => ffmpeg::format::Pixel::UYVY422,
_ => ffmpeg::format::Pixel::NV12,
};
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

Silent fallback to NV12 for unknown pixel formats risks corrupted output.

When video_config.pixel_format doesn't match known variants, the code silently defaults to NV12. This could cause frame data misinterpretation and video corruption. Consider returning an error or at least logging a warning.

🔎 Suggested improvement
                 let pixel_format = match video_config.pixel_format {
                     cap_media_info::Pixel::NV12 => ffmpeg::format::Pixel::NV12,
                     cap_media_info::Pixel::BGRA => ffmpeg::format::Pixel::BGRA,
                     cap_media_info::Pixel::UYVY422 => ffmpeg::format::Pixel::UYVY422,
-                    _ => ffmpeg::format::Pixel::NV12,
+                    other => {
+                        warn!("Unsupported pixel format {:?}, defaulting to NV12", other);
+                        ffmpeg::format::Pixel::NV12
+                    }
                 };
📝 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
let pixel_format = match video_config.pixel_format {
cap_media_info::Pixel::NV12 => ffmpeg::format::Pixel::NV12,
cap_media_info::Pixel::BGRA => ffmpeg::format::Pixel::BGRA,
cap_media_info::Pixel::UYVY422 => ffmpeg::format::Pixel::UYVY422,
_ => ffmpeg::format::Pixel::NV12,
};
let pixel_format = match video_config.pixel_format {
cap_media_info::Pixel::NV12 => ffmpeg::format::Pixel::NV12,
cap_media_info::Pixel::BGRA => ffmpeg::format::Pixel::BGRA,
cap_media_info::Pixel::UYVY422 => ffmpeg::format::Pixel::UYVY422,
other => {
tracing::warn!("Unsupported pixel format {:?}, defaulting to NV12", other);
ffmpeg::format::Pixel::NV12
}
};
🤖 Prompt for AI Agents
In crates/recording/src/output_pipeline/macos_fragmented_m4s.rs around lines 278
to 283, the match on video_config.pixel_format currently falls through to NV12
for unknown variants which can corrupt output; update it to handle unknown
formats explicitly by returning an Err (or propagating a suitable error) or at
minimum logging a warning and failing the encoding path. Replace the silent
default arm with logic that maps known variants to ffmpeg pixels and for _
returns an Err with a clear message (or calls a function to convert the pixel
format that returns Result), and update callers to propagate/handle that Result
so encoding is aborted on unsupported formats.

Comment on lines 320 to 324
if let Ok(mut encoder) = encoder_clone.lock() {
if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
warn!("Failed to encode frame: {e}");
}
}
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

Silently ignoring poisoned mutex may hide encoder panics.

The if let Ok(mut encoder) = encoder_clone.lock() pattern silently drops frames when the mutex is poisoned. If the encoder panics, this becomes a silent failure. Consider logging when the lock fails.

🔎 Suggested improvement
-                            if let Ok(mut encoder) = encoder_clone.lock() {
-                                if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
-                                    warn!("Failed to encode frame: {e}");
-                                }
+                            match encoder_clone.lock() {
+                                Ok(mut encoder) => {
+                                    if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
+                                        warn!("Failed to encode frame: {e}");
+                                    }
+                                }
+                                Err(e) => {
+                                    warn!("Encoder mutex poisoned, dropping frame: {e}");
+                                }
                             }
🤖 Prompt for AI Agents
In crates/recording/src/output_pipeline/macos_fragmented_m4s.rs around lines
320-324, the current `if let Ok(mut encoder) = encoder_clone.lock()` silently
ignores a poisoned mutex and drops frames when the encoder panics; change this
to explicitly handle the `Err` case: match on `encoder_clone.lock()`, on `Ok(mut
encoder)` call `encoder.queue_frame(owned_frame, timestamp)` as before, and on
`Err(poisoned)` log a warning including the poison error message and then
recover the inner guard with `poisoned.into_inner()` (or, if recovery is
undesirable, log and return/skip) so that poisoned mutexes are not silently
ignored and you retain diagnostic information.

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

♻️ Duplicate comments (3)
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (3)

219-224: Config preset and output_size fields are still ignored.

Both start_encoder implementations hardcode H264Preset::Ultrafast and output_size: None instead of using self.preset and self.output_size from the config. The config fields preset and output_size are defined but never used.

🔎 Suggested fix

Store the config values and use them:

         let encoder_config = SegmentedVideoEncoderConfig {
             segment_duration: self.segment_duration,
-            preset: H264Preset::Ultrafast,
+            preset: self.preset,
             bpp: H264EncoderBuilder::QUALITY_BPP,
-            output_size: None,
+            output_size: self.output_size,
         };

This requires storing preset and output_size in the muxer struct during setup.

Also applies to: 682-687


235-240: Silent fallback to NV12 for unknown pixel formats.

Both encoder threads silently default to NV12 for unrecognized pixel formats. This could cause frame data misinterpretation and corrupted video output.

🔎 Suggested improvement
                 let pixel_format = match video_config.pixel_format {
                     cap_media_info::Pixel::NV12 => ffmpeg::format::Pixel::NV12,
                     cap_media_info::Pixel::BGRA => ffmpeg::format::Pixel::BGRA,
                     cap_media_info::Pixel::UYVY422 => ffmpeg::format::Pixel::UYVY422,
-                    _ => ffmpeg::format::Pixel::NV12,
+                    other => {
+                        warn!("Unsupported pixel format {:?}, defaulting to NV12", other);
+                        ffmpeg::format::Pixel::NV12
+                    }
                 };

Also applies to: 698-703


277-281: Silently ignoring poisoned mutex may hide encoder panics.

The if let Ok(mut encoder) = encoder_clone.lock() pattern silently drops frames when the mutex is poisoned. If the encoder panics, this becomes a silent failure with no diagnostic output.

🔎 Suggested improvement
-                            if let Ok(mut encoder) = encoder_clone.lock() {
-                                if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
-                                    warn!("Failed to encode frame: {e}");
-                                }
+                            match encoder_clone.lock() {
+                                Ok(mut encoder) => {
+                                    if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
+                                        warn!("Failed to encode frame: {e}");
+                                    }
+                                }
+                                Err(e) => {
+                                    warn!("Encoder mutex poisoned, dropping frame: {e}");
+                                }
                             }

Also applies to: 742-746

🧹 Nitpick comments (6)
crates/timestamp/src/macos.rs (1)

42-53: LGTM with optional optimization suggestion.

The implementation correctly handles signed time differences by branching on the comparison to determine sign, which is necessary given the u64 internal representation. The logic is sound.

One optional optimization: TimeBaseInfo::new() is called in every method (lines 23, 34, 43, 66, 77). If these timestamp methods are called frequently, consider caching the TimeBaseInfo to avoid repeated system calls.

Optional: Cache TimeBaseInfo

If performance profiling shows this as a hot path, you could cache the frequency calculation:

use std::sync::OnceLock;

static TIME_BASE_FREQ: OnceLock<f64> = OnceLock::new();

fn time_base_freq() -> f64 {
    *TIME_BASE_FREQ.get_or_init(|| {
        let info = TimeBaseInfo::new();
        info.numer as f64 / info.denom as f64
    })
}

Then replace TimeBaseInfo::new() calls with time_base_freq().

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

843-844: Consider making max_fps configurable or documenting the rationale.

The hardcoded values (60 for fragmented, 120 otherwise) may not be optimal for all hardware configurations. Consider adding this as a configurable option or at minimum documenting why these specific values were chosen.

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

28-87: Consider consolidating with Windows PauseTracker implementation.

The SharedPauseState logic here closely mirrors the PauseTracker in crates/recording/src/output_pipeline/win.rs (lines 32-71 in the relevant snippets). Both implement identical timestamp adjustment logic with paused_at, offset, and the same error messages. Consider extracting a shared implementation to reduce duplication and ensure consistent behavior across platforms.

crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (3)

21-26: Environment variable parsing could fail silently for invalid values.

If CAP_MUXER_BUFFER_SIZE contains a non-numeric value, parse().ok() silently falls back to the default. Consider logging when an invalid value is provided to help with debugging configuration issues.

🔎 Suggested improvement
 fn get_muxer_buffer_size() -> usize {
-    std::env::var("CAP_MUXER_BUFFER_SIZE")
-        .ok()
-        .and_then(|s| s.parse().ok())
-        .unwrap_or(3)
+    match std::env::var("CAP_MUXER_BUFFER_SIZE") {
+        Ok(s) => match s.parse() {
+            Ok(v) => v,
+            Err(_) => {
+                warn!("Invalid CAP_MUXER_BUFFER_SIZE value '{}', using default 3", s);
+                3
+            }
+        },
+        Err(_) => 3,
+    }
 }

95-103: Significant code duplication between MacOSFragmentedM4SMuxer and MacOSFragmentedM4SCameraMuxer.

The two muxer implementations are nearly identical, differing only in:

  • Type name and log messages
  • VideoFrame type (screen_capture::VideoFrame vs NativeCameraFrame)

Consider extracting the common logic into a generic implementation or shared helper to reduce maintenance burden and ensure consistent behavior.

Also applies to: 557-565


166-204: Thread join timeout loop could be simplified.

The polling loop with is_finished() + sleep(50ms) is functional but could be cleaner. Consider using a dedicated thread join with timeout pattern or documenting why polling is preferred.

Also applies to: 629-667

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bfdbbc and 9b03f4e.

📒 Files selected for processing (9)
  • crates/recording/src/capture_pipeline.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/recording/src/output_pipeline/ffmpeg.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/recovery.rs
  • crates/recording/src/studio_recording.rs
  • crates/timestamp/src/lib.rs
  • crates/timestamp/src/macos.rs
  • crates/timestamp/src/win.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/timestamp/src/lib.rs
  • crates/timestamp/src/win.rs
  • crates/recording/src/output_pipeline/ffmpeg.rs
  • crates/recording/src/recovery.rs
  • crates/timestamp/src/macos.rs
  • crates/recording/src/studio_recording.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/recording/src/output_pipeline/core.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/timestamp/src/lib.rs
  • crates/timestamp/src/win.rs
  • crates/recording/src/output_pipeline/ffmpeg.rs
  • crates/recording/src/recovery.rs
  • crates/timestamp/src/macos.rs
  • crates/recording/src/studio_recording.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/recording/src/output_pipeline/core.rs
🧠 Learnings (6)
📓 Common learnings
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.
📚 Learning: 2025-12-07T14:29:40.743Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T14:29:40.743Z
Learning: Applies to **/*.rs : Use `saturating_sub` instead of `-` for `Duration` to avoid panics (Clippy: `unchecked_duration_subtraction` = deny)

Applied to files:

  • crates/timestamp/src/lib.rs
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.rs : Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow

Applied to files:

  • crates/timestamp/src/win.rs
  • crates/timestamp/src/macos.rs
📚 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/output_pipeline/ffmpeg.rs
  • crates/recording/src/studio_recording.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/recording/src/output_pipeline/core.rs
📚 Learning: 2025-10-17T05:58:22.586Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.

Applied to files:

  • crates/recording/src/output_pipeline/ffmpeg.rs
  • crates/recording/src/recovery.rs
  • crates/recording/src/studio_recording.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/recording/src/capture_pipeline.rs
  • crates/recording/src/output_pipeline/core.rs
📚 Learning: 2025-12-07T14:29:40.743Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T14:29:40.743Z
Learning: Applies to **/*.rs : Never write `let _ = async_fn()` which silently drops futures; await or explicitly handle them (Clippy: `let_underscore_future` = deny)

Applied to files:

  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
🧬 Code graph analysis (5)
crates/timestamp/src/lib.rs (2)
crates/timestamp/src/macos.rs (1)
  • signed_duration_since_secs (42-53)
crates/timestamp/src/win.rs (1)
  • signed_duration_since_secs (68-72)
crates/recording/src/recovery.rs (1)
crates/enc-ffmpeg/src/remux.rs (8)
  • concatenate_audio_to_ogg (183-211)
  • concatenate_m4s_segments_with_init (400-438)
  • concatenate_video_fragments (34-62)
  • get_media_duration (349-356)
  • get_video_fps (358-366)
  • probe_m4s_can_decode_with_init (368-398)
  • probe_media_valid (272-274)
  • probe_video_can_decode (276-347)
crates/timestamp/src/macos.rs (2)
crates/timestamp/src/lib.rs (1)
  • signed_duration_since_secs (48-71)
crates/timestamp/src/win.rs (1)
  • signed_duration_since_secs (68-72)
crates/recording/src/capture_pipeline.rs (2)
crates/recording/src/output_pipeline/ffmpeg.rs (1)
  • default (211-216)
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (2)
  • default (113-120)
  • default (575-582)
crates/recording/src/output_pipeline/core.rs (1)
crates/recording/src/output_pipeline/win.rs (3)
  • new (33-39)
  • adjust (41-72)
  • timestamp (437-439)
⏰ 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: Clippy (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (17)
crates/timestamp/src/win.rs (1)

68-72: LGTM!

Clean implementation that leverages f64 arithmetic to naturally handle both positive and negative time differences. The use of the cached perf_freq() helper is efficient.

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

48-71: LGTM!

The implementation properly handles signed duration calculation across all timestamp variants:

  • Instant: Uses checked_duration_since with fallback to reversed calculation for negative differences.
  • SystemTime: Leverages the error case to extract backward duration.
  • Platform-specific types: Correctly delegates to their respective implementations.

The approach is consistent with existing duration methods and handles both forward and backward time differences correctly.

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

629-630: Verify the switch to signed_duration_since_secs.

The change from duration_since to signed_duration_since_secs allows negative durations. Ensure this is intentional and that downstream consumers of start_time handle negative values correctly, as this could produce negative start times if timestamps are out of order.


870-880: LGTM on shared pause state initialization.

The platform-specific handling is appropriate: macOS gets a properly initialized SharedPauseState for fragmented mode while Windows receives None. The AtomicBool initialization with false correctly indicates recording is not paused at start.

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

48-58: LGTM on trait signature update.

The MakeCapturePipeline trait correctly adds shared_pause_state: Option<SharedPauseState> parameter, enabling coordinated pause handling across platform implementations.


92-95: LGTM on macOS fragmented pipeline construction.

The config properly passes shared_pause_state and uses struct update syntax with ..Default::default() to maintain forward compatibility.

crates/recording/src/output_pipeline/ffmpeg.rs (2)

200-217: LGTM on SegmentedAudioMuxer refactoring.

The transition from tuple struct to named fields improves readability. The config struct with shared_pause_state follows the same pattern as the video muxers, maintaining consistency.


256-269: LGTM on pause-aware audio frame handling.

The logic correctly:

  1. Applies timestamp adjustment when pause state exists
  2. Skips frames (returns Ok(())) when paused
  3. Uses original timestamp when no pause state is configured
crates/recording/src/output_pipeline/core.rs (2)

50-54: Mutex poison handling returns error but recording continues.

When lock() fails due to poisoning, the error propagates up. This is reasonable for correctness, but consider whether the recording should attempt recovery (e.g., by resetting the pause state) rather than failing the frame entirely.


550-590: LGTM on enhanced drain logic.

The improvements add important safeguards:

  • 2-second timeout prevents indefinite blocking
  • 30-frame cap limits resource usage
  • Separate skipped counter provides visibility into dropped frames
  • Logging includes both drained and skipped counts with elapsed time
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (2)

449-520: LGTM on fill_frame_from_sample_buf implementation.

The function properly:

  • Handles multiple pixel formats (_420V, _32_BGRA, _2VUY)
  • Uses RAII-based BaseAddrLockGuard for safe memory access
  • Returns typed errors via SampleBufConversionError
  • Correctly calculates plane dimensions for each format

530-555: LGTM on BaseAddrLockGuard RAII pattern.

The guard correctly implements the lock/unlock pattern with Drop to ensure the base address is always unlocked, preventing resource leaks even on early returns or panics.

crates/recording/src/recovery.rs (5)

31-31: LGTM: Clean structural additions for init segment support.

The new optional init segment fields and the FragmentsInfo helper struct provide a clear foundation for handling M4S fragments with separate initialization segments.

Also applies to: 33-33, 45-49


364-368: LGTM: M4S extension handling added correctly.

The is_video_file function and probe logic now correctly recognize .m4s files as video-capable media.

Also applies to: 384-384


481-532: LGTM: Correct concatenation logic for M4S segments with init support.

The recovery logic properly handles three cases:

  1. Single fragment without init → direct move
  2. Multiple fragments or init present → concatenate with appropriate function
  3. Cleanup of both fragments and init segments after processing

The branching at lines 494-505 (display) and 549-556 (camera) correctly uses concatenate_m4s_segments_with_init when an init segment exists, with proper fallback to standard concatenation.

Also applies to: 536-583


769-839: LGTM: Metadata propagation preserves critical timing information.

The enhanced metadata building correctly extracts and propagates start_time and fps from original segments when available. This is important for maintaining audio-video synchronization after recovery.

The optional chaining at lines 797, 802-808, 816-818, and 826-828 gracefully handles cases where original segment metadata is unavailable, falling back to reasonable defaults.


224-362: Comprehensive M4S fragment discovery with proper validation paths.

The refactored find_complete_fragments_with_init correctly handles multiple manifest types and validates M4S segments with/without init segments. The branching logic at lines 300-339 properly uses probe_m4s_can_decode_with_init when an init segment exists, falling back to probe_video_can_decode otherwise. The max version of 4 for m4s_segments aligns with the manifest format created by segmented_stream.rs, and the probe function correctly combines init and segment data for validation.

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

Caution

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

⚠️ Outside diff range comments (1)
crates/rendering/src/layers/camera.rs (1)

58-64: Fix call sites to wrap uniforms in Option.

The prepare method signature now expects uniforms: Option<CompositeVideoFrameUniforms>, but callers are passing uniforms.camera unwrapped. Update both call sites in lib.rs (lines 1694 and 1706) to wrap the uniforms in Some():

self.camera.prepare(
    &constants.device,
    &constants.queue,
    Some(uniforms.camera),
    constants.options.camera_size.and_then(|size| {
        segment_frames
            .camera_frame
            .as_ref()
            .map(|frame| (size, frame, segment_frames.recording_time))
    }),
);

Apply the same fix to the camera_only.prepare() call.

🧹 Nitpick comments (5)
crates/editor/examples/decode-benchmark.rs (4)

42-51: Don't use .cloned() on Copy types.

The code calls .cloned() on iterators over f64 values, which implement Copy. Use .copied() instead, or simply dereference in the closure.

🔎 Proposed fix
 let min = self
     .sequential_decode_times_ms
     .iter()
-    .cloned()
+    .copied()
     .fold(f64::INFINITY, f64::min);
 let max = self
     .sequential_decode_times_ms
     .iter()
-    .cloned()
+    .copied()
     .fold(f64::NEG_INFINITY, f64::max);

Apply the same change to the random access section:

 let min = self
     .random_access_times_ms
     .iter()
-    .cloned()
+    .copied()
     .fold(f64::INFINITY, f64::min);
 let max = self
     .random_access_times_ms
     .iter()
-    .cloned()
+    .copied()
     .fold(f64::NEG_INFINITY, f64::max);

Based on coding guidelines: don't call .clone() on Copy types; copy them directly.

Also applies to: 77-86


125-133: Consider handling NaN values to avoid potential panics.

Line 130 uses .unwrap() on partial_cmp, which will panic if the data contains NaN values. For a benchmark example this may be acceptable, but consider using sort_by(|a, b| a.total_cmp(b)) (available for f64) for more robust sorting, or document the assumption that inputs are non-NaN.


135-157: Refactor parameter type and error handling.

  1. Accept &Path instead of &PathBuf for more flexibility (line 135).
  2. Using -1.0 as an error sentinel (line 150) is not idiomatic Rust—consider returning Result<f64, String>.
  3. Decoder creation failures after the first iteration (lines 147-152) are silently ignored, which could skew the average. Consider logging all failures or tracking success count.
🔎 Proposed fix for parameter type
-async fn benchmark_decoder_creation(path: &PathBuf, fps: u32, iterations: usize) -> f64 {
+async fn benchmark_decoder_creation(path: &Path, fps: u32, iterations: usize) -> f64 {

Callers can pass &config.video_path directly since PathBuf derefs to Path.

Based on coding guidelines: accept &[T] or &str instead of &Vec<T> or &String (same principle applies to Path/PathBuf).


159-195: Consider handling frame retrieval results.

Lines 171, 188, and 191 ignore the results of get_frame() calls with let _frame = .... If get_frame returns a Result or Option, failed retrievals could skew timing measurements. Consider checking for errors or at least logging failures to ensure benchmark validity.

Additionally, the _fps parameter in benchmark_seek (line 184) is unused. If it's not needed, remove it for clarity; otherwise, document why it's present.

crates/rendering/src/layers/camera.rs (1)

133-159: Consider extracting NV12 conversion to a helper method for clarity.

The macOS-specific iosurface handling with software fallback is functionally correct but involves complex nested conditionals. Extracting this into a helper method would improve readability.

💡 Optional refactor to improve readability

Consider extracting the conversion logic:

fn try_convert_nv12(
    &mut self,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    camera_frame: &DecodedFrame,
    frame_size: XY<u32>,
) -> bool {
    #[cfg(target_os = "macos")]
    if let Some(Ok(_)) = camera_frame.iosurface_backing()
        .map(|buf| self.yuv_converter.convert_nv12_from_iosurface(device, queue, buf))
    {
        return true;
    }

    if let (Some(y_data), Some(uv_data)) = (camera_frame.y_plane(), camera_frame.uv_plane()) {
        self.yuv_converter
            .convert_nv12(
                device,
                queue,
                y_data,
                uv_data,
                frame_size.x,
                frame_size.y,
                camera_frame.y_stride(),
                camera_frame.uv_stride(),
            )
            .is_ok()
    } else {
        false
    }
}

Then in the main match arm:

PixelFormat::Nv12 => {
    if self.try_convert_nv12(device, queue, camera_frame, frame_size) {
        self.copy_from_yuv_output(device, queue, next_texture, frame_size);
    }
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b03f4e and ef81bbc.

📒 Files selected for processing (4)
  • crates/editor/Cargo.toml
  • crates/editor/examples/decode-benchmark.rs
  • crates/rendering/src/layers/camera.rs
  • crates/rendering/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/rendering/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/editor/examples/decode-benchmark.rs
  • crates/rendering/src/layers/camera.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/editor/examples/decode-benchmark.rs
  • crates/rendering/src/layers/camera.rs
🧠 Learnings (3)
📓 Common learnings
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.
📚 Learning: 2025-12-07T14:29:40.743Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T14:29:40.743Z
Learning: Applies to **/*.rs : Use `rustfmt` and workspace clippy lints for Rust code formatting and linting

Applied to files:

  • crates/editor/Cargo.toml
📚 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/rendering/src/layers/camera.rs
🧬 Code graph analysis (1)
crates/editor/examples/decode-benchmark.rs (1)
crates/rendering/src/decoder/mod.rs (1)
  • spawn_decoder (601-763)
⏰ 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 (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (6)
crates/editor/Cargo.toml (1)

9-11: LGTM!

The example target is properly configured and aligns with the new benchmark file.

crates/rendering/src/layers/camera.rs (5)

17-17: LGTM! Time-based tracking is cleaner than pointer-based.

The refactor from last_frame_ptr: usize to last_recording_time: Option<f32> improves frame deduplication by using timestamps instead of memory addresses, which is more reliable and semantically clearer.

Also applies to: 53-53


65-71: LGTM! Hidden state logic correctly handles frame persistence.

The logic properly hides the camera only when there's no uniforms, or when there's neither new frame data nor a previous frame to display. This allows the last frame to persist when new data isn't available.


73-73: Confirm that early uniforms write is intentional.

The uniforms buffer is now written before checking if frame_data exists. This means camera positioning/scaling uniforms update even when there's no new frame data. Verify this is the desired behavior to allow independent uniform updates.


82-85: LGTM! Time-based deduplication with appropriate tolerance.

The 0.001-second tolerance is suitable for detecting duplicate frame timestamps while accounting for floating-point precision. This won't incorrectly skip distinct frames even at high frame rates (60fps ≈ 16.7ms between frames).


206-206: LGTM! Completes time-based deduplication refactor.

Correctly updates last_recording_time after processing a new frame, completing the transition from pointer-based to timestamp-based frame tracking.

Comment on lines +21 to +22
cache_hits: usize,
cache_misses: usize,
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

Unused fields lead to misleading output.

The cache_hits and cache_misses fields are never populated anywhere in the code, so they remain at their default value of 0. The cache statistics section in print_report() (lines 106-119) will always display "0 (0.0%)".

Either populate these fields with actual cache metrics from the decoder or remove them from the struct and report.

🤖 Prompt for AI Agents
In crates/editor/examples/decode-benchmark.rs around lines 21-22, the struct
defines cache_hits and cache_misses but these fields are never updated,
producing misleading "0 (0.0%)" output; either remove these fields and strip the
cache statistics block from print_report(), or update the benchmark to collect
real metrics from the decoder (increment cache_hits/cache_misses where cache
lookups occur or read counters from the decoder API) and ensure print_report()
uses those populated values; pick one approach and update the struct, reporting
logic, and any decoder call sites accordingly so reported cache numbers reflect
real data or are omitted entirely.

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 (4)
crates/rendering/src/decoder/mod.rs (1)

573-582: LGTM: Timeout prevents indefinite blocking.

The 500ms timeout is a good defensive measure to prevent the UI from hanging if the decoder stalls. The debug logging provides useful diagnostics.

Optional: Extract timeout as named constant

Consider extracting the timeout value to a named constant for easier tuning and maintainability:

const FRAME_REQUEST_TIMEOUT_MS: u64 = 500;

match tokio::time::timeout(std::time::Duration::from_millis(FRAME_REQUEST_TIMEOUT_MS), rx).await {
    Ok(result) => result.ok(),
    Err(_) => {
        debug!(
            adjusted_time = adjusted_time,
            "get_frame timed out after {}ms", FRAME_REQUEST_TIMEOUT_MS
        );
        None
    }
}
crates/editor/src/playback.rs (1)

954-954: Consider using tracing::error! for consistency.

The audio error callback uses eprintln! while the rest of the codebase uses tracing for logging. However, this may be intentional if tracing is not safe to call from the audio callback context.

crates/video-decode/src/avassetreader.rs (2)

19-19: Accept &Path instead of &PathBuf for flexibility.

Rust best practices recommend accepting &Path instead of &PathBuf in function parameters for greater flexibility. Callers can pass Path, PathBuf, or string types that can be converted to Path.

🔎 Proposed fix
-    pub fn build(path: &PathBuf) -> Result<Self, String> {
+    pub fn build(path: &Path) -> Result<Self, String> {

As per coding guidelines, accept &[T] or &str instead of &Vec<T> or &String in function parameters.


22-44: Reuse the input context instead of reopening the file.

The function opens the video file twice: once at line 22 to extract stream metadata, then again at line 43 to iterate packets. After extracting stream_index, time_base, and fps from the first input, it's immediately abandoned. Since FFmpeg's packet iterator does not consume the input context, you can reuse the same input for both operations and eliminate the I/O overhead of reopening.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef81bbc and 4ff10db.

📒 Files selected for processing (7)
  • .claude/settings.local.json
  • crates/editor/src/playback.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/lib.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/video-decode/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/video-decode/src/avassetreader.rs
  • crates/editor/src/playback.rs
  • crates/video-decode/src/lib.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/lib.rs
  • crates/rendering/src/decoder/avassetreader.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/video-decode/src/avassetreader.rs
  • crates/editor/src/playback.rs
  • crates/video-decode/src/lib.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/lib.rs
  • crates/rendering/src/decoder/avassetreader.rs
🧠 Learnings (2)
📓 Common learnings
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.
📚 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/editor/src/playback.rs
  • crates/video-decode/src/lib.rs
  • crates/rendering/src/decoder/avassetreader.rs
🧬 Code graph analysis (2)
crates/editor/src/playback.rs (6)
crates/video-decode/src/avassetreader.rs (1)
  • fps (90-92)
crates/media-info/src/lib.rs (1)
  • fps (265-267)
crates/recording/src/sources/screen_capture/mod.rs (1)
  • fps (275-277)
crates/rendering/src/project_recordings.rs (1)
  • fps (50-52)
crates/editor/src/segments.rs (1)
  • get_audio_segments (6-37)
crates/audio/src/latency.rs (1)
  • initial_compensation_secs (141-143)
crates/rendering/src/decoder/avassetreader.rs (3)
crates/rendering/src/decoder/ffmpeg.rs (4)
  • to_decoded_frame (34-52)
  • None (187-187)
  • None (189-189)
  • None (190-190)
crates/rendering/src/decoder/mod.rs (11)
  • new_with_arc (210-223)
  • data (454-456)
  • new_nv12_zero_copy (344-360)
  • y_stride (532-534)
  • uv_stride (536-538)
  • new (87-89)
  • new (111-118)
  • new (195-208)
  • width (458-460)
  • height (462-464)
  • format (466-468)
crates/video-decode/src/avassetreader.rs (4)
  • width (273-275)
  • height (277-279)
  • pixel_format_to_pixel (312-319)
  • frames (267-271)
⏰ 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: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Clippy (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (27)
.claude/settings.local.json (1)

53-54: LGTM! Permission aligns with new benchmarking workflow.

The added permission for the decode-benchmark binary supports the performance optimization work in this PR. The wildcard pattern allows flexible argument passing, which is appropriate for a local development benchmarking tool.

crates/rendering/src/decoder/mod.rs (6)

210-223: LGTM: Arc-backed constructor enables efficient data sharing.

The new_with_arc constructor allows callers to pass pre-wrapped Arc data, avoiding unnecessary cloning or double-wrapping. This is beneficial for performance-sensitive frame handling.


240-259: LGTM: Consistent Arc pattern for NV12 frames.

The implementation correctly follows the Arc-backed pattern for NV12 pixel format with appropriate stride parameters.


282-301: LGTM: Completes Arc-backed constructors for all formats.

The YUV420P Arc constructor maintains consistency with the other pixel format constructors.


323-341: LGTM: macOS IOSurface constructor with Arc support.

The Arc variant correctly combines pre-wrapped data with IOSurface backing for macOS hardware acceleration paths.


343-360: LGTM: Zero-copy constructor for macOS hardware paths.

This constructor correctly implements zero-copy semantics by creating an empty data buffer and relying entirely on IOSurface backing. The function name clearly communicates this behavior, and callers can access frame data via iosurface_backing() instead of data().


550-550: Verify the 80% frame cache reduction doesn't impact performance.

FRAME_CACHE_SIZE has been reduced from 750 to 150 frames. While this significantly reduces memory usage, it may increase cache misses during playback, especially for scrubbing or non-linear playback patterns.

Ensure this reduction has been validated against typical editor usage patterns (seeking, scrubbing, frame-by-frame navigation) and doesn't introduce performance regressions.

crates/rendering/src/lib.rs (3)

156-162: LGTM — correct per-segment camera FPS retrieval.

This fix properly retrieves the camera FPS from the specific segment being decoded (inner.segments[segment_i]) rather than potentially using a different segment's metadata. The pattern is consistent with the SingleSegment case on lines 157-159.


1706-1716: LGTM — consistent recording_time handling for camera_only layer.

The same pattern is correctly applied to the camera_only layer preparation, maintaining consistency with the camera layer changes.


1694-1704: LGTM — recording_time context added for camera layer.

The addition of segment_frames.recording_time to the tuple enables timing-aware rendering. The CameraLayer::prepare method uses this value to detect frame duplication by comparing against last_recording_time, preventing redundant texture uploads for the same frame time.

crates/editor/src/playback.rs (6)

33-37: LGTM!

The adjusted prefetch and cache constants appear reasonable for the playback pipeline optimization.


336-347: LGTM!

The watch channel is well-suited for signaling the latest video playhead to the audio thread, as the audio only needs the most recent value rather than every intermediate update.


359-360: Aggressive warmup parameters may cause initial stutter.

Reducing warmup_target_frames to 1 and timeout to 16ms means playback starts almost immediately. If the first frame decode is slow (e.g., on a cold start or with a complex segment), this could cause a brief stutter. Consider whether this trade-off is acceptable for all use cases, especially on lower-end hardware.


632-634: LGTM!

Sending the playhead update after frame progression correctly synchronizes the audio with the current video position. Ignoring the send result is acceptable since the audio receiver may have been dropped if playback ended.


679-686: LGTM!

The playhead_rx field is correctly added to propagate video playhead updates to the audio stream.


925-948: Audio-video synchronization approach is sound.

The drift-based correction design effectively handles both gradual drift and sudden jumps:

  • Detects drift when exceeded (0.15s threshold)
  • Applies latency compensation when resyncing
  • Uses non-blocking watch channel operations

The set_playhead() call within the audio callback is safe: it's only invoked when drift is detected (infrequent), and the underlying operations (resampler.reset(), resampled_buffer.clear()) are lock-free O(1) operations suitable for real-time contexts.

crates/video-decode/src/lib.rs (1)

8-8: LGTM: Public API expanded to include KeyframeIndex.

The addition of KeyframeIndex to the public re-exports aligns with the new keyframe indexing functionality introduced in avassetreader.rs. This enables downstream consumers to leverage keyframe-aware seeking capabilities.

crates/video-decode/src/avassetreader.rs (4)

72-97: LGTM: Keyframe lookup and accessor methods are well-implemented.

The nearest_keyframe_before method correctly uses binary search to find the closest keyframe before a target frame. The edge cases (empty list, exact match, no earlier keyframe) are properly handled. The accessor methods are straightforward and efficient.


112-122: LGTM: Graceful degradation for keyframe index failures.

The constructor appropriately handles keyframe index build failures by logging a warning and continuing without the index. This ensures the decoder remains functional even if keyframe indexing fails, which is good defensive programming.


161-201: LGTM: Keyframe-based seeking improves performance.

The reset() method now leverages the keyframe index to seek to the nearest keyframe before the requested time (line 166-179), then passes this seek_time to get_reader_track_output (line 183). This is a standard video decoder optimization—seeking to keyframes is more efficient than seeking to arbitrary frames, as it avoids partial decoding of inter-frame dependencies.

The added instrumentation (lines 191-198) provides valuable debugging information for seek operations.


203-205: LGTM: Clean accessor for keyframe index.

Simple, idiomatic accessor method that provides read-only access to the optional keyframe index.

crates/rendering/src/decoder/avassetreader.rs (6)

33-91: LGTM: Clean consolidation of frame data.

The refactoring consolidates frame metadata into a single FrameData struct, improving code organization. The to_decoded_frame implementation correctly:

  • Uses Arc::clone for efficient data sharing
  • Selects the appropriate DecodedFrame constructor based on pixel format
  • Leverages zero-copy NV12 path when an image buffer is available (lines 64-71)

232-292: LGTM: Efficient pixel format handling with appropriate fallbacks.

The implementation efficiently handles different pixel formats:

  • NV12: Retains the image buffer for zero-copy access (lines 236-245)
  • RGBA/BGRA/YUV420P: Extracts raw data into owned buffers (lines 246-260)
  • Unsupported formats: Provides a black frame fallback (lines 262-276)

The early returns prevent unnecessary processing and the logic is clear.


369-456: LGTM: Well-designed request batching with intelligent caching.

The refactored batching logic efficiently handles multiple frame requests:

  1. Batch collection (lines 370-396): Drains the channel to collect all pending requests
  2. Cache serving (lines 398-413): Immediately serves frames already in cache
  3. Smart reset logic (lines 419-441): Determines whether to reset based on seek distance and cache coverage
  4. Cache retention (line 454): Retains relevant cached frames within the new bounds after reset

The use of saturating_sub (line 426) correctly prevents underflow. The complexity is justified by the performance benefits of batched processing.


459-536: LGTM: Robust frame iteration with cache management.

The frame iteration loop correctly:

  • Captures first frame (lines 477-479): Stores the first decoded frame for use as a fallback
  • Manages cache size (lines 487-502): Evicts frames based on playback direction (forward/backward)
  • Fulfills requests (lines 506-526): Serves matching and past frames from cache as they're decoded
  • Respects cache bounds (line 533): Exits when exceeding the cache window or all requests are fulfilled

The logic handles forward and backward playback appropriately by evicting from the opposite end of the cache.


540-581: LGTM: Comprehensive fallback strategy for unfulfilled requests.

The fallback hierarchy for unfulfilled requests is well-designed:

  1. Cache lookup (lines 551-556): First attempts to serve from cache
  2. Last sent frame (lines 556-564): Falls back to the most recently sent frame
  3. First ever frame (lines 565-573): Uses the initial frame as a last resort
  4. Drop request (lines 575-579): Logs when no frame is available

This ensures maximum resilience when the decoder cannot fulfill a request at the exact frame. The debug logging at each stage (lines 407, 512, 519, 554, 563, 572, 578) aids in diagnosing decoder behavior.


23-31: No issues found. The unsafe Send and Sync implementations for SendableImageBuf (lines 24-25) are correct. The wrapper properly delegates to the underlying R<cv::ImageBuf> which is reference-counted and thread-safe for read-only access. The retained() method correctly increments the reference count, and all access to the image buffer after wrapping in Arc is immutable. No thread safety violations exist in this implementation.

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

Caution

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

⚠️ Outside diff range comments (1)
crates/video-decode/src/avassetreader.rs (1)

213-226: Potential panic from unwrap() calls on fallible operations.

Lines 214 and 220 use unwrap() which will panic if the file cannot be opened or if no video stream is found. This is inconsistent with the rest of the constructor which uses proper error handling with ?.

🔎 Proposed fix
         let (pixel_format, width, height) = {
-            let input = ffmpeg::format::input(&path).unwrap();
+            let input = ffmpeg::format::input(&path)
+                .map_err(|e| format!("Failed to open video: {e}"))?;

             let input_stream = input
                 .streams()
                 .best(ffmpeg::media::Type::Video)
-                .ok_or("Could not find a video stream")
-                .unwrap();
+                .ok_or("Could not find a video stream")?;
♻️ Duplicate comments (1)
crates/rendering/src/decoder/avassetreader.rs (1)

229-229: Remove underscore prefix from _processor parameter.

This was flagged in a previous review. The parameter is used on line 249 but has an underscore prefix suggesting it's unused.

🧹 Nitpick comments (4)
crates/video-decode/src/avassetreader.rs (2)

53-54: Redundant file open - the input context is opened twice.

The file is opened at line 23 and then reopened at line 53. The first input context could be reused for packet iteration, avoiding the overhead of reopening the file.

🔎 Proposed fix
-        let input = avformat::input(path)
-            .map_err(|e| format!("Failed to open video for keyframe scan: {e}"))?;
+        let mut input = avformat::input(path)
+            .map_err(|e| format!("Failed to open video for keyframe scan: {e}"))?;

         let video_stream = input
             .streams()
@@ -50,9 +50,6 @@

         let mut keyframes = Vec::new();

-        let mut input =
-            avformat::input(path).map_err(|e| format!("Failed to reopen video for scan: {e}"))?;
-
         for (stream, packet) in input.packets() {

301-308: Consider using debug! instead of info! for reset logging.

The info! level logging on every reset operation may be too verbose for production use, especially during scrubbing where resets can happen frequently. Consider using debug! level.

🔎 Proposed fix
         let elapsed = reset_start.elapsed();
-        tracing::info!(
+        tracing::debug!(
             requested_time = requested_time,
             seek_time = seek_time,
             keyframe_frame = ?keyframe_frame,
             reset_ms = elapsed.as_millis(),
             has_keyframe_index = self.keyframe_index.is_some(),
             "AVAssetReader reset completed"
         );
crates/rendering/src/decoder/multi_position.rs (1)

1-1: Unused import: Arc is imported but not used in this module.

The Arc type is imported but MultiPositionDecoderConfig.keyframe_index uses Option<Arc<KeyframeIndex>>, which is passed in from outside. However, Arc itself is not constructed in this module.

#!/bin/bash
# Verify if Arc is actually used in this file
rg -n '\bArc\b' crates/rendering/src/decoder/multi_position.rs
crates/rendering/src/decoder/avassetreader.rs (1)

656-658: Consider making the scrub frame limit configurable or documented.

The hardcoded limit of 3 frames during scrubbing (frames_iterated > 3) affects responsiveness. While reasonable, consider making this a constant for clarity.

🔎 Proposed fix

At the top of the file, add:

const SCRUB_MAX_FRAMES_PER_REQUEST: u32 = 3;

Then update line 656:

-                    if is_scrubbing && frames_iterated > 3 {
+                    if is_scrubbing && frames_iterated > SCRUB_MAX_FRAMES_PER_REQUEST {
                         break;
                     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ff10db and cf96969.

📒 Files selected for processing (4)
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/decoder/multi_position.rs
  • crates/video-decode/src/avassetreader.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/rendering/src/decoder/multi_position.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/decoder/avassetreader.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/rendering/src/decoder/multi_position.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/decoder/avassetreader.rs
🧠 Learnings (2)
📓 Common learnings
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.
📚 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/video-decode/src/avassetreader.rs
  • crates/rendering/src/decoder/avassetreader.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). (2)
  • GitHub Check: Clippy (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (15)
crates/rendering/src/decoder/mod.rs (5)

19-20: LGTM!

The macOS-specific module exposure for multi-position decoder support is properly gated with #[cfg(target_os = "macos")].


212-225: LGTM!

The Arc-based constructors follow the same pattern as existing constructors and provide efficient zero-copy frame creation when the data is already in an Arc<Vec<u8>>.

Also applies to: 242-261, 284-303


345-362: LGTM!

The new_nv12_zero_copy constructor correctly creates an empty Arc<Vec<u8>> when the image buffer backing is used directly, avoiding unnecessary allocations in the zero-copy path.


552-552: Significant cache size reduction from 750 to 150 frames.

This is a 5x reduction in cache size. Verify this aligns with the new multi-decoder pool architecture where each decoder maintains its own position, potentially reducing the need for a large shared cache.


575-584: LGTM on the timeout addition.

The 500ms timeout prevents indefinite blocking on frame requests. The debug logging on timeout provides useful diagnostics without being too noisy at debug level.

crates/video-decode/src/avassetreader.rs (2)

87-103: LGTM!

The binary search implementation for nearest_keyframe_before and nearest_keyframe_after is correct and efficient. Edge cases (empty keyframes, exact match, boundary conditions) are properly handled.

Also applies to: 105-132


134-151: LGTM!

The get_strategic_positions method correctly distributes decoder positions across keyframes, with appropriate handling for edge cases when there are fewer keyframes than requested positions.

crates/rendering/src/decoder/multi_position.rs (3)

85-122: LGTM on decoder selection logic.

The two-phase selection (first checking usable decoders within threshold, then falling back to closest) is a sound approach for minimizing seeks while ensuring responsiveness.


175-241: LGTM on ScrubDetector implementation.

The exponential weighted moving average (0.7/0.3 smoothing) provides good responsiveness while filtering noise. The cooldown mechanism prevents rapid state oscillation.


243-246: LGTM!

The Default implementation correctly delegates to Self::new().

crates/rendering/src/decoder/avassetreader.rs (5)

24-32: LGTM!

The SendableImageBuf wrapper with manual Send/Sync implementations and the Clone implementation using retained() correctly enables cross-thread usage of cv::ImageBuf.


34-40: LGTM!

The FrameData struct consolidates frame data fields cleanly, supporting both Arc-backed data and optional image buffer for zero-copy paths.


415-435: LGTM on decoder selection logic.

The select_best_decoder method correctly bounds the decoder index using saturating_sub and updates pool positions after reset, aligning with the coding guidelines.


484-516: LGTM on request batching.

The batching of pending requests with try_recv() and sorting by frame number is an efficient approach for handling burst requests during scrubbing.


683-714: LGTM on fallback frame handling.

The cascading fallback strategy (cache → last sent → first ever → drop) provides good resilience during edge cases while logging appropriately at debug level.

Comment on lines +348 to +349
let keyframe_index = primary_decoder.take_keyframe_index();
let keyframe_index_arc: Option<Arc<cap_video_decode::avassetreader::KeyframeIndex>> = None;
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

keyframe_index_arc is always None - the extracted keyframe index is never used.

The keyframe_index is extracted from primary_decoder on line 348 but keyframe_index_arc is hardcoded to None on line 349. This appears to be incomplete code where the extracted index should be wrapped in an Arc and passed to the config.

🔎 Proposed fix
         let keyframe_index = primary_decoder.take_keyframe_index();
-        let keyframe_index_arc: Option<Arc<cap_video_decode::avassetreader::KeyframeIndex>> = None;
+        let keyframe_index_arc = keyframe_index.map(Arc::new);

         let fps = keyframe_index
-            .as_ref()
+            keyframe_index_arc.as_ref()
             .map(|kf| kf.fps() as u32)
             .unwrap_or(30);
         let duration_secs = keyframe_index
-            .as_ref()
+            keyframe_index_arc.as_ref()
             .map(|kf| kf.duration_secs())
             .unwrap_or(0.0);
📝 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
let keyframe_index = primary_decoder.take_keyframe_index();
let keyframe_index_arc: Option<Arc<cap_video_decode::avassetreader::KeyframeIndex>> = None;
let keyframe_index = primary_decoder.take_keyframe_index();
let keyframe_index_arc = keyframe_index.map(Arc::new);
let fps = keyframe_index_arc
.as_ref()
.map(|kf| kf.fps() as u32)
.unwrap_or(30);
let duration_secs = keyframe_index_arc
.as_ref()
.map(|kf| kf.duration_secs())
.unwrap_or(0.0);
🤖 Prompt for AI Agents
In crates/rendering/src/decoder/avassetreader.rs around lines 348 to 349,
keyframe_index is taken from primary_decoder but keyframe_index_arc is hardcoded
to None; replace that with constructing an Option<Arc<...>> from the extracted
keyframe_index (e.g., if let Some(kf) = keyframe_index { Some(Arc::new(kf)) }
else { None }) so the value is actually wrapped in Arc and assigned to
keyframe_index_arc, and then pass this keyframe_index_arc into the decoder
config where needed; ensure correct ownership/clone semantics and imports for
Arc are used.

Comment on lines +134 to +164
pub fn get_rebalance_positions(&self) -> Vec<f32> {
if self.access_history.is_empty() {
return self.positions.iter().map(|p| p.position_secs).collect();
}

let mut hotspots: Vec<(u32, u64)> = self
.access_history
.iter()
.map(|(&frame, &count)| (frame, count))
.collect();
hotspots.sort_by(|a, b| b.1.cmp(&a.1));

let top_hotspots: Vec<f32> = hotspots
.into_iter()
.take(MAX_DECODER_POOL_SIZE)
.map(|(frame, _)| frame as f32 / self.config.fps as f32)
.collect();

if top_hotspots.len() < MAX_DECODER_POOL_SIZE {
let mut result = top_hotspots;
let remaining = MAX_DECODER_POOL_SIZE - result.len();
let duration = self.config.duration_secs as f32;
for i in 0..remaining {
let frac = (i + 1) as f32 / (remaining + 1) as f32;
result.push(duration * frac);
}
result
} else {
top_hotspots
}
}
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

Potential division by zero if fps is 0.

In get_rebalance_positions at line 149, dividing by self.config.fps could cause a panic if fps is 0. While unlikely in practice, defensive handling would be safer.

🔎 Proposed fix
         let top_hotspots: Vec<f32> = hotspots
             .into_iter()
             .take(MAX_DECODER_POOL_SIZE)
-            .map(|(frame, _)| frame as f32 / self.config.fps as f32)
+            .map(|(frame, _)| {
+                if self.config.fps == 0 {
+                    0.0
+                } else {
+                    frame as f32 / self.config.fps as f32
+                }
+            })
             .collect();
📝 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
pub fn get_rebalance_positions(&self) -> Vec<f32> {
if self.access_history.is_empty() {
return self.positions.iter().map(|p| p.position_secs).collect();
}
let mut hotspots: Vec<(u32, u64)> = self
.access_history
.iter()
.map(|(&frame, &count)| (frame, count))
.collect();
hotspots.sort_by(|a, b| b.1.cmp(&a.1));
let top_hotspots: Vec<f32> = hotspots
.into_iter()
.take(MAX_DECODER_POOL_SIZE)
.map(|(frame, _)| frame as f32 / self.config.fps as f32)
.collect();
if top_hotspots.len() < MAX_DECODER_POOL_SIZE {
let mut result = top_hotspots;
let remaining = MAX_DECODER_POOL_SIZE - result.len();
let duration = self.config.duration_secs as f32;
for i in 0..remaining {
let frac = (i + 1) as f32 / (remaining + 1) as f32;
result.push(duration * frac);
}
result
} else {
top_hotspots
}
}
pub fn get_rebalance_positions(&self) -> Vec<f32> {
if self.access_history.is_empty() {
return self.positions.iter().map(|p| p.position_secs).collect();
}
let mut hotspots: Vec<(u32, u64)> = self
.access_history
.iter()
.map(|(&frame, &count)| (frame, count))
.collect();
hotspots.sort_by(|a, b| b.1.cmp(&a.1));
let top_hotspots: Vec<f32> = hotspots
.into_iter()
.take(MAX_DECODER_POOL_SIZE)
.map(|(frame, _)| {
if self.config.fps == 0 {
0.0
} else {
frame as f32 / self.config.fps as f32
}
})
.collect();
if top_hotspots.len() < MAX_DECODER_POOL_SIZE {
let mut result = top_hotspots;
let remaining = MAX_DECODER_POOL_SIZE - result.len();
let duration = self.config.duration_secs as f32;
for i in 0..remaining {
let frac = (i + 1) as f32 / (remaining + 1) as f32;
result.push(duration * frac);
}
result
} else {
top_hotspots
}
}
🤖 Prompt for AI Agents
In crates/rendering/src/decoder/multi_position.rs around lines 134 to 164, the
conversion frame as f32 / self.config.fps as f32 can panic if fps == 0; add a
defensive check at the start of get_rebalance_positions: if self.config.fps ==
0, return a sensible fallback (for example, return self.positions.iter().map(|p|
p.position_secs).collect()) or otherwise handle/log the invalid config and avoid
performing the division; ensure any subsequent code uses the validated nonzero
fps value when converting frames to seconds.

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 (3)
crates/rendering/src/decoder/avassetreader.rs (2)

228-293: Remove underscore prefix from _processor parameter.

The parameter _processor at line 229 has an underscore prefix suggesting it's unused, but it's actually used on line 249. This is a known issue from a previous review.


343-413: Critical: Extracted keyframe index is never used.

Lines 348-349 extract keyframe_index from the primary decoder but then hardcode keyframe_index_arc to None. This means:

  • The keyframe index is built but discarded
  • Strategic positioning in DecoderPoolManager will fall back to evenly-spaced positions
  • Additional decoders created at line 381 receive None for their keyframe index

This appears to be incomplete implementation that defeats the purpose of the keyframe indexing system introduced in the first file.

🔎 Proposed fix
         let keyframe_index = primary_decoder.take_keyframe_index();
-        let keyframe_index_arc: Option<Arc<cap_video_decode::avassetreader::KeyframeIndex>> = None;
+        let keyframe_index_arc = keyframe_index.as_ref().map(Arc::new);

         let fps = keyframe_index
-            .as_ref()
+            _arc.as_ref()
             .map(|kf| kf.fps() as u32)
             .unwrap_or(30);
         let duration_secs = keyframe_index
-            .as_ref()
+            _arc.as_ref()
             .map(|kf| kf.duration_secs())
             .unwrap_or(0.0);

Then pass the keyframe_index (not the Arc) to DecoderInstance::new at line 381 where it expects Option<KeyframeIndex>.

crates/rendering/src/decoder/multi_position.rs (1)

134-164: Potential division by zero if fps is 0.

Line 149 divides by self.config.fps as f32 without checking if fps is zero. While unlikely in practice, this could panic.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9232b71 and c44ad76.

📒 Files selected for processing (6)
  • crates/export/src/mp4.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/multi_position.rs
  • crates/rendering/src/layers/display.rs
  • crates/rendering/src/lib.rs
  • crates/video-decode/src/avassetreader.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/rendering/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/rendering/src/layers/display.rs
  • crates/export/src/mp4.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/rendering/src/decoder/multi_position.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/rendering/src/layers/display.rs
  • crates/export/src/mp4.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/rendering/src/decoder/multi_position.rs
🧠 Learnings (3)
📓 Common learnings
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.
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.
📚 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/export/src/mp4.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/video-decode/src/avassetreader.rs
📚 Learning: 2025-10-17T05:58:22.586Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.

Applied to files:

  • crates/export/src/mp4.rs
🧬 Code graph analysis (2)
crates/rendering/src/decoder/avassetreader.rs (4)
crates/rendering/src/decoder/mod.rs (10)
  • pts_to_frame (547-550)
  • data (456-458)
  • y_stride (534-536)
  • uv_stride (538-540)
  • new (89-91)
  • new (113-120)
  • new (197-210)
  • width (460-462)
  • height (464-466)
  • format (468-470)
crates/rendering/src/decoder/ffmpeg.rs (1)
  • to_decoded_frame (34-52)
crates/video-decode/src/avassetreader.rs (9)
  • new (183-185)
  • width (393-395)
  • height (397-399)
  • pixel_format (315-317)
  • pixel_format_to_pixel (432-439)
  • path (311-313)
  • keyframe_index (323-325)
  • new_with_keyframe_index (207-267)
  • fps (153-155)
crates/rendering/src/decoder/multi_position.rs (2)
  • config (170-172)
  • is_scrubbing (225-227)
crates/rendering/src/decoder/multi_position.rs (1)
crates/rendering/src/decoder/avassetreader.rs (7)
  • new (100-102)
  • new (229-293)
  • new (307-323)
  • new (344-413)
  • None (472-472)
  • None (473-473)
  • None (474-474)
⏰ 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 (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (20)
crates/rendering/src/layers/display.rs (1)

83-85: LGTM – Frame deduplication threshold improved.

The change from f32::EPSILON to 0.001 seconds makes the deduplication more practical by skipping frames with timestamps within 1 millisecond of each other. This aligns the display layer with the camera layer's deduplication behavior and supports the audio-video synchronization improvements in this PR.

crates/video-decode/src/avassetreader.rs (4)

13-168: LGTM: KeyframeIndex implementation is solid.

The KeyframeIndex implementation provides efficient keyframe lookup with binary search, sensible fallbacks (default fps of 30.0, pts fallback to 0), and strategic position calculation with proper step size handling (.max(1) prevents zero-step panics).


183-205: LGTM: Constructor delegation pattern is clean.

The new()new_at_position()new_with_keyframe_index() delegation chain is well-structured. The warning log when keyframe index building fails provides good observability while allowing graceful degradation.


235-254: LGTM: Keyframe-aware seeking logic is correct.

The seek time calculation properly uses the nearest prior keyframe when available, ensuring efficient decoder positioning. Fallback to the requested start_time when no keyframe index exists maintains backward compatibility.


269-301: LGTM: Reset with keyframe-based seeking.

The reset method correctly computes seek targets using the keyframe index and updates the current position. The logic mirrors the initialization path and properly handles cases where no keyframe index is available.

crates/rendering/src/decoder/multi_position.rs (4)

10-31: LGTM: DecoderPosition tracking is straightforward.

The DecoderPosition struct provides clear position tracking with access metrics and timestamp management via the touch() method.


66-83: LGTM: Safe initial position calculation.

The calculate_initial_positions method properly handles edge cases: uses strategic keyframe positions when available, falls back to evenly-spaced positions, and handles zero/negative duration with a single position at 0.0.


175-232: LGTM: ScrubDetector with proper edge case handling.

The scrub detection logic uses exponential smoothing for the request rate, prevents division by zero with .max(0.001), and implements a cooldown period. The thresholds and rate calculation appear reasonable.


85-122: The concern on line 88 is not applicable. The code performs floating-point multiplication (requested_time * self.config.fps as f32), not division. Rust panics when an integer is divided by zero, but floating-point multiplication by zero safely returns 0.0. No zero-check is needed.

Likely an incorrect or invalid review comment.

crates/rendering/src/decoder/avassetreader.rs (9)

24-32: LGTM: SendableImageBuf wrapper enables cross-thread image buffer sharing.

The unsafe Send and Sync implementations are necessary for Arc-based sharing of cidre ImageBuf references across threads. The retained() call in clone() properly manages reference counting.


34-40: LGTM: FrameData consolidates per-frame metadata.

The FrameData struct cleanly encapsulates frame buffer data with stride information and an optional zero-copy image buffer reference, supporting both copied and zero-copy paths.


51-92: LGTM: ProcessedFrame refactored for Arc-based sharing.

The refactored to_decoded_frame() method properly handles RGBA, NV12 (with zero-copy and Arc paths), and YUV420P formats. The use of Arc::clone avoids unnecessary copies while maintaining proper ownership.


300-334: LGTM: DecoderInstance wrapper provides per-decoder state.

The DecoderInstance struct cleanly wraps the inner decoder with state flags (is_done, frames_iter_valid) and provides a reset method that properly reinitializes the state.


415-429: LGTM: Decoder selection with safe bounds checking.

The select_best_decoder method properly bounds the decoder index with saturating_sub, resets the decoder when needed, and updates the pool manager's position tracking. The min operation at line 419 ensures the index is always valid.


483-529: LGTM: Batch processing with efficient cache lookups.

The request batching logic efficiently drains messages from the channel, sorts by frame number, detects scrubbing, and fulfills requests from cache before falling through to decoding. The early-exit when all requests are cached is efficient.


536-548: LGTM: Dynamic cache bounds with scrub awareness.

The cache bounds adjust based on scrubbing state—smaller lookahead during scrubbing reduces wasted decoding. The cache is properly pruned on reset to maintain the desired window around the requested frames.


556-644: LGTM: Complex frame processing loop with proper state management.

The main decoding loop correctly:

  • Iterates frames from the selected decoder
  • Updates decoder position tracking
  • Manages cache eviction with directional awareness (forward vs backward playback)
  • Fulfills pending requests as matching frames arrive
  • Implements scrubbing throttling (breaks after 3 frames)
  • Updates pool manager position after decoding

The logic for determining which cached frame to remove (lines 589-599) intelligently considers playback direction.


650-665: LGTM: Fallback chain for unfulfilled requests.

The final fallback logic properly tries: cached frame → last sent frame → first ever frame → log and drop. This ensures a reasonable experience even when exact frames aren't available.

crates/export/src/mp4.rs (2)

128-169: LGTM! Well-designed cursor-based audio generation.

The refactored audio frame generation correctly:

  • Calculates sample boundaries per frame using (n * sample_rate) / fps
  • Prevents duplicate audio rendering via the early-return check at line 159
  • Sets PTS from the cursor position for proper audio timeline alignment
  • Handles fractional samples per frame gracefully

This approach improves audio-video synchronization and aligns with the PR's optimization goals.


261-285: LGTM! Excellent test coverage for audio sample calculation.

The test validates that the cursor-based audio calculation doesn't lose or duplicate samples due to integer division rounding across frame boundaries. Testing multiple FPS values (24, 30, 60, 90, 120, 144) ensures the algorithm is robust across common frame rates.

frame.video,
Duration::from_secs_f32(encoded_frames as f32 / fps as f32),
)
.queue_video_frame(frame.video, Duration::MAX)
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

🏁 Script executed:

find . -name "mp4.rs" -path "*/export/*" | head -5

Repository: CapSoftware/Cap

Length of output: 85


🏁 Script executed:

cat -n crates/export/src/mp4.rs | sed -n '85,130p'

Repository: CapSoftware/Cap

Length of output: 2062


🏁 Script executed:

cat -n crates/export/src/mp4.rs | sed -n '125,175p'

Repository: CapSoftware/Cap

Length of output: 2620


🏁 Script executed:

cat -n crates/export/src/mp4.rs | sed -n '175,220p'

Repository: CapSoftware/Cap

Length of output: 2294


🏁 Script executed:

find . -name "*.rs" -path "*mp4*" | grep -E "(mp4|encoder)" | head -10

Repository: CapSoftware/Cap

Length of output: 157


🏁 Script executed:

cat -n crates/enc-avfoundation/src/mp4.rs | sed -n '1,50p'

Repository: CapSoftware/Cap

Length of output: 2201


🏁 Script executed:

grep -n "queue_video_frame" crates/enc-avfoundation/src/mp4.rs | head -5

Repository: CapSoftware/Cap

Length of output: 92


🏁 Script executed:

cat -n crates/enc-avfoundation/src/mp4.rs | sed -n '242,290p'

Repository: CapSoftware/Cap

Length of output: 1990


🏁 Script executed:

cat -n crates/enc-avfoundation/src/mp4.rs | sed -n '290,330p'

Repository: CapSoftware/Cap

Length of output: 1591


🏁 Script executed:

grep -n "Duration::MAX" crates/enc-avfoundation/src/mp4.rs

Repository: CapSoftware/Cap

Length of output: 41


🏁 Script executed:

grep -n "queue_video_frame\|Duration::MAX" crates/enc-ffmpeg/src/mux/mp4.rs

Repository: CapSoftware/Cap

Length of output: 91


🏁 Script executed:

cat -n crates/enc-ffmpeg/src/mux/mp4.rs | sed -n '87,140p'

Repository: CapSoftware/Cap

Length of output: 1956


🏁 Script executed:

grep -rn "Duration::MAX" crates/export/src/mp4.rs crates/enc-avfoundation/src/mp4.rs crates/enc-ffmpeg/src/mux/mp4.rs

Repository: CapSoftware/Cap

Length of output: 154


Verify Duration::MAX handling—this parameter is actively used by the encoder and should match frame timing.

The timestamp parameter passed to queue_video_frame is not ignored; it's actively used for session timing initialization, pause/resume offset calculations, and PTS adjustments in lines 258, 263-268, and 270-293 of the MP4Encoder. Passing Duration::MAX while the frame contains correct timing information (frame_number) creates a mismatch that will break synchronization. Either pass the correct frame timestamp or confirm the encoder has been refactored to handle this sentinel value specially.

🤖 Prompt for AI Agents
In crates/export/src/mp4.rs at line ~100, calling
.queue_video_frame(frame.video, Duration::MAX) uses a sentinel that the
MP4Encoder expects to be a real frame timestamp (it's used for session timing
init, pause/resume offsets and PTS adjustments around lines 258, 263–268,
270–293). Replace Duration::MAX with the correct frame timestamp derived from
the frame (e.g., frame.timestamp or compute from frame.frame_number and the
known frame rate) so encoder timing stays consistent, or if the encoder was
intentionally changed to accept a sentinel, update the encoder code and tests to
explicitly handle Duration::MAX throughout the referenced logic paths.

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

Caution

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

⚠️ Outside diff range comments (1)
crates/video-decode/src/avassetreader.rs (1)

211-218: Replace unwrap() calls with proper error propagation.

Multiple unwrap() calls here could cause panics if the file can't be opened or has no video stream. Since this function returns Result<Self, String>, these errors should be propagated.

🔎 Proposed fix
         let (pixel_format, width, height) = {
-            let input = ffmpeg::format::input(&path).unwrap();
+            let input = ffmpeg::format::input(&path)
+                .map_err(|e| format!("Failed to open video: {e}"))?;

             let input_stream = input
                 .streams()
                 .best(ffmpeg::media::Type::Video)
-                .ok_or("Could not find a video stream")
-                .unwrap();
+                .ok_or("Could not find a video stream")?;

             let decoder = avcodec::Context::from_parameters(input_stream.parameters())
♻️ Duplicate comments (7)
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (4)

245-249: Silent fallback to NV12 for unknown pixel formats risks corrupted output.

When video_config.pixel_format doesn't match known variants, the code silently defaults to NV12. This could cause frame data misinterpretation and video corruption.

🔎 Proposed fix
                 let pixel_format = match video_config.pixel_format {
                     cap_media_info::Pixel::NV12 => ffmpeg::format::Pixel::NV12,
                     cap_media_info::Pixel::BGRA => ffmpeg::format::Pixel::BGRA,
                     cap_media_info::Pixel::UYVY422 => ffmpeg::format::Pixel::UYVY422,
-                    _ => ffmpeg::format::Pixel::NV12,
+                    other => {
+                        error!("Unsupported pixel format {:?}", other);
+                        return Err(anyhow!("Unsupported pixel format: {:?}", other));
+                    }
                 };

287-291: Silently ignoring poisoned mutex may hide encoder panics.

The if let Ok(mut encoder) = encoder_clone.lock() pattern silently drops frames when the mutex is poisoned. If the encoder panics, this becomes a silent failure.

Based on learnings, sequential frame processing requires holding the lock, but error visibility should be preserved.

🔎 Proposed fix
-                            if let Ok(mut encoder) = encoder_clone.lock() {
-                                if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
-                                    warn!("Failed to encode frame: {e}");
-                                }
+                            match encoder_clone.lock() {
+                                Ok(mut encoder) => {
+                                    if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
+                                        warn!("Failed to encode frame: {e}");
+                                    }
+                                }
+                                Err(poisoned) => {
+                                    error!("Encoder mutex poisoned: {}", poisoned);
+                                    return Err(anyhow!("Encoder mutex poisoned"));
+                                }
                             }

718-722: Silent fallback to NV12 for unknown pixel formats risks corrupted output.

Same issue as the main muxer: when video_config.pixel_format doesn't match known variants, the code silently defaults to NV12, risking frame corruption.

🔎 Proposed fix
                 let pixel_format = match video_config.pixel_format {
                     cap_media_info::Pixel::NV12 => ffmpeg::format::Pixel::NV12,
                     cap_media_info::Pixel::BGRA => ffmpeg::format::Pixel::BGRA,
                     cap_media_info::Pixel::UYVY422 => ffmpeg::format::Pixel::UYVY422,
-                    _ => ffmpeg::format::Pixel::NV12,
+                    other => {
+                        error!("Unsupported camera pixel format {:?}", other);
+                        return Err(anyhow!("Unsupported camera pixel format: {:?}", other));
+                    }
                 };

762-766: Silently ignoring poisoned mutex may hide encoder panics.

Same issue as the main muxer: the if let Ok(mut encoder) pattern silently drops frames when the mutex is poisoned.

🔎 Proposed fix
-                            if let Ok(mut encoder) = encoder_clone.lock() {
-                                if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
-                                    warn!("Failed to encode camera frame: {e}");
-                                }
+                            match encoder_clone.lock() {
+                                Ok(mut encoder) => {
+                                    if let Err(e) = encoder.queue_frame(owned_frame, timestamp) {
+                                        warn!("Failed to encode camera frame: {e}");
+                                    }
+                                }
+                                Err(poisoned) => {
+                                    error!("Camera encoder mutex poisoned: {}", poisoned);
+                                    return Err(anyhow!("Camera encoder mutex poisoned"));
+                                }
                             }
crates/editor/examples/decode-benchmark.rs (1)

54-55: Unused fields lead to misleading output.

The cache_hits and cache_misses fields are never populated anywhere in the code, remaining at their default value of 0. The cache statistics section in print_report() (lines 156-169) is guarded by if total > 0 so it won't print, but having these unused fields in the struct is still misleading.

Either populate these fields with actual cache metrics from the decoder or remove them from the struct and report.

crates/rendering/src/decoder/avassetreader.rs (1)

360-361: keyframe_index_arc is always None - the extracted keyframe index is never used in config.

The keyframe_index is extracted from primary_decoder on line 360 but keyframe_index_arc is hardcoded to None on line 361. This means the MultiPositionDecoderConfig never receives the keyframe index, which defeats the purpose of keyframe-aware positioning for additional decoders.

🔎 Proposed fix
         let keyframe_index = primary_decoder.take_keyframe_index();
-        let keyframe_index_arc: Option<Arc<cap_video_decode::avassetreader::KeyframeIndex>> = None;
+        let keyframe_index_arc = keyframe_index.as_ref().map(|kf| Arc::new(kf.clone()));

         let fps = keyframe_index
+            keyframe_index_arc
             .as_ref()
             .map(|kf| kf.fps() as u32)
             .unwrap_or(30);
         let duration_secs = keyframe_index
+            keyframe_index_arc
             .as_ref()
             .map(|kf| kf.duration_secs())
             .unwrap_or(0.0);

Note: This requires KeyframeIndex to implement Clone. If it doesn't, consider restructuring to share the Arc or build the index separately.

crates/rendering/src/decoder/multi_position.rs (1)

146-150: Potential division by zero if fps is 0.

If self.config.fps is 0 (e.g., from corrupted metadata), the division frame as f32 / self.config.fps as f32 produces infinity, which could cause issues downstream.

🔎 Proposed fix
         let top_hotspots: Vec<f32> = hotspots
             .into_iter()
             .take(MAX_DECODER_POOL_SIZE)
-            .map(|(frame, _)| frame as f32 / self.config.fps as f32)
+            .map(|(frame, _)| {
+                if self.config.fps == 0 {
+                    0.0
+                } else {
+                    frame as f32 / self.config.fps as f32
+                }
+            })
             .collect();
🧹 Nitpick comments (9)
crates/rendering/src/yuv_converter.rs (1)

178-201: Consider using get_or_insert_with for cleaner bind group creation.

The current pattern works correctly, but get_or_insert_with would be more idiomatic and avoid the separate is_none() check and unwrap().

🔎 Suggested refactor
-        if self.nv12_bind_groups[output_index].is_none() {
-            self.nv12_bind_groups[output_index] =
-                Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
-                    label: Some("NV12 Converter Bind Group (Cached)"),
-                    layout,
-                    entries: &[
-                        wgpu::BindGroupEntry {
-                            binding: 0,
-                            resource: wgpu::BindingResource::TextureView(y_view),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 1,
-                            resource: wgpu::BindingResource::TextureView(uv_view),
-                        },
-                        wgpu::BindGroupEntry {
-                            binding: 2,
-                            resource: wgpu::BindingResource::TextureView(output_view),
-                        },
-                    ],
-                }));
-        }
-
-        self.nv12_bind_groups[output_index].as_ref().unwrap()
+        self.nv12_bind_groups[output_index].get_or_insert_with(|| {
+            device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: Some("NV12 Converter Bind Group (Cached)"),
+                layout,
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: wgpu::BindingResource::TextureView(y_view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: wgpu::BindingResource::TextureView(uv_view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 2,
+                        resource: wgpu::BindingResource::TextureView(output_view),
+                    },
+                ],
+            })
+        })

Same pattern applies to get_or_create_yuv420p at lines 222-249.

crates/recording/src/output_pipeline/macos_fragmented_m4s.rs (2)

179-203: Consider using a condvar or channel for encoder completion signaling.

The manual polling loop with sleep(Duration::from_millis(50)) works but is less efficient than signaling-based approaches. If the encoder thread finishes immediately, you still wait 50ms before checking. Consider having the encoder thread signal completion via a channel or using JoinHandle::join() with a timeout wrapper.


242-333: Significant code duplication between main and camera muxer encoder threads.

The encoder thread spawning logic in start_encoder is nearly identical between MacOSFragmentedM4SMuxer (lines 242-333) and MacOSFragmentedM4SCameraMuxer (lines 715-808), with only minor differences in logging messages. This duplication makes maintenance harder and increases the risk of inconsistent bug fixes.

Consider extracting the encoder thread logic into a shared function that accepts a name prefix and returns the handle:

fn spawn_encoder_thread(
    video_config: VideoInfo,
    video_rx: Receiver<Option<(cidre::arc::R<cidre::cm::SampleBuf>, Duration)>>,
    ready_tx: SyncSender<anyhow::Result<()>>,
    encoder_clone: Arc<Mutex<SegmentedVideoEncoder>>,
    thread_name: &str,
) -> anyhow::Result<JoinHandle<anyhow::Result<()>>> {
    // shared implementation
}

Then both muxers can call this shared function.

Also applies to: 715-808

crates/enc-ffmpeg/src/video/h264.rs (2)

133-138: Redundant "WARNING:" prefix in log message.

The warn! macro already indicates warning level; the embedded "WARNING:" prefix is redundant and creates noisy output like WARN ... WARNING: Using SOFTWARE....

Suggested fix
                     } else {
                         warn!(
                             encoder = %codec_name,
                             input_width = input_config.width,
                             input_height = input_config.height,
-                            "WARNING: Using SOFTWARE H264 encoder (high CPU usage expected)"
+                            "Using SOFTWARE H264 encoder (high CPU usage expected)"
                         );
                     }

231-239: Avoid unnecessary allocations in structured logs.

Using format!() inside the log macro creates intermediate String allocations. Use separate fields or the % formatting specifier directly.

Suggested fix
                 Ok(context) => {
                     debug!(
                         encoder = %codec.name(),
                         src_format = ?input_config.pixel_format,
-                        src_size = %format!("{}x{}", input_config.width, input_config.height),
+                        src_width = input_config.width,
+                        src_height = input_config.height,
                         dst_format = ?output_format,
-                        dst_size = %format!("{}x{}", output_width, output_height),
+                        dst_width = output_width,
+                        dst_height = output_height,
                         needs_scaling = needs_scaling,
                         "Created SOFTWARE scaler for pixel format conversion (CPU-intensive)"
                     );
                     Some(context)
                 }
crates/rendering/src/decoder/avassetreader.rs (2)

500-505: Single-variant match could be simplified.

The match statement has only one variant (VideoDecoderMessage::GetFrame). This could be simplified to direct destructuring or a let ... = ... pattern if GetFrame is the only message type.

🔎 Proposed simplification
-            match r {
-                VideoDecoderMessage::GetFrame(requested_time, sender) => {
-                    let frame = (requested_time * fps as f32).floor() as u32;
-                    if !sender.is_closed() {
-                        pending_requests.push(PendingRequest { frame, sender });
-                    }
-                }
-            }
+            let VideoDecoderMessage::GetFrame(requested_time, sender) = r;
+            let frame = (requested_time * fps as f32).floor() as u32;
+            if !sender.is_closed() {
+                pending_requests.push(PendingRequest { frame, sender });
+            }

This applies to both the initial match r and the while let Ok(msg) loop.


675-679: Empty if blocks for send errors can be simplified.

The pattern if req.sender.send(...).is_err() {} with empty blocks is unusual. Since the result is intentionally ignored, this is clearer:

🔎 Proposed simplification
-                } else if let Some(last) = last_sent_frame.borrow().clone() {
-                    if req.sender.send(last.to_decoded_frame()).is_err() {}
-                } else if let Some(first) = first_ever_frame.borrow().clone() {
-                    if req.sender.send(first.to_decoded_frame()).is_err() {}
+                } else if let Some(last) = last_sent_frame.borrow().clone() {
+                    let _ = req.sender.send(last.to_decoded_frame());
+                } else if let Some(first) = first_ever_frame.borrow().clone() {
+                    let _ = req.sender.send(first.to_decoded_frame());
crates/video-decode/src/avassetreader.rs (1)

51-54: Consider reusing the input context instead of reopening the file.

The file is opened twice: once for metadata extraction (line 23) and again for packet iteration (line 53-54). This adds I/O overhead. Consider reusing the first input context if the ffmpeg API allows iteration after metadata access.

🔎 Proposed optimization
-        let mut input =
-            avformat::input(path).map_err(|e| format!("Failed to reopen video for scan: {e}"))?;
+        let mut input = input;  // Reuse the existing input context

If this doesn't work due to ownership/borrowing constraints, consider documenting why the double-open is necessary.

crates/rendering/src/decoder/multi_position.rs (1)

207-207: Unused variable _was_scrubbing.

The variable _was_scrubbing is assigned but never used. While the underscore prefix suppresses the warning, it should either be removed or used for its intended purpose (e.g., logging state transitions).

🔎 Proposed fix
-        let _was_scrubbing = self.is_scrubbing;
-
         if self.request_rate > Self::SCRUB_THRESHOLD_RATE && frame_delta > 1 {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c44ad76 and c1c98f1.

📒 Files selected for processing (11)
  • .claude/settings.local.json
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/routes/editor/index.tsx
  • crates/editor/examples/decode-benchmark.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/multi_position.rs
  • crates/rendering/src/yuv_converter.rs
  • crates/video-decode/src/avassetreader.rs
🧰 Additional context used
📓 Path-based instructions (5)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/rendering/src/yuv_converter.rs
  • crates/editor/examples/decode-benchmark.rs
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/rendering/src/decoder/multi_position.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/rendering/src/decoder/avassetreader.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/rendering/src/yuv_converter.rs
  • crates/editor/examples/decode-benchmark.rs
  • apps/desktop/src/routes/editor/index.tsx
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/rendering/src/decoder/multi_position.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/rendering/src/decoder/avassetreader.rs
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/index.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • apps/desktop/src/routes/editor/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • apps/desktop/src/routes/editor/index.tsx
🧠 Learnings (5)
📓 Common learnings
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.
📚 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/editor/examples/decode-benchmark.rs
  • crates/enc-ffmpeg/src/video/h264.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/enc-ffmpeg/src/remux.rs
  • crates/rendering/src/decoder/avassetreader.rs
📚 Learning: 2025-10-17T05:58:22.586Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.

Applied to files:

  • crates/editor/examples/decode-benchmark.rs
  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use static skeletons for loading states that mirror content; avoid bouncing animations

Applied to files:

  • apps/desktop/src/routes/editor/index.tsx
📚 Learning: 2025-12-07T14:29:40.743Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T14:29:40.743Z
Learning: Applies to **/*.rs : Never write `let _ = async_fn()` which silently drops futures; await or explicitly handle them (Clippy: `let_underscore_future` = deny)

Applied to files:

  • crates/recording/src/output_pipeline/macos_fragmented_m4s.rs
🧬 Code graph analysis (6)
crates/rendering/src/yuv_converter.rs (1)
crates/rendering/src/layers/camera.rs (1)
  • new (22-56)
crates/editor/examples/decode-benchmark.rs (1)
crates/rendering/src/decoder/mod.rs (1)
  • spawn_decoder (612-774)
crates/enc-ffmpeg/src/video/h264.rs (3)
crates/recording/src/sources/screen_capture/macos.rs (1)
  • pixel_format (47-49)
crates/recording/src/sources/screen_capture/windows.rs (1)
  • pixel_format (39-41)
crates/frame-converter/src/lib.rs (1)
  • needs_scaling (118-120)
apps/desktop/src-tauri/src/lib.rs (2)
crates/project/src/meta.rs (5)
  • path (127-129)
  • path (312-314)
  • path (363-365)
  • load_for_project (131-137)
  • studio_meta (177-182)
apps/desktop/src-tauri/src/recording.rs (5)
  • app (1171-1171)
  • app (1387-1387)
  • app (1443-1443)
  • needs_fragment_remux (1995-2008)
  • remux_fragmented_recording (2010-2030)
crates/rendering/src/decoder/multi_position.rs (1)
crates/video-decode/src/avassetreader.rs (2)
  • path (305-307)
  • new (181-183)
crates/video-decode/src/avassetreader.rs (1)
crates/video-decode/src/ffmpeg.rs (2)
  • start_time (281-283)
  • reset (259-266)
🪛 GitHub Check: Typecheck
apps/desktop/src/routes/editor/index.tsx

[failure] 8-8:
Cannot find module './EditorSkeleton' or its corresponding type declarations.

⏰ 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 (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (27)
crates/rendering/src/yuv_converter.rs (4)

136-158: Cache design is appropriate for the double-buffering pattern.

The BindGroupCache correctly tracks bind groups per output slot and invalidates when dimensions change, which aligns well with the double-buffered output texture approach used in this converter.


625-629: Cache invalidation is correctly placed after texture reallocation.

The call to self.bind_group_cache.invalidate() after reassigning all texture views ensures that cached bind groups referencing the old views will be recreated on the next conversion call.


696-718: Bind group caching implementation is correct.

The cache correctly uses allocated dimensions as the key, and the flow ensures views are always valid: ensure_texture_size() → potential invalidation → get_or_create_nv12() with current views.


855-878: YUV420P caching follows the same correct pattern.

The implementation correctly mirrors the NV12 caching approach with the additional U and V plane bindings.

crates/enc-ffmpeg/src/video/h264.rs (1)

389-417: LGTM!

The queue_frame_reusable method correctly implements frame reuse optimization by lazily initializing the converted frame buffer via get_or_insert_with and reusing it across calls. This reduces allocations in segment-based encoding workflows.

crates/enc-ffmpeg/src/remux.rs (5)

106-175: LGTM!

The remux_streams function correctly handles stream mapping, DTS monotonicity adjustments for concatenated segments, and proper header/trailer writing. The unsafe blocks are necessary for FFmpeg's low-level API.


177-188: LGTM!

Clean refactor that delegates to the shared remux_streams helper, reducing code duplication.


409-453: LGTM with same temp file note.

The implementation correctly validates inputs, concatenates init with segments, remuxes to a regular MP4, and cleans up. The same temp file collision consideration from probe_m4s_can_decode_with_init applies here for the combined file path.


455-459: LGTM!

Simple and focused helper that delegates to remux_streams.


375-407: No action needed. The temp file naming is safe and unique across all segments in the directory, as each temp file is derived from its corresponding segment's unique path. No concurrent calls occur due to sequential iteration over entries.

.claude/settings.local.json (1)

54-55: LGTM!

The new permissions for running the decode-benchmark example binary align with the PR's testing and benchmark infrastructure improvements. The format is consistent with existing entries.

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

93-93: LGTM!

The watch import addition supports the new finalization coordination mechanism.


2631-2631: LGTM!

Proper registration of FinalizingRecordings state during app initialization.


2177-2177: LGTM!

The integration of wait_for_recording_ready ensures recordings are fully finalized before editor creation, preventing potential corruption or UI blocking issues. This aligns with the PR's goal of asynchronous finalization.


2207-2238: LGTM!

The wait_for_recording_ready implementation correctly handles the coordination flow:

  • Waits for ongoing finalization if detected
  • Falls back to crash-recovery remux if needed
  • Uses spawn_blocking appropriately for potentially blocking I/O operations

The double ?? operator at line 2232 correctly handles both task panics (JoinError) and remux errors, with the "task panicked" message only applying to actual panics.


109-138: The implementation correctly handles concurrent access to finalizing recordings. When start_finalizing is called for the same path multiple times, the watch channel's inherent behavior handles this appropriately: old receiver clones detect the channel closure when the previous sender is dropped, while new callers receive a fresh receiver from the new sender. This is safe and expected behavior for watch channels; no additional guards are needed.

crates/rendering/src/decoder/avassetreader.rs (4)

34-92: LGTM!

The FrameData struct consolidation and ProcessedFrame::to_decoded_frame implementation cleanly handle multiple pixel formats with appropriate Arc-based sharing for zero-copy paths.


228-293: LGTM!

The CachedFrame::new implementation correctly handles different pixel formats with zero-copy optimization for NV12 and proper data extraction for other formats.


300-346: LGTM!

The DecoderInstance wrapper properly manages per-decoder state with robust error handling in reset() that logs failures and marks the decoder as invalid rather than panicking.


24-32: Add a safety comment documenting why SendableImageBuf is thread-safe.

The unsafe impl Send and unsafe impl Sync for SendableImageBuf wrapping R<cv::ImageBuf> requires justification. Apple's Core Video ImageBuf uses atomic reference counting and is thread-safe, making this safe. Add a comment explaining this to future reviewers: // Safety: cv::ImageBuf from Apple Core Video uses atomic ref counting and is thread-safe across threads; retained() maintains correct reference counts.

⛔ Skipped due to learnings
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.
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.
crates/video-decode/src/avassetreader.rs (3)

87-130: LGTM!

The nearest_keyframe_before and nearest_keyframe_after methods correctly use binary search with proper edge case handling for exact matches and boundary conditions.


267-295: LGTM!

The reset() method correctly uses keyframe-aware seeking to find the nearest keyframe before the target time, ensuring proper decoder positioning for efficient playback.


321-379: LGTM!

The get_reader_track_output method correctly sets up the AVAssetReader with the specified time range and configures the track output for efficient zero-copy sample data access.

crates/rendering/src/decoder/multi_position.rs (4)

10-31: LGTM!

The DecoderPosition struct cleanly tracks per-decoder state with touch() providing efficient access tracking.


48-83: LGTM!

The initialization logic correctly prioritizes keyframe-based strategic positions when available, with a sensible fallback to evenly distributed positions across the video duration.


85-122: LGTM!

The two-pass decoder selection strategy correctly prioritizes decoders that can decode forward to the target time (avoiding reset) before falling back to the closest decoder.


225-238: LGTM!

The accessor methods and Default implementation are clean and idiomatic.

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 (11)
apps/desktop/src-tauri/src/export.rs (2)

189-192: Simplify the helper function.

The temporary variable assignment is unnecessary. Per coding guidelines, prefer direct returns.

🔎 Suggested simplification
 fn bpp_to_jpeg_quality(bpp: f32) -> u8 {
-    let quality = ((bpp - 0.04) / (0.3 - 0.04) * (95.0 - 40.0) + 40.0).clamp(40.0, 95.0) as u8;
-    quality
+    ((bpp - 0.04) / (0.3 - 0.04) * (95.0 - 40.0) + 40.0).clamp(40.0, 95.0) as u8
 }

337-433: Consider extracting shared logic to reduce duplication.

Both generate_export_preview and generate_export_preview_fast share significant identical code:

  1. RGBA→RGB conversion (lines 293-301 vs 396-404)
  2. JPEG encoding (lines 303-310 vs 406-413)
  3. Bitrate/size estimation (lines 314-327 vs 417-425)

While the two functions have different data sources, extracting helpers for these common operations would improve maintainability.

🔎 Example helper extraction
fn encode_frame_to_jpeg(frame: &RenderedFrame, quality: u8) -> Result<Vec<u8>, String> {
    let rgb_data: Vec<u8> = frame
        .data
        .chunks(frame.padded_bytes_per_row as usize)
        .flat_map(|row| {
            row[0..(frame.width * 4) as usize]
                .chunks(4)
                .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
        })
        .collect();

    let mut jpeg_buffer = Vec::new();
    let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, quality);
    encoder
        .encode(&rgb_data, frame.width, frame.height, image::ExtendedColorType::Rgb8)
        .map_err(|e| format!("Failed to encode JPEG: {e}"))?;
    Ok(jpeg_buffer)
}

fn estimate_size_mb(resolution: XY<u32>, fps: u32, bpp: f32, duration: f64) -> f64 {
    let total_pixels = (resolution.x * resolution.y) as f64;
    let video_bitrate = total_pixels * bpp as f64 * fps as f64;
    let total_bitrate = video_bitrate + 192_000.0;
    (total_bitrate * duration) / (8.0 * 1024.0 * 1024.0)
}
apps/desktop/src/routes/editor/EditorSkeleton.tsx (1)

4-6: Verify skeleton constants match Editor.tsx values.

The constants DEFAULT_TIMELINE_HEIGHT, MIN_PLAYER_HEIGHT, and RESIZE_HANDLE_HEIGHT should match those in Editor.tsx for consistent visual appearance during loading. The values appear correct based on Editor.tsx (lines 49-53), but this duplication could lead to drift.

Consider extracting these to a shared constants file to ensure consistency.

apps/desktop/src/routes/editor/Editor.tsx (1)

431-444: Canvas frame capture for crop preview.

The approach of capturing the current frame from the player canvas for the crop preview is clean. The silent catch block with fallback to null (which then falls back to the screenshot file on line 610-613) provides graceful degradation.

Consider using a constant for the canvas ID "canvas" to avoid magic strings and ensure consistency with wherever the canvas element is defined.

apps/desktop/src/routes/editor/ExportPage.tsx (7)

61-66: Floating-point keys may cause lookup failures.

Using floating-point numbers as object keys is error-prone. When accessed via BPP_TO_COMPRESSION[v] where v is a computed float from the slider (e.g., 0.15000000000000002), the lookup will fail due to precision issues. This is currently mitigated by the closest-match logic on lines 823-830, so the code works, but the BPP_TO_COMPRESSION object is never actually used for direct lookups.

Consider removing this unused mapping or using string keys if direct lookup is needed in the future.


175-177: Remove comment per coding guidelines.

As per coding guidelines, comments should not be added to code. The code should be self-explanatory through naming and structure.

🔎 Proposed fix
-		// Ensure GIF is not selected when exportTo is "link"
-		else if (_settings.format === "Gif" && _settings.exportTo === "link")
+		else if (_settings.format === "Gif" && _settings.exportTo === "link")

180-187: Unconventional use of Object.defineProperty in reactive context.

Using Object.defineProperty to define a getter inside mergeProps is unusual and may cause confusion. A cleaner approach would be to compute organizationId directly in the returned object.

🔎 Proposed fix
-		Object.defineProperty(ret, "organizationId", {
-			get() {
-				if (!_settings.organizationId && organisations().length > 0)
-					return organisations()[0].id;
-
-				return _settings.organizationId;
-			},
-		});
-
-		return ret;
+		ret.organizationId =
+			!_settings.organizationId && organisations().length > 0
+				? organisations()[0].id
+				: _settings.organizationId;
+		return ret;

466-468: Remove comments and clarify createSignInMutation usage.

Line 466 has a comment that should be removed per coding guidelines. Additionally, createSignInMutation() is called without using its return value. If this is intentional (triggering the sign-in flow as a side effect), consider renaming or restructuring for clarity.

🔎 Proposed fix
-			// Check authentication first
 			const existingAuth = await authStore.get();
 			if (!existingAuth) createSignInMutation();

485-487: Remove comment per coding guidelines.

The comment on line 485 should be removed. The delay pattern before throwing is also unusual—consider if the error should be thrown immediately with the window show happening asynchronously.

🔎 Proposed fix
 			if (!canShare.allowed) {
 				if (canShare.reason === "upgrade_required") {
 					await commands.showWindow("Upgrade");
-					// The window takes a little to show and this prevents the user seeing it glitch
 					await new Promise((resolve) => setTimeout(resolve, 1000));
 					throw new SilentError();
 				}

491-513: Remove debug console.log and comments.

Lines 492 and 511 contain console.log statements that appear to be debug artifacts. Lines 511 and 513 also contain comments that should be removed per coding guidelines.

🔎 Proposed fix
 			const uploadChannel = new Channel<UploadProgress>((progress) => {
-				console.log("Upload progress:", progress);
 				setExportState(
 					produce((state) => {
 						if (state.type !== "uploading") return;

 						state.progress = Math.round(progress.progress * 100);
 					}),
 				);
 			});

 			await exportWithSettings((progress) => {
 				if (isCancelled()) throw new SilentError("Cancelled");
 				setExportState({ type: "rendering", progress });
 			});

 			if (isCancelled()) throw new SilentError("Cancelled");

 			setExportState({ type: "uploading", progress: 0 });

-			console.log({ organizationId: settings.organizationId });
-
-			// Now proceed with upload
 			const result = meta().sharing

795-796: Stub history object appears intentional but could be clearer.

The history prop { pause: () => () => {} } is a no-op stub. If the Slider component requires this prop but history tracking isn't needed here, this is acceptable. Consider adding proper types or using a shared stub constant if this pattern is repeated.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1c98f1 and b15ae5d.

📒 Files selected for processing (8)
  • apps/desktop/src-tauri/src/export.rs
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/routes/editor/Editor.tsx
  • apps/desktop/src/routes/editor/EditorSkeleton.tsx
  • apps/desktop/src/routes/editor/ExportDialog.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/utils/tauri.ts
  • packages/ui-solid/src/auto-imports.d.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/routes/editor/ExportDialog.tsx
🧰 Additional context used
📓 Path-based instructions (7)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use TanStack Query v5 for all client-side server state and data fetching in TypeScript files

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/utils/tauri.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/editor/EditorSkeleton.tsx
  • apps/desktop/src/utils/tauri.ts
  • apps/desktop/src/routes/editor/Editor.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/editor/EditorSkeleton.tsx
  • apps/desktop/src/utils/tauri.ts
  • apps/desktop/src/routes/editor/Editor.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/editor/EditorSkeleton.tsx
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/utils/tauri.ts
  • apps/desktop/src-tauri/src/export.rs
  • apps/desktop/src/routes/editor/Editor.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/EditorSkeleton.tsx
  • apps/desktop/src/routes/editor/Editor.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src-tauri/src/export.rs
apps/desktop/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/**/*.ts: Use @tanstack/solid-query for server state management in SolidJS components
Use generated commands and events from tauri_specta for IPC; never manually construct IPC calls
Listen directly to generated events from tauri_specta and use typed event interfaces

Files:

  • apps/desktop/src/utils/tauri.ts
🧠 Learnings (7)
📓 Common learnings
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.
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use strict TypeScript; avoid any type; leverage shared types from cap/* packages

Applied to files:

  • packages/ui-solid/src/auto-imports.d.ts
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use static skeletons for loading states that mirror content; avoid bouncing animations

Applied to files:

  • apps/desktop/src/routes/editor/EditorSkeleton.tsx
📚 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/lib.rs
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to apps/desktop/**/*.ts : Use generated commands and events from tauri_specta for IPC; never manually construct IPC calls

Applied to files:

  • apps/desktop/src/utils/tauri.ts
📚 Learning: 2025-12-07T14:29:40.743Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T14:29:40.743Z
Learning: Applies to **/tauri.ts|**/queries.ts|apps/desktop/src-tauri/gen/** : Never edit auto-generated files: `**/tauri.ts`, `**/queries.ts`, `apps/desktop/src-tauri/gen/**`

Applied to files:

  • apps/desktop/src/utils/tauri.ts
📚 Learning: 2025-11-19T18:25:41.457Z
Learnt from: ItsEeleeya
Repo: CapSoftware/Cap PR: 1396
File: apps/desktop/src/routes/(window-chrome)/settings/general.tsx:183-192
Timestamp: 2025-11-19T18:25:41.457Z
Learning: In SolidJS, when using the Show component with the `keyed` attribute and a dynamic reactive value (like a memo result), the component will recreate its children when the condition value changes by reference. This pattern is useful for triggering CSS animations (like fade-in) when the rendered content needs to update. For example, in apps/desktop/src/routes/(window-chrome)/settings/general.tsx, `<Show when={previews()[theme.id]} keyed>` ensures the image re-animates when switching between different preview sources.

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧬 Code graph analysis (2)
apps/desktop/src/routes/editor/Editor.tsx (2)
apps/desktop/src/routes/editor/ExportPage.tsx (1)
  • ExportPage (125-1357)
apps/desktop/src/routes/editor/Header.tsx (1)
  • Header (44-186)
apps/desktop/src/routes/editor/ExportPage.tsx (4)
apps/desktop/src/utils/tauri.ts (4)
  • ExportCompression (415-415)
  • ExportSettings (419-419)
  • commands (7-310)
  • FramesRendered (422-422)
apps/desktop/src/utils/export.ts (1)
  • createExportTask (4-27)
apps/desktop/src/routes/editor/Header.tsx (1)
  • RESOLUTION_OPTIONS (32-36)
apps/desktop/src/routes/editor/context.ts (1)
  • RenderState (90-92)
⏰ 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 (16)
packages/ui-solid/src/auto-imports.d.ts (1)

9-9: LGTM! Auto-generated icon type declarations.

The three new global icon type declarations are correctly generated and follow the established pattern in the file. These additions appear to be the result of regenerating the auto-imports configuration after new icons were added to the project.

Also applies to: 71-71, 90-90

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

109-138: Well-structured thread-safe finalization tracker.

The FinalizingRecordings implementation correctly uses std::sync::Mutex for short-lived synchronous access and tokio::sync::watch channels for async notification. The API is clean and follows the expected patterns.

One minor observation: finish_finalizing silently handles the case where the path doesn't exist in the map (the if let Some(...) pattern). This is likely intentional for idempotency, but consider whether a warning log would be helpful for debugging unexpected states.


3209-3240: Solid async wait-for-ready implementation with proper error handling.

The function correctly:

  1. Checks for active finalization and waits using watch::Receiver::wait_for
  2. Falls back to checking if crash recovery remux is needed
  3. Uses spawn_blocking appropriately for the CPU-bound remux operation
  4. Properly propagates both task panics and remux errors with the double ? operator

3172-3207: Correct integration of wait-for-ready in editor creation flow.

The wait_for_recording_ready call is properly placed before EditorInstance::new, ensuring the recording is finalized and ready before the editor attempts to load it. This prevents race conditions between background finalization and editor initialization.

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

293-310: RGBA to RGB conversion and JPEG encoding looks correct.

The conversion from RGBA (4 bytes per pixel) to RGB (3 bytes per pixel) properly handles the padded row stride and extracts only the RGB channels. The JPEG encoding uses the quality derived from the compression settings.


417-425: Duration source differs between preview functions.

generate_export_preview_fast uses editor.recordings.duration() directly (line 420), while generate_export_preview fetches metadata and checks for timeline configuration. This is appropriate since the fast path leverages in-memory data, but note that the size estimates may differ slightly if timeline segments modify the effective duration.

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

1-3: Auto-generated file - no manual changes needed.

This file is generated by tauri-specta as indicated in the header comment. The new generateExportPreview, generateExportPreviewFast commands and associated types (ExportPreviewResult, ExportPreviewSettings) are correctly generated from the Rust backend definitions.

Based on learnings, this file should not be manually edited.

apps/desktop/src/routes/editor/EditorSkeleton.tsx (2)

205-232: Well-structured skeleton matching editor layout.

The EditorSkeleton component properly mirrors the editor's layout structure with HeaderSkeleton, PlayerSkeleton, SidebarSkeleton, and TimelineSkeleton. The use of static skeletons follows the coding guidelines for loading states.


72-84: IconCapLogo requires an explicit import.

The VideoPreviewSkeleton component uses IconCapLogo at line 78 but does not import it. While other files in the codebase also use this icon without explicit imports, this suggests a missing import statement. Add the import: import IconCapLogo from "~icons/cap/logo";

apps/desktop/src/routes/editor/Editor.tsx (3)

202-210: Clean export mode gating implementation.

The isExportMode() function correctly checks the dialog state to determine if the export flow should be rendered. Using a Show component with the ExportPage as fallback is an elegant way to swap between editor and export views without mounting both simultaneously.


606-615: Proper fallback chain for crop preview image.

The image source correctly prioritizes the dynamically captured frameDataUrl() and falls back to the static screenshot file using convertFileSrc. This ensures the crop dialog always has a valid image to display.


285-289: Dialog filter correctly excludes export type.

The condition d.type !== "export" ensures the export flow is handled by the ExportPage component (via the Show fallback) rather than through the Dialog component. This aligns with the architectural change from modal export to full-page export.

apps/desktop/src/routes/editor/ExportPage.tsx (4)

1-48: LGTM!

Imports are well-organized and the SilentError pattern for handling user cancellations is appropriate.


361-396: LGTM!

The copy mutation properly handles cancellation, distinguishes between user-initiated cancellations and actual errors, and provides appropriate user feedback.


398-458: LGTM!

The save mutation follows consistent patterns with proper dialog handling and file operations.


1359-1392: LGTM!

The helper components RenderProgress and ProgressView are clean, focused, and correctly implement the progress visualization.

Comment on lines 1271 to 1297
<a
href={meta().sharing?.link}
target="_blank"
rel="noreferrer"
class="block"
>
<Button
onClick={() => {
setCopyPressed(true);
setTimeout(() => {
setCopyPressed(false);
}, 2000);
navigator.clipboard.writeText(
meta().sharing?.link!,
);
}}
variant="dark"
class="flex gap-2 justify-center items-center"
>
{!copyPressed() ? (
<IconCapCopy class="transition-colors duration-200 text-gray-1 size-4 group-hover:text-gray-12" />
) : (
<IconLucideCheck class="transition-colors duration-200 text-gray-1 size-4 svgpathanimation group-hover:text-gray-12" />
)}
<p>Open Link</p>
</Button>
</a>
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

Nested interactive elements cause conflicting behavior.

The <Button> is nested inside an <a> tag. When clicked, both the button's onClick (copying to clipboard) and the anchor's navigation (opening link) will trigger simultaneously. This may cause unexpected behavior where the link opens while the clipboard is also modified.

Consider separating these into distinct buttons or preventing event propagation.

🔎 Proposed fix - separate the actions
-										<div class="relative">
-											<a
-												href={meta().sharing?.link}
-												target="_blank"
-												rel="noreferrer"
-												class="block"
-											>
-												<Button
-													onClick={() => {
-														setCopyPressed(true);
-														setTimeout(() => {
-															setCopyPressed(false);
-														}, 2000);
-														navigator.clipboard.writeText(
-															meta().sharing?.link!,
-														);
-													}}
-													variant="dark"
-													class="flex gap-2 justify-center items-center"
-												>
-													{!copyPressed() ? (
-														<IconCapCopy class="transition-colors duration-200 text-gray-1 size-4 group-hover:text-gray-12" />
-													) : (
-														<IconLucideCheck class="transition-colors duration-200 text-gray-1 size-4 svgpathanimation group-hover:text-gray-12" />
-													)}
-													<p>Open Link</p>
-												</Button>
-											</a>
-										</div>
+										<div class="flex gap-2">
+											<Button
+												onClick={() => {
+													setCopyPressed(true);
+													setTimeout(() => {
+														setCopyPressed(false);
+													}, 2000);
+													navigator.clipboard.writeText(
+														meta().sharing?.link!,
+													);
+												}}
+												variant="dark"
+												class="flex gap-2 justify-center items-center"
+											>
+												{!copyPressed() ? (
+													<IconCapCopy class="transition-colors duration-200 text-gray-1 size-4 group-hover:text-gray-12" />
+												) : (
+													<IconLucideCheck class="transition-colors duration-200 text-gray-1 size-4 svgpathanimation group-hover:text-gray-12" />
+												)}
+												<p>Copy Link</p>
+											</Button>
+											<Button
+												as="a"
+												href={meta().sharing?.link}
+												target="_blank"
+												rel="noreferrer"
+												variant="dark"
+												class="flex gap-2 justify-center items-center"
+											>
+												<IconLucideExternalLink class="size-4" />
+												<p>Open Link</p>
+											</Button>
+										</div>

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

🤖 Prompt for AI Agents
In apps/desktop/src/routes/editor/ExportPage.tsx around lines 1271-1297, the
Button is nested inside an anchor causing both the button's onClick and the
anchor navigation to fire; split the actions into two separate interactive
elements: render one anchor-wrapped Button (or plain anchor styled like a
button) that only opens the link (no onClick), and render a second standalone
Button for copying that handles setCopyPressed and
navigator.clipboard.writeText. For the copy Button, ensure the click handler
calls event.stopPropagation() and event.preventDefault() if necessary, and add
appropriate aria-labels/roles so each control is distinct and accessible.

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/routes/editor/ExportPage.tsx (1)

1250-1278: Nested interactive elements cause conflicting behavior.

The <Button> is nested inside an <a> tag, causing both the button's onClick (copying to clipboard) and the anchor's navigation to fire simultaneously.

The previous review comment on these lines provides a detailed solution to separate these actions into distinct buttons.

🧹 Nitpick comments (3)
apps/desktop/src/routes/editor/ExportPage.tsx (3)

176-194: Consider using a memo instead of Object.defineProperty for reactive organizationId.

Using Object.defineProperty with a getter inside mergeProps mixes reactive and non-reactive patterns. While it may work, a createMemo is more idiomatic in SolidJS and ensures proper dependency tracking.

🔎 Proposed refactor
+	const effectiveOrganizationId = () => {
+		if (!_settings.organizationId && organisations().length > 0)
+			return organisations()[0].id;
+		return _settings.organizationId;
+	};
+
 	const settings = mergeProps(_settings, () => {
 		const ret: Partial<Settings> = {};
 		if (hasTransparentBackground() && _settings.format === "Mp4")
 			ret.format = "Gif";
 		else if (_settings.format === "Gif" && _settings.exportTo === "link")
 			ret.format = "Mp4";
 		else if (!["Mp4", "Gif"].includes(_settings.format)) ret.format = "Mp4";
-
-		Object.defineProperty(ret, "organizationId", {
-			get() {
-				if (!_settings.organizationId && organisations().length > 0)
-					return organisations()[0].id;
-
-				return _settings.organizationId;
-			},
-		});
-
 		return ret;
 	});

Then access it via the function:

-	settings.organizationId
+	effectiveOrganizationId()

370-559: Consider extracting common error handling logic.

The three mutations (copy, save, upload) share nearly identical error handling patterns with isCancelled() checks, SilentError handling, and state resets. Extracting this logic would improve maintainability and reduce duplication.

🔎 Example refactor
const handleMutationError = (error: unknown) => {
  if (isCancelled() || isCancellationError(error)) {
    setExportState(reconcile({ type: "idle" }));
    return true;
  }
  return false;
};

const copy = createMutation(() => ({
  mutationFn: async () => {
    // ... existing logic
  },
  onError: (error) => {
    if (handleMutationError(error)) return;
    commands.globalMessageDialog(
      error instanceof Error ? error.message : "Failed to copy recording",
    );
    setExportState(reconcile({ type: "idle" }));
  },
  onSuccess() {
    // ... existing logic
  },
}));

938-938: Remove the explicit no-op history prop and rely on context history, unless there's a specific reason to disable history pause for compression changes.

The history prop on Slider is optional and defaults to context?.projectHistory when omitted. Passing history={{ pause: () => () => {} }} explicitly disables history pausing during slider interaction. If this is intentional, it should be clarified; otherwise, remove the prop to use the standard history behavior.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b15ae5d and e4517f1.

📒 Files selected for processing (3)
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/tailwind.config.js
  • packages/ui-solid/src/auto-imports.d.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.config.{js,ts,mjs,cjs}

📄 CodeRabbit inference engine (CLAUDE.md)

Configuration files must follow their respective tool conventions (Next.js, Tailwind, etc.)

Files:

  • apps/desktop/tailwind.config.js
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • apps/desktop/tailwind.config.js
  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • apps/desktop/tailwind.config.js
  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use TanStack Query v5 for all client-side server state and data fetching in TypeScript files

Files:

  • packages/ui-solid/src/auto-imports.d.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧠 Learnings (3)
📓 Common learnings
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.
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use strict TypeScript; avoid any type; leverage shared types from cap/* packages

Applied to files:

  • packages/ui-solid/src/auto-imports.d.ts
📚 Learning: 2025-11-19T18:25:41.457Z
Learnt from: ItsEeleeya
Repo: CapSoftware/Cap PR: 1396
File: apps/desktop/src/routes/(window-chrome)/settings/general.tsx:183-192
Timestamp: 2025-11-19T18:25:41.457Z
Learning: In SolidJS, when using the Show component with the `keyed` attribute and a dynamic reactive value (like a memo result), the component will recreate its children when the condition value changes by reference. This pattern is useful for triggering CSS animations (like fade-in) when the rendered content needs to update. For example, in apps/desktop/src/routes/(window-chrome)/settings/general.tsx, `<Show when={previews()[theme.id]} keyed>` ensures the image re-animates when switching between different preview sources.

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/editor/ExportPage.tsx (6)
apps/desktop/src/utils/tauri.ts (4)
  • ExportCompression (415-415)
  • ExportSettings (419-419)
  • commands (7-310)
  • FramesRendered (422-422)
apps/desktop/src/store.ts (1)
  • authStore (59-59)
apps/desktop/src/utils/queries.ts (1)
  • createOrganizationsQuery (275-289)
apps/desktop/src/utils/export.ts (1)
  • createExportTask (4-27)
apps/desktop/src/routes/editor/Header.tsx (1)
  • RESOLUTION_OPTIONS (32-36)
apps/desktop/src/routes/editor/context.ts (1)
  • RenderState (90-92)
🪛 GitHub Check: Typecheck
apps/desktop/src/routes/editor/ExportPage.tsx

[failure] 201-201:
A spread argument must either have a tuple type or be passed to a rest parameter.


[failure] 199-199:
Rest parameter 'args' implicitly has an 'any[]' type.

⏰ 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 (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (8)
apps/desktop/tailwind.config.js (1)

16-19: LGTM! Shimmer animation follows Tailwind conventions.

The new shimmer animation is properly configured with appropriate keyframes and timing. The implementation follows the same pattern as the existing gentleBounce animation and will work well for skeleton loaders or loading states.

Also applies to: 24-24

packages/ui-solid/src/auto-imports.d.ts (1)

9-9: Remove unused icon declarations IconCapArrowLeft and IconCapFilm from auto-imports.d.ts or verify they are needed.

Two of the eight new icon declarations (lines 9 and 30) are not used anywhere in the codebase: IconCapArrowLeft and IconCapFilm. The other six icons are actively used. Since this file is auto-generated by unplugin-auto-import, these unused declarations should not be present—check if the auto-import configuration accidentally included them.

Also applies to: 30-30

⛔ Skipped due to learnings
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T14:29:40.743Z
Learning: Applies to **/tauri.ts|**/queries.ts|apps/desktop/src-tauri/gen/** : Never edit auto-generated files: `**/tauri.ts`, `**/queries.ts`, `apps/desktop/src-tauri/gen/**`
apps/desktop/src/routes/editor/ExportPage.tsx (6)

568-576: formatDuration implementation looks good.

The duration formatting logic is straightforward and correct. While you could use a shared utility or library, this lightweight implementation is appropriate for this use case.


608-1334: UI structure is well-organized with proper use of SolidJS control flow.

The nested Show/Switch/Match components correctly handle the complex state machine for export flows. The use of keyed on line 1023 ensures proper re-rendering when export state changes.

The component is large but well-structured. If this grows further, consider extracting sub-components for each export state (IdleState, RenderingState, CompletionState).


315-335: Cancel handling with cleanup is well-implemented.

The confirmation dialog, cancellation, and file cleanup logic work correctly. The try-catch around remove() ensures the UI doesn't break if cleanup fails.

Note: If remove() fails, the file remains on disk with only a console log. This is acceptable for error cases, though you could optionally show a toast notification to inform the user.


218-248: Preview generation with debouncing is well-implemented.

The 300ms debounce prevents excessive preview generation calls, and the proper cleanup of old blob URLs prevents memory leaks. The error handling and loading state management are appropriate.


52-75: Compression option constants are well-defined.

The compression options and their bidirectional mappings are clear and consistent. While the mappings could be derived from the array, the explicit definitions improve code clarity and avoid runtime overhead.


337-368: Export estimates query is properly implemented.

The use of keepPreviousData and comprehensive query keys ensures smooth UI updates when settings change. The query key includes all relevant parameters for accurate cache invalidation.

Comment on lines 199 to 202
const updateSettings: typeof setSettings = (...args) => {
setPreviewLoading(true);
return setSettings(...args);
};
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

Fix TypeScript errors in the updateSettings wrapper.

The spread operator and type inference fail here because setSettings has complex overloaded signatures from solid-js/store. The wrapper loses type safety and creates implicit any types.

🔎 Proposed fix

Remove the wrapper and call setPreviewLoading(true) directly before each setSettings call, or use a properly typed wrapper:

-	const updateSettings: typeof setSettings = (...args) => {
-		setPreviewLoading(true);
-		return setSettings(...args);
-	};

Then update each call site to set loading first:

 							onClick={() => {
+								setPreviewLoading(true);
-								updateSettings(
+								setSettings(
 									produce((newSettings) => {

Alternatively, create type-safe wrappers for specific use cases:

const updateFormat = (format: ExportFormat) => {
  setPreviewLoading(true);
  setSettings('format', format);
};

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

🧰 Tools
🪛 GitHub Check: Typecheck

[failure] 201-201:
A spread argument must either have a tuple type or be passed to a rest parameter.


[failure] 199-199:
Rest parameter 'args' implicitly has an 'any[]' type.

🤖 Prompt for AI Agents
In apps/desktop/src/routes/editor/ExportPage.tsx around lines 199-202, the
updateSettings wrapper loses type safety because setSettings from solid-js/store
has overloaded signatures and using a rest/spread causes implicit any; remove
the generic wrapper and instead either (a) call setPreviewLoading(true)
immediately before each direct setSettings call at each call site, or (b)
implement small, strongly-typed helper functions for the specific setting
updates you need (e.g., updateFormat, updateSomeNestedKey) that call
setPreviewLoading(true) then call setSettings with the precise typed parameters
— update all call sites accordingly.

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 (2)
apps/desktop/src/routes/editor/ExportPage.tsx (2)

213-220: Type assertion workaround may hide type safety issues.

The double cast through Parameters<typeof setSettings> and back to typeof setSettings works around SolidJS store's complex overloaded signatures, but it loses compile-time type checking for individual call sites. This was flagged in a previous review.

Consider creating typed helper functions for specific update patterns if type safety becomes a concern.


1290-1318: Nested interactive elements cause conflicting behavior.

The <Button> is nested inside an <a> tag. Clicking triggers both the button's onClick (copying to clipboard) and the anchor's navigation. The button label says "Open Link" but also copies to clipboard, which is confusing UX.

🔎 Proposed fix - separate the actions
-<div class="relative">
-  <a
-    href={meta().sharing?.link}
-    target="_blank"
-    rel="noreferrer"
-    class="block"
-  >
-    <Button
-      onClick={() => {
-        setCopyPressed(true);
-        setTimeout(() => {
-          setCopyPressed(false);
-        }, 2000);
-        navigator.clipboard.writeText(
-          meta().sharing?.link!,
-        );
-      }}
-      variant="dark"
-      class="flex gap-2 justify-center items-center"
-    >
-      {!copyPressed() ? (
-        <IconCapCopy class="transition-colors ..." />
-      ) : (
-        <IconLucideCheck class="transition-colors ..." />
-      )}
-      <p>Open Link</p>
-    </Button>
-  </a>
-</div>
+<div class="flex gap-2">
+  <Button
+    onClick={() => {
+      setCopyPressed(true);
+      setTimeout(() => setCopyPressed(false), 2000);
+      navigator.clipboard.writeText(meta().sharing?.link!);
+    }}
+    variant="dark"
+    class="flex gap-2 justify-center items-center"
+  >
+    {!copyPressed() ? (
+      <IconCapCopy class="size-4" />
+    ) : (
+      <IconLucideCheck class="size-4" />
+    )}
+    <p>Copy Link</p>
+  </Button>
+  <Button
+    as="a"
+    href={meta().sharing?.link}
+    target="_blank"
+    rel="noreferrer"
+    variant="dark"
+    class="flex gap-2 justify-center items-center"
+  >
+    <IconLucideExternalLink class="size-4" />
+    <p>Open Link</p>
+  </Button>
+</div>
🧹 Nitpick comments (7)
apps/desktop/src-tauri/src/export.rs (2)

191-194: Remove redundant let binding.

The variable quality is assigned and immediately returned. Per Rust idioms and coding guidelines, return the expression directly.

🔎 Proposed fix
 fn bpp_to_jpeg_quality(bpp: f32) -> u8 {
-    let quality = ((bpp - 0.04) / (0.3 - 0.04) * (95.0 - 40.0) + 40.0).clamp(40.0, 95.0) as u8;
-    quality
+    ((bpp - 0.04) / (0.3 - 0.04) * (95.0 - 40.0) + 40.0).clamp(40.0, 95.0) as u8
 }

300-335: Consider extracting shared JPEG encoding and estimate logic.

The RGBA-to-RGB conversion, JPEG encoding, base64 encoding, and size estimation logic (lines 300-335) is duplicated in generate_export_preview_fast (lines 411-441). Extracting a helper function would reduce duplication and make future changes easier.

🔎 Proposed helper function
struct EncodedPreview {
    jpeg_base64: String,
    estimated_size_mb: f64,
}

fn encode_frame_to_preview(
    frame: &RenderedFrame,
    compression_bpp: f32,
    resolution_base: XY<u32>,
    fps: u32,
    duration_seconds: f64,
) -> Result<EncodedPreview, String> {
    let rgb_data: Vec<u8> = frame
        .data
        .chunks(frame.padded_bytes_per_row as usize)
        .flat_map(|row| {
            row[0..(frame.width * 4) as usize]
                .chunks(4)
                .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
        })
        .collect();

    let jpeg_quality = bpp_to_jpeg_quality(compression_bpp);
    let mut jpeg_buffer = Vec::new();
    {
        let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality);
        encoder
            .encode(&rgb_data, frame.width, frame.height, image::ExtendedColorType::Rgb8)
            .map_err(|e| format!("Failed to encode JPEG: {e}"))?;
    }

    let jpeg_base64 = STANDARD.encode(&jpeg_buffer);

    let total_pixels = (resolution_base.x * resolution_base.y) as f64;
    let fps_f64 = fps as f64;
    let video_bitrate = total_pixels * compression_bpp as f64 * fps_f64;
    let audio_bitrate = 192_000.0;
    let total_bitrate = video_bitrate + audio_bitrate;
    let estimated_size_mb = (total_bitrate * duration_seconds) / (8.0 * 1024.0 * 1024.0);

    Ok(EncodedPreview { jpeg_base64, estimated_size_mb })
}
apps/desktop/src/routes/editor/ExportPage.tsx (5)

180-187: Unusual use of Object.defineProperty for computed property.

Using Object.defineProperty with a getter inside mergeProps is an unconventional pattern in SolidJS. Consider using a memo or computed signal instead for clearer reactive semantics:

🔎 Alternative approach
const effectiveOrganizationId = () => {
  if (!_settings.organizationId && organisations().length > 0)
    return organisations()[0].id;
  return _settings.organizationId;
};

// Then use effectiveOrganizationId() where settings.organizationId is accessed

502-502: Remove debug console.log statement.

This console.log("Upload progress:", progress) appears to be debugging code that should be removed before merging.

🔎 Proposed fix
 const uploadChannel = new Channel<UploadProgress>((progress) => {
-  console.log("Upload progress:", progress);
   setExportState(
     produce((state) => {

521-521: Remove debug console.log statement.

This console.log({ organizationId: settings.organizationId }) appears to be debugging code.

🔎 Proposed fix
-console.log({ organizationId: settings.organizationId });
-
 const result = meta().sharing

571-579: Consider flooring seconds for consistent formatting.

If seconds has a fractional part, secs will also have decimals (e.g., 65.5 % 60 = 5.5), causing inconsistent output like "1:05.5". While callers currently use Math.round(), the function would be more robust by flooring internally:

🔎 Proposed fix
 const formatDuration = (seconds: number) => {
+  seconds = Math.floor(seconds);
   const hours = Math.floor(seconds / 3600);
   const minutes = Math.floor((seconds % 3600) / 60);
-  const secs = seconds % 60;
+  const secs = Math.floor(seconds % 60);

686-694: Extract magic multipliers as named constants.

The speed multipliers (4/10) and size multipliers (0.7/0.5) are used here and duplicated at lines 1049/1053. Extracting them as named constants would improve maintainability and make their purpose clearer.

🔎 Proposed fix
const EXPORT_SPEED_MULTIPLIER = { Gif: 4, Mp4: 10 } as const;
const SIZE_ESTIMATE_MULTIPLIER = { Gif: 0.7, Mp4: 0.5 } as const;

// Then use:
const exportSpeedMultiplier = EXPORT_SPEED_MULTIPLIER[settings.format];
const sizeMultiplier = SIZE_ESTIMATE_MULTIPLIER[settings.format];
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4517f1 and da6841b.

📒 Files selected for processing (3)
  • apps/desktop/src-tauri/src/export.rs
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/utils/tauri.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/utils/tauri.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • apps/desktop/src-tauri/src/export.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • apps/desktop/src-tauri/src/export.rs
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧠 Learnings (3)
📓 Common learnings
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.
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use strict TypeScript; avoid any type; leverage shared types from cap/* packages

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
📚 Learning: 2025-11-19T18:25:41.457Z
Learnt from: ItsEeleeya
Repo: CapSoftware/Cap PR: 1396
File: apps/desktop/src/routes/(window-chrome)/settings/general.tsx:183-192
Timestamp: 2025-11-19T18:25:41.457Z
Learning: In SolidJS, when using the Show component with the `keyed` attribute and a dynamic reactive value (like a memo result), the component will recreate its children when the condition value changes by reference. This pattern is useful for triggering CSS animations (like fade-in) when the rendered content needs to update. For example, in apps/desktop/src/routes/(window-chrome)/settings/general.tsx, `<Show when={previews()[theme.id]} keyed>` ensures the image re-animates when switching between different preview sources.

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧬 Code graph analysis (2)
apps/desktop/src-tauri/src/export.rs (5)
apps/desktop/src-tauri/src/lib.rs (4)
  • get_video_metadata (1423-1497)
  • new (833-835)
  • new (1307-1311)
  • new (1878-1908)
crates/project/src/meta.rs (3)
  • path (127-129)
  • path (312-314)
  • path (363-365)
apps/desktop/src-tauri/src/recording.rs (2)
  • project_path (262-267)
  • recordings (1962-1972)
crates/rendering/src/lib.rs (6)
  • new (116-186)
  • new (386-459)
  • new (501-503)
  • new (1091-1566)
  • new (1583-1588)
  • new (1636-1638)
crates/export/src/lib.rs (1)
  • total_frames (131-140)
apps/desktop/src/routes/editor/ExportPage.tsx (5)
apps/desktop/src/utils/tauri.ts (5)
  • ExportCompression (415-415)
  • ExportSettings (419-419)
  • commands (7-310)
  • FramesRendered (422-422)
  • UploadProgress (516-516)
apps/desktop/src/utils/queries.ts (1)
  • createOrganizationsQuery (275-289)
apps/desktop/src/utils/export.ts (1)
  • createExportTask (4-27)
apps/desktop/src/routes/editor/Header.tsx (1)
  • RESOLUTION_OPTIONS (32-36)
apps/desktop/src/routes/editor/context.ts (1)
  • RenderState (90-92)
⏰ 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 (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (5)
apps/desktop/src-tauri/src/export.rs (2)

199-345: LGTM!

The generate_export_preview function is well-structured with proper error handling using map_err throughout. The async workflow correctly loads metadata, creates render constants, decodes the target frame, and encodes it to JPEG for preview.


435-436: generate_export_preview_fast bypasses timeline segment trimming in duration calculation.

This function uses editor.recordings.duration() directly, which sums raw recording durations and ignores the timeline. In contrast, generate_export_preview (lines 325–329) respects timeline segments:

let duration_seconds = if let Some(timeline) = &project_config.timeline {
    timeline.segments.iter().map(|s| s.duration()).sum()
} else {
    metadata.duration
};

When a project has a timeline with trimmed segments, the two functions will calculate different total_frames and estimated_size_mb values. Update generate_export_preview_fast to apply the same timeline logic.

apps/desktop/src/routes/editor/ExportPage.tsx (3)

46-46: LGTM!

Using a custom SilentError class for cancellation handling is a clean pattern that allows distinguishing user-initiated cancellations from actual errors.


236-284: LGTM!

Good implementation of debounced preview fetching with proper memory management - revoking old object URLs before creating new ones, and cleaning up in onCleanup. The caching strategy for estimates is also a nice optimization.


1379-1412: LGTM!

The RenderProgress and ProgressView helper components are clean and well-structured presentational components.

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 (3)
apps/desktop/src/routes/editor/ExportPage.tsx (3)

478-479: Remove or properly use the unused createSignInMutation call.

The createSignInMutation() call on line 479 creates a mutation hook but discards the return value. Solid Query mutation hooks must be stored and triggered with .mutate() to initiate the flow. Either store the hook and trigger sign-in when needed, or remove this line if authentication should be handled elsewhere.


213-220: Refactor the type-unsafe updateSettings wrapper.

The updateSettings wrapper loses type safety because setSettings from solid-js/store has complex overloaded signatures that don't work well with rest/spread parameters. The type casting on lines 217-218 masks underlying type issues. Remove this wrapper and call setPreviewLoading(true) directly before each setSettings call at the call sites (lines 826, 892, 930).

🔎 Proposed approach

Remove the wrapper:

-const updateSettings: typeof setSettings = ((
-  ...args: Parameters<typeof setSettings>
-) => {
-  setPreviewLoading(true);
-  return (setSettings as (...args: Parameters<typeof setSettings>) => void)(
-    ...args,
-  );
-}) as typeof setSettings;

Then at each call site, add setPreviewLoading(true) before setSettings:

onClick={() => {
  setPreviewLoading(true);
  setSettings("resolution", option);
}}

1284-1312: Separate the conflicting nested interactive elements.

The Button is nested inside an anchor tag, causing both the clipboard copy (onClick) and link navigation (href) to trigger simultaneously. The button label "Open Link" is also misleading since it copies to clipboard. Separate these into two distinct buttons: one for copying the link and one for opening it.

🔎 Proposed fix
-<div class="relative">
-  <a
-    href={meta().sharing?.link}
-    target="_blank"
-    rel="noreferrer"
-    class="block"
-  >
-    <Button
-      onClick={() => {
-        setCopyPressed(true);
-        setTimeout(() => {
-          setCopyPressed(false);
-        }, 2000);
-        navigator.clipboard.writeText(
-          meta().sharing?.link!,
-        );
-      }}
-      variant="dark"
-      class="flex gap-2 justify-center items-center"
-    >
-      {!copyPressed() ? (
-        <IconCapCopy class="transition-colors duration-200 text-gray-1 size-4 group-hover:text-gray-12" />
-      ) : (
-        <IconLucideCheck class="transition-colors duration-200 text-gray-1 size-4 svgpathanimation group-hover:text-gray-12" />
-      )}
-      <p>Open Link</p>
-    </Button>
-  </a>
-</div>
+<div class="flex gap-4">
+  <Button
+    onClick={() => {
+      setCopyPressed(true);
+      setTimeout(() => {
+        setCopyPressed(false);
+      }, 2000);
+      navigator.clipboard.writeText(
+        meta().sharing?.link!,
+      );
+    }}
+    variant="dark"
+    class="flex gap-2 justify-center items-center"
+  >
+    {!copyPressed() ? (
+      <IconCapCopy class="size-4" />
+    ) : (
+      <IconLucideCheck class="size-4 svgpathanimation" />
+    )}
+    <p>Copy Link</p>
+  </Button>
+  <Button
+    as="a"
+    href={meta().sharing?.link}
+    target="_blank"
+    rel="noreferrer"
+    variant="dark"
+    class="flex gap-2 justify-center items-center"
+  >
+    <IconLucideExternalLink class="size-4" />
+    <p>Open Link</p>
+  </Button>
+</div>
🧹 Nitpick comments (2)
apps/desktop/src/routes/editor/ExportPage.tsx (2)

278-278: Consider removing debug logging statements.

Several console.log and console.error statements are present (lines 278, 367, 502, 521, 553). For production readiness, consider removing debug logs (lines 502, 521) or integrating with a proper logging service that can be configured for different environments.

Also applies to: 367-367, 502-502, 521-521, 553-553


126-1381: Consider splitting the large ExportPage component.

The ExportPage component (~1250 lines) handles multiple concerns including settings, preview generation, and three export flows. For improved maintainability, consider extracting sub-components for export settings panel, preview area, and export progress overlay.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da6841b and e9809d1.

📒 Files selected for processing (2)
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/tailwind.config.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/tailwind.config.js
🧰 Additional context used
📓 Path-based instructions (4)
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧠 Learnings (3)
📓 Common learnings
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.
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use strict TypeScript; avoid any type; leverage shared types from cap/* packages

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
📚 Learning: 2025-11-19T18:25:41.457Z
Learnt from: ItsEeleeya
Repo: CapSoftware/Cap PR: 1396
File: apps/desktop/src/routes/(window-chrome)/settings/general.tsx:183-192
Timestamp: 2025-11-19T18:25:41.457Z
Learning: In SolidJS, when using the Show component with the `keyed` attribute and a dynamic reactive value (like a memo result), the component will recreate its children when the condition value changes by reference. This pattern is useful for triggering CSS animations (like fade-in) when the rendered content needs to update. For example, in apps/desktop/src/routes/(window-chrome)/settings/general.tsx, `<Show when={previews()[theme.id]} keyed>` ensures the image re-animates when switching between different preview sources.

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/editor/ExportPage.tsx (3)
apps/desktop/src/utils/tauri.ts (5)
  • ExportCompression (415-415)
  • ExportSettings (419-419)
  • commands (7-310)
  • FramesRendered (422-422)
  • UploadProgress (516-516)
apps/desktop/src/utils/export.ts (1)
  • createExportTask (4-27)
apps/desktop/src/routes/editor/context.ts (1)
  • RenderState (90-92)
🪛 GitHub Check: Typecheck
apps/desktop/src/routes/editor/ExportPage.tsx

[failure] 1367-1367:
Type '"secondary"' is not assignable to type '"primary" | "blue" | "destructive" | "outline" | "white" | "ghost" | "gray" | "dark" | "darkgradient" | "radialblue" | null | undefined'.

⏰ 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: Clippy (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)

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

♻️ Duplicate comments (2)
apps/desktop/src/routes/editor/ExportPage.tsx (2)

1387-1387: Fix invalid Button variant.

The variant="secondary" is not a valid Button variant and causes a TypeScript compilation error. Use "gray" or "outline" instead.

🔎 Proposed fix
 <Button
-  variant="secondary"
+  variant="gray"
   class="mt-4 hover:underline"
   onClick={() => setExportState({ type: "idle" })}
 >

1304-1332: Separate nested interactive elements.

The Button is nested inside an anchor, causing both the clipboard copy and link navigation to fire simultaneously. This creates conflicting user experience.

Consider splitting into two separate buttons: one for copying and one for opening the link.

🧹 Nitpick comments (2)
apps/desktop/src/routes/editor/ExportPage.tsx (2)

213-220: Refactor to preserve type safety.

The updateSettings wrapper uses a rest parameter spread pattern with complex store setter overloads, which can compromise type safety. While the explicit cast forces compilation, it bypasses TypeScript's type checking.

Consider these alternatives:

  • Option 1: Call setPreviewLoading(true) directly before each updateSettings call at the call sites
  • Option 2: Create small, strongly-typed helper functions for specific updates (e.g., updateFormat, updateResolution)
🔎 Example refactor using Option 1

Remove the wrapper:

-const updateSettings: typeof setSettings = ((
-  ...args: Parameters<typeof setSettings>
-) => {
-  setPreviewLoading(true);
-  return (setSettings as (...args: Parameters<typeof setSettings>) => void)(
-    ...args,
-  );
-}) as typeof setSettings;

Then at each call site (e.g., lines 834, 900, 938):

+setPreviewLoading(true);
 setSettings(/* ... */);

172-190: Consider simplifying format validation.

The check at line 178 (!["Mp4", "Gif"].includes(_settings.format)) is defensive against invalid persisted state, but given the TypeScript type constraints, this should never occur unless the storage is corrupted.

This is acceptable defensive coding, but you could optionally remove it if you're confident in the persistence layer's type safety.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9809d1 and 1f1fb45.

📒 Files selected for processing (2)
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/routes/editor/Header.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/Header.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • apps/desktop/src/routes/editor/Header.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • apps/desktop/src/routes/editor/Header.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • apps/desktop/src/routes/editor/Header.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
🧠 Learnings (3)
📓 Common learnings
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.
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use strict TypeScript; avoid any type; leverage shared types from cap/* packages

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
📚 Learning: 2025-11-19T18:25:41.457Z
Learnt from: ItsEeleeya
Repo: CapSoftware/Cap PR: 1396
File: apps/desktop/src/routes/(window-chrome)/settings/general.tsx:183-192
Timestamp: 2025-11-19T18:25:41.457Z
Learning: In SolidJS, when using the Show component with the `keyed` attribute and a dynamic reactive value (like a memo result), the component will recreate its children when the condition value changes by reference. This pattern is useful for triggering CSS animations (like fade-in) when the rendered content needs to update. For example, in apps/desktop/src/routes/(window-chrome)/settings/general.tsx, `<Show when={previews()[theme.id]} keyed>` ensures the image re-animates when switching between different preview sources.

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/editor/ExportPage.tsx (4)
apps/desktop/src/utils/tauri.ts (4)
  • ExportCompression (415-415)
  • ExportSettings (419-419)
  • commands (7-310)
  • FramesRendered (422-422)
apps/desktop/src/utils/export.ts (1)
  • createExportTask (4-27)
apps/desktop/src/routes/editor/Header.tsx (1)
  • RESOLUTION_OPTIONS (32-36)
apps/desktop/src/routes/editor/context.ts (1)
  • RenderState (90-92)
🪛 GitHub Check: Typecheck
apps/desktop/src/routes/editor/ExportPage.tsx

[failure] 1387-1387:
Type '"secondary"' is not assignable to type '"primary" | "blue" | "destructive" | "outline" | "white" | "ghost" | "gray" | "dark" | "darkgradient" | "radialblue" | null | undefined'.

⏰ 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 (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (8)
apps/desktop/src/routes/editor/Header.tsx (2)

168-168: LGTM - Export button styling updated.

The variant change from "dark" to "blue" aligns with the broader export UI refactor described in the PR objectives.


179-179: LGTM - Icon color inheritance simplified.

Removing the explicit text-gray-1 class allows the icon to inherit color from parent styles, which pairs well with the new "blue" variant styling.

apps/desktop/src/routes/editor/ExportPage.tsx (6)

46-46: LGTM - Clean cancellation error pattern.

The SilentError class provides a clear way to distinguish user-initiated cancellations from actual errors, used consistently throughout the export flow.


236-284: LGTM - Well-implemented preview generation.

The debounced preview generation with caching and proper blob URL cleanup is well-structured. The 300ms debounce delay is reasonable for responsive UI updates without excessive API calls.


316-346: LGTM - Robust cancellable export pattern.

The export task creation properly manages the cancel function reference and cleans it up on completion, preventing stale cancel calls.


351-371: LGTM - Comprehensive cancel handling.

The cancel flow properly confirms with the user, cleans up state, and attempts to remove partial output files. The try-catch around file deletion appropriately handles cases where the file might not exist or be locked.


472-562: LGTM - Comprehensive upload mutation.

The upload mutation properly handles authentication checks, plan verification, progress tracking, and various error modes. The cancellation handling is consistent with the other export mutations.


1071-1398: LGTM - Well-structured export overlay UI.

The conditional overlay properly uses SolidJS reactive patterns with keyed Show for state transitions. The Switch/Match structure clearly routes between different export actions and their respective progress/completion states.

Note: Line 1387 has a separate critical issue with the invalid Button variant already flagged above.

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 (5)
crates/editor/examples/decode-benchmark.rs (1)

54-55: Cache statistics fields remain unpopulated.

The cache_hits and cache_misses fields are still never updated, so the cache statistics section in print_report() will always display "0 (0.0%)". This issue was previously flagged in an earlier review.

Also applies to: 156-169

apps/desktop/src/routes/editor/ExportPage.tsx (3)

479-479: Remove unused createSignInMutation() call.

This function call creates a mutation hook but discards the result, leaving it unused. Either assign the result to a variable and invoke it when needed, or remove this line entirely.

🔎 Proposed fix
 			const existingAuth = await authStore.get();
-			if (!existingAuth) createSignInMutation();
 			trackEvent("create_shareable_link_clicked", {

If sign-in is required before upload, the authentication check should be moved earlier in the flow or handled differently.


213-220: Type safety lost in updateSettings wrapper.

The wrapper uses rest parameters and spread with an overloaded setSettings function from solid-js/store, which causes TypeScript to infer any[] types and lose type safety.

🔎 Proposed fix

Remove the wrapper and call setPreviewLoading(true) directly before each setSettings call:

-	const updateSettings: typeof setSettings = ((
-		...args: Parameters<typeof setSettings>
-	) => {
-		setPreviewLoading(true);
-		return (setSettings as (...args: Parameters<typeof setSettings>) => void)(
-			...args,
-		);
-	}) as typeof setSettings;

Then update call sites (e.g., lines 834, 900, 934):

 							onClick={() => {
+								setPreviewLoading(true);
-								updateSettings(
+								setSettings(

1387-1387: Fix invalid Button variant.

The variant "secondary" is not valid according to the Button component's TypeScript definitions. Use "gray" or "outline" instead.

🔎 Proposed fix
 							<Button
-								variant="secondary"
+								variant="gray"
 								class="mt-4 hover:underline"
 								onClick={() => setExportState({ type: "idle" })}
 							>
crates/rendering/src/decoder/avassetreader.rs (1)

313-314: Critical: Extracted keyframe index is discarded and never used.

Line 313 extracts keyframe_index from primary_decoder, but line 314 immediately sets keyframe_index_arc to None, discarding the extracted index. This means:

  1. The keyframe index is never passed to the pool manager configuration
  2. All keyframe-aware seeking optimizations are disabled
  3. Lines 316-323 use .as_ref() on the local keyframe_index (which has been moved), not keyframe_index_arc
🔎 Proposed fix
         let keyframe_index = primary_decoder.take_keyframe_index();
-        let keyframe_index_arc: Option<Arc<cap_video_decode::avassetreader::KeyframeIndex>> = None;
+        let keyframe_index_arc = keyframe_index.map(Arc::new);

         let fps = keyframe_index
+            _arc
             .as_ref()
-            .map(|kf| kf.fps() as u32)
+            .map(|kf| kf.fps() as u32)
             .unwrap_or(30);
         let duration_secs = keyframe_index
+            _arc
             .as_ref()
-            .map(|kf| kf.duration_secs())
+            .map(|kf| kf.duration_secs())
             .unwrap_or(0.0);

Based on learnings, this issue was previously flagged in past reviews and needs to be addressed.

🧹 Nitpick comments (1)
crates/editor/src/playback.rs (1)

935-958: Well-designed audio-video sync mechanism.

The sync logic correctly handles both gradual drift and sudden seeks by checking the dual condition. The 150ms threshold aligns with research on perceptible audio-video desynchronization.

Consider moving SYNC_THRESHOLD_SECS to module level (near line 37) for consistency with other playback constants.

🔎 Optional: Move constant to module level
 const PREFETCH_BEHIND: u32 = 15;
 const FRAME_CACHE_SIZE: usize = 60;
+const AUDIO_SYNC_THRESHOLD_SECS: f64 = 0.15;

Then at line 951:

-                        if drift > SYNC_THRESHOLD_SECS
-                            || (video_playhead - last_video_playhead).abs() > SYNC_THRESHOLD_SECS
+                        if drift > AUDIO_SYNC_THRESHOLD_SECS
+                            || (video_playhead - last_video_playhead).abs() > AUDIO_SYNC_THRESHOLD_SECS
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1fb45 and d7edd42.

📒 Files selected for processing (14)
  • .claude/settings.local.json
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/routes/editor/Editor.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/routes/editor/editor-skeleton.tsx
  • apps/desktop/src/routes/editor/index.tsx
  • crates/editor/examples/decode-benchmark.rs
  • crates/editor/src/playback.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/mod.rs
  • crates/rendering/src/layers/camera.rs
  • crates/rendering/src/layers/display.rs
  • crates/video-decode/src/avassetreader.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/rendering/src/layers/display.rs
  • apps/desktop/src/routes/editor/index.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/mod.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/rendering/src/layers/camera.rs
  • crates/editor/src/playback.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/editor/examples/decode-benchmark.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/rendering/src/decoder/avassetreader.rs
  • apps/desktop/src/routes/editor/editor-skeleton.tsx
  • crates/rendering/src/decoder/mod.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/rendering/src/layers/camera.rs
  • crates/editor/src/playback.rs
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/routes/editor/Editor.tsx
  • crates/recording/src/output_pipeline/core.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/editor/examples/decode-benchmark.rs
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/editor-skeleton.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/routes/editor/Editor.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • apps/desktop/src/routes/editor/editor-skeleton.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/routes/editor/Editor.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • apps/desktop/src/routes/editor/editor-skeleton.tsx
  • apps/desktop/src/routes/editor/ExportPage.tsx
  • apps/desktop/src/routes/editor/Editor.tsx
🧠 Learnings (5)
📓 Common learnings
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.
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.
📚 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/rendering/src/decoder/avassetreader.rs
  • crates/rendering/src/decoder/mod.rs
  • apps/desktop/src-tauri/src/lib.rs
  • crates/rendering/src/layers/camera.rs
  • crates/editor/src/playback.rs
  • crates/recording/src/output_pipeline/core.rs
  • crates/video-decode/src/avassetreader.rs
  • crates/editor/examples/decode-benchmark.rs
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use static skeletons for loading states that mirror content; avoid bouncing animations

Applied to files:

  • apps/desktop/src/routes/editor/editor-skeleton.tsx
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use strict TypeScript; avoid any type; leverage shared types from cap/* packages

Applied to files:

  • apps/desktop/src/routes/editor/ExportPage.tsx
📚 Learning: 2025-10-17T05:58:22.586Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.

Applied to files:

  • crates/recording/src/output_pipeline/core.rs
  • crates/editor/examples/decode-benchmark.rs
🧬 Code graph analysis (5)
apps/desktop/src-tauri/src/lib.rs (2)
crates/project/src/meta.rs (4)
  • path (127-129)
  • path (312-314)
  • path (363-365)
  • load_for_project (131-137)
apps/desktop/src-tauri/src/editor_window.rs (1)
  • app (85-85)
crates/rendering/src/layers/camera.rs (3)
apps/desktop/src/utils/tauri.ts (1)
  • XY (526-526)
crates/rendering/src/project_recordings.rs (1)
  • Some (197-204)
crates/rendering/src/decoder/mod.rs (1)
  • format (367-369)
crates/editor/src/playback.rs (1)
crates/audio/src/latency.rs (1)
  • initial_compensation_secs (141-143)
crates/video-decode/src/avassetreader.rs (2)
crates/rendering/src/decoder/avassetreader.rs (4)
  • new (76-78)
  • new (205-246)
  • new (260-276)
  • new (309-378)
crates/video-decode/src/ffmpeg.rs (1)
  • start_time (281-283)
crates/editor/examples/decode-benchmark.rs (1)
crates/rendering/src/decoder/mod.rs (1)
  • spawn_decoder (511-673)
🪛 GitHub Check: Typecheck
apps/desktop/src/routes/editor/ExportPage.tsx

[failure] 1387-1387:
Type '"secondary"' is not assignable to type '"primary" | "blue" | "destructive" | "outline" | "white" | "ghost" | "gray" | "dark" | "darkgradient" | "radialblue" | null | undefined'.

⏰ 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: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Clippy (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (39)
apps/desktop/src-tauri/src/lib.rs (2)

109-138: LGTM! Finalization tracking implementation is sound.

The FinalizingRecordings struct properly uses watch channels to coordinate async finalization. Using std::sync::Mutex is appropriate here since the critical sections are very short (just HashMap operations) and contain no await points.


3209-3240: LGTM! Async finalization and crash-recovery remux integration is correct.

The function properly:

  • Waits for ongoing finalization using watch channels
  • Loads recording metadata
  • Triggers crash-recovery remux in a blocking task when fragmentation is detected

The double ?? operator on line 3235 correctly handles both the JoinError from the task and the String error from the remux operation.

crates/editor/examples/decode-benchmark.rs (1)

9-34: LGTM! Video duration is now properly detected.

The addition of get_video_duration() using ffprobe resolves the previous issue of hardcoded duration. The function properly falls back to a default value with a warning when ffprobe is unavailable, and the duration is correctly used to filter seek distances and constrain random access sampling.

Also applies to: 333-334, 351-352, 368-368

crates/rendering/src/layers/camera.rs (5)

17-17: LGTM: Improved deduplication strategy.

Switching from pointer-based (last_frame_ptr: usize) to time-based (last_recording_time: Option<f32>) deduplication is more reliable, as pointer addresses can be reused across allocations.


62-68: LGTM: Cleaner API separation.

Splitting uniforms and frame_data into separate parameters improves clarity, and the early return pattern is idiomatic.


70-71: LGTM: Smart frame persistence.

The visibility logic correctly displays the last valid frame when new frame data is unavailable, preventing flickering during transient frame drops.


177-177: LGTM: Correct state tracking.

Updating last_recording_time after processing ensures the deduplication logic has accurate state for the next frame.


82-86: The tolerance is appropriate. The recording_time is in seconds (confirmed by frame duration calculations using 1.0 / fps_f32), making 0.001 equivalent to 1 millisecond—a reasonable threshold for frame deduplication in video rendering.

apps/desktop/src/routes/editor/Editor.tsx (1)

436-446: The canvas element access silently fails if unavailable.

The code correctly handles the null case to prevent crashes, but if the canvas element with id "canvas" is not found in the DOM, the frame capture will silently fail and frameBlobUrl will remain null, resulting in a missing preview. Consider using a ref callback from PlayerContent instead of document.getElementById, or add a logging mechanism to detect when the canvas is unexpectedly unavailable.

crates/rendering/src/decoder/mod.rs (6)

19-20: LGTM: Multi-position decoder module exposed for macOS.

This change appropriately exposes the new multi-position decoder infrastructure on macOS, aligning with the PR objectives to improve the recording pipeline.


186-197: LGTM: Arc-based constructor added for efficient frame sharing.

The new_with_arc constructor enables zero-copy frame sharing across the multi-decoder pool, which is essential for the new architecture.


212-229: LGTM: NV12 constructor updated to use Arc-based data.

This change consistently adopts the Arc-based data model for NV12 frames, supporting efficient sharing across decoders.


231-267: LGTM: YUV420p constructors added with Arc support.

Both owned and Arc-based constructors are provided for YUV420p format, maintaining consistency with other pixel format constructors.


474-483: LGTM: Timeout prevents indefinite blocking on frame requests.

Adding a 500ms timeout with proper logging improves reliability by preventing the UI from hanging indefinitely when the decoder is unresponsive.


451-451: The multi-position decoder pool with adaptive cache sizing (including scrub detection) is in place, but performance validation for the cache size reduction requires benchmarking or testing data that cannot be verified from the codebase alone. Confirm through performance testing that rapid seeking and scrubbing maintain acceptable responsiveness with the reduced 150-frame cache.

crates/rendering/src/decoder/avassetreader.rs (8)

24-29: LGTM: FrameData wrapper enables efficient Arc-based sharing.

The new FrameData struct consolidates frame buffer data with strides, allowing multiple decoders to share frame data without copying.


40-67: LGTM: ProcessedFrame correctly uses Arc-based constructors.

The refactored to_decoded_frame method properly clones the Arc for each pixel format, enabling zero-copy sharing of frame data.


204-245: LGTM: CachedFrame constructor properly wraps extracted data in Arc.

The frame data extraction and Arc wrapping is handled correctly for all supported pixel formats (NV12, RGBA, BGRA, YUV420P), with appropriate fallback to black frames for unsupported formats.


253-299: LGTM: DecoderInstance lifecycle management is well-structured.

The DecoderInstance wrapper provides proper error handling for reset operations and tracks decoder state (is_done, frames_iter_valid) to prevent using invalid decoders.


380-394: LGTM: Decoder selection and reset logic is sound.

The select_best_decoder method properly:

  • Clamps the decoder index to valid bounds
  • Resets the decoder when needed
  • Updates the pool manager with the new position
  • Returns both the index and reset status

443-498: LGTM: Batched request handling improves efficiency.

The refactored run loop:

  • Batches multiple pending requests for efficient processing
  • Sorts requests by frame number
  • Serves requests from cache before decoding
  • Properly handles closed channels

524-621: LGTM: Frame iteration with robust error handling and cache management.

The frame processing loop properly:

  • Logs and continues on frame read errors instead of crashing
  • Updates decoder position for pool management
  • Implements scrub-aware cache expansion
  • Handles cache eviction intelligently based on playback direction
  • Limits iteration during scrubbing to prevent UI lag

627-642: LGTM: Fallback chain ensures requests are always fulfilled.

The final request fulfillment logic provides a proper fallback hierarchy:

  1. Cached frame (exact match)
  2. Last sent frame (temporal continuity)
  3. First ever frame (safe fallback)
  4. Log and drop (only if no frames decoded yet)
crates/video-decode/src/avassetreader.rs (9)

13-85: LGTM: KeyframeIndex provides comprehensive keyframe management.

The KeyframeIndex::build method properly:

  • Scans the video file for keyframes using FFmpeg
  • Computes FPS and duration with appropriate fallbacks
  • Logs build time and metadata for observability
  • Returns a structured index for efficient seeking

87-130: LGTM: Binary search implementations are correct.

Both nearest_keyframe_before and nearest_keyframe_after properly handle:

  • Empty keyframe lists
  • Exact matches
  • Boundary conditions (beginning/end of video)
  • Out-of-range requests

132-149: LGTM: Strategic position selection distributes decoders evenly.

The get_strategic_positions method properly:

  • Handles edge cases (empty index, zero positions requested)
  • Returns all keyframes when fewer than requested positions
  • Uses step-by-step sampling for even distribution
  • Uses step.max(1) to prevent division by zero

151-166: LGTM: Accessors provide clean interface to keyframe data.

The public accessor methods properly expose FPS, duration, keyframe count, and raw keyframe data without breaking encapsulation.


168-177: LGTM: Seek time computation finds nearest prior keyframe.

The compute_seek_time function correctly:

  • Converts requested time to frame number
  • Finds the nearest prior keyframe
  • Falls back to requested time if no keyframe index exists

192-214: LGTM: Constructor properly builds keyframe index with fallback.

The new_at_position constructor:

  • Attempts to build a keyframe index
  • Logs a warning (not an error) if index building fails
  • Continues with degraded performance rather than failing
  • Delegates to the full constructor

216-266: LGTM: Primary constructor properly uses keyframe-based seeking.

The new_with_keyframe_index constructor:

  • Computes seek time using keyframe index
  • Passes the computed seek time to reader initialization
  • Stores both the keyframe index and current position

268-285: LGTM: Reset properly cancels and re-initializes with keyframe seeking.

The reset method:

  • Cancels the existing reader before creating a new one
  • Uses keyframe-based seek time computation
  • Updates the stored position after resetting

287-309: LGTM: Public accessors provide necessary decoder state.

The accessor methods properly expose:

  • Current position for pool management
  • Path and pixel format for debugging
  • Keyframe index access and ownership transfer
crates/editor/src/playback.rs (5)

33-37: LGTM — reduced buffer and prefetch constants.

The lowered values should decrease memory footprint while maintaining smooth playback for typical use cases. Verify these settings perform well on lower-end hardware during QA.


336-346: LGTM — audio playhead synchronization channel.

Using a watch channel is well-suited here since only the latest playhead value matters to the audio renderer. The initialization with the starting playhead ensures audio begins at the correct position.


359-360: Aggressive warmup timing — verify on constrained devices.

Reducing to 1 frame and 16ms timeout minimizes startup latency but may cause initial frame drops on slower systems or when the decoder hasn't had time to warm up. Confirm this doesn't regress playback smoothness.


634-639: Proper error handling for playhead channel.

Breaking the playback loop when the audio receiver is dropped is the correct behavior — if audio playback has failed, continuing video-only would create a poor user experience.


659-664: Consistent playhead update after frame skipping.

Keeping audio in sync even when frames are skipped ensures the user doesn't experience audio-video drift during high-load scenarios.

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

28-87: Excellent pause state implementation with comprehensive error handling.

The SharedPauseState abstraction correctly handles pause/resume timing with proper synchronization. Key strengths:

  • Accumulates pause durations in offset to adjust timestamps consistently across multiple pause/resume cycles
  • All duration arithmetic uses checked operations to prevent panics
  • Clear error messages for backward timestamps, overflow, and underflow scenarios
  • Uses std::sync::Mutex appropriately for the small critical section (more efficient than async mutex here)
  • Atomic flag with Relaxed ordering is safe since the mutex provides additional synchronization guarantees

550-595: Well-designed bounded draining logic.

The enhanced draining implementation correctly prevents blocking during async finalization:

  • Enforces reasonable limits (2-second timeout, 30-frame cap) to ensure timely cleanup
  • Separately tracks successfully processed frames and frames with errors for observability
  • Clear logging indicates whether limits were reached and provides timing information
  • Aligns well with the PR objective of preventing UI blocking during recording completion

<div class="relative w-full h-full flex justify-center items-center p-4">
<div class="relative bg-gray-3 dark:bg-gray-4 rounded-lg w-full max-w-[85%] aspect-video flex items-center justify-center">
<div class="animate-spin grayscale opacity-60">
<IconCapLogo class="size-[4rem] text-gray-6" />
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

Missing import for IconCapLogo.

The IconCapLogo component is referenced but not imported, which will cause a runtime error.

🔎 Proposed fix

Add the import at the top of the file:

 import { type as ostype } from "@tauri-apps/plugin-os";
 import { cx } from "cva";
+

Note: You'll need to add the correct import path for IconCapLogo based on your icon component location (e.g., ~/components/icons or similar).

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

🤖 Prompt for AI Agents
In apps/desktop/src/routes/editor/editor-skeleton.tsx around line 78 the JSX
references <IconCapLogo /> but the component is not imported, causing a runtime
error; add an import for IconCapLogo at the top of the file from the correct
icons/components path used by the project (e.g., import { IconCapLogo } from
'~/components/icons' or the appropriate relative path), ensuring the
named/default import matches how IconCapLogo is exported.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

216-229: Consider whether Arc is necessary here.

The Arc::new() wrappers around ProjectRecordingsMeta and RenderVideoConstants may be unnecessary if these values aren't shared across threads. If the APIs don't require Arc, consider using direct ownership to reduce overhead.


130-130: Extract audio bitrate as a constant.

The audio bitrate value 192_000.0 appears three times (lines 130, 332, 438). Extract this as a named constant to improve maintainability and ensure consistency.

🔎 Proposed constant

Add at the top of the file:

const AUDIO_BITRATE_BPS: f64 = 192_000.0;

Then replace all occurrences:

-let audio_bitrate = 192_000.0;
+let audio_bitrate = AUDIO_BITRATE_BPS;

Also applies to: 332-332, 438-438


203-205: Consider moving imports to module level.

While function-scoped imports are valid Rust, moving them to the module level (top of file) would align with conventional style and improve consistency.

Also applies to: 354-355

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7edd42 and 4a07659.

📒 Files selected for processing (1)
  • apps/desktop/src-tauri/src/export.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • apps/desktop/src-tauri/src/export.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • apps/desktop/src-tauri/src/export.rs
🧠 Learnings (1)
📓 Common learnings
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.
⏰ 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: Clippy (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Clippy (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (2)
apps/desktop/src-tauri/src/export.rs (2)

174-193: LGTM! Well-structured preview types and quality mapping.

The ExportPreviewSettings and ExportPreviewResult types are well-designed for the preview workflow. The bpp_to_jpeg_quality helper correctly maps bits-per-pixel to JPEG quality using linear interpolation with appropriate bounds.


357-357: No issues identified. project_config.1 is a tokio::sync::watch::Receiver, not a RefCell. The borrow() method on watch channels is designed for safe concurrent read access and cannot panic. This usage pattern is correct and matches the style used throughout the codebase.

Likely an incorrect or invalid review comment.

Comment on lines +243 to +247
let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else {
return Err("Frame time is outside video duration".to_string());
};

let render_segment = &render_segments[segment.recording_clip as usize];
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

Verify array bounds before indexing.

The indexing at line 247 (&render_segments[segment.recording_clip as usize]) could panic if segment.recording_clip is out of bounds. While get_segment_time validates the segment exists, it doesn't guarantee the clip index is within render_segments.len().

🔎 Proposed bounds check
 let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else {
     return Err("Frame time is outside video duration".to_string());
 };
 
+let clip_index = segment.recording_clip as usize;
+if clip_index >= render_segments.len() {
+    return Err(format!("Invalid clip index: {}", clip_index));
+}
+
-let render_segment = &render_segments[segment.recording_clip as usize];
+let render_segment = &render_segments[clip_index];
🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/export.rs around lines 243 to 247, the code
indexes render_segments with segment.recording_clip as usize without verifying
bounds which can panic; before indexing, convert segment.recording_clip to usize
and check it is < render_segments.len(), returning an Err with a clear message
if out of range, otherwise safely use render_segments[index] (or
render_segments.get(index).ok_or(...)?) to avoid panics.

Comment on lines +299 to +316
let rgb_data: Vec<u8> = frame
.data
.chunks(frame.padded_bytes_per_row as usize)
.flat_map(|row| {
row[0..(frame.width * 4) as usize]
.chunks(4)
.flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
})
.collect();

let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp);
let mut jpeg_buffer = Vec::new();
{
let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality);
encoder
.encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| format!("Failed to encode JPEG: {e}"))?;
}
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 duplicated RGB conversion and JPEG encoding into helper functions.

The RGB data conversion (lines 299-307, 410-418) and JPEG encoding logic (lines 309-316, 420-427) are identical in both preview functions, violating DRY principles. This increases maintenance burden and the risk of inconsistent fixes.

🔎 Proposed helper functions

Add these helper functions before generate_export_preview:

fn frame_to_rgb(frame: &cap_rendering::FrameData) -> Vec<u8> {
    frame
        .data
        .chunks(frame.padded_bytes_per_row as usize)
        .flat_map(|row| {
            row[0..(frame.width * 4) as usize]
                .chunks(4)
                .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
        })
        .collect()
}

fn encode_jpeg_base64(rgb_data: &[u8], width: u32, height: u32, quality: u8) -> Result<String, String> {
    use base64::{Engine, engine::general_purpose::STANDARD};
    
    let mut jpeg_buffer = Vec::new();
    {
        let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, quality);
        encoder
            .encode(rgb_data, width, height, image::ExtendedColorType::Rgb8)
            .map_err(|e| format!("Failed to encode JPEG: {e}"))?;
    }
    Ok(STANDARD.encode(&jpeg_buffer))
}

Then replace the duplicated code with:

-let rgb_data: Vec<u8> = frame
-    .data
-    .chunks(frame.padded_bytes_per_row as usize)
-    .flat_map(|row| {
-        row[0..(frame.width * 4) as usize]
-            .chunks(4)
-            .flat_map(|chunk| [chunk[0], chunk[1], chunk[2]])
-    })
-    .collect();
+let rgb_data = frame_to_rgb(&frame);

 let jpeg_quality = bpp_to_jpeg_quality(settings.compression_bpp);
-let mut jpeg_buffer = Vec::new();
-{
-    let mut encoder = JpegEncoder::new_with_quality(&mut jpeg_buffer, jpeg_quality);
-    encoder
-        .encode(&rgb_data, width, height, image::ExtendedColorType::Rgb8)
-        .map_err(|e| format!("Failed to encode JPEG: {e}"))?;
-}
-
-let jpeg_base64 = STANDARD.encode(&jpeg_buffer);
+let jpeg_base64 = encode_jpeg_base64(&rgb_data, width, height, jpeg_quality)?;

Also applies to: 410-427

🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/export.rs around lines 299-316 (and similarly
410-427), extract the duplicated RGB conversion and JPEG encoding into two
helpers: one fn frame_to_rgb(frame: &cap_rendering::FrameData) -> Vec<u8> that
performs the padded-row -> RGB byte vector extraction, and another fn
encode_jpeg_base64(rgb_data: &[u8], width: u32, height: u32, quality: u8) ->
Result<String, String> that creates a JpegEncoder, writes the JPEG into a
Vec<u8>, and returns the base64 string or an error; then replace the duplicated
blocks in both preview functions with calls to frame_to_rgb and
encode_jpeg_base64, passing settings.compression_bpp converted to the encoder
quality, and propagate errors as before.

Comment on lines +359 to +363
let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else {
return Err("Frame time is outside video duration".to_string());
};

let segment_media = &editor.segment_medias[segment.recording_clip as usize];
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

Verify array bounds before indexing.

Similar to the issue at line 247, the indexing &editor.segment_medias[segment.recording_clip as usize] could panic if the clip index exceeds the segment_medias length.

🔎 Proposed bounds check
 let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else {
     return Err("Frame time is outside video duration".to_string());
 };
 
+let clip_index = segment.recording_clip as usize;
+if clip_index >= editor.segment_medias.len() {
+    return Err(format!("Invalid clip index: {}", clip_index));
+}
+
-let segment_media = &editor.segment_medias[segment.recording_clip as usize];
+let segment_media = &editor.segment_medias[clip_index];
📝 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
let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else {
return Err("Frame time is outside video duration".to_string());
};
let segment_media = &editor.segment_medias[segment.recording_clip as usize];
let Some((segment_time, segment)) = project_config.get_segment_time(frame_time) else {
return Err("Frame time is outside video duration".to_string());
};
let clip_index = segment.recording_clip as usize;
if clip_index >= editor.segment_medias.len() {
return Err(format!("Invalid clip index: {}", clip_index));
}
let segment_media = &editor.segment_medias[clip_index];
🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/export.rs around lines 359 to 363, the code
indexes editor.segment_medias using segment.recording_clip as usize without
verifying the index is within bounds; add a safe bounds check (convert/check the
recording_clip value to a usize and ensure it is < editor.segment_medias.len())
and return an Err with a clear message if it is out of range, before using the
reference, to avoid potential panics.

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)
crates/enc-ffmpeg/src/mux/segmented_stream.rs (3)

15-45: Extract duplicated utility functions to a shared module.

Both atomic_write_json and sync_file are duplicated from crates/enc-ffmpeg/src/mux/segmented_audio.rs. Consider extracting these to a shared utility module (e.g., crates/enc-ffmpeg/src/mux/utils.rs) to eliminate duplication and ensure consistent behavior across audio and video segmented encoders.


243-284: Consider whether both manifest writes are necessary.

Lines 279-280 write both write_manifest() and write_in_progress_manifest(). Since write_in_progress_manifest() includes all completed segments plus the current in-progress segment, writing both may be redundant. Verify if both are needed, or if only write_in_progress_manifest() is sufficient during recording.


576-582: Consider simplifying accessor return types.

current_encoder() and current_encoder_mut() always return Some(...), so wrapping in Option adds no value. If these always succeed, consider returning &H264Encoder and &mut H264Encoder directly instead. If the Option wrapper is for API consistency with similar code elsewhere, this can be deferred.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a07659 and bbd80f3.

📒 Files selected for processing (2)
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
  • crates/recording/src/cursor.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Never use dbg!() macro; use proper logging (tracing::debug!, etc.) instead
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them
Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow
Merge nested if statements: write 'if a && b { }' instead of 'if a { if b { } }'
Don't call .clone() on Copy types (integers, bools, etc.); copy them directly
Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x))
Accept &[T] or &str instead of &Vec or &String in function parameters for flexibility
Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0
Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit
Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value
Use 'for item in &collection' or 'for (i, item) in collection.iter().enumerate()' instead of 'for i in 0..collection.len()'
Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns
Always handle Result/Option or types marked #[must_use]; never ignore them

**/*.rs: Use rustfmt and workspace clippy lints for Rust code formatting and linting
Use snake_case for Rust module names and kebab-case for crate names
Never use dbg!() macro in Rust code; use proper logging instead (Clippy: dbg_macro = deny)
Always handle Result/Option or types marked #[must_use]; never ignore them (Rust compiler lint: unused_must_use = deny)
Never write let _ = async_fn() which silently drops futures; await or explicitly handle them (Clippy: let_underscore_future = deny)
Use saturating_sub instead of - for Duration to avoid panics (Clippy: unchecked_duration_subtraction = deny)
Merge nested if statements: use if a && b { } instead of if a { if b { } } (Clippy: collapsible_if = deny)
Don't call .clone() on Copy types; just copy them directly (Clippy: clone_on_copy = deny)
U...

Files:

  • crates/recording/src/cursor.rs
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • crates/recording/src/cursor.rs
  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
🧠 Learnings (5)
📓 Common learnings
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.
📚 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/enc-ffmpeg/src/mux/segmented_stream.rs
📚 Learning: 2025-12-07T14:29:40.743Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T14:29:40.743Z
Learning: Applies to **/*.rs : Use `saturating_sub` instead of `-` for `Duration` to avoid panics (Clippy: `unchecked_duration_subtraction` = deny)

Applied to files:

  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to **/*.rs : Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow

Applied to files:

  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
📚 Learning: 2025-10-17T05:58:22.586Z
Learnt from: Brendonovich
Repo: CapSoftware/Cap PR: 1219
File: crates/enc-avfoundation/src/mp4.rs:350-373
Timestamp: 2025-10-17T05:58:22.586Z
Learning: In crates/enc-avfoundation/src/mp4.rs, the `finish()` method intentionally skips video extension when `is_paused` is true. This is correct behavior because if recording is paused, the video should not be extended beyond the pause point—the pause is user-initiated, unlike the case where ScreenCaptureKit stops providing frames during static content.

Applied to files:

  • crates/enc-ffmpeg/src/mux/segmented_stream.rs
🧬 Code graph analysis (2)
crates/recording/src/cursor.rs (1)
crates/cursor-capture/src/position.rs (1)
  • get (12-20)
crates/enc-ffmpeg/src/mux/segmented_stream.rs (1)
crates/enc-ffmpeg/src/mux/segmented_audio.rs (4)
  • atomic_write_json (11-33)
  • sync_file (35-41)
  • finalize_manifest (418-450)
  • completed_segments (452-454)
⏰ 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 (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (7)
crates/enc-ffmpeg/src/mux/segmented_stream.rs (1)

151-169: LGTM!

The unsafe FFmpeg option configuration correctly uses as_secs_f64() to preserve fractional segment durations, addressing the previous review concern. The FFI calls are properly wrapped and the options are appropriate for DASH segmentation.

crates/recording/src/cursor.rs (6)

75-76: LGTM! Deterministic hashing addresses past review concerns.

The addition of SHA-256 for cursor image hashing correctly addresses the previous issue with non-deterministic DefaultHasher. The deterministic approach ensures consistent cursor deduplication across recording segments.


100-100: LGTM! Proper Option type addresses initialization issue.

The change to Option<String> initialized to None correctly addresses the previous critical issue where last_cursor_id was incorrectly initialized to the literal "default" string. The logic at lines 163-165 properly handles the None case.


103-103: LGTM! Reasonable polling interval.

The 16ms interval (~62.5 fps) is appropriate for cursor tracking and aligns with typical display refresh rates.


120-161: LGTM! Unconditional cursor data capture fixes shape-change detection.

The refactored logic correctly addresses the previous major issue where cursor shape changes were missed when the mouse was stationary:

  • get_cursor_data() is now called unconditionally (line 120), not gated by position_changed
  • SHA-256 provides deterministic hashing for reliable deduplication across segments
  • The fallback to last_cursor_id.clone() when cursor data is unavailable is appropriate

The expect at lines 122-126 is safe since SHA-256 always produces at least 32 bytes.


163-165: LGTM! Proper guard against missing cursor ID.

The early continue when cursor_id is None correctly prevents events from being generated without a valid cursor reference, particularly during the initial iterations before any cursor is captured.


167-183: LGTM! Position-gated move event generation is correct.

The logic correctly generates move events only when the position changes, while cursor shape changes (captured unconditionally above) are reflected in subsequent events. The transformation pipeline (display-relative → normalized → cropped) is sound.

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