mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
325 lines
10 KiB
C++
325 lines
10 KiB
C++
#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);
|
|
}
|