diff --git a/electron/native/windows-capture/CMakeLists.txt b/electron/native/windows-capture/CMakeLists.txt deleted file mode 100644 index 773452c4..00000000 --- a/electron/native/windows-capture/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(windows-capture LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -add_executable(windows-capture - src/main.cpp - src/dxgi_session.cpp - src/mf_encoder.cpp - src/monitor_utils.cpp - src/wasapi_loopback.cpp -) - -target_compile_options(windows-capture PRIVATE /EHsc /W3 /utf-8) - -target_link_libraries(windows-capture PRIVATE - d3d11 - dxgi - dwmapi - mfplat - mfreadwrite - mf - mfuuid - ole32 - shcore -) diff --git a/electron/native/windows-capture/src/dxgi_session.cpp b/electron/native/windows-capture/src/dxgi_session.cpp deleted file mode 100644 index 76c91402..00000000 --- a/electron/native/windows-capture/src/dxgi_session.cpp +++ /dev/null @@ -1,343 +0,0 @@ -#include "dxgi_session.h" - -#include - -#include -#include -#include - -namespace { - -bool intersectRectChecked(const RECT& lhs, const RECT& rhs, RECT& result) { - return IntersectRect(&result, &lhs, &rhs) != FALSE; -} - -int evenFloor(int value) { - return value > 1 ? (value & ~1) : value; -} - -RECT getExtendedWindowBounds(HWND hwnd) { - RECT bounds{}; - if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &bounds, sizeof(bounds)))) { - return bounds; - } - - GetWindowRect(hwnd, &bounds); - return bounds; -} - -} // namespace - -DxgiSession::DxgiSession() {} - -DxgiSession::~DxgiSession() { - stopCapture(); -} - -bool DxgiSession::findOutputForMonitor(HMONITOR monitor, ComPtr& adapter, ComPtr& output) { - ComPtr factory; - HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); - if (FAILED(hr)) { - std::cerr << "ERROR: CreateDXGIFactory1 failed: 0x" << std::hex << hr << std::endl; - return false; - } - - for (UINT adapterIndex = 0;; ++adapterIndex) { - ComPtr candidateAdapter; - hr = factory->EnumAdapters1(adapterIndex, &candidateAdapter); - if (hr == DXGI_ERROR_NOT_FOUND) { - break; - } - if (FAILED(hr)) { - continue; - } - - for (UINT outputIndex = 0;; ++outputIndex) { - ComPtr candidateOutput; - hr = candidateAdapter->EnumOutputs(outputIndex, &candidateOutput); - if (hr == DXGI_ERROR_NOT_FOUND) { - break; - } - if (FAILED(hr)) { - continue; - } - - DXGI_OUTPUT_DESC desc{}; - if (FAILED(candidateOutput->GetDesc(&desc))) { - continue; - } - - if (!desc.AttachedToDesktop || desc.Monitor != monitor) { - continue; - } - - ComPtr output1; - hr = candidateOutput.As(&output1); - if (FAILED(hr)) { - continue; - } - - adapter = candidateAdapter; - output = output1; - outputDesc_ = desc; - return true; - } - } - - return false; -} - -bool DxgiSession::createD3DDevice(IDXGIAdapter1* adapter) { - UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; - D3D_FEATURE_LEVEL featureLevels[] = { - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - }; - - HRESULT hr = D3D11CreateDevice( - adapter, - D3D_DRIVER_TYPE_UNKNOWN, - nullptr, - creationFlags, - featureLevels, - ARRAYSIZE(featureLevels), - D3D11_SDK_VERSION, - &d3dDevice_, - nullptr, - &d3dContext_); - - if (FAILED(hr)) { - std::cerr << "ERROR: D3D11CreateDevice failed: 0x" << std::hex << hr << std::endl; - return false; - } - - return true; -} - -bool DxgiSession::createDuplication(IDXGIOutput1* output) { - HRESULT hr = output->DuplicateOutput(d3dDevice_.Get(), &duplication_); - if (FAILED(hr)) { - std::cerr << "ERROR: DuplicateOutput failed: 0x" << std::hex << hr << std::endl; - return false; - } - - return true; -} - -bool DxgiSession::createCaptureTexture() { - if (captureWidth_ <= 0 || captureHeight_ <= 0) { - return false; - } - - D3D11_TEXTURE2D_DESC desc{}; - desc.Width = static_cast(captureWidth_); - desc.Height = static_cast(captureHeight_); - desc.MipLevels = 1; - desc.ArraySize = 1; - desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - desc.SampleDesc.Count = 1; - desc.Usage = D3D11_USAGE_DEFAULT; - - HRESULT hr = d3dDevice_->CreateTexture2D(&desc, nullptr, &captureTexture_); - if (FAILED(hr)) { - std::cerr << "ERROR: CreateTexture2D failed: 0x" << std::hex << hr << std::endl; - return false; - } - - return true; -} - -bool DxgiSession::initializeFullMonitorRect() { - const int width = outputDesc_.DesktopCoordinates.right - outputDesc_.DesktopCoordinates.left; - const int height = outputDesc_.DesktopCoordinates.bottom - outputDesc_.DesktopCoordinates.top; - - captureWidth_ = evenFloor(width); - captureHeight_ = evenFloor(height); - if (captureWidth_ <= 0 || captureHeight_ <= 0) { - return false; - } - - captureRect_.left = 0; - captureRect_.top = 0; - captureRect_.right = captureWidth_; - captureRect_.bottom = captureHeight_; - return true; -} - -bool DxgiSession::updateWindowCaptureRect() { - if (!captureWindow_ || !windowHandle_) { - return false; - } - - RECT windowRect = getExtendedWindowBounds(windowHandle_); - RECT monitorRect = outputDesc_.DesktopCoordinates; - RECT clippedRect{}; - if (!intersectRectChecked(windowRect, monitorRect, clippedRect)) { - return false; - } - - int left = clippedRect.left - monitorRect.left; - int top = clippedRect.top - monitorRect.top; - const int monitorWidth = monitorRect.right - monitorRect.left; - const int monitorHeight = monitorRect.bottom - monitorRect.top; - - left = std::clamp(left, 0, std::max(0, monitorWidth - captureWidth_)); - top = std::clamp(top, 0, std::max(0, monitorHeight - captureHeight_)); - - captureRect_.left = left; - captureRect_.top = top; - captureRect_.right = left + captureWidth_; - captureRect_.bottom = top + captureHeight_; - return true; -} - -bool DxgiSession::initializeWindowRect(HWND hwnd) { - windowHandle_ = hwnd; - captureWindow_ = true; - - RECT windowRect = getExtendedWindowBounds(hwnd); - RECT monitorRect = outputDesc_.DesktopCoordinates; - RECT clippedRect{}; - if (!intersectRectChecked(windowRect, monitorRect, clippedRect)) { - return false; - } - - captureWidth_ = evenFloor(clippedRect.right - clippedRect.left); - captureHeight_ = evenFloor(clippedRect.bottom - clippedRect.top); - if (captureWidth_ <= 0 || captureHeight_ <= 0) { - return false; - } - - return updateWindowCaptureRect(); -} - -bool DxgiSession::initializeForMonitor(HMONITOR monitor, int fps) { - fps_ = fps; - frameIntervalHns_ = 10000000LL / fps_; - - ComPtr adapter; - ComPtr output; - if (!findOutputForMonitor(monitor, adapter, output)) { - std::cerr << "ERROR: Failed to find DXGI output for monitor" << std::endl; - return false; - } - - if (!createD3DDevice(adapter.Get())) { - return false; - } - - if (!createDuplication(output.Get())) { - return false; - } - - return true; -} - -bool DxgiSession::initialize(HMONITOR monitor, int fps) { - captureWindow_ = false; - windowHandle_ = nullptr; - - if (!initializeForMonitor(monitor, fps)) { - return false; - } - - return initializeFullMonitorRect() && createCaptureTexture(); -} - -bool DxgiSession::initialize(HWND hwnd, int fps) { - HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - if (!monitor) { - std::cerr << "ERROR: MonitorFromWindow failed" << std::endl; - return false; - } - - if (!initializeForMonitor(monitor, fps)) { - return false; - } - - return initializeWindowRect(hwnd) && createCaptureTexture(); -} - -void DxgiSession::setFrameCallback(FrameCallback callback) { - frameCallback_ = std::move(callback); -} - -bool DxgiSession::startCapture() { - if (!duplication_ || !captureTexture_) { - return false; - } - - capturing_ = true; - lastFrameTimeHns_ = 0; - captureThread_ = std::thread(&DxgiSession::captureLoop, this); - return true; -} - -void DxgiSession::stopCapture() { - capturing_ = false; - - if (captureThread_.joinable()) { - captureThread_.join(); - } - - duplication_.Reset(); - captureTexture_.Reset(); -} - -int64_t DxgiSession::nowHns() const { - return std::chrono::duration_cast>>( - std::chrono::steady_clock::now().time_since_epoch()).count(); -} - -void DxgiSession::captureLoop() { - while (capturing_) { - DXGI_OUTDUPL_FRAME_INFO frameInfo{}; - ComPtr desktopResource; - HRESULT hr = duplication_->AcquireNextFrame(100, &frameInfo, &desktopResource); - - if (hr == DXGI_ERROR_WAIT_TIMEOUT) { - continue; - } - - if (FAILED(hr)) { - if (hr != DXGI_ERROR_ACCESS_LOST) { - std::cerr << "ERROR: AcquireNextFrame failed: 0x" << std::hex << hr << std::endl; - } - break; - } - - ComPtr sourceTexture; - hr = desktopResource.As(&sourceTexture); - if (SUCCEEDED(hr) && sourceTexture) { - if (!captureWindow_ || updateWindowCaptureRect()) { - const int64_t timestampHns = nowHns(); - if (lastFrameTimeHns_ == 0 || (timestampHns - lastFrameTimeHns_) >= (frameIntervalHns_ * 7 / 10)) { - D3D11_BOX sourceBox{}; - sourceBox.left = static_cast(captureRect_.left); - sourceBox.top = static_cast(captureRect_.top); - sourceBox.front = 0; - sourceBox.right = static_cast(captureRect_.right); - sourceBox.bottom = static_cast(captureRect_.bottom); - sourceBox.back = 1; - - d3dContext_->CopySubresourceRegion( - captureTexture_.Get(), - 0, - 0, - 0, - 0, - sourceTexture.Get(), - 0, - &sourceBox); - - if (frameCallback_) { - lastFrameTimeHns_ = timestampHns; - frameCallback_(captureTexture_.Get(), timestampHns); - } - } - } - } - - duplication_->ReleaseFrame(); - } -} \ No newline at end of file diff --git a/electron/native/windows-capture/src/dxgi_session.h b/electron/native/windows-capture/src/dxgi_session.h deleted file mode 100644 index 37cd04b7..00000000 --- a/electron/native/windows-capture/src/dxgi_session.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include -#include - -using Microsoft::WRL::ComPtr; - -class DxgiSession { -public: - using FrameCallback = std::function; - - DxgiSession(); - ~DxgiSession(); - - bool initialize(HMONITOR monitor, int fps); - bool initialize(HWND hwnd, int fps); - void setFrameCallback(FrameCallback callback); - bool startCapture(); - void stopCapture(); - - int captureWidth() const { return captureWidth_; } - int captureHeight() const { return captureHeight_; } - ID3D11Device* device() const { return d3dDevice_.Get(); } - ID3D11DeviceContext* context() const { return d3dContext_.Get(); } - -private: - ComPtr d3dDevice_; - ComPtr d3dContext_; - ComPtr duplication_; - ComPtr captureTexture_; - - FrameCallback frameCallback_; - std::atomic capturing_{false}; - std::thread captureThread_; - - DXGI_OUTPUT_DESC outputDesc_{}; - RECT captureRect_{}; - HWND windowHandle_ = nullptr; - bool captureWindow_ = false; - int fps_ = 60; - int captureWidth_ = 0; - int captureHeight_ = 0; - int64_t frameIntervalHns_ = 0; - int64_t lastFrameTimeHns_ = 0; - - bool initializeForMonitor(HMONITOR monitor, int fps); - bool findOutputForMonitor(HMONITOR monitor, ComPtr& adapter, ComPtr& output); - bool createD3DDevice(IDXGIAdapter1* adapter); - bool createDuplication(IDXGIOutput1* output); - bool createCaptureTexture(); - bool initializeFullMonitorRect(); - bool initializeWindowRect(HWND hwnd); - bool updateWindowCaptureRect(); - void captureLoop(); - int64_t nowHns() const; -}; \ No newline at end of file diff --git a/electron/native/windows-capture/src/main.cpp b/electron/native/windows-capture/src/main.cpp deleted file mode 100644 index 2aac86a7..00000000 --- a/electron/native/windows-capture/src/main.cpp +++ /dev/null @@ -1,324 +0,0 @@ -#include "dxgi_session.h" -#include "mf_encoder.h" -#include "monitor_utils.h" -#include "wasapi_loopback.h" - -#include -#include -#include -#include -#include -#include -#include - -static std::atomic g_stopRequested{false}; -static std::atomic g_pauseRequested{false}; -static std::atomic g_resumePending{false}; -static std::atomic g_lastFrameTimestampHns{0}; -static std::atomic g_pauseStartTimestampHns{0}; -static std::atomic g_accumulatedPausedHns{0}; -static std::mutex g_stopMutex; -static std::condition_variable g_stopCv; - -struct CaptureConfig { - int64_t displayId = 0; - int64_t windowHandle = 0; - std::string outputPath; - std::string audioOutputPath; - std::string micOutputPath; - std::string micDeviceName; - int fps = 60; - int width = 0; - int height = 0; - bool captureSystemAudio = false; - bool captureMic = false; -}; - -static bool parseSimpleJson(const std::string& json, CaptureConfig& config) { - auto findInt = [&](const std::string& key) -> int { - auto pos = json.find("\"" + key + "\""); - if (pos == std::string::npos) return -1; - pos = json.find(':', pos); - if (pos == std::string::npos) return -1; - pos++; - while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; - try { - return std::stoi(json.substr(pos)); - } catch (...) { - return -1; - } - }; - - auto findInt64 = [&](const std::string& key) -> int64_t { - auto pos = json.find("\"" + key + "\""); - if (pos == std::string::npos) return -1; - pos = json.find(':', pos); - if (pos == std::string::npos) return -1; - pos++; - while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; - try { - return std::stoll(json.substr(pos)); - } catch (...) { - return -1; - } - }; - - auto findString = [&](const std::string& key) -> std::string { - auto pos = json.find("\"" + key + "\""); - if (pos == std::string::npos) return ""; - pos = json.find(':', pos); - if (pos == std::string::npos) return ""; - pos++; - while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; - if (pos >= json.size() || json[pos] != '"') return ""; - pos++; - std::string result; - while (pos < json.size() && json[pos] != '"') { - if (json[pos] == '\\' && pos + 1 < json.size()) { - pos++; - if (json[pos] == 'n') result += '\n'; - else if (json[pos] == 't') result += '\t'; - else if (json[pos] == '\\') result += '\\'; - else if (json[pos] == '"') result += '"'; - else if (json[pos] == '/') result += '/'; - else result += json[pos]; - } else { - result += json[pos]; - } - pos++; - } - return result; - }; - - config.outputPath = findString("outputPath"); - if (config.outputPath.empty()) return false; - - int64_t displayId = findInt64("displayId"); - if (displayId >= 0) config.displayId = displayId; - - int64_t windowHandle = findInt64("windowHandle"); - if (windowHandle > 0) config.windowHandle = windowHandle; - - int fps = findInt("fps"); - if (fps > 0) config.fps = fps; - - int width = findInt("width"); - if (width > 0) config.width = width; - - int height = findInt("height"); - if (height > 0) config.height = height; - - config.audioOutputPath = findString("audioOutputPath"); - config.micOutputPath = findString("micOutputPath"); - config.micDeviceName = findString("micDeviceName"); - - auto findBool = [&](const std::string& key) -> bool { - auto pos = json.find("\"" + key + "\""); - if (pos == std::string::npos) return false; - auto colonPos = json.find(':', pos); - if (colonPos == std::string::npos) return false; - auto valStart = json.find_first_not_of(" \t", colonPos + 1); - return valStart != std::string::npos && json.substr(valStart, 4) == "true"; - }; - - config.captureSystemAudio = findBool("captureSystemAudio"); - config.captureMic = findBool("captureMic"); - - return true; -} - -static std::wstring utf8ToWide(const std::string& str) { - if (str.empty()) return L""; - int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0); - std::wstring wstr(len, L'\0'); - MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), &wstr[0], len); - return wstr; -} - -static void stdinListenerThread() { - std::string line; - while (std::getline(std::cin, line)) { - // Trim whitespace - while (!line.empty() && (line.back() == '\r' || line.back() == '\n' || line.back() == ' ')) { - line.pop_back(); - } - - if (line == "pause") { - g_pauseRequested = true; - g_pauseStartTimestampHns = g_lastFrameTimestampHns.load(); - continue; - } - - if (line == "resume") { - g_pauseRequested = false; - g_resumePending = true; - continue; - } - - if (line == "stop") { - g_stopRequested = true; - g_stopCv.notify_all(); - return; - } - } - - // stdin closed (parent process died) - g_stopRequested = true; - g_stopCv.notify_all(); -} - -int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << "ERROR: Missing JSON config argument" << std::endl; - return 1; - } - - CaptureConfig config; - if (!parseSimpleJson(argv[1], config)) { - std::cerr << "ERROR: Failed to parse config JSON" << std::endl; - return 1; - } - - DxgiSession session; - - if (config.windowHandle > 0) { - HWND hwnd = reinterpret_cast(static_cast(config.windowHandle)); - if (!IsWindow(hwnd)) { - std::cerr << "ERROR: Invalid window handle " << config.windowHandle << std::endl; - return 1; - } - if (!session.initialize(hwnd, config.fps)) { - std::cerr << "ERROR: Failed to initialize DXGI window capture session" << std::endl; - return 1; - } - } else { - HMONITOR monitor = findMonitorByDisplayId(config.displayId); - if (!monitor) { - std::cerr << "ERROR: Could not find monitor for displayId " << config.displayId << std::endl; - return 1; - } - if (!session.initialize(monitor, config.fps)) { - std::cerr << "ERROR: Failed to initialize DXGI capture session" << std::endl; - return 1; - } - } - - int captureWidth = config.width > 0 ? config.width : session.captureWidth(); - int captureHeight = config.height > 0 ? config.height : session.captureHeight(); - - // Ensure even dimensions for H.264 - captureWidth = (captureWidth / 2) * 2; - captureHeight = (captureHeight / 2) * 2; - - // Initialize encoder - MFEncoder encoder; - std::wstring outputPathW = utf8ToWide(config.outputPath); - if (!encoder.initialize(outputPathW, captureWidth, captureHeight, config.fps, - session.device(), session.context())) { - std::cerr << "ERROR: Failed to initialize Media Foundation encoder" << std::endl; - return 1; - } - - // Set up frame callback - std::atomic frameCount{0}; - session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) { - g_lastFrameTimestampHns = timestampHns; - if (g_stopRequested) return; - - if (g_pauseRequested) return; - - int64_t adjustedTimestampHns = timestampHns; - if (g_resumePending.exchange(false)) { - const int64_t pauseStart = g_pauseStartTimestampHns.load(); - if (pauseStart > 0 && timestampHns > pauseStart) { - g_accumulatedPausedHns += (timestampHns - pauseStart); - } - } - - adjustedTimestampHns -= g_accumulatedPausedHns.load(); - if (adjustedTimestampHns < 0) { - adjustedTimestampHns = 0; - } - - if (encoder.writeFrame(texture, adjustedTimestampHns)) { - frameCount++; - } - }); - - // Start stdin listener - std::thread stdinThread(stdinListenerThread); - stdinThread.detach(); - - // Initialize WASAPI captures (but don't start yet) - WasapiCapture loopback; - WasapiCapture micCapture; - bool audioActive = false; - bool audioInitialized = false; - bool micActive = false; - bool micInitialized = false; - - if (config.captureSystemAudio && !config.audioOutputPath.empty()) { - audioInitialized = loopback.initializeLoopback(config.audioOutputPath); - if (!audioInitialized) { - std::cerr << "WARNING: Failed to initialize WASAPI loopback" << std::endl; - } - } - - if (config.captureMic && !config.micOutputPath.empty()) { - micInitialized = micCapture.initializeMic(config.micOutputPath, config.micDeviceName); - if (!micInitialized) { - std::cerr << "WARNING: Failed to initialize WASAPI mic capture" << std::endl; - } - } - - // Start video capture, then audio immediately after for sync - if (!session.startCapture()) { - std::cerr << "ERROR: Failed to start DXGI capture" << std::endl; - return 1; - } - - if (audioInitialized) { - audioActive = loopback.start(); - } - if (micInitialized) { - micActive = micCapture.start(); - } - - std::cout << "Recording started" << std::endl; - std::cout.flush(); - - // Wait for stop signal - while (!g_stopRequested) { - if (g_pauseRequested) { - if (audioActive) loopback.pause(); - if (micActive) micCapture.pause(); - } else { - if (audioActive) loopback.resume(); - if (micActive) micCapture.resume(); - } - - std::unique_lock lock(g_stopMutex); - g_stopCv.wait_for(lock, std::chrono::milliseconds(20), [] { return g_stopRequested.load(); }); - } - - // Stop capture and finalize - session.stopCapture(); - if (audioActive) loopback.stop(); - if (micActive) micCapture.stop(); - encoder.finalize(); - - std::cout << "Recording stopped. Output path: " << config.outputPath << std::endl; - if (audioActive) { - std::cout << "Audio path: " << config.audioOutputPath << std::endl; - } - if (micActive) { - std::cout << "Mic path: " << config.micOutputPath << std::endl; - } - std::cout.flush(); - - // Allow pipe buffers to drain before forceful exit - Sleep(100); - - // Fast exit to avoid WinRT/COM teardown crashes during apartment cleanup - ExitProcess(0); -} diff --git a/electron/native/windows-capture/src/mf_encoder.cpp b/electron/native/windows-capture/src/mf_encoder.cpp deleted file mode 100644 index a1474c20..00000000 --- a/electron/native/windows-capture/src/mf_encoder.cpp +++ /dev/null @@ -1,205 +0,0 @@ -#include "mf_encoder.h" -#include -#include -#include -#include -#include - -#pragma comment(lib, "mfplat.lib") -#pragma comment(lib, "mfreadwrite.lib") -#pragma comment(lib, "mf.lib") -#pragma comment(lib, "mfuuid.lib") - -static int clampByte(int v) { - return v < 0 ? 0 : (v > 255 ? 255 : v); -} - -MFEncoder::MFEncoder() {} - -MFEncoder::~MFEncoder() { - finalize(); -} - -bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height, int fps, - ID3D11Device* device, ID3D11DeviceContext* context) { - if (initialized_) return false; - - if (width % 2 != 0 || height % 2 != 0) { - std::cerr << "ERROR: Encoder dimensions must be even, got " << width << "x" << height << std::endl; - return false; - } - - width_ = width; - height_ = height; - fps_ = fps; - device_ = device; - context_ = context; - - HRESULT hr = MFStartup(MF_VERSION); - if (FAILED(hr)) { - std::cerr << "ERROR: MFStartup failed: 0x" << std::hex << hr << std::endl; - return false; - } - - // Output media type (H.264) - ComPtr outputType; - hr = MFCreateMediaType(&outputType); - if (FAILED(hr)) return false; - - outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); - outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264); - outputType->SetUINT32(MF_MT_AVG_BITRATE, 20000000); - MFSetAttributeSize(outputType.Get(), MF_MT_FRAME_SIZE, width_, height_); - MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, fps_, 1); - MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); - outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); - - // Input media type (NV12) - ComPtr inputType; - hr = MFCreateMediaType(&inputType); - if (FAILED(hr)) return false; - - inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); - inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12); - MFSetAttributeSize(inputType.Get(), MF_MT_FRAME_SIZE, width_, height_); - MFSetAttributeRatio(inputType.Get(), MF_MT_FRAME_RATE, fps_, 1); - MFSetAttributeRatio(inputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); - inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); - - // Create SinkWriter with MPEG4 container - ComPtr writerAttrs; - hr = MFCreateAttributes(&writerAttrs, 1); - if (FAILED(hr)) return false; - - writerAttrs->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE); - - hr = MFCreateSinkWriterFromURL(outputPath.c_str(), nullptr, writerAttrs.Get(), &sinkWriter_); - if (FAILED(hr)) { - std::cerr << "ERROR: MFCreateSinkWriterFromURL failed: 0x" << std::hex << hr << std::endl; - return false; - } - - hr = sinkWriter_->AddStream(outputType.Get(), &streamIndex_); - if (FAILED(hr)) { - std::cerr << "ERROR: AddStream failed: 0x" << std::hex << hr << std::endl; - return false; - } - - hr = sinkWriter_->SetInputMediaType(streamIndex_, inputType.Get(), nullptr); - if (FAILED(hr)) { - std::cerr << "ERROR: SetInputMediaType failed: 0x" << std::hex << hr << std::endl; - return false; - } - - hr = sinkWriter_->BeginWriting(); - if (FAILED(hr)) { - std::cerr << "ERROR: BeginWriting failed: 0x" << std::hex << hr << std::endl; - return false; - } - - // Pre-allocate staging texture - D3D11_TEXTURE2D_DESC stagingDesc = {}; - stagingDesc.Width = width_; - stagingDesc.Height = height_; - stagingDesc.MipLevels = 1; - stagingDesc.ArraySize = 1; - stagingDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - stagingDesc.SampleDesc.Count = 1; - stagingDesc.Usage = D3D11_USAGE_STAGING; - stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - - hr = device_->CreateTexture2D(&stagingDesc, nullptr, &stagingTexture_); - if (FAILED(hr)) { - std::cerr << "ERROR: Failed to create staging texture: 0x" << std::hex << hr << std::endl; - return false; - } - - // Pre-allocate NV12 buffer - const int ySize = width_ * height_; - const int uvSize = (width_ / 2) * (height_ / 2) * 2; - nv12Buffer_.resize(ySize + uvSize); - - initialized_ = true; - return true; -} - -bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) { - if (!initialized_ || !sinkWriter_) return false; - - context_->CopyResource(stagingTexture_.Get(), texture); - - D3D11_MAPPED_SUBRESOURCE mapped; - HRESULT hr = context_->Map(stagingTexture_.Get(), 0, D3D11_MAP_READ, 0, &mapped); - if (FAILED(hr)) return false; - - // Convert BGRA → NV12 - const uint8_t* bgra = static_cast(mapped.pData); - const int bgraPitch = static_cast(mapped.RowPitch); - - // Y plane - for (int y = 0; y < height_; y++) { - for (int x = 0; x < width_; x++) { - const uint8_t* pixel = bgra + y * bgraPitch + x * 4; - uint8_t b = pixel[0], g = pixel[1], r = pixel[2]; - int yVal = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16; - nv12Buffer_[y * width_ + x] = static_cast(clampByte(yVal)); - } - } - - // UV plane (interleaved, subsampled 2x2) - const int ySize = width_ * height_; - uint8_t* uvPlane = nv12Buffer_.data() + ySize; - for (int y = 0; y < height_; y += 2) { - for (int x = 0; x < width_; x += 2) { - const uint8_t* pixel = bgra + y * bgraPitch + x * 4; - uint8_t b = pixel[0], g = pixel[1], r = pixel[2]; - int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; - int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; - int uvIdx = (y / 2) * width_ + (x / 2) * 2; - uvPlane[uvIdx] = static_cast(clampByte(u)); - uvPlane[uvIdx + 1] = static_cast(clampByte(v)); - } - } - - context_->Unmap(stagingTexture_.Get(), 0); - - // Create MF sample - DWORD bufferSize = static_cast(nv12Buffer_.size()); - ComPtr buffer; - hr = MFCreateMemoryBuffer(bufferSize, &buffer); - if (FAILED(hr)) return false; - - BYTE* bufferData = nullptr; - hr = buffer->Lock(&bufferData, nullptr, nullptr); - if (FAILED(hr)) return false; - - std::memcpy(bufferData, nv12Buffer_.data(), bufferSize); - buffer->Unlock(); - buffer->SetCurrentLength(bufferSize); - - ComPtr sample; - hr = MFCreateSample(&sample); - if (FAILED(hr)) return false; - - sample->AddBuffer(buffer.Get()); - sample->SetSampleTime(timestampHns); - sample->SetSampleDuration(10000000LL / fps_); - - hr = sinkWriter_->WriteSample(streamIndex_, sample.Get()); - return SUCCEEDED(hr); -} - -bool MFEncoder::finalize() { - if (!initialized_) return false; - initialized_ = false; - - stagingTexture_.Reset(); - nv12Buffer_.clear(); - nv12Buffer_.shrink_to_fit(); - - if (!sinkWriter_) return false; - HRESULT hr = sinkWriter_->Finalize(); - sinkWriter_.Reset(); - MFShutdown(); - return SUCCEEDED(hr); -} diff --git a/electron/native/windows-capture/src/mf_encoder.h b/electron/native/windows-capture/src/mf_encoder.h deleted file mode 100644 index 7eb8e642..00000000 --- a/electron/native/windows-capture/src/mf_encoder.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -using Microsoft::WRL::ComPtr; - -class MFEncoder { -public: - MFEncoder(); - ~MFEncoder(); - - bool initialize(const std::wstring& outputPath, int width, int height, int fps, - ID3D11Device* device, ID3D11DeviceContext* context); - bool writeFrame(ID3D11Texture2D* texture, int64_t timestampHns); - bool finalize(); - -private: - ComPtr sinkWriter_; - ID3D11Device* device_ = nullptr; - ID3D11DeviceContext* context_ = nullptr; - ComPtr stagingTexture_; - std::vector nv12Buffer_; - DWORD streamIndex_ = 0; - int width_ = 0; - int height_ = 0; - int fps_ = 60; - bool initialized_ = false; -}; diff --git a/electron/native/windows-capture/src/monitor_utils.cpp b/electron/native/windows-capture/src/monitor_utils.cpp deleted file mode 100644 index 56962aeb..00000000 --- a/electron/native/windows-capture/src/monitor_utils.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "monitor_utils.h" -#include - -static BOOL CALLBACK enumMonitorCallback(HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) { - auto* monitors = reinterpret_cast*>(lParam); - - MONITORINFOEXW mi = {}; - mi.cbSize = sizeof(mi); - if (GetMonitorInfoW(hMonitor, &mi)) { - MonitorInfo info; - info.handle = hMonitor; - info.x = mi.rcMonitor.left; - info.y = mi.rcMonitor.top; - info.width = mi.rcMonitor.right - mi.rcMonitor.left; - info.height = mi.rcMonitor.bottom - mi.rcMonitor.top; - info.deviceName = mi.szDevice; - monitors->push_back(info); - } - - return TRUE; -} - -std::vector enumerateMonitors() { - std::vector monitors; - EnumDisplayMonitors(nullptr, nullptr, enumMonitorCallback, reinterpret_cast(&monitors)); - return monitors; -} - -// Electron uses the HMONITOR handle value cast to a number as the display ID. -HMONITOR findMonitorByDisplayId(int64_t displayId) { - auto monitors = enumerateMonitors(); - - for (const auto& m : monitors) { - if (static_cast(reinterpret_cast(m.handle)) == displayId) { - return m.handle; - } - } - - if (!monitors.empty()) { - return monitors[0].handle; - } - - return MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY); -} - -MonitorInfo getMonitorInfo(HMONITOR monitor) { - MonitorInfo info; - info.handle = monitor; - - MONITORINFOEXW mi = {}; - mi.cbSize = sizeof(mi); - if (GetMonitorInfoW(monitor, &mi)) { - info.x = mi.rcMonitor.left; - info.y = mi.rcMonitor.top; - info.width = mi.rcMonitor.right - mi.rcMonitor.left; - info.height = mi.rcMonitor.bottom - mi.rcMonitor.top; - info.deviceName = mi.szDevice; - } - - return info; -} diff --git a/electron/native/windows-capture/src/monitor_utils.h b/electron/native/windows-capture/src/monitor_utils.h deleted file mode 100644 index 8889410d..00000000 --- a/electron/native/windows-capture/src/monitor_utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -struct MonitorInfo { - HMONITOR handle; - int x; - int y; - int width; - int height; - std::wstring deviceName; -}; - -std::vector enumerateMonitors(); -HMONITOR findMonitorByDisplayId(int64_t displayId); -MonitorInfo getMonitorInfo(HMONITOR monitor); diff --git a/electron/native/windows-capture/src/wasapi_loopback.cpp b/electron/native/windows-capture/src/wasapi_loopback.cpp deleted file mode 100644 index 8ae15ad1..00000000 --- a/electron/native/windows-capture/src/wasapi_loopback.cpp +++ /dev/null @@ -1,274 +0,0 @@ -#include "wasapi_loopback.h" -#include -#include -#include - -#pragma comment(lib, "ole32.lib") - -static const CLSID CLSID_MMDeviceEnumerator_ = __uuidof(MMDeviceEnumerator); -static const IID IID_IMMDeviceEnumerator_ = __uuidof(IMMDeviceEnumerator); -static const IID IID_IAudioClient_ = __uuidof(IAudioClient); -static const IID IID_IAudioCaptureClient_ = __uuidof(IAudioCaptureClient); - -WasapiCapture::WasapiCapture() {} - -WasapiCapture::~WasapiCapture() { - stop(); - if (mixFormat_) CoTaskMemFree(mixFormat_); - if (captureClient_) captureClient_->Release(); - if (audioClient_) audioClient_->Release(); - if (device_) device_->Release(); - if (enumerator_) enumerator_->Release(); -} - -static std::wstring utf8ToWide(const std::string& str) { - if (str.empty()) return L""; - int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0); - std::wstring wstr(len, L'\0'); - MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), &wstr[0], len); - return wstr; -} - -IMMDevice* WasapiCapture::findCaptureDeviceByName(const std::wstring& targetName) { - IMMDeviceCollection* collection = nullptr; - HRESULT hr = enumerator_->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &collection); - if (FAILED(hr)) return nullptr; - - UINT count = 0; - collection->GetCount(&count); - - for (UINT i = 0; i < count; i++) { - IMMDevice* dev = nullptr; - collection->Item(i, &dev); - - IPropertyStore* store = nullptr; - dev->OpenPropertyStore(STGM_READ, &store); - PROPVARIANT pv; - PropVariantInit(&pv); - store->GetValue(PKEY_Device_FriendlyName, &pv); - std::wstring name = pv.pwszVal ? pv.pwszVal : L""; - PropVariantClear(&pv); - store->Release(); - - if (name.find(targetName) != std::wstring::npos || targetName.find(name) != std::wstring::npos) { - collection->Release(); - return dev; - } - dev->Release(); - } - - collection->Release(); - return nullptr; -} - -bool WasapiCapture::initializeLoopback(const std::string& outputPath) { - outputPath_ = outputPath; - streamFlags_ = AUDCLNT_STREAMFLAGS_LOOPBACK; - - HRESULT hr = CoCreateInstance( - CLSID_MMDeviceEnumerator_, nullptr, CLSCTX_ALL, - IID_IMMDeviceEnumerator_, reinterpret_cast(&enumerator_)); - if (FAILED(hr)) return false; - - hr = enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device_); - if (FAILED(hr)) return false; - - return initializeCommon(); -} - -bool WasapiCapture::initializeMic(const std::string& outputPath, const std::string& deviceName) { - outputPath_ = outputPath; - streamFlags_ = 0; - - HRESULT hr = CoCreateInstance( - CLSID_MMDeviceEnumerator_, nullptr, CLSCTX_ALL, - IID_IMMDeviceEnumerator_, reinterpret_cast(&enumerator_)); - if (FAILED(hr)) return false; - - if (!deviceName.empty()) { - device_ = findCaptureDeviceByName(utf8ToWide(deviceName)); - } - if (!device_) { - hr = enumerator_->GetDefaultAudioEndpoint(eCapture, eCommunications, &device_); - if (FAILED(hr)) { - hr = enumerator_->GetDefaultAudioEndpoint(eCapture, eConsole, &device_); - if (FAILED(hr)) return false; - } - } - - return initializeCommon(); -} - -bool WasapiCapture::initializeCommon() { - HRESULT hr = device_->Activate(IID_IAudioClient_, CLSCTX_ALL, nullptr, - reinterpret_cast(&audioClient_)); - if (FAILED(hr)) return false; - - hr = audioClient_->GetMixFormat(&mixFormat_); - if (FAILED(hr)) return false; - - REFERENCE_TIME bufferDuration = 200000; // 20ms - hr = audioClient_->Initialize( - AUDCLNT_SHAREMODE_SHARED, - streamFlags_, - bufferDuration, 0, mixFormat_, nullptr); - if (FAILED(hr)) return false; - - hr = audioClient_->GetBufferSize(&bufferFrameCount_); - if (FAILED(hr)) return false; - - hr = audioClient_->GetService(IID_IAudioCaptureClient_, - reinterpret_cast(&captureClient_)); - if (FAILED(hr)) return false; - - return true; -} - -bool WasapiCapture::start() { - if (capturing_) return true; - - outputFile_ = CreateFileA( - outputPath_.c_str(), GENERIC_WRITE, 0, nullptr, - CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - - if (outputFile_ == INVALID_HANDLE_VALUE) { - std::cerr << "ERROR: Cannot create audio output file" << std::endl; - return false; - } - - totalDataBytes_ = 0; - writeWavHeader(outputFile_, 0); - - HRESULT hr = audioClient_->Start(); - if (FAILED(hr)) { - CloseHandle(outputFile_); - outputFile_ = INVALID_HANDLE_VALUE; - return false; - } - - capturing_ = true; - paused_ = false; - thread_ = std::thread(&WasapiCapture::captureThread, this); - return true; -} - -bool WasapiCapture::pause() { - if (!capturing_ || paused_) return true; - paused_ = true; - return audioClient_ ? SUCCEEDED(audioClient_->Stop()) : false; -} - -bool WasapiCapture::resume() { - if (!capturing_ || !paused_) return true; - HRESULT hr = audioClient_ ? audioClient_->Start() : E_FAIL; - if (FAILED(hr)) return false; - paused_ = false; - return true; -} - -void WasapiCapture::stop() { - if (!capturing_) return; - capturing_ = false; - if (thread_.joinable()) thread_.join(); - if (audioClient_) audioClient_->Stop(); - - if (outputFile_ != INVALID_HANDLE_VALUE) { - SetFilePointer(outputFile_, 0, nullptr, FILE_BEGIN); - writeWavHeader(outputFile_, totalDataBytes_); - CloseHandle(outputFile_); - outputFile_ = INVALID_HANDLE_VALUE; - } - - paused_ = false; -} - -static int16_t floatToInt16(float v) { - v = v < -1.0f ? -1.0f : (v > 1.0f ? 1.0f : v); - return static_cast(v * 32767.0f); -} - -bool WasapiCapture::writeWavHeader(HANDLE file, DWORD dataSize) { - WORD channels = static_cast(mixFormat_->nChannels); - DWORD sampleRate = mixFormat_->nSamplesPerSec; - WORD bitsPerSample = 16; - WORD blockAlign = channels * (bitsPerSample / 8); - DWORD byteRate = sampleRate * blockAlign; - - DWORD written; - WriteFile(file, "RIFF", 4, &written, nullptr); - DWORD chunkSize = 36 + dataSize; - WriteFile(file, &chunkSize, 4, &written, nullptr); - WriteFile(file, "WAVE", 4, &written, nullptr); - WriteFile(file, "fmt ", 4, &written, nullptr); - DWORD fmtSize = 16; - WriteFile(file, &fmtSize, 4, &written, nullptr); - WORD audioFormat = 1; - WriteFile(file, &audioFormat, 2, &written, nullptr); - WriteFile(file, &channels, 2, &written, nullptr); - WriteFile(file, &sampleRate, 4, &written, nullptr); - WriteFile(file, &byteRate, 4, &written, nullptr); - WriteFile(file, &blockAlign, 2, &written, nullptr); - WriteFile(file, &bitsPerSample, 2, &written, nullptr); - WriteFile(file, "data", 4, &written, nullptr); - WriteFile(file, &dataSize, 4, &written, nullptr); - return true; -} - -void WasapiCapture::captureThread() { - WORD channels = static_cast(mixFormat_->nChannels); - bool isFloat = (mixFormat_->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) || - (mixFormat_->wFormatTag == WAVE_FORMAT_EXTENSIBLE && - reinterpret_cast(mixFormat_)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT); - - std::vector pcmBuffer; - - DWORD sleepMs = static_cast((static_cast(bufferFrameCount_) / mixFormat_->nSamplesPerSec) * 500.0); - if (sleepMs < 5) sleepMs = 5; - - while (capturing_) { - if (paused_) { - Sleep(10); - continue; - } - - Sleep(sleepMs); - - UINT32 packetLength = 0; - HRESULT hr = captureClient_->GetNextPacketSize(&packetLength); - if (FAILED(hr)) break; - - while (packetLength > 0) { - BYTE* data = nullptr; - UINT32 numFrames = 0; - DWORD flags = 0; - - hr = captureClient_->GetBuffer(&data, &numFrames, &flags, nullptr, nullptr); - if (FAILED(hr)) break; - - UINT32 totalSamples = numFrames * channels; - - if (flags & AUDCLNT_BUFFERFLAGS_SILENT) { - pcmBuffer.assign(totalSamples, 0); - } else if (isFloat) { - pcmBuffer.resize(totalSamples); - const float* src = reinterpret_cast(data); - for (UINT32 i = 0; i < totalSamples; i++) { - pcmBuffer[i] = floatToInt16(src[i]); - } - } else { - pcmBuffer.resize(totalSamples); - std::memcpy(pcmBuffer.data(), data, totalSamples * sizeof(int16_t)); - } - - captureClient_->ReleaseBuffer(numFrames); - - DWORD bytesToWrite = totalSamples * sizeof(int16_t); - DWORD written; - WriteFile(outputFile_, pcmBuffer.data(), bytesToWrite, &written, nullptr); - totalDataBytes_ += written; - - hr = captureClient_->GetNextPacketSize(&packetLength); - if (FAILED(hr)) break; - } - } -} diff --git a/electron/native/windows-capture/src/wasapi_loopback.h b/electron/native/windows-capture/src/wasapi_loopback.h deleted file mode 100644 index a4facf13..00000000 --- a/electron/native/windows-capture/src/wasapi_loopback.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -class WasapiCapture { -public: - WasapiCapture(); - ~WasapiCapture(); - - bool initializeLoopback(const std::string& outputPath); - bool initializeMic(const std::string& outputPath, const std::string& deviceName = ""); - bool start(); - bool pause(); - bool resume(); - void stop(); - -private: - bool initializeCommon(); - void captureThread(); - bool writeWavHeader(HANDLE file, DWORD dataSize); - IMMDevice* findCaptureDeviceByName(const std::wstring& name); - - std::string outputPath_; - std::thread thread_; - std::atomic capturing_{false}; - std::atomic paused_{false}; - HANDLE outputFile_ = INVALID_HANDLE_VALUE; - DWORD totalDataBytes_ = 0; - - IMMDeviceEnumerator* enumerator_ = nullptr; - IMMDevice* device_ = nullptr; - IAudioClient* audioClient_ = nullptr; - IAudioCaptureClient* captureClient_ = nullptr; - WAVEFORMATEX* mixFormat_ = nullptr; - DWORD streamFlags_ = 0; - - UINT32 bufferFrameCount_ = 0; -};