Revert "fix(recording): validate native captures and tighten mic menu spacing"

This reverts commit 1900c0e737.
This commit is contained in:
webadderall
2026-03-27 11:31:02 +11:00
parent 7fb4c5de9e
commit 70cefabd57
17 changed files with 1408 additions and 102 deletions
+2 -26
View File
@@ -29,7 +29,6 @@ struct CaptureConfig {
std::string outputPath;
std::string audioOutputPath;
std::string micOutputPath;
std::string micDeviceId;
std::string micDeviceName;
int fps = 60;
int width = 0;
@@ -114,7 +113,6 @@ static bool parseSimpleJson(const std::string& json, CaptureConfig& config) {
config.audioOutputPath = findString("audioOutputPath");
config.micOutputPath = findString("micOutputPath");
config.micDeviceId = findString("micDeviceId");
config.micDeviceName = findString("micDeviceName");
auto findBool = [&](const std::string& key) -> bool {
@@ -228,7 +226,6 @@ int main(int argc, char* argv[]) {
// Set up frame callback
std::atomic<int64_t> frameCount{0};
std::atomic<bool> frameWriteFailed{false};
session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) {
g_lastFrameTimestampHns = timestampHns;
if (g_stopRequested) return;
@@ -250,12 +247,7 @@ int main(int argc, char* argv[]) {
if (encoder.writeFrame(texture, adjustedTimestampHns)) {
frameCount++;
return;
}
frameWriteFailed = true;
g_stopRequested = true;
g_stopCv.notify_all();
});
// Start stdin listener
@@ -278,10 +270,7 @@ int main(int argc, char* argv[]) {
}
if (config.captureMic && !config.micOutputPath.empty()) {
micInitialized = micCapture.initializeMic(
config.micOutputPath,
config.micDeviceId,
config.micDeviceName);
micInitialized = micCapture.initializeMic(config.micOutputPath, config.micDeviceName);
if (!micInitialized) {
std::cerr << "WARNING: Failed to initialize WASAPI mic capture" << std::endl;
}
@@ -321,20 +310,7 @@ int main(int argc, char* argv[]) {
session.stopCapture();
if (audioActive) loopback.stop();
if (micActive) micCapture.stop();
if (frameWriteFailed.load()) {
std::cerr << "ERROR: Failed to encode one or more video frames" << std::endl;
return 1;
}
if (frameCount.load() <= 0) {
std::cerr << "ERROR: No video frames were written" << std::endl;
return 1;
}
if (!encoder.finalize()) {
std::cerr << "ERROR: Failed to finalize Media Foundation encoder" << std::endl;
return 1;
}
encoder.finalize();
std::cout << "Recording stopped. Output path: " << config.outputPath << std::endl;
if (audioActive) {
@@ -124,7 +124,6 @@ bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height
}
bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) {
std::lock_guard<std::mutex> lock(writeMutex_);
if (!initialized_ || !sinkWriter_) return false;
context_->CopyResource(stagingTexture_.Get(), texture);
@@ -191,7 +190,6 @@ bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) {
}
bool MFEncoder::finalize() {
std::lock_guard<std::mutex> lock(writeMutex_);
if (!initialized_) return false;
initialized_ = false;
@@ -8,7 +8,6 @@
#include <wrl/client.h>
#include <string>
#include <vector>
#include <mutex>
using Microsoft::WRL::ComPtr;
@@ -33,5 +32,4 @@ private:
int height_ = 0;
int fps_ = 60;
bool initialized_ = false;
std::mutex writeMutex_;
};
@@ -29,36 +29,6 @@ static std::wstring utf8ToWide(const std::string& str) {
return wstr;
}
IMMDevice* WasapiCapture::findCaptureDeviceById(const std::wstring& targetId) {
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);
LPWSTR deviceId = nullptr;
hr = dev->GetId(&deviceId);
if (SUCCEEDED(hr) && deviceId) {
const bool matches = targetId == deviceId;
CoTaskMemFree(deviceId);
if (matches) {
collection->Release();
return dev;
}
}
dev->Release();
}
collection->Release();
return nullptr;
}
IMMDevice* WasapiCapture::findCaptureDeviceByName(const std::wstring& targetName) {
IMMDeviceCollection* collection = nullptr;
HRESULT hr = enumerator_->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &collection);
@@ -67,8 +37,6 @@ IMMDevice* WasapiCapture::findCaptureDeviceByName(const std::wstring& targetName
UINT count = 0;
collection->GetCount(&count);
IMMDevice* partialMatch = nullptr;
for (UINT i = 0; i < count; i++) {
IMMDevice* dev = nullptr;
collection->Item(i, &dev);
@@ -82,21 +50,15 @@ IMMDevice* WasapiCapture::findCaptureDeviceByName(const std::wstring& targetName
PropVariantClear(&pv);
store->Release();
if (name == targetName) {
if (name.find(targetName) != std::wstring::npos || targetName.find(name) != std::wstring::npos) {
collection->Release();
return dev;
}
if (!partialMatch && (name.find(targetName) != std::wstring::npos || targetName.find(name) != std::wstring::npos)) {
partialMatch = dev;
continue;
}
dev->Release();
}
collection->Release();
return partialMatch;
return nullptr;
}
bool WasapiCapture::initializeLoopback(const std::string& outputPath) {
@@ -114,10 +76,7 @@ bool WasapiCapture::initializeLoopback(const std::string& outputPath) {
return initializeCommon();
}
bool WasapiCapture::initializeMic(
const std::string& outputPath,
const std::string& deviceId,
const std::string& deviceName) {
bool WasapiCapture::initializeMic(const std::string& outputPath, const std::string& deviceName) {
outputPath_ = outputPath;
streamFlags_ = 0;
@@ -126,10 +85,7 @@ bool WasapiCapture::initializeMic(
IID_IMMDeviceEnumerator_, reinterpret_cast<void**>(&enumerator_));
if (FAILED(hr)) return false;
if (!deviceId.empty()) {
device_ = findCaptureDeviceById(utf8ToWide(deviceId));
}
if (!device_ && !deviceName.empty()) {
if (!deviceName.empty()) {
device_ = findCaptureDeviceByName(utf8ToWide(deviceName));
}
if (!device_) {
@@ -14,10 +14,7 @@ public:
~WasapiCapture();
bool initializeLoopback(const std::string& outputPath);
bool initializeMic(
const std::string& outputPath,
const std::string& deviceId = "",
const std::string& deviceName = "");
bool initializeMic(const std::string& outputPath, const std::string& deviceName = "");
bool start();
bool pause();
bool resume();
@@ -27,7 +24,6 @@ private:
bool initializeCommon();
void captureThread();
bool writeWavHeader(HANDLE file, DWORD dataSize);
IMMDevice* findCaptureDeviceById(const std::wstring& id);
IMMDevice* findCaptureDeviceByName(const std::wstring& name);
std::string outputPath_;
@@ -0,0 +1,27 @@
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
)
@@ -0,0 +1,343 @@
#include "dxgi_session.h"
#include <dwmapi.h>
#include <algorithm>
#include <chrono>
#include <iostream>
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<IDXGIAdapter1>& adapter, ComPtr<IDXGIOutput1>& output) {
ComPtr<IDXGIFactory1> 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<IDXGIAdapter1> candidateAdapter;
hr = factory->EnumAdapters1(adapterIndex, &candidateAdapter);
if (hr == DXGI_ERROR_NOT_FOUND) {
break;
}
if (FAILED(hr)) {
continue;
}
for (UINT outputIndex = 0;; ++outputIndex) {
ComPtr<IDXGIOutput> 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<IDXGIOutput1> 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<UINT>(captureWidth_);
desc.Height = static_cast<UINT>(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<IDXGIAdapter1> adapter;
ComPtr<IDXGIOutput1> 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::duration<int64_t, std::ratio<1, 10000000>>>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
void DxgiSession::captureLoop() {
while (capturing_) {
DXGI_OUTDUPL_FRAME_INFO frameInfo{};
ComPtr<IDXGIResource> 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<ID3D11Texture2D> 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<UINT>(captureRect_.left);
sourceBox.top = static_cast<UINT>(captureRect_.top);
sourceBox.front = 0;
sourceBox.right = static_cast<UINT>(captureRect_.right);
sourceBox.bottom = static_cast<UINT>(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();
}
}
@@ -0,0 +1,62 @@
#pragma once
#include <windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <wrl/client.h>
#include <functional>
#include <atomic>
#include <thread>
using Microsoft::WRL::ComPtr;
class DxgiSession {
public:
using FrameCallback = std::function<void(ID3D11Texture2D*, int64_t timestampHns)>;
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<ID3D11Device> d3dDevice_;
ComPtr<ID3D11DeviceContext> d3dContext_;
ComPtr<IDXGIOutputDuplication> duplication_;
ComPtr<ID3D11Texture2D> captureTexture_;
FrameCallback frameCallback_;
std::atomic<bool> 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<IDXGIAdapter1>& adapter, ComPtr<IDXGIOutput1>& 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;
};
@@ -0,0 +1,324 @@
#include "dxgi_session.h"
#include "mf_encoder.h"
#include "monitor_utils.h"
#include "wasapi_loopback.h"
#include <iostream>
#include <string>
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <chrono>
static std::atomic<bool> g_stopRequested{false};
static std::atomic<bool> g_pauseRequested{false};
static std::atomic<bool> g_resumePending{false};
static std::atomic<int64_t> g_lastFrameTimestampHns{0};
static std::atomic<int64_t> g_pauseStartTimestampHns{0};
static std::atomic<int64_t> 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<int>(str.size()), nullptr, 0);
std::wstring wstr(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast<int>(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<HWND>(static_cast<intptr_t>(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<int64_t> 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<std::mutex> 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);
}
@@ -0,0 +1,205 @@
#include "mf_encoder.h"
#include <mfapi.h>
#include <mferror.h>
#include <codecapi.h>
#include <iostream>
#include <cstring>
#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<IMFMediaType> 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<IMFMediaType> 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<IMFAttributes> 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<const uint8_t*>(mapped.pData);
const int bgraPitch = static_cast<int>(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<uint8_t>(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<uint8_t>(clampByte(u));
uvPlane[uvIdx + 1] = static_cast<uint8_t>(clampByte(v));
}
}
context_->Unmap(stagingTexture_.Get(), 0);
// Create MF sample
DWORD bufferSize = static_cast<DWORD>(nv12Buffer_.size());
ComPtr<IMFMediaBuffer> 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<IMFSample> 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);
}
@@ -0,0 +1,35 @@
#pragma once
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <d3d11.h>
#include <wrl/client.h>
#include <string>
#include <vector>
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<IMFSinkWriter> sinkWriter_;
ID3D11Device* device_ = nullptr;
ID3D11DeviceContext* context_ = nullptr;
ComPtr<ID3D11Texture2D> stagingTexture_;
std::vector<uint8_t> nv12Buffer_;
DWORD streamIndex_ = 0;
int width_ = 0;
int height_ = 0;
int fps_ = 60;
bool initialized_ = false;
};
@@ -0,0 +1,61 @@
#include "monitor_utils.h"
#include <ShellScalingApi.h>
static BOOL CALLBACK enumMonitorCallback(HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) {
auto* monitors = reinterpret_cast<std::vector<MonitorInfo>*>(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<MonitorInfo> enumerateMonitors() {
std::vector<MonitorInfo> monitors;
EnumDisplayMonitors(nullptr, nullptr, enumMonitorCallback, reinterpret_cast<LPARAM>(&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<int64_t>(reinterpret_cast<intptr_t>(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;
}
@@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
#include <windows.h>
#include <string>
#include <vector>
struct MonitorInfo {
HMONITOR handle;
int x;
int y;
int width;
int height;
std::wstring deviceName;
};
std::vector<MonitorInfo> enumerateMonitors();
HMONITOR findMonitorByDisplayId(int64_t displayId);
MonitorInfo getMonitorInfo(HMONITOR monitor);
@@ -0,0 +1,274 @@
#include "wasapi_loopback.h"
#include <functiondiscoverykeys_devpkey.h>
#include <iostream>
#include <cstring>
#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<int>(str.size()), nullptr, 0);
std::wstring wstr(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast<int>(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<void**>(&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<void**>(&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<void**>(&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<void**>(&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<int16_t>(v * 32767.0f);
}
bool WasapiCapture::writeWavHeader(HANDLE file, DWORD dataSize) {
WORD channels = static_cast<WORD>(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<WORD>(mixFormat_->nChannels);
bool isFloat = (mixFormat_->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) ||
(mixFormat_->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(mixFormat_)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
std::vector<int16_t> pcmBuffer;
DWORD sleepMs = static_cast<DWORD>((static_cast<double>(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<const float*>(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;
}
}
}
@@ -0,0 +1,44 @@
#pragma once
#include <windows.h>
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <string>
#include <thread>
#include <atomic>
#include <vector>
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<bool> capturing_{false};
std::atomic<bool> 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;
};
@@ -217,14 +217,6 @@
color: #6360f5;
}
.ddSeparator {
width: 100%;
height: 1px;
margin: 4px 0;
background: rgba(255, 255, 255, 0.07);
flex-shrink: 0;
}
.recBtn {
position: relative;
width: 46px;
+7 -11
View File
@@ -111,14 +111,10 @@ function DropdownItem({
);
}
function BarSeparator() {
function Separator() {
return <div className={styles.sep} />;
}
function DropdownSeparator() {
return <div className={styles.ddSeparator} />;
}
function MicDeviceRow({
device,
selected,
@@ -639,7 +635,7 @@ export function LaunchWindow() {
{formatTime(elapsed)}
</span>
<BarSeparator />
<Separator />
<IconButton
title={
@@ -650,7 +646,7 @@ export function LaunchWindow() {
{microphoneEnabled ? <Mic size={18} /> : <MicOff size={18} />}
</IconButton>
<BarSeparator />
<Separator />
<IconButton
onClick={paused ? resumeRecording : pauseRecording}
@@ -695,7 +691,7 @@ export function LaunchWindow() {
/>
</button>
<BarSeparator />
<Separator />
<IconButton
onClick={toggleMicrophone}
@@ -723,7 +719,7 @@ export function LaunchWindow() {
<Timer size={18} />
</IconButton>
<BarSeparator />
<Separator />
<button
type="button"
@@ -735,7 +731,7 @@ export function LaunchWindow() {
<div className={styles.recDot} />
</button>
<BarSeparator />
<Separator />
<IconButton
buttonRef={moreButtonRef}
@@ -854,7 +850,7 @@ export function LaunchWindow() {
? t("recording.disableSystemAudio")
: t("recording.enableSystemAudio")}
</DropdownItem>
<DropdownSeparator />
<Separator />
{microphoneEnabled && (
<DropdownItem
icon={<MicOff size={16} />}