Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,9 +552,7 @@ interface Window {
setHasUnsavedChanges: (hasChanges: boolean) => void;
onRequestSaveBeforeClose: (callback: () => Promise<boolean>) => () => void;
isNativeWindowsCaptureAvailable: () => Promise<{ available: boolean }>;
muxNativeWindowsRecording: (
pauseSegments?: Array<{ startMs: number; endMs: number }>,
) => Promise<{
muxNativeWindowsRecording: (expectedDurationMs?: number) => Promise<{
success: boolean;
path?: string;
message?: string;
Expand Down
98 changes: 60 additions & 38 deletions electron/ipc/recording/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { getFfmpegBinaryPath } from "../ffmpeg/binary";
import {
appendSyncedAudioFilter,
applyRecordedAudioStartDelay,
buildPausedAudioFilter,
getAudioSyncAdjustment,
normalizePauseSegments,
} from "../ffmpeg/filters";
import { getWindowsCaptureExePath } from "../paths/binaries";
import {
Expand All @@ -23,7 +21,7 @@ import {
windowsCaptureTargetPath,
windowsNativeCaptureActive,
} from "../state";
import type { AudioSyncAdjustment, PauseSegment } from "../types";
import type { AudioSyncAdjustment } from "../types";
import { moveFileWithOverwrite } from "../utils";
import {
getCompanionAudioStartDelayMs,
Expand All @@ -36,6 +34,7 @@ const execFileAsync = promisify(execFile);
// Match the browser path's "usable speech level" intent with a standard
// loudness pass on native Windows mic audio instead of a fixed tiny boost.
const WINDOWS_NATIVE_MIC_PRE_FILTERS = ["loudnorm=I=-16:TP=-1.5:LRA=11"];
const MIN_NATIVE_WINDOWS_VIDEO_PAD_MS = 500;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Lower or remove the 500ms padding cutoff.

Line 177 still allows up to 499ms of missing tail to ship uncorrected, so short/static endings can remain visibly truncated even after this fallback runs. That misses the PR’s “preserve full recorded duration” goal.

Also applies to: 175-179

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/recording/windows.ts` at line 37, The constant
MIN_NATIVE_WINDOWS_VIDEO_PAD_MS (currently 500) allows up to 499ms of unpadded
tail to go uncorrected; reduce or remove this cutoff (e.g., set to 0–100ms) so
short/static endings are preserved and update any logic that compares against it
(the branch that checks remainingTail < MIN_NATIVE_WINDOWS_VIDEO_PAD_MS) to use
the new threshold or strict comparison as appropriate; ensure all references to
MIN_NATIVE_WINDOWS_VIDEO_PAD_MS in the file (including the fallback handling
around the tail-padding logic) are updated so the fallback will run for short
tails and preserve the full recorded duration.


export async function isNativeWindowsCaptureAvailable(): Promise<boolean> {
if (process.platform !== "win32") return false;
Expand Down Expand Up @@ -160,11 +159,65 @@ export function attachWindowsCaptureLifecycle(proc: ChildProcessWithoutNullStrea
});
}

export async function extendNativeWindowsVideoToDuration(
videoPath: string,
targetDurationMs: number | null | undefined,
) {
if (!Number.isFinite(targetDurationMs) || (targetDurationMs ?? 0) <= 0) {
return { padded: false, durationSeconds: 0 };
}

const currentDurationSeconds = await probeMediaDurationSeconds(videoPath);
if (currentDurationSeconds <= 0) {
return { padded: false, durationSeconds: currentDurationSeconds };
}

const targetDurationSeconds = (targetDurationMs ?? 0) / 1000;
const padDurationSeconds = targetDurationSeconds - currentDurationSeconds;
if (padDurationSeconds * 1000 < MIN_NATIVE_WINDOWS_VIDEO_PAD_MS) {
return { padded: false, durationSeconds: currentDurationSeconds };
}

const ffmpegPath = getFfmpegBinaryPath();
const paddedOutputPath = `${videoPath}.duration-padded.mp4`;

try {
await execFileAsync(
ffmpegPath,
[
"-y",
"-i",
videoPath,
"-vf",
`tpad=stop_mode=clone:stop_duration=${padDurationSeconds.toFixed(3)}`,
"-an",
"-c:v",
"libx264",
"-preset",
"veryfast",
"-crf",
"18",
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
paddedOutputPath,
],
{ timeout: 300000, maxBuffer: 10 * 1024 * 1024 },
);
await validateRecordedVideo(paddedOutputPath);
await moveFileWithOverwrite(paddedOutputPath, videoPath);
return { padded: true, durationSeconds: targetDurationSeconds };
} catch (error) {
await fs.rm(paddedOutputPath, { force: true }).catch(() => undefined);
throw error;
Comment on lines +184 to +213
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat duration padding as best-effort, not fatal.

If ffprobe/ffmpeg/validation fails here, the whole stop/finalization path will reject even though the original video already exists. This fallback should log, clean up, and continue with the unmodified file instead of aborting the recording flow.

Proposed fix
 	} catch (error) {
 		await fs.rm(paddedOutputPath, { force: true }).catch(() => undefined);
-		throw error;
+		console.warn(
+			"[mux-win] Failed to extend native Windows video duration; continuing with original file",
+			error,
+		);
+		return { padded: false, durationSeconds: currentDurationSeconds };
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/recording/windows.ts` around lines 184 - 213, The padding step
currently throws on any error which aborts finalization; change it to
best-effort: in the catch after
execFileAsync/validateRecordedVideo/moveFileWithOverwrite (referencing
execFileAsync, validateRecordedVideo, moveFileWithOverwrite, paddedOutputPath,
videoPath, padDurationSeconds, targetDurationSeconds) remove the rethrow and
instead log the caught error (use the existing logger if available or
console.warn), ensure paddedOutputPath is removed with
fs.rm(...).catch(()=>undefined) to ignore cleanup errors, and return { padded:
false, durationSeconds: targetDurationSeconds } so the original unmodified
videoPath is used and the recording flow continues.

}
}

export async function muxNativeWindowsVideoWithAudio(
videoPath: string,
systemAudioPath: string | null,
micAudioPath: string | null,
pauseSegments: PauseSegment[] = [],
) {
const ffmpegPath = getFfmpegBinaryPath();
const inputs: string[] = ["-i", videoPath];
Expand Down Expand Up @@ -230,7 +283,6 @@ export async function muxNativeWindowsVideoWithAudio(
}

const mixedOutputPath = `${videoPath}.muxed.mp4`;
const normalizedPauseSegments = normalizePauseSegments(pauseSegments);
const systemAdjustment = audioAdjustments.get("system") ?? {
mode: "none",
delayMs: 0,
Expand All @@ -247,29 +299,8 @@ export async function muxNativeWindowsVideoWithAudio(
try {
if (audioInputs.length === 2) {
const filterParts: string[] = [];
const systemPauseFilter = buildPausedAudioFilter(
"1:a",
"system_trimmed",
normalizedPauseSegments,
);
const micPauseFilter = buildPausedAudioFilter(
"2:a",
"mic_trimmed",
normalizedPauseSegments,
);

if (systemPauseFilter) {
filterParts.push(systemPauseFilter);
}
if (micPauseFilter) {
filterParts.push(micPauseFilter);
}

const systemLabel = systemPauseFilter ? "[system_trimmed]" : "[1:a]";
const micLabel = micPauseFilter ? "[mic_trimmed]" : "[2:a]";

appendSyncedAudioFilter(filterParts, systemLabel, "s", systemAdjustment);
appendSyncedAudioFilter(filterParts, micLabel, "m", micAdjustment, {
appendSyncedAudioFilter(filterParts, "[1:a]", "s", systemAdjustment);
appendSyncedAudioFilter(filterParts, "[2:a]", "m", micAdjustment, {
preFilters: WINDOWS_NATIVE_MIC_PRE_FILTERS,
});
filterParts.push("[s][m]amix=inputs=2:duration=longest:normalize=0[aout]");
Expand Down Expand Up @@ -297,11 +328,6 @@ export async function muxNativeWindowsVideoWithAudio(
{ timeout: 120000, maxBuffer: 10 * 1024 * 1024 },
);
} else {
const pauseFilter = buildPausedAudioFilter(
"1:a",
"trimmed_audio",
normalizedPauseSegments,
);
const singleAdjustment = audioAdjustments.get(audioInputs[0]) ?? {
mode: "none",
delayMs: 0,
Expand All @@ -313,13 +339,9 @@ export async function muxNativeWindowsVideoWithAudio(
// applied. This corrects progressive clock drift between video and
// audio tracks that a simple duration comparison cannot detect.
const filterParts: string[] = [];
if (pauseFilter) {
filterParts.push(pauseFilter);
}
const srcLabel = pauseFilter ? "[trimmed_audio]" : "[1:a]";
appendSyncedAudioFilter(
filterParts,
srcLabel,
"[1:a]",
"aout",
singleAdjustment,
audioInputs[0] === "mic" ? { preFilters: WINDOWS_NATIVE_MIC_PRE_FILTERS } : 1,
Expand Down
22 changes: 19 additions & 3 deletions electron/ipc/register/recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
} from "../recording/mac";
import {
attachWindowsCaptureLifecycle,
extendNativeWindowsVideoToDuration,
isNativeWindowsCaptureAvailable,
muxNativeWindowsVideoWithAudio,
waitForWindowsCaptureStart,
Expand Down Expand Up @@ -124,7 +125,6 @@ import {
import type {
CursorTelemetryPoint,
NativeMacRecordingOptions,
PauseSegment,
SelectedSource,
} from "../types";
import {
Expand Down Expand Up @@ -1034,7 +1034,7 @@ export function registerRecordingHandlers(

ipcMain.handle(
"mux-native-windows-recording",
async (_event, pauseSegments?: PauseSegment[]) => {
async (_event, expectedDurationMs?: number) => {
const videoPath = windowsPendingVideoPath;
const orphanedMicAudioPath = windowsOrphanedMicAudioPath;
setWindowsPendingVideoPath(null);
Expand All @@ -1045,12 +1045,28 @@ export function registerRecordingHandlers(
}

try {
try {
const padding = await extendNativeWindowsVideoToDuration(
videoPath,
expectedDurationMs,
);
if (padding.padded) {
console.log(
`[mux-win] Extended native Windows video to ${padding.durationSeconds.toFixed(3)}s using the final frame`,
);
}
} catch (paddingError) {
console.warn(
"[mux-win] Failed to extend native Windows video duration:",
paddingError,
);
}

if (windowsSystemAudioPath || windowsMicAudioPath) {
await muxNativeWindowsVideoWithAudio(
videoPath,
windowsSystemAudioPath,
windowsMicAudioPath,
pauseSegments ?? [],
);
setWindowsSystemAudioPath(null);
setWindowsMicAudioPath(null);
Expand Down
6 changes: 3 additions & 3 deletions electron/native/bin/win32-x64/helpers-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"helpers": {
"wgc-capture": {
"binaryName": "wgc-capture.exe",
"binarySha256": "bb4c2aa4141e81e1a05b54bd49dbb114eb3a5ff4a666bf7d26525a9da585128b",
"binarySha256": "501335fa59300aecfe6725bc81001462bad64494fb29986bc2f2bccd37c38bbf",
"sourceDir": "electron/native/wgc-capture",
"sourceFingerprint": "5bb02a69049a02909d38188cae1dfdfb94fd49465b51ed6ab78a98092b7520cc",
"updatedAt": "2026-04-25T09:17:16.237Z"
"sourceFingerprint": "0d989a0e3ccd628959781e38a3129e57a92a962a19adc0cc9e5fbdc8c5fc7f76",
"updatedAt": "2026-05-03T14:26:22.012Z"
},
"cursor-monitor": {
"binaryName": "cursor-monitor.exe",
Expand Down
Binary file modified electron/native/bin/win32-x64/wgc-capture.exe
Binary file not shown.
66 changes: 49 additions & 17 deletions electron/native/wgc-capture/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

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};
Expand Down Expand Up @@ -157,6 +156,47 @@ static std::wstring utf8ToWide(const std::string& str) {
return wstr;
}

static int64_t queryPerformanceCounterHns() {
LARGE_INTEGER counter;
LARGE_INTEGER frequency;
if (!QueryPerformanceCounter(&counter) || !QueryPerformanceFrequency(&frequency) || frequency.QuadPart <= 0) {
return g_lastFrameTimestampHns.load();
}

return static_cast<int64_t>(
(static_cast<long double>(counter.QuadPart) * 10000000.0L) /
static_cast<long double>(frequency.QuadPart));
}

static int64_t adjustedVideoTimestampHns(int64_t timestampHns) {
int64_t accumulatedPausedHns = g_accumulatedPausedHns.load();
if (g_pauseRequested.load()) {
const int64_t pauseStart = g_pauseStartTimestampHns.load();
if (pauseStart > 0 && timestampHns > pauseStart) {
accumulatedPausedHns += (timestampHns - pauseStart);
}
}

int64_t adjustedTimestampHns = timestampHns - accumulatedPausedHns;
if (adjustedTimestampHns < 0) {
adjustedTimestampHns = 0;
}
return adjustedTimestampHns;
}

static void openActivePauseAt(int64_t timestampHns) {
g_pauseStartTimestampHns.store(timestampHns);
g_pauseRequested.store(true);
}

static void closeActivePauseAt(int64_t timestampHns) {
const int64_t pauseStart = g_pauseStartTimestampHns.exchange(0);
if (pauseStart > 0 && timestampHns > pauseStart) {
g_accumulatedPausedHns.fetch_add(timestampHns - pauseStart);
}
g_pauseRequested.store(false);
}

static void writeCompanionAudioTimingMetadata(
const std::string& audioPath,
int64_t firstVideoTimestampHns,
Expand Down Expand Up @@ -203,14 +243,12 @@ static void stdinListenerThread() {
}

if (line == "pause") {
g_pauseRequested = true;
g_pauseStartTimestampHns = g_lastFrameTimestampHns.load();
openActivePauseAt(queryPerformanceCounterHns());
continue;
}

if (line == "resume") {
g_pauseRequested = false;
g_resumePending = true;
closeActivePauseAt(queryPerformanceCounterHns());
continue;
}

Expand Down Expand Up @@ -298,18 +336,7 @@ int main(int argc, char* argv[]) {

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;
}
const int64_t adjustedTimestampHns = adjustedVideoTimestampHns(timestampHns);

if (encoder.writeFrame(texture, adjustedTimestampHns)) {
const int64_t writtenFrames = frameCount.fetch_add(1) + 1;
Expand Down Expand Up @@ -374,6 +401,7 @@ int main(int argc, char* argv[]) {
}

// Stop capture and finalize
const int64_t adjustedStopTimestampHns = adjustedVideoTimestampHns(queryPerformanceCounterHns());
session.stopCapture();
if (audioActive) loopback.stop();
if (micActive) micCapture.stop();
Expand All @@ -397,6 +425,10 @@ int main(int argc, char* argv[]) {
return 1;
}

if (!encoder.extendLastFrameTo(adjustedStopTimestampHns)) {
std::cerr << "WARNING: Failed to extend the last video frame to the stop timestamp" << std::endl;
}

if (!encoder.finalize()) {
std::cerr << "ERROR: Failed to finalize Media Foundation encoder" << std::endl;
return 1;
Expand Down
Loading