Merge remote-tracking branch 'origin/main' into codex/fix-windows-paused-recording

# Conflicts:
#	src/hooks/useScreenRecorder.ts
This commit is contained in:
Siddharth
2026-05-22 20:08:26 -07:00
17 changed files with 813 additions and 142 deletions
+62 -18
View File
@@ -30,6 +30,7 @@ struct CaptureConfig {
std::string sourceId;
std::string windowHandle;
std::string outputPath;
std::string webcamOutputPath;
int fps = 60;
int width = 0;
int height = 0;
@@ -343,6 +344,7 @@ bool parseConfig(const std::string& json, CaptureConfig& config) {
config.webcamDeviceId = findString(json, "webcamDeviceId");
config.webcamDeviceName = findString(json, "webcamDeviceName");
config.webcamDirectShowClsid = findString(json, "webcamDirectShowClsid");
config.webcamOutputPath = findString(json, "webcamPath");
config.webcamWidth = findInt(json, "webcamWidth", 0);
config.webcamHeight = findInt(json, "webcamHeight", 0);
config.webcamFps = findInt(json, "webcamFps", 0);
@@ -435,6 +437,7 @@ int main(int argc, char* argv[]) {
WebcamCapture webcamCapture;
bool webcamActive = false;
bool writeSeparateWebcam = false;
if (config.webcamEnabled) {
if (!webcamCapture.initialize(
utf8ToWide(config.webcamDeviceId),
@@ -451,6 +454,7 @@ int main(int argc, char* argv[]) {
<< ",\"fps\":" << webcamCapture.fps()
<< ",\"deviceName\":\"" << jsonEscape(wideToUtf8(webcamCapture.selectedDeviceName()))
<< "\"}" << std::endl;
writeSeparateWebcam = !config.webcamOutputPath.empty();
}
WasapiLoopbackCapture loopbackCapture;
@@ -512,6 +516,24 @@ int main(int argc, char* argv[]) {
return 1;
}
MFEncoder webcamEncoder;
if (writeSeparateWebcam) {
const int webcamPixels = std::max(1, webcamCapture.width()) * std::max(1, webcamCapture.height());
const int webcamBitrate = webcamPixels >= 1280 * 720 ? 8'000'000 : 4'000'000;
if (!webcamEncoder.initialize(
utf8ToWide(config.webcamOutputPath),
webcamCapture.width(),
webcamCapture.height(),
webcamCapture.fps(),
webcamBitrate,
session.device(),
session.context(),
nullptr)) {
std::cerr << "ERROR: Failed to initialize native webcam encoder" << std::endl;
return 1;
}
}
std::mutex mutex;
CaptureControl control;
std::atomic<bool> firstFrameWritten = false;
@@ -522,6 +544,7 @@ int main(int argc, char* argv[]) {
std::vector<BYTE> latestWebcamFrame;
int latestWebcamWidth = 0;
int latestWebcamHeight = 0;
uint64_t latestWebcamSequence = 0;
bool hasVisibleWebcamFrame = false;
session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) {
@@ -555,6 +578,8 @@ int main(int argc, char* argv[]) {
const auto frameDuration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(1.0 / config.fps));
uint64_t frameIndex = 0;
uint64_t lastWrittenWebcamSequence = 0;
uint64_t webcamOutputFrameIndex = 0;
int64_t lastEncodedVideoTimestampHns = -1;
while (!control.stopRequested && !encodeFailed) {
@@ -569,14 +594,14 @@ int main(int argc, char* argv[]) {
break;
}
if (webcamActive) {
std::vector<BYTE> candidateWebcamFrame;
int candidateWebcamWidth = 0;
int candidateWebcamHeight = 0;
if (webcamCapture.copyLatestFrame(candidateWebcamFrame, candidateWebcamWidth, candidateWebcamHeight) &&
hasVisibleBgraContent(candidateWebcamFrame)) {
latestWebcamFrame = std::move(candidateWebcamFrame);
latestWebcamWidth = candidateWebcamWidth;
latestWebcamHeight = candidateWebcamHeight;
WebcamFrameSnapshot candidateWebcamFrame;
if (webcamCapture.copyLatestFrame(candidateWebcamFrame) &&
candidateWebcamFrame.sequence != latestWebcamSequence &&
hasVisibleBgraContent(candidateWebcamFrame.data)) {
latestWebcamFrame = std::move(candidateWebcamFrame.data);
latestWebcamWidth = candidateWebcamFrame.width;
latestWebcamHeight = candidateWebcamFrame.height;
latestWebcamSequence = candidateWebcamFrame.sequence;
hasVisibleWebcamFrame = true;
}
}
@@ -601,10 +626,23 @@ int main(int argc, char* argv[]) {
frameTimestampHns =
lastEncodedVideoTimestampHns + static_cast<int64_t>(10'000'000ULL / config.fps);
}
if (writeSeparateWebcam && webcamFrame.data &&
latestWebcamSequence != lastWrittenWebcamSequence) {
const int64_t webcamTimestampHns = static_cast<int64_t>(
(webcamOutputFrameIndex * 10'000'000ULL) / std::max(1, webcamCapture.fps()));
if (!webcamEncoder.writeBgraFrame(webcamFrame, webcamTimestampHns)) {
encodeFailed = true;
stopRequested = true;
cv.notify_all();
return;
}
lastWrittenWebcamSequence = latestWebcamSequence;
webcamOutputFrameIndex += 1;
}
if (latestFrameTexture && !encoder.writeFrame(
latestFrameTexture.Get(),
frameTimestampHns,
webcamFrame.data ? &webcamFrame : nullptr)) {
!writeSeparateWebcam && webcamFrame.data ? &webcamFrame : nullptr)) {
encodeFailed = true;
control.stopRequested = true;
control.cv.notify_all();
@@ -712,14 +750,13 @@ int main(int argc, char* argv[]) {
webcamActive = true;
const auto webcamDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < webcamDeadline && !hasVisibleWebcamFrame) {
std::vector<BYTE> candidateWebcamFrame;
int candidateWebcamWidth = 0;
int candidateWebcamHeight = 0;
if (webcamCapture.copyLatestFrame(candidateWebcamFrame, candidateWebcamWidth, candidateWebcamHeight) &&
hasVisibleBgraContent(candidateWebcamFrame)) {
latestWebcamFrame = std::move(candidateWebcamFrame);
latestWebcamWidth = candidateWebcamWidth;
latestWebcamHeight = candidateWebcamHeight;
WebcamFrameSnapshot candidateWebcamFrame;
if (webcamCapture.copyLatestFrame(candidateWebcamFrame) &&
hasVisibleBgraContent(candidateWebcamFrame.data)) {
latestWebcamFrame = std::move(candidateWebcamFrame.data);
latestWebcamWidth = candidateWebcamFrame.width;
latestWebcamHeight = candidateWebcamFrame.height;
latestWebcamSequence = candidateWebcamFrame.sequence;
hasVisibleWebcamFrame = true;
break;
}
@@ -797,6 +834,9 @@ int main(int argc, char* argv[]) {
{
std::scoped_lock lock(mutex);
encoder.finalize();
if (writeSeparateWebcam) {
webcamEncoder.finalize();
}
}
if (stdinThread.joinable()) {
@@ -809,7 +849,11 @@ int main(int argc, char* argv[]) {
}
std::cout << "{\"event\":\"recording-stopped\",\"schemaVersion\":2,\"screenPath\":\""
<< jsonEscape(config.outputPath) << "\"}" << std::endl;
<< jsonEscape(config.outputPath) << "\"";
if (writeSeparateWebcam) {
std::cout << ",\"webcamPath\":\"" << jsonEscape(config.webcamOutputPath) << "\"";
}
std::cout << "}" << std::endl;
std::cout << "Recording stopped. Output path: " << config.outputPath << std::endl;
return 0;
}