Skip to content

Import recordings into editor and improve rendering stability#1837

Merged
richiemcilroy merged 5 commits into
mainfrom
editor-import-render-stability
May 18, 2026
Merged

Import recordings into editor and improve rendering stability#1837
richiemcilroy merged 5 commits into
mainfrom
editor-import-render-stability

Conversation

@richiemcilroy
Copy link
Copy Markdown
Member

@richiemcilroy richiemcilroy commented May 18, 2026

Summary

  • Add editor import flow for existing Cap recordings and MP4 files
  • Improve rendering/export adapter handling and YUV/frame-size stability
  • Make sccache setup opt-in and probe before enabling it

Validation

  • cargo fmt --all
  • pnpm exec biome check --write apps/desktop/src/routes/editor/Header.tsx scripts/setup.js
  • cargo check -p cap-desktop -p cap-rendering -p cap-enc-ffmpeg -p cap-frame-converter

Greptile Summary

This PR adds an editor import flow for existing Cap recordings and MP4 files, improves rendering/export adapter handling, and makes sccache opt-in. The previous security review findings have all been addressed: path-traversal is blocked by normalized_metadata_relative_path (rejects .., absolute paths, Windows drive letters) plus a canonical-path containment check that also catches symlink escapes; comprehensive unit tests cover these paths; and the frontend now correctly uses the generated commands.addExistingRecordingToEditor typed binding.

  • New import command (import.rs): add_existing_recording_to_editor copies Cap project segments or transcodes an MP4 into the target project with full path validation, extension allowlists, and cursor-ID remapping.
  • Rendering stability (display.rs, yuv_converter.rs): Frame textures are now sized to the actual source frame rather than the configured frame size, and YUV plane stride is validated before upload.
  • Adapter detection (d3d11.rs, rendering/lib.rs, enc-ffmpeg): Non-hardware adapters (Parsec, DisplayLink, Splashtop, WARP, etc.) are now detected consistently and excluded from hardware-encoder selection.

Confidence Score: 5/5

Safe to merge; the new import path is well-guarded against path traversal and the rendering fixes are mechanical substitutions of source_size for frame_size.

All changed paths are either additive (new import command with thorough validation and tests) or targeted bug fixes (frame-size mismatches, stride guards, adapter detection). No regressions were identified in the existing export or rendering flows.

No files require special attention; the duplicate marker list in crates/rendering/src/lib.rs and crates/frame-converter/src/d3d11.rs is worth a future clean-up but does not affect correctness today.

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/import.rs Adds add_existing_recording_to_editor command with robust path-traversal defenses (normalized relative paths, extension allowlists, canonical path containment checks, symlink escape tests), plus helpers for copying segments/cursors/audio into target projects.
crates/frame-converter/src/d3d11.rs Adds NON_HARDWARE_ADAPTER_MARKERS list to detect virtual adapters (Parsec, DisplayLink, Splashtop, WARP, etc.) and consolidates the is_software detection into is_non_hardware_adapter; is_warp() retains its original narrow semantics.
crates/rendering/src/lib.rs Expands is_software_wgpu_adapter to also flag wgpu::DeviceType::VirtualGpu and matches against the same set of non-hardware adapter names as d3d11.rs.
crates/rendering/src/layers/display.rs Fixes frame-size mismatch by using the actual source frame dimensions (source_size) instead of the configured frame_size for texture creation, upload, and YUV conversion, and scales crop/frame uniforms accordingly.
crates/enc-ffmpeg/src/video/h264.rs Skips the software-encoder fallback when is_export is true and constrains the AMD export priority override to adapters that actually support hardware encoding.
apps/desktop/src/routes/editor/Header.tsx Adds import recording UI (dialog + context menu) using commands.addExistingRecordingToEditor typed binding; filters out the current project and non-complete recordings before display.

Comments Outside Diff (1)

  1. apps/desktop/src-tauri/src/import.rs, line 928-1046 (link)

    P2 No progress events emitted during Cap project import

    append_cap_project_to_editor_project can copy many segments, cursor images, and event files without emitting any emit_progress calls, while append_mp4_to_editor_project emits ImportStage::Probing, ImportStage::Converting, and ImportStage::Complete. For a large multi-segment project the user will see only the "Importing recording…" toast with no progress indication until the operation finishes.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/import.rs
    Line: 928-1046
    
    Comment:
    **No progress events emitted during Cap project import**
    
    `append_cap_project_to_editor_project` can copy many segments, cursor images, and event files without emitting any `emit_progress` calls, while `append_mp4_to_editor_project` emits `ImportStage::Probing`, `ImportStage::Converting`, and `ImportStage::Complete`. For a large multi-segment project the user will see only the "Importing recording…" toast with no progress indication until the operation finishes.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/rendering/src/lib.rs:100-110
**Duplicate adapter marker list**

`NON_HARDWARE_WGPU_ADAPTER_MARKERS` here is identical to `NON_HARDWARE_ADAPTER_MARKERS` in `crates/frame-converter/src/d3d11.rs`. If a new virtual-adapter name is added to one list in the future it is easy to miss updating the other, silently leaving one subsystem blind to the new adapter. Extracting a shared constant (e.g. in a common crate, or by re-exporting from `cap-frame-converter`) would keep the two in sync automatically.

Reviews (3): Last reviewed commit: "fix: validate imported recording asset p..." | Re-trigger Greptile

@richiemcilroy richiemcilroy marked this pull request as ready for review May 18, 2026 13:49
@superagent-security superagent-security Bot added contributor:verified Contributor passed trust analysis. pr:flagged PR flagged for review by security analysis. labels May 18, 2026
Comment thread crates/frame-converter/src/d3d11.rs Outdated
Comment on lines +108 to +111
}

pub fn is_warp(&self) -> bool {
self.description.contains("Microsoft Basic Render Driver")
|| self.description.contains("WARP")
matches_non_hardware_adapter_marker(&self.description)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 is_warp() now matches non-WARP adapters, causing semantic drift

After this change, is_warp() delegates to matches_non_hardware_adapter_marker, so it returns true for Parsec, DisplayLink, Splashtop, and other virtual adapters — not just WARP. Any call site that uses is_warp() to distinguish WARP-specific behavior from, say, "Parsec virtual display" will silently conflate them. Consider renaming the method to is_non_hardware_adapter() or is_virtual_display() to match the expanded semantics, or keeping a narrower WARP-only check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/frame-converter/src/d3d11.rs
Line: 108-111

Comment:
**`is_warp()` now matches non-WARP adapters, causing semantic drift**

After this change, `is_warp()` delegates to `matches_non_hardware_adapter_marker`, so it returns `true` for Parsec, DisplayLink, Splashtop, and other virtual adapters — not just WARP. Any call site that uses `is_warp()` to distinguish WARP-specific behavior from, say, "Parsec virtual display" will silently conflate them. Consider renaming the method to `is_non_hardware_adapter()` or `is_virtual_display()` to match the expanded semantics, or keeping a narrower WARP-only check.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +134 to +137
const importedCount = await invoke<number>(
"add_existing_recording_to_editor",
{ sourcePath },
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The command invocation uses the raw invoke function instead of the generated typed binding commands.addExistingRecordingToEditor that was added in this same PR to tauri.ts. The rest of this file consistently uses commands.*, and bypassing the generated binding means any future signature change won't be caught by the TypeScript compiler.

Suggested change
const importedCount = await invoke<number>(
"add_existing_recording_to_editor",
{ sourcePath },
);
const importedCount =
await commands.addExistingRecordingToEditor(sourcePath);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/editor/Header.tsx
Line: 134-137

Comment:
The command invocation uses the raw `invoke` function instead of the generated typed binding `commands.addExistingRecordingToEditor` that was added in this same PR to `tauri.ts`. The rest of this file consistently uses `commands.*`, and bypassing the generated binding means any future signature change won't be caught by the TypeScript compiler.

```suggestion
			const importedCount =
				await commands.addExistingRecordingToEditor(sourcePath);
```

How can I resolve this? If you propose a fix, please make it concise.

@@ -1,14 +1,20 @@
import { Button } from "@cap/ui-solid";
import { Dialog as KDialog } from "@kobalte/core/dialog";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You can drop the direct invoke import once the call site uses the typed commands wrapper.

Suggested change
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { convertFileSrc } from "@tauri-apps/api/core";

Comment on lines +133 to +137
await commands.setProjectConfig(serializeProjectConfiguration(project));
const importedCount = await invoke<number>(
"add_existing_recording_to_editor",
{ sourcePath },
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor consistency/typing win: use the generated commands binding instead of a raw string command.

Suggested change
await commands.setProjectConfig(serializeProjectConfiguration(project));
const importedCount = await invoke<number>(
"add_existing_recording_to_editor",
{ sourcePath },
);
await commands.setProjectConfig(serializeProjectConfiguration(project));
const importedCount = await commands.addExistingRecordingToEditor(sourcePath);

Comment thread scripts/setup.js Outdated
Comment on lines +438 to +462
await fs.writeFile(probePath, "");
await execFile(sccachePath, [
rustcPath,
probePath,
"--crate-name",
"___",
"--print=file-names",
"--crate-type",
"bin",
"--crate-type",
"rlib",
"--crate-type",
"dylib",
"--crate-type",
"cdylib",
"--crate-type",
"staticlib",
"--crate-type",
"proc-macro",
"--print=sysroot",
"--print=split-debuginfo",
"--print=crate-name",
"--print=cfg",
"-Wwarnings",
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This probe is likely to fail with an empty source file (e.g. --crate-type bin expects main), which would make sccache look unusable even when it is. Also, proc-macro generally needs special source.

Suggested change
await fs.writeFile(probePath, "");
await execFile(sccachePath, [
rustcPath,
probePath,
"--crate-name",
"___",
"--print=file-names",
"--crate-type",
"bin",
"--crate-type",
"rlib",
"--crate-type",
"dylib",
"--crate-type",
"cdylib",
"--crate-type",
"staticlib",
"--crate-type",
"proc-macro",
"--print=sysroot",
"--print=split-debuginfo",
"--print=crate-name",
"--print=cfg",
"-Wwarnings",
]);
await fs.writeFile(probePath, "fn main() {}\n");
await execFile(sccachePath, [
rustcPath,
probePath,
"--crate-name",
"___",
"--print=file-names",
"--crate-type",
"bin",
"--crate-type",
"rlib",
"--crate-type",
"dylib",
"--crate-type",
"cdylib",
"--crate-type",
"staticlib",
"--print=sysroot",
"--print=split-debuginfo",
"--print=crate-name",
"--print=cfg",
"-Wwarnings",
]);

Comment thread crates/enc-ffmpeg/src/video/h264.rs Outdated
GpuVendor::Intel => ENCODER_PRIORITY_INTEL,
_ => ENCODER_PRIORITY_DEFAULT,
},
None => &["libx264"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If DXGI enumeration fails (detect_primary_gpu() == None), forcing libx264 can unnecessarily disable HW encoding on systems where encoders work but detection is flaky. I'd keep the old default order in that case and let the encoder-open step decide.

Suggested change
None => &["libx264"],
None => ENCODER_PRIORITY_DEFAULT,

@richiemcilroy
Copy link
Copy Markdown
Member Author

please re-review the pr @greptileai

Ok(1)
}

async fn append_cap_project_to_editor_project(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice import path. One UX thing: append_cap_project_to_editor_project can copy a lot of segment/cursor/event data but doesn't emit any emit_progress updates (unlike append_mp4_to_editor_project). Even a couple of stage + percentage updates in the copy loop would help avoid a "stuck" toast on large projects.

Suggested change
async fn append_cap_project_to_editor_project(
let project_path_str = target_project_path.to_string_lossy().to_string();
emit_progress(&app, &project_path_str, ImportStage::Finalizing, 0.0, "Importing Cap project...");

@superagent-security superagent-security Bot added pr:verified PR passed security analysis. and removed pr:flagged PR flagged for review by security analysis. labels May 18, 2026
@richiemcilroy
Copy link
Copy Markdown
Member Author

please re-review the pr @greptile just addressed the security review

: `${importedCount} recordings imported`,
{ id: toastId },
);
window.location.reload();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This reload happens immediately after toast.success(...), so the success state likely never renders. Either drop the success toast or delay the reload slightly.

Suggested change
window.location.reload();
setTimeout(() => window.location.reload(), 750);


let can_decode =
probe_video_can_decode(&source_path).map_err(|e| format!("Cannot decode video: {e}"))?;
if !can_decode {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

unique_segment_dir(...) creates the segment directory before probing/transcoding. If we return early here, that new directory becomes an unreferenced disk leak.

Suggested change
if !can_decode {
if !can_decode {
let _ = std::fs::remove_dir_all(target_project_path.join(&target_relative_dir));
return Err("Video format not supported or file is corrupted".to_string());
}

)
})
.await
.map_err(|e| format!("Video import task failed: {e}"))?
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Similarly, if transcode_video fails after the dir is created, we return early and leave the orphaned segment dir behind. Cleaning up in the error mapping keeps the filesystem tidy.

Suggested change
.map_err(|e| format!("Video import task failed: {e}"))?
.await
.map_err(|e| {
let _ = std::fs::remove_dir_all(target_project_path.join(&target_relative_dir));
format!("Video import task failed: {e}")
})?
.map_err(|e| {
let _ = std::fs::remove_dir_all(target_project_path.join(&target_relative_dir));
e.to_string()
})?;

@richiemcilroy richiemcilroy merged commit 6859fb4 into main May 18, 2026
19 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant