diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 0b1932a..a8ea362 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -69,6 +69,19 @@ function isPathAllowed(filePath: string): boolean { return getAllowedReadDirs().some((dir) => isPathWithinDir(resolved, dir)); } +function resolveApprovedVideoPath(videoPath?: string | null): string | null { + const normalizedPath = normalizeVideoSourcePath(videoPath); + if (!normalizedPath) { + return null; + } + + if (!hasAllowedImportVideoExtension(normalizedPath) || !isPathAllowed(normalizedPath)) { + return null; + } + + return normalizedPath; +} + /** * Helper function to build dialog options with a parent window only when it's valid. * This prevents passing stale or destroyed BrowserWindow references to dialog calls. @@ -1363,7 +1376,7 @@ export function registerIpcHandlers( }); ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => { - const targetVideoPath = normalizeVideoSourcePath( + const targetVideoPath = resolveApprovedVideoPath( videoPath ?? currentRecordingSession?.screenVideoPath, ); if (!targetVideoPath) { @@ -1494,11 +1507,18 @@ export function registerIpcHandlers( return { success: false, canceled: true }; } - approveFilePath(result.filePaths[0]); + const normalizedPath = await approveReadableVideoPath(result.filePaths[0]); + if (!normalizedPath) { + return { + success: false, + message: "Selected file is not a supported readable video file", + }; + } + currentProjectPath = null; return { success: true, - path: result.filePaths[0], + path: normalizedPath, }; } catch (error) { console.error("Failed to open file picker:", error); diff --git a/electron/native/wgc-capture/src/dshow_webcam_capture.cpp b/electron/native/wgc-capture/src/dshow_webcam_capture.cpp index 6e0ec4f..14cb888 100644 --- a/electron/native/wgc-capture/src/dshow_webcam_capture.cpp +++ b/electron/native/wgc-capture/src/dshow_webcam_capture.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include namespace { @@ -43,115 +43,6 @@ bool succeeded(HRESULT hr, const char* label) { return false; } -std::wstring readPropertyString(IPropertyBag* bag, LPCOLESTR key) { - VARIANT value; - VariantInit(&value); - if (FAILED(bag->Read(key, &value, nullptr)) || value.vt != VT_BSTR || !value.bstrVal) { - VariantClear(&value); - return {}; - } - - std::wstring result(value.bstrVal); - VariantClear(&value); - return result; -} - -bool containsInsensitive(const std::wstring& haystack, const std::wstring& needle) { - if (haystack.empty() || needle.empty()) { - return false; - } - - std::wstring lowerHaystack = haystack; - std::wstring lowerNeedle = needle; - std::transform(lowerHaystack.begin(), lowerHaystack.end(), lowerHaystack.begin(), ::towlower); - std::transform(lowerNeedle.begin(), lowerNeedle.end(), lowerNeedle.begin(), ::towlower); - return lowerHaystack.find(lowerNeedle) != std::wstring::npos || - lowerNeedle.find(lowerHaystack) != std::wstring::npos; -} - -std::wstring normalizeDeviceName(const std::wstring& value) { - std::wstring normalized; - normalized.reserve(value.size()); - bool lastWasSpace = true; - for (const wchar_t ch : value) { - if (std::iswalnum(ch)) { - normalized.push_back(static_cast(std::towlower(ch))); - lastWasSpace = false; - continue; - } - if (!lastWasSpace) { - normalized.push_back(L' '); - lastWasSpace = true; - } - } - while (!normalized.empty() && normalized.back() == L' ') { - normalized.pop_back(); - } - return normalized; -} - -std::vector splitWords(const std::wstring& value) { - std::vector words; - size_t start = 0; - while (start < value.size()) { - const size_t end = value.find(L' ', start); - const auto word = value.substr(start, end == std::wstring::npos ? std::wstring::npos : end - start); - if (word.size() > 1 && word != L"camera" && word != L"webcam" && word != L"video" && word != L"input") { - words.push_back(word); - } - if (end == std::wstring::npos) { - break; - } - start = end + 1; - } - return words; -} - -int deviceMatchScore( - const std::wstring& candidateName, - const std::wstring& candidatePath, - const std::wstring& requestedName, - const std::wstring& requestedId) { - int score = 0; - const auto normalizedName = normalizeDeviceName(candidateName); - const auto normalizedPath = normalizeDeviceName(candidatePath); - const auto normalizedRequestedName = normalizeDeviceName(requestedName); - const auto normalizedRequestedId = normalizeDeviceName(requestedId); - - if (!normalizedRequestedName.empty()) { - if (normalizedName == normalizedRequestedName) { - score = std::max(score, 1000); - } - if (containsInsensitive(normalizedName, normalizedRequestedName)) { - score = std::max(score, 900); - } - if (containsInsensitive(normalizedPath, normalizedRequestedName)) { - score = std::max(score, 800); - } - - int wordScore = 0; - for (const auto& word : splitWords(normalizedRequestedName)) { - if (normalizedName.find(word) != std::wstring::npos) { - wordScore += 100; - } else if (normalizedPath.find(word) != std::wstring::npos) { - wordScore += 50; - } - } - score = std::max(score, wordScore); - } - - if (!normalizedRequestedId.empty()) { - if (containsInsensitive(normalizedPath, normalizedRequestedId)) { - score = std::max(score, 700); - } - if (containsInsensitive(normalizedName, normalizedRequestedId)) { - score = std::max(score, 600); - } - } - - return score; -} - void freeMediaType(AM_MEDIA_TYPE& type) { if (type.cbFormat != 0) { CoTaskMemFree(type.pbFormat); @@ -164,70 +55,6 @@ void freeMediaType(AM_MEDIA_TYPE& type) { } } -bool readRegistryString(HKEY key, const wchar_t* valueName, std::wstring& value) { - DWORD type = 0; - DWORD size = 0; - if (RegGetValueW(key, nullptr, valueName, RRF_RT_REG_SZ, &type, nullptr, &size) != ERROR_SUCCESS || size == 0) { - return false; - } - - std::wstring buffer(size / sizeof(wchar_t), L'\0'); - if (RegGetValueW(key, nullptr, valueName, RRF_RT_REG_SZ, &type, buffer.data(), &size) != ERROR_SUCCESS) { - return false; - } - while (!buffer.empty() && buffer.back() == L'\0') { - buffer.pop_back(); - } - value = buffer; - return true; -} - -bool findRegisteredVideoInput( - const std::wstring& deviceId, - const std::wstring& deviceName, - CLSID& selectedClsid, - std::wstring& selectedName, - int& bestScore) { - HKEY instanceKey = nullptr; - if (RegOpenKeyExW( - HKEY_CLASSES_ROOT, - L"CLSID\\{860BB310-5D01-11D0-BD3B-00A0C911CE86}\\Instance", - 0, - KEY_READ, - &instanceKey) != ERROR_SUCCESS) { - return false; - } - - DWORD index = 0; - wchar_t subkeyName[128]; - DWORD subkeyNameLength = ARRAYSIZE(subkeyName); - bool found = false; - while (RegEnumKeyExW(instanceKey, index, subkeyName, &subkeyNameLength, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) { - HKEY filterKey = nullptr; - if (RegOpenKeyExW(instanceKey, subkeyName, 0, KEY_READ, &filterKey) == ERROR_SUCCESS) { - std::wstring friendlyName; - std::wstring clsidText; - readRegistryString(filterKey, L"FriendlyName", friendlyName); - readRegistryString(filterKey, L"CLSID", clsidText); - const int score = deviceMatchScore(friendlyName, clsidText, deviceName, deviceId); - std::wcerr << L"INFO: Registered DirectShow webcam candidate name=\"" << friendlyName << L"\" score=" << score << std::endl; - CLSID clsid{}; - if (!clsidText.empty() && SUCCEEDED(CLSIDFromString(clsidText.c_str(), &clsid)) && (!found || score > bestScore)) { - selectedClsid = clsid; - selectedName = friendlyName; - bestScore = score; - found = true; - } - RegCloseKey(filterKey); - } - index += 1; - subkeyNameLength = ARRAYSIZE(subkeyName); - } - - RegCloseKey(instanceKey); - return found; -} - } // namespace struct DirectShowWebcamCapture::Impl { @@ -254,6 +81,7 @@ bool DirectShowWebcamCapture::initialize( int requestedWidth, int requestedHeight, int requestedFps) { + (void)deviceId; stop(); delete impl_; impl_ = nullptr; @@ -371,9 +199,21 @@ bool DirectShowWebcamCapture::start() { if (!succeeded(hr, "Run(DirectShow webcam)")) { return false; } - impl_->running = true; stopRequested_ = false; - thread_ = std::thread(&DirectShowWebcamCapture::captureLoop, this); + try { + thread_ = std::thread(&DirectShowWebcamCapture::captureLoop, this); + } catch (const std::exception& error) { + stopRequested_ = true; + impl_->mediaControl->Stop(); + std::cerr << "ERROR: Failed to start DirectShow webcam capture thread: " << error.what() << std::endl; + return false; + } catch (...) { + stopRequested_ = true; + impl_->mediaControl->Stop(); + std::cerr << "ERROR: Failed to start DirectShow webcam capture thread" << std::endl; + return false; + } + impl_->running = true; return true; } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 4634037..4971ff7 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -402,24 +402,30 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } activeNativeRecording.finalizing = true; - nativeWindowsRecording.current = null; - setRecording(false); - setPaused(false); - setElapsedSeconds(0); - accumulatedDurationMs.current = 0; - segmentStartedAt.current = null; + + const clearNativeRecordingState = () => { + nativeWindowsRecording.current = null; + setRecording(false); + setPaused(false); + setElapsedSeconds(0); + accumulatedDurationMs.current = 0; + segmentStartedAt.current = null; + }; try { const result = await window.electronAPI.stopNativeWindowsRecording(discard); if (discard || result.discarded) { + clearNativeRecordingState(); return true; } if (!result.success) { console.error("Failed to stop native Windows recording:", result.error); toast.error(result.error ?? "Failed to stop native Windows recording"); + activeNativeRecording.finalizing = false; return true; } + clearNativeRecordingState(); if (result.session) { await window.electronAPI.setCurrentRecordingSession(result.session); } else if (result.path) { @@ -433,6 +439,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { toast.error( error instanceof Error ? error.message : "Failed to save native Windows recording", ); + activeNativeRecording.finalizing = false; return true; } finally { if (discardRecordingId.current === activeNativeRecording.recordingId) { @@ -582,6 +589,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const availability = await window.electronAPI.isNativeWindowsCaptureAvailable(); if (!availability.success || !availability.available) { + if (availability.reason === "unsupported-os") { + return false; + } + throw new Error( availability.reason === "missing-helper" ? "Native Windows capture helper is not available."