Merge pull request #639 from siddharthvaddem/codex/fix-windows-paused-recording

[codex] Fix native Windows recording pause
This commit is contained in:
Sid
2026-05-22 20:09:05 -07:00
committed by GitHub
8 changed files with 257 additions and 38 deletions
+8
View File
@@ -117,6 +117,14 @@ interface Window {
discarded?: boolean;
error?: string;
}>;
pauseNativeWindowsRecording: () => Promise<{
success: boolean;
error?: string;
}>;
resumeNativeWindowsRecording: () => Promise<{
success: boolean;
error?: string;
}>;
startNativeMacRecording: (
request: import("../src/lib/nativeMacRecording").NativeMacRecordingRequest,
) => Promise<import("../src/lib/nativeMacRecording").NativeMacRecordingStartResult>;
+75
View File
@@ -384,6 +384,10 @@ let nativeWindowsCaptureWebcamTargetPath: string | null = null;
let nativeWindowsCaptureRecordingId: number | null = null;
let nativeWindowsCursorOffsetMs = 0;
let nativeWindowsCursorCaptureMode: CursorCaptureMode = "editable-overlay";
let nativeWindowsCursorRecordingStartMs = 0;
let nativeWindowsPauseStartedAtMs: number | null = null;
let nativeWindowsPauseRanges: Array<{ startMs: number; endMs: number }> = [];
let nativeWindowsIsPaused = false;
const NATIVE_WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 15_000;
let nativeMacCaptureProcess: ChildProcessWithoutNullStreams | null = null;
let nativeMacCaptureOutput = "";
@@ -873,6 +877,18 @@ function completeNativeMacCursorPauseRange(endMs = Date.now()) {
nativeMacPauseStartedAtMs = null;
}
function completeNativeWindowsCursorPauseRange(endMs = Date.now()) {
if (nativeWindowsPauseStartedAtMs === null || nativeWindowsCursorRecordingStartMs <= 0) {
return;
}
nativeWindowsPauseRanges.push({
startMs: Math.max(0, nativeWindowsPauseStartedAtMs - nativeWindowsCursorRecordingStartMs),
endMs: Math.max(0, endMs - nativeWindowsCursorRecordingStartMs),
});
nativeWindowsPauseStartedAtMs = null;
}
function waitForNativeWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) {
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
@@ -1583,9 +1599,14 @@ export function registerIpcHandlers(
nativeWindowsCaptureRecordingId = recordingId;
nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = cursorCaptureMode;
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
const cursorStartTimeMs = Date.now();
if (cursorCaptureMode === "editable-overlay") {
nativeWindowsCursorRecordingStartMs = cursorStartTimeMs;
await startCursorRecording(cursorStartTimeMs);
console.info("[native-wgc] cursor sampler ready", {
cursorStartTimeMs,
@@ -1635,6 +1656,10 @@ export function registerIpcHandlers(
nativeWindowsCaptureRecordingId = null;
nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = "editable-overlay";
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
await stopCursorRecording();
return { success: false, error: String(error) };
}
@@ -1836,6 +1861,50 @@ export function registerIpcHandlers(
}
});
ipcMain.handle("pause-native-windows-recording", async () => {
const proc = nativeWindowsCaptureProcess;
if (!proc) {
return { success: false, error: "Native Windows capture is not running." };
}
if (nativeWindowsIsPaused) {
return { success: true };
}
if (!proc.stdin.writable) {
return { success: false, error: "Native Windows capture command channel is closed." };
}
try {
proc.stdin.write("pause\n");
nativeWindowsIsPaused = true;
nativeWindowsPauseStartedAtMs = Date.now();
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});
ipcMain.handle("resume-native-windows-recording", async () => {
const proc = nativeWindowsCaptureProcess;
if (!proc) {
return { success: false, error: "Native Windows capture is not running." };
}
if (!nativeWindowsIsPaused) {
return { success: true };
}
if (!proc.stdin.writable) {
return { success: false, error: "Native Windows capture command channel is closed." };
}
try {
proc.stdin.write("resume\n");
completeNativeWindowsCursorPauseRange();
nativeWindowsIsPaused = false;
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});
ipcMain.handle("stop-native-windows-recording", async (_, discard?: boolean) => {
const proc = nativeWindowsCaptureProcess;
const preferredPath = nativeWindowsCaptureTargetPath;
@@ -1848,6 +1917,7 @@ export function registerIpcHandlers(
}
try {
completeNativeWindowsCursorPauseRange();
const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc);
proc.stdin.write("stop\n");
const stoppedPath = await stoppedPathPromise;
@@ -1872,6 +1942,7 @@ export function registerIpcHandlers(
}
if (cursorCaptureMode === "editable-overlay") {
compactPendingCursorTelemetryPauseRanges(nativeWindowsPauseRanges);
shiftPendingCursorTelemetry(nativeWindowsCursorOffsetMs);
await writePendingCursorTelemetry(screenVideoPath);
}
@@ -1913,6 +1984,10 @@ export function registerIpcHandlers(
nativeWindowsCaptureRecordingId = null;
nativeWindowsCursorOffsetMs = 0;
nativeWindowsCursorCaptureMode = "editable-overlay";
nativeWindowsCursorRecordingStartMs = 0;
nativeWindowsPauseStartedAtMs = null;
nativeWindowsPauseRanges = [];
nativeWindowsIsPaused = false;
const source = selectedSource || { name: "Screen" };
if (onRecordingStateChange) {
onRecordingStateChange(false, source.name);
@@ -279,6 +279,7 @@ bool AudioMixer::start() {
stopRequested_ = false;
emittedFrames_ = 0;
timelineStarted_ = false;
paused_ = false;
thread_ = std::thread([this] {
mixLoop();
});
@@ -296,6 +297,18 @@ void AudioMixer::beginTimeline() {
cv_.notify_all();
}
void AudioMixer::setPaused(bool paused) {
{
std::scoped_lock lock(mutex_);
paused_ = paused;
if (paused_) {
systemQueue_.clear();
microphoneQueue_.clear();
}
}
cv_.notify_all();
}
void AudioMixer::stop() {
stopRequested_ = true;
cv_.notify_all();
@@ -311,6 +324,9 @@ void AudioMixer::pushSystem(const BYTE* data, DWORD byteCount) {
{
std::scoped_lock lock(mutex_);
if (paused_) {
return;
}
append(systemQueue_, data, byteCount, systemFormat_, 1.0);
}
cv_.notify_all();
@@ -323,6 +339,9 @@ void AudioMixer::pushMicrophone(const BYTE* data, DWORD byteCount) {
{
std::scoped_lock lock(mutex_);
if (paused_) {
return;
}
append(microphoneQueue_, data, byteCount, microphoneFormat_, microphoneGain_);
}
cv_.notify_all();
@@ -371,13 +390,13 @@ void AudioMixer::mixLoop() {
const bool hasMicrophone = !includeMicrophone_ || microphoneQueue_.size() >= chunkBytes;
const bool hasAnySource = !systemQueue_.empty() || !microphoneQueue_.empty();
return stopRequested_.load() ||
(timelineStarted_ && (hasSystem || hasMicrophone) && hasAnySource);
(timelineStarted_ && !paused_ && (hasSystem || hasMicrophone) && hasAnySource);
});
if (stopRequested_) {
break;
}
if (!timelineStarted_) {
if (!timelineStarted_ || paused_) {
continue;
}
@@ -52,6 +52,7 @@ public:
bool start();
void beginTimeline();
void setPaused(bool paused);
void stop();
void pushSystem(const BYTE* data, DWORD byteCount);
void pushMicrophone(const BYTE* data, DWORD byteCount);
@@ -81,5 +82,6 @@ private:
std::thread thread_;
std::atomic<bool> stopRequested_ = false;
bool timelineStarted_ = false;
bool paused_ = false;
uint64_t emittedFrames_ = 0;
};
+89 -32
View File
@@ -13,6 +13,7 @@
#include <condition_variable>
#include <cctype>
#include <cstdint>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
@@ -50,6 +51,37 @@ struct CaptureConfig {
int webcamFps = 0;
};
struct CaptureControl {
std::atomic<bool> stopRequested = false;
std::atomic<bool> paused = false;
std::mutex mutex;
std::condition_variable cv;
std::chrono::steady_clock::time_point pauseStartedAt;
std::chrono::steady_clock::duration totalPausedDuration{};
int64_t pausedDurationHns() {
std::scoped_lock lock(mutex);
auto total = totalPausedDuration;
if (paused.load()) {
total += std::chrono::steady_clock::now() - pauseStartedAt;
}
return std::chrono::duration_cast<std::chrono::nanoseconds>(total).count() / 100;
}
void setPaused(bool nextPaused) {
std::scoped_lock lock(mutex);
if (nextPaused == paused.load()) {
return;
}
if (nextPaused) {
pauseStartedAt = std::chrono::steady_clock::now();
} else {
totalPausedDuration += std::chrono::steady_clock::now() - pauseStartedAt;
}
paused = nextPaused;
}
};
std::wstring utf8ToWide(const std::string& value) {
if (value.empty()) {
return {};
@@ -319,17 +351,31 @@ bool parseConfig(const std::string& json, CaptureConfig& config) {
return true;
}
void readStopCommands(std::atomic<bool>& stopRequested, std::condition_variable& cv) {
void readCaptureCommands(CaptureControl& control, const std::function<void(bool)>& onPauseChanged) {
std::string line;
while (std::getline(std::cin, line)) {
if (line == "stop" || line == "q" || line == "quit") {
stopRequested = true;
cv.notify_all();
control.stopRequested = true;
control.cv.notify_all();
return;
}
if (line == "pause") {
control.setPaused(true);
onPauseChanged(true);
std::cout << "{\"event\":\"recording-paused\",\"schemaVersion\":2}" << std::endl;
control.cv.notify_all();
continue;
}
if (line == "resume") {
control.setPaused(false);
onPauseChanged(false);
std::cout << "{\"event\":\"recording-resumed\",\"schemaVersion\":2}" << std::endl;
control.cv.notify_all();
continue;
}
}
stopRequested = true;
cv.notify_all();
control.stopRequested = true;
control.cv.notify_all();
}
} // namespace
@@ -489,8 +535,7 @@ int main(int argc, char* argv[]) {
}
std::mutex mutex;
std::condition_variable cv;
std::atomic<bool> stopRequested = false;
CaptureControl control;
std::atomic<bool> firstFrameWritten = false;
std::atomic<bool> encodeFailed = false;
Microsoft::WRL::ComPtr<ID3D11Texture2D> latestFrameTexture;
@@ -503,7 +548,7 @@ int main(int argc, char* argv[]) {
bool hasVisibleWebcamFrame = false;
session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) {
if (stopRequested) {
if (control.stopRequested || control.paused) {
return;
}
@@ -516,8 +561,8 @@ int main(int argc, char* argv[]) {
desc.MiscFlags = 0;
if (FAILED(session.device()->CreateTexture2D(&desc, nullptr, &latestFrameTexture))) {
encodeFailed = true;
stopRequested = true;
cv.notify_all();
control.stopRequested = true;
control.cv.notify_all();
return;
}
}
@@ -525,20 +570,29 @@ int main(int argc, char* argv[]) {
session.context()->CopyResource(latestFrameTexture.Get(), texture);
latestFrameTimestampHns = timestampHns;
if (!firstFrameWritten.exchange(true)) {
cv.notify_all();
control.cv.notify_all();
}
});
auto writeVideoFrames = [&]() {
const auto startedAt = std::chrono::steady_clock::now();
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 (!stopRequested && !encodeFailed) {
while (!control.stopRequested && !encodeFailed) {
{
std::scoped_lock lock(mutex);
std::unique_lock lock(mutex);
control.cv.wait(lock, [&] {
return control.stopRequested.load() ||
encodeFailed.load() ||
(!control.paused.load() && latestFrameTexture);
});
if (control.stopRequested || encodeFailed) {
break;
}
if (webcamActive) {
WebcamFrameSnapshot candidateWebcamFrame;
if (webcamCapture.copyLatestFrame(candidateWebcamFrame) &&
@@ -564,7 +618,9 @@ int main(int argc, char* argv[]) {
firstFrameTimestampHns = sourceTimestampHns;
}
int64_t frameTimestampHns =
std::max<int64_t>(0, sourceTimestampHns - firstFrameTimestampHns);
std::max<int64_t>(
0,
sourceTimestampHns - firstFrameTimestampHns - control.pausedDurationHns());
if (lastEncodedVideoTimestampHns >= 0 &&
frameTimestampHns <= lastEncodedVideoTimestampHns) {
frameTimestampHns =
@@ -588,8 +644,8 @@ int main(int argc, char* argv[]) {
frameTimestampHns,
!writeSeparateWebcam && webcamFrame.data ? &webcamFrame : nullptr)) {
encodeFailed = true;
stopRequested = true;
cv.notify_all();
control.stopRequested = true;
control.cv.notify_all();
return;
}
if (latestFrameTexture) {
@@ -598,10 +654,7 @@ int main(int argc, char* argv[]) {
}
frameIndex += 1;
const auto nextDeadline = startedAt +
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(static_cast<double>(frameIndex) / config.fps));
std::this_thread::sleep_until(nextDeadline);
std::this_thread::sleep_for(frameDuration);
}
};
@@ -633,8 +686,8 @@ int main(int argc, char* argv[]) {
[&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) {
if (!encoder.writeAudio(data, byteCount, timestampHns, durationHns)) {
encodeFailed = true;
stopRequested = true;
cv.notify_all();
control.stopRequested = true;
control.cv.notify_all();
return false;
}
return true;
@@ -649,7 +702,7 @@ int main(int argc, char* argv[]) {
if (!microphoneCapture.start([&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) {
(void)timestampHns;
(void)durationHns;
if (stopRequested || !audioMixer) {
if (control.stopRequested || !audioMixer) {
return;
}
@@ -665,7 +718,7 @@ int main(int argc, char* argv[]) {
if (!loopbackCapture.start([&](const BYTE* data, DWORD byteCount, int64_t timestampHns, int64_t durationHns) {
(void)timestampHns;
(void)durationHns;
if (stopRequested || !audioMixer) {
if (control.stopRequested || !audioMixer) {
return;
}
@@ -726,16 +779,20 @@ int main(int argc, char* argv[]) {
return 1;
}
std::thread stdinThread(readStopCommands, std::ref(stopRequested), std::ref(cv));
std::thread stdinThread(readCaptureCommands, std::ref(control), [&](bool isPaused) {
if (audioMixer) {
audioMixer->setPaused(isPaused);
}
});
{
std::unique_lock lock(mutex);
const bool started = cv.wait_for(lock, std::chrono::seconds(10), [&] {
return firstFrameWritten.load() || stopRequested.load();
const bool started = control.cv.wait_for(lock, std::chrono::seconds(10), [&] {
return firstFrameWritten.load() || control.stopRequested.load();
});
if (!started || !firstFrameWritten) {
stopRequested = true;
cv.notify_all();
control.stopRequested = true;
control.cv.notify_all();
if (stdinThread.joinable()) {
stdinThread.detach();
}
@@ -761,8 +818,8 @@ int main(int argc, char* argv[]) {
{
std::unique_lock lock(mutex);
cv.wait(lock, [&] {
return stopRequested.load();
control.cv.wait(lock, [&] {
return control.stopRequested.load();
});
}
+6
View File
@@ -87,6 +87,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
stopNativeWindowsRecording: (discard?: boolean) => {
return ipcRenderer.invoke("stop-native-windows-recording", discard);
},
pauseNativeWindowsRecording: () => {
return ipcRenderer.invoke("pause-native-windows-recording");
},
resumeNativeWindowsRecording: () => {
return ipcRenderer.invoke("resume-native-windows-recording");
},
startNativeMacRecording: (request: NativeMacRecordingRequest) => {
return ipcRenderer.invoke("start-native-mac-recording", request);
},
+19 -2
View File
@@ -37,6 +37,21 @@ function findVcVarsAll() {
return null;
}
function findWindowsSdkUmLibDir() {
const sdkLibRoot = "C:\\Program Files (x86)\\Windows Kits\\10\\Lib";
if (!fs.existsSync(sdkLibRoot)) {
return null;
}
return fs
.readdirSync(sdkLibRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(sdkLibRoot, entry.name, "um", "x64"))
.filter((candidate) => fs.existsSync(path.join(candidate, "kernel32.lib")))
.sort()
.at(-1);
}
function run(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
@@ -64,6 +79,8 @@ async function runInVsEnv(command) {
);
}
const sdkUmLibDir = findWindowsSdkUmLibDir();
const cmdPath = path.join(os.tmpdir(), `openscreen-build-wgc-${process.pid}-${Date.now()}.cmd`);
fs.writeFileSync(
cmdPath,
@@ -72,9 +89,9 @@ async function runInVsEnv(command) {
`call "${vcvarsAll}" x64`,
"if errorlevel 1 exit /b %errorlevel%",
`if not exist "${COMPAT_LIB_DIR}" mkdir "${COMPAT_LIB_DIR}"`,
`for %%L in (gdi32.lib winspool.lib shell32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib) do if not exist "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\%%L" copy /Y "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\kernel32.Lib" "${COMPAT_LIB_DIR}\\%%L" >nul`,
`for %%L in (gdi32.lib gdiplus.lib winspool.lib shell32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib) do if not exist "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\%%L" copy /Y "%WindowsSdkDir%Lib\\%WindowsSDKLibVersion%um\\x64\\kernel32.Lib" "${COMPAT_LIB_DIR}\\%%L" >nul`,
"if errorlevel 1 exit /b %errorlevel%",
`set "LIB=${COMPAT_LIB_DIR};%LIB%"`,
`set "LIB=${sdkUmLibDir ? `${sdkUmLibDir};` : ""}%LIB%;${COMPAT_LIB_DIR}"`,
command,
"exit /b %errorlevel%",
"",
+37 -2
View File
@@ -82,6 +82,7 @@ type RecorderHandle = {
type NativeWindowsRecordingHandle = {
recordingId: number;
finalizing: boolean;
paused: boolean;
webcamRecorder: RecorderHandle | null;
};
@@ -148,9 +149,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const webcamAcquireId = useRef(0);
const canPauseRecording =
recording &&
!nativeWindowsRecording.current &&
Boolean(
(nativeMacRecording.current && !nativeMacRecording.current.finalizing) ||
(nativeWindowsRecording.current && !nativeWindowsRecording.current.finalizing) ||
(nativeMacRecording.current && !nativeMacRecording.current.finalizing) ||
(screenRecorder.current && screenRecorder.current.recorder.state !== "inactive"),
);
@@ -875,6 +876,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
nativeWindowsRecording.current = {
recordingId: result.recordingId,
finalizing: false,
paused: false,
webcamRecorder: browserWebcamRecorder,
};
webcamRecorder.current = browserWebcamRecorder;
@@ -1403,6 +1405,39 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
};
const togglePaused = () => {
const activeNativeWindowsRecording = nativeWindowsRecording.current;
if (activeNativeWindowsRecording && !activeNativeWindowsRecording.finalizing) {
void (async () => {
try {
if (activeNativeWindowsRecording.paused) {
const result = await window.electronAPI.resumeNativeWindowsRecording();
if (!result.success) {
throw new Error(result.error ?? "Failed to resume native Windows recording");
}
activeNativeWindowsRecording.paused = false;
segmentStartedAt.current = Date.now();
setPaused(false);
return;
}
const pausedAtMs = getRecordingDurationMs();
const result = await window.electronAPI.pauseNativeWindowsRecording();
if (!result.success) {
throw new Error(result.error ?? "Failed to pause native Windows recording");
}
activeNativeWindowsRecording.paused = true;
accumulatedDurationMs.current = pausedAtMs;
segmentStartedAt.current = null;
setElapsedSeconds(Math.floor(accumulatedDurationMs.current / 1000));
setPaused(true);
} catch (error) {
console.error("Failed to toggle native Windows pause state:", error);
toast.error(error instanceof Error ? error.message : "Failed to toggle pause state");
}
})();
return;
}
const activeNativeMacRecording = nativeMacRecording.current;
if (activeNativeMacRecording && !activeNativeMacRecording.finalizing) {
void (async () => {