Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/mediafoundation-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ windows = { workspace = true, features = [
"Win32_Media_MediaFoundation",
"Win32_Media_DirectShow",
"Win32_System_Com",
"Win32_System_LibraryLoader",
] }

[lints]
Expand Down
70 changes: 68 additions & 2 deletions crates/mediafoundation-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,85 @@ use std::{
use windows::{
Win32::{
Media::MediaFoundation::{IMFMediaBuffer, MFSTARTUP_FULL, MFStartup},
System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize},
System::{
LibraryLoader::GetModuleHandleW,
WinRT::{RO_INIT_MULTITHREADED, RoInitialize},
},
},
core::Result,
core::{Result, PCWSTR},
};
Comment on lines +15 to 16
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 Result type clash and make MFStartup idempotent.

  • use windows::core::Result shadows std::result::Result, causing wrong error type in functions returning InitError.
  • Treat MF_E_ALREADY_INITIALIZED as success to match prior behavior and avoid spurious failures on repeated init.

Apply:

@@
-    core::{Result, PCWSTR},
+    core::{PCWSTR, Result as WinResult},
@@
-pub fn check_compatibility() -> Result<(), InitError> {
+pub fn check_compatibility() -> std::result::Result<(), InitError> {
@@
-pub fn thread_init_checked() -> Result<(), InitError> {
+pub fn thread_init_checked() -> std::result::Result<(), InitError> {
@@
-    // Initialize Media Foundation
-    unsafe { MFStartup(MF_VERSION, MFSTARTUP_FULL) }
-        .map_err(InitError::MFStartupFailed)?;
+    // Initialize Media Foundation (idempotent)
+    match unsafe { MFStartup(MF_VERSION, MFSTARTUP_FULL) } {
+        Ok(()) => {}
+        Err(e) if e.code() == windows::Win32::Media::MediaFoundation::MF_E_ALREADY_INITIALIZED => {}
+        Err(e) => return Err(InitError::MFStartupFailed(e)),
+    }
@@
-pub trait IMFMediaBufferExt {
-    fn lock(&self) -> Result<IMFMediaBufferLock<'_>>;
+pub trait IMFMediaBufferExt {
+    fn lock(&self) -> WinResult<IMFMediaBufferLock<'_>>;
 }
@@
-    fn lock(&self) -> Result<IMFMediaBufferLock<'_>> {
+    fn lock(&self) -> WinResult<IMFMediaBufferLock<'_>> {

Also applies to: 54-64, 72-87, 89-132

🤖 Prompt for AI Agents
In crates/mediafoundation-utils/src/lib.rs around lines 15-16 (and likewise
adjust occurrences around 54-64, 72-87, 89-132), fix the Result type clash and
make MFStartup idempotent: replace the unqualified import of
windows::core::Result (or avoid importing Result) so functions that return your
crate's InitError use std::result::Result<..., InitError> (or introduce a local
type alias like type StdResult<T> = std::result::Result<T, InitError>) to avoid
shadowing; then update MFStartup error handling to treat
MF_E_ALREADY_INITIALIZED as success by checking the HRESULT from MFStartup and
considering S_OK or MF_E_ALREADY_INITIALIZED as non-error (i.e., map both to
Ok(()), and map other failure HRESULTs to Err(InitError) with the original error
info).


// This is the value for Win7+
pub const MF_VERSION: u32 = 131184;

/// Custom error type for initialization failures
#[derive(Debug, thiserror::Error)]
pub enum InitError {
#[error("Third-party software conflict detected: {software}. {message}")]
ThirdPartyConflict { software: String, message: String },
#[error("Failed to initialize Windows Runtime: {0}")]
RoInitializeFailed(windows::core::Error),
#[error("Failed to initialize Media Foundation: {0}")]
MFStartupFailed(windows::core::Error),
}

/// Detects if Wondershare DemoCreator's hook DLL is loaded in the process.
/// This DLL is known to cause crashes when interfering with WinRT initialization.
pub fn detect_wondershare_democreator() -> bool {
unsafe {
// Check for CaptureGameHook_64.dll (64-bit version)
let dll_name = windows::core::w!("CaptureGameHook_64.dll");
let handle = GetModuleHandleW(PCWSTR(dll_name.as_ptr())).ok();

if handle.is_some() {
return true;
}

// Also check for the 32-bit version just in case
let dll_name_32 = windows::core::w!("CaptureGameHook.dll");
let handle_32 = GetModuleHandleW(PCWSTR(dll_name_32.as_ptr())).ok();

handle_32.is_some()
}
}

/// Checks for known incompatible third-party software before initializing.
/// Returns an error if incompatible software is detected.
pub fn check_compatibility() -> Result<(), InitError> {
if detect_wondershare_democreator() {
return Err(InitError::ThirdPartyConflict {
software: "Wondershare DemoCreator".to_string(),
message: "The Wondershare DemoCreator hook DLL (CaptureGameHook_64.dll) is interfering with Cap's screen capture system. Please close Wondershare DemoCreator and try again.".to_string(),
});
}

Ok(())
}
Comment on lines +52 to +63
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

🧩 Analysis chain

Avoid redundant pre-check at call sites.

thread_init_checked() already calls check_compatibility(). Consider removing separate check_compatibility() calls at call sites to reduce duplication.

Also applies to: 72-87


🏁 Script executed:

#!/usr/bin/env bash
# Find call sites doing both checks; expect to keep only thread_init_checked().
rg -nP --glob '!target' '(check_compatibility\(\).*\n.*thread_init_checked\(\))|thread_init_checked\(\)' -C2

Length of output: 2084


[/run_scripts]

Based on the script execution results and my analysis, the review comment contains an incorrect assumption.

The ripgrep search showed that:

  1. thread_init_checked() is called at exactly 3 locations in the codebase
  2. All three call sites only call thread_init_checked() — there are no separate calls to check_compatibility() at those locations
  3. thread_init_checked() internally calls check_compatibility() as the first operation

The review comment's suggestion to "remove separate check_compatibility() calls at call sites to reduce duplication" cannot be applied because no such separate calls exist. The codebase is already structured correctly with the compatibility check encapsulated within thread_init_checked().

Ignore the review comment—the code is already correctly organized.

[incorrect_review_comment]


🏁 Script executed:

#!/bin/bash
# Search for ALL calls to check_compatibility() function (not just the definition)
# Exclude the definition at lines 52-63 and look for actual calls
rg -nP --glob '!target' '\bcheck_compatibility\s*\(' --type=rust | grep -v '^\s*pub fn check_compatibility' | grep -v '^\s*//'

Length of output: 542


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Show context around the three direct check_compatibility() calls
echo "=== output_pipeline/win.rs around line 68 ==="
sed -n '65,80p' crates/recording/src/output_pipeline/win.rs

echo ""
echo "=== instant_recording.rs around line 293 ==="
sed -n '290,300p' crates/recording/src/instant_recording.rs

echo ""
echo "=== screen_capture/windows.rs around line 189 ==="
sed -n '186,200p' crates/recording/src/sources/screen_capture/windows.rs

Length of output: 2066


Remove redundant check_compatibility() calls at all three call sites.

thread_init_checked() already calls check_compatibility() as its first operation. Remove the separate compatibility checks:

  • crates/recording/src/output_pipeline/win.rs (lines 68–72): Remove the check_compatibility() block and keep only thread_init_checked().
  • crates/recording/src/instant_recording.rs (lines 293–295): Remove the standalone check_compatibility() call and keep only thread_init_checked().
  • crates/recording/src/sources/screen_capture/windows.rs (lines 189–194): Remove the check_compatibility() block and keep only thread_init_checked().
🤖 Prompt for AI Agents
In crates/mediafoundation-utils/src/lib.rs around lines 52 to 63,
thread_init_checked() already calls check_compatibility(), so remove the
redundant explicit calls to check_compatibility() at the three call sites: in
crates/recording/src/output_pipeline/win.rs (remove the check_compatibility()
block at lines 68–72 and keep only thread_init_checked()), in
crates/recording/src/instant_recording.rs (remove the standalone
check_compatibility() call at lines 293–295 and keep only
thread_init_checked()), and in
crates/recording/src/sources/screen_capture/windows.rs (remove the
check_compatibility() block at lines 189–194 and keep only
thread_init_checked()); update imports if any becomes unused after removal and
run cargo build/tests to ensure no unused warnings remain.


/// Initializes Windows Runtime and Media Foundation for the current thread.
/// Should be called on threads that use WinRT or Media Foundation APIs.
pub fn thread_init() {
let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) };
let _ = unsafe { MFStartup(MF_VERSION, MFSTARTUP_FULL) };
}

/// Initializes Windows Runtime and Media Foundation for the current thread with error handling.
/// Returns an error if initialization fails or if incompatible software is detected.
pub fn thread_init_checked() -> Result<(), InitError> {
// First check for known incompatible software
check_compatibility()?;

// Initialize Windows Runtime
unsafe { RoInitialize(RO_INIT_MULTITHREADED) }
.map_err(InitError::RoInitializeFailed)?;

// Initialize Media Foundation
unsafe { MFStartup(MF_VERSION, MFSTARTUP_FULL) }
.map_err(InitError::MFStartupFailed)?;

Ok(())
}

pub trait IMFMediaBufferExt {
fn lock(&self) -> Result<IMFMediaBufferLock<'_>>;
}
Expand Down
8 changes: 7 additions & 1 deletion crates/recording/src/instant_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,13 @@ pub async fn spawn_instant_recording_actor(
let content_dir = ensure_dir(&recording_dir.join("content"))?;

#[cfg(windows)]
cap_mediafoundation_utils::thread_init();
{
// Check for incompatible third-party software before initializing
cap_mediafoundation_utils::check_compatibility()?;

// Initialize Windows Runtime and Media Foundation with error handling
cap_mediafoundation_utils::thread_init_checked()?;
}

#[cfg(windows)]
let d3d_device = crate::capture_pipeline::create_d3d_device()?;
Expand Down
14 changes: 13 additions & 1 deletion crates/recording/src/output_pipeline/win.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,19 @@ impl Muxer for WindowsMuxer {
let output = output.clone();

tasks.spawn_thread("windows-encoder", move || {
cap_mediafoundation_utils::thread_init();
// Check for incompatible third-party software before initializing
if let Err(e) = cap_mediafoundation_utils::check_compatibility() {
error!("Third-party software conflict detected: {}", e);
let _ = ready_tx.send(Err(anyhow!("{}", e)));
return;
}

// Initialize Windows Runtime and Media Foundation with error handling
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

Nit: trailing whitespace (if any).

Double-check for trailing spaces around the inserted block to keep CI green, similar to the other file.


🏁 Script executed:

#!/usr/bin/env bash
rg -nP '\s+$' crates/recording/src/output_pipeline/win.rs

Length of output: 19


Remove trailing whitespace from line 73.

The verification confirmed trailing whitespace exists on line 73 (the line immediately before the comment). This needs to be cleaned up to keep CI green.

🤖 Prompt for AI Agents
crates/recording/src/output_pipeline/win.rs around lines 73-74: line 73 contains
trailing whitespace; remove the trailing space(s) at the end of that line (and
scan adjacent lines for any other trailing spaces) so the file has no trailing
whitespace, then save and re-run formatting/linters before committing.

if let Err(e) = cap_mediafoundation_utils::thread_init_checked() {
error!("Failed to initialize encoder thread: {}", e);
let _ = ready_tx.send(Err(anyhow!("{}", e)));
return;
}

let encoder = (|| {
let mut output = output.lock().unwrap();
Expand Down
14 changes: 13 additions & 1 deletion crates/recording/src/sources/screen_capture/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,19 @@ impl output_pipeline::VideoSource for VideoSource {
let tokio_rt = tokio::runtime::Handle::current();

ctx.tasks().spawn_thread("d3d-capture-thread", move || {
cap_mediafoundation_utils::thread_init();
// Check for incompatible third-party software before initializing
if let Err(e) = cap_mediafoundation_utils::check_compatibility() {
error!("Third-party software conflict detected: {}", e);
let _ = error_tx.send(anyhow!("{}", e));
return;
}

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

Fix CI: trailing whitespace.

Remove trailing whitespace at Line 194.

-            
+
📝 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
🧰 Tools
🪛 GitHub Actions: CI

[error] 194-194: left behind trailing whitespace.

🤖 Prompt for AI Agents
In crates/recording/src/sources/screen_capture/windows.rs around line 194, there
is trailing whitespace that breaks CI; open the file and remove any spaces or
tabs at the end of line 194 (and optionally run a project-wide trim or
rustfmt/EditorConfig to remove other trailing whitespace), save the file, and
commit the change.

// Initialize Windows Runtime and Media Foundation with error handling
if let Err(e) = cap_mediafoundation_utils::thread_init_checked() {
error!("Failed to initialize capture thread: {}", e);
let _ = error_tx.send(anyhow!("{}", e));
return;
}

Comment on lines +188 to 201
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

Dropped futures: use try_send() instead of send() in blocking thread.

futures::mpsc::Sender::send is async; here it’s called without .await, so the error is never sent. Use try_send.

Apply:

-                let _ = error_tx.send(anyhow!("{}", e));
+                let _ = error_tx.try_send(anyhow!("{}", e));
@@
-                let _ = error_tx.send(anyhow!("{}", e));
+                let _ = error_tx.try_send(anyhow!("{}", e));
@@
-                    let mut error_tx = error_tx.clone();
+                    let mut error_tx = error_tx.clone();
                     move || {
-                        let _ = error_tx.send(anyhow!("closed"));
+                        let _ = error_tx.try_send(anyhow!("closed"));
                     }

Also applies to: 244-250

🧰 Tools
🪛 GitHub Actions: CI

[error] 194-194: left behind trailing whitespace.

🤖 Prompt for AI Agents
In crates/recording/src/sources/screen_capture/windows.rs around lines 188-201
(and also apply the same change at lines 244-250), the code currently calls the
async futures::mpsc::Sender::send without awaiting, which drops the future and
never delivers the error; replace those send(...) calls with
error_tx.try_send(anyhow!(...)) and handle the Result (e.g., log a warning on
Err or ignore specific Closed/Full cases) so the send is attempted synchronously
and failures are observed; make the same replacement for both occurrences and
ensure you import anyhow! if needed.

// Look up the display and create the GraphicsCaptureItem on this thread to avoid COM threading issues
let capture_item = match Display::from_id(&display_id) {
Expand Down
Loading