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
33 changes: 33 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,39 @@
"command": ".\\4-example_with_glfw_c.exe",
"problemMatcher": "$msCompile"
}
},
{
"label": "Run Device Hot-plug Test (Debug)",
"type": "shell",
"command": "bash",
"args": [
"-l",
"-c",
"( if [[ $(pwd) =~ ^/mnt ]]; then ./ccap_device_hotplug_test.exe; else ./ccap_device_hotplug_test; fi )"
],
"options": {
"cwd": "${workspaceFolder}/build/tests/Debug"
},
"group": "test",
"problemMatcher": "$gcc",
"dependsOn": [
"Build Project (Debug)"
],
"windows": {
"command": ".\\ccap_device_hotplug_test.exe",
"options": {
"cwd": "${workspaceFolder}/build/tests/Debug"
},
"problemMatcher": "$msCompile"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "dedicated",
"showReuseMessage": false,
"clear": true
}
}
]
}
90 changes: 75 additions & 15 deletions src/ccap_imp_windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,58 @@ using namespace ccap;
namespace {
constexpr FrameOrientation kDefaultFrameOrientation = FrameOrientation::BottomToTop;

#if defined(_MSC_VER) || (defined(__MINGW64__) && defined(__SEH__))
#define CCAP_SEH_SUPPORTED 1
#else
#define CCAP_SEH_SUPPORTED 0
#endif

enum class DeviceBindResult {
Success = 0,
Failed = 1,
Exception = 2
};

/// Bind moniker to filter with SEH protection for device validation
DeviceBindResult tryBindMonikerToFilter(IMoniker* moniker, std::string_view name) {
#if CCAP_SEH_SUPPORTED
__try {
#endif
IBaseFilter* filter = nullptr;
HRESULT hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&filter);
if (SUCCEEDED(hr) && filter) {
filter->Release();
return DeviceBindResult::Success;
} else {
CCAP_LOG_I("ccap: \"%s\" is not a valid video capture device, removed\n", name.data());
return DeviceBindResult::Failed;
}
#if CCAP_SEH_SUPPORTED
} __except (EXCEPTION_EXECUTE_HANDLER) {
CCAP_LOG_W("ccap: \"%s\" caused an exception during device binding, skipping\n", name.data());
return DeviceBindResult::Exception;
}
#endif
}

/// Bind moniker to device filter with SEH protection for device opening
DeviceBindResult tryBindMonikerForOpen(IMoniker* moniker, IBaseFilter** deviceFilter) {
#if CCAP_SEH_SUPPORTED
__try {
#endif
HRESULT hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)deviceFilter);
if (SUCCEEDED(hr)) {
return DeviceBindResult::Success;
} else {
return DeviceBindResult::Failed;
}
#if CCAP_SEH_SUPPORTED
} __except (EXCEPTION_EXECUTE_HANDLER) {
return DeviceBindResult::Exception;
}
#endif
}

// Release the format block for a media type.
void freeMediaType(AM_MEDIA_TYPE& mt) {
if (mt.cbFormat != 0) {
Expand Down Expand Up @@ -400,15 +452,12 @@ std::vector<std::string> ProviderDirectShow::findDeviceNames() {

enumerateDevices([&](IMoniker* moniker, std::string_view name) {
// Try to bind device, check if available
IBaseFilter* filter = nullptr;
HRESULT hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&filter);
if (SUCCEEDED(hr) && filter) {
// Use helper function with SEH to catch crashes from exception-throwing devices
DeviceBindResult result = tryBindMonikerToFilter(moniker, name);
if (result == DeviceBindResult::Success) {
m_allDeviceNames.emplace_back(name.data(), name.size());
filter->Release();
} else {
CCAP_LOG_I("ccap: \"%s\" is not a valid video capture device, removed\n", name.data());
}
// Unavailable devices are not added to the list
// Unavailable devices (Failed or Exception) are not added to the list
return false; // Continue enumeration
});

Expand Down Expand Up @@ -660,19 +709,30 @@ bool ProviderDirectShow::open(std::string_view deviceName) {

enumerateDevices([&](IMoniker* moniker, std::string_view name) {
if (deviceName.empty() || deviceName == name) {
auto hr = moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&m_deviceFilter);
if (SUCCEEDED(hr)) {
// Use helper function with SEH to catch crashes from exception-throwing devices
DeviceBindResult result = tryBindMonikerForOpen(moniker, &m_deviceFilter);

if (result == DeviceBindResult::Success) {
// Success - save device name and stop enumeration
CCAP_LOG_V("ccap: Using video capture device: %s\n", name.data());
m_deviceName = name;
found = true;
return true; // stop enumeration when returning true
} else {
if (!deviceName.empty()) {
return true;
} else if (!deviceName.empty()) {
// Specific device was requested but failed
if (result == DeviceBindResult::Exception) {
reportError(ErrorCode::InvalidDevice, "\"" + std::string(deviceName) + "\" caused an exception during binding");
} else {
reportError(ErrorCode::InvalidDevice, "\"" + std::string(deviceName) + "\" is not a valid video capture device, bind failed");
return true; // stop enumeration when returning true
}

CCAP_LOG_I("ccap: bind \"%s\" failed(result=%x), try next device...\n", name.data(), hr);
return true; // stop enumeration
} else {
// No specific device requested and this one failed - log and continue trying next device
if (result == DeviceBindResult::Exception) {
CCAP_LOG_W("ccap: \"%s\" caused an exception during device binding, skipping\n", name.data());
} else {
CCAP_LOG_I("ccap: bind \"%s\" failed, try next device...\n", name.data());
}
}
}
// continue enumerating when returning false
Expand Down
64 changes: 64 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,67 @@ else()
-O3
)
endif()

# ============================================================
# Interactive Device Hot-plug Test
# Tests crash prevention when enumerating unplugged devices
# with buggy drivers (Windows DirectShow)
# This test requires manual interaction and is NOT part of
# the automated test suite
# ============================================================

add_executable(
ccap_device_hotplug_test
test_device_hotplug.cpp
)

target_link_libraries(
ccap_device_hotplug_test
ccap
)

set_target_properties(ccap_device_hotplug_test PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)

# Platform-specific compile options
if(MSVC)
target_compile_options(ccap_device_hotplug_test PRIVATE
/MP
/std:c++17
/Zc:__cplusplus
/Zc:preprocessor
/source-charset:utf-8
/bigobj
/wd4996
/D_CRT_SECURE_NO_WARNINGS
)
else()
target_compile_options(ccap_device_hotplug_test PRIVATE
-std=c++17
)
endif()

# Add a custom target to run this interactive test manually
# Usage: cmake --build . --target run_device_hotplug_test
if(WIN32)
add_custom_target(run_device_hotplug_test
COMMAND ${CMAKE_COMMAND} -E echo "=========================================="
COMMAND ${CMAKE_COMMAND} -E echo "Running Device Hot-plug Interactive Test"
COMMAND ${CMAKE_COMMAND} -E echo "=========================================="
COMMAND $<TARGET_FILE:ccap_device_hotplug_test>
DEPENDS ccap_device_hotplug_test
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Running interactive device hot-plug test (buggy driver crash prevention)"
)
endif()

message(STATUS "Device hot-plug interactive test configured: ccap_device_hotplug_test")
if(WIN32)
message(STATUS " Run manually with: cmake --build . --target run_device_hotplug_test")
message(STATUS " Or execute: ${CMAKE_BINARY_DIR}/tests/Debug/ccap_device_hotplug_test.exe")
else()
message(STATUS " Run manually with: ./ccap_device_hotplug_test")
endif()

Loading