mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-27 00:05:39 +00:00
Merge pull request #314 from soufian3hm/fix/windows-recording-validation
Fix invalid Windows recording finalization
This commit is contained in:
@@ -132,4 +132,15 @@ describe("getCompanionAudioFallbackPaths", () => {
|
||||
micPath,
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects tiny MP4 container-only outputs before they reach the editor", async () => {
|
||||
const videoPath = path.join(tempRoot, "recording-123.mp4");
|
||||
await fs.writeFile(videoPath, Buffer.alloc(261));
|
||||
|
||||
const { validateRecordedVideo } = await import("./diagnostics");
|
||||
|
||||
await expect(validateRecordedVideo(videoPath)).rejects.toThrow(
|
||||
"Recorded output is too small to contain playable video",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { lastNativeCaptureDiagnostics, setLastNativeCaptureDiagnostics } from ".
|
||||
import type { CompanionAudioCandidate, NativeCaptureDiagnostics } from "../types";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
export const MIN_VALID_RECORDED_VIDEO_BYTES = 1024;
|
||||
|
||||
export function recordNativeCaptureDiagnostics(
|
||||
diagnostics: Omit<NativeCaptureDiagnostics, "timestamp">,
|
||||
@@ -152,6 +153,12 @@ export async function validateRecordedVideo(videoPath: string) {
|
||||
throw new Error(`Recorded output is empty: ${videoPath}`);
|
||||
}
|
||||
|
||||
if (stat.size < MIN_VALID_RECORDED_VIDEO_BYTES) {
|
||||
throw new Error(
|
||||
`Recorded output is too small to contain playable video (${stat.size} bytes): ${videoPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
const ffmpegPath = getFfmpegBinaryPath();
|
||||
let stderr = "";
|
||||
|
||||
@@ -173,7 +180,7 @@ export async function validateRecordedVideo(videoPath: string) {
|
||||
}
|
||||
|
||||
const durationSeconds = parseFfmpegDurationSeconds(stderr);
|
||||
if (durationSeconds !== null && durationSeconds <= 0) {
|
||||
if (durationSeconds === null || durationSeconds <= 0) {
|
||||
throw new Error(`Recorded output has an invalid duration: ${videoPath}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { getFfmpegBinaryPath } from "../ffmpeg/binary";
|
||||
import {
|
||||
getAudioSyncAdjustment,
|
||||
appendSyncedAudioFilter,
|
||||
} from "../ffmpeg/filters";
|
||||
import type { AudioSyncAdjustment } from "../types";
|
||||
persistPendingCursorTelemetry,
|
||||
snapshotCursorTelemetryForPersistence,
|
||||
} from "../cursor/telemetry";
|
||||
import { getFfmpegBinaryPath } from "../ffmpeg/binary";
|
||||
import { appendSyncedAudioFilter, getAudioSyncAdjustment } from "../ffmpeg/filters";
|
||||
import {
|
||||
nativeScreenRecordingActive,
|
||||
setNativeScreenRecordingActive,
|
||||
@@ -27,20 +27,17 @@ import {
|
||||
setCurrentProjectPath,
|
||||
selectedSource,
|
||||
} from "../state";
|
||||
import { moveFileWithOverwrite, isAutoRecordingPath } from "../utils";
|
||||
import type { AudioSyncAdjustment } from "../types";
|
||||
import { isAutoRecordingPath, moveFileWithOverwrite } from "../utils";
|
||||
import {
|
||||
recordNativeCaptureDiagnostics,
|
||||
getFileSizeIfPresent,
|
||||
validateRecordedVideo,
|
||||
getUsableCompanionAudioCandidates,
|
||||
probeMediaDurationSeconds,
|
||||
recordNativeCaptureDiagnostics,
|
||||
validateRecordedVideo,
|
||||
} from "./diagnostics";
|
||||
import { probeMediaDurationSeconds } from "./diagnostics";
|
||||
import { emitRecordingInterrupted } from "./events";
|
||||
import { pruneAutoRecordings } from "./prune";
|
||||
import {
|
||||
snapshotCursorTelemetryForPersistence,
|
||||
persistPendingCursorTelemetry,
|
||||
} from "../cursor/telemetry";
|
||||
import { muxNativeWindowsVideoWithAudio } from "./windows";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -259,9 +256,11 @@ export async function muxNativeMacRecordingWithAudio(
|
||||
|
||||
try {
|
||||
await execFileAsync(ffmpegPath, args, { timeout: 120000, maxBuffer: 10 * 1024 * 1024 });
|
||||
await validateRecordedVideo(mixedOutputPath);
|
||||
} catch (error) {
|
||||
const execError = error as NodeJS.ErrnoException & { stderr?: string };
|
||||
console.error("[mux] ffmpeg failed:", execError.stderr || execError.message);
|
||||
console.error("[mux] failed:", execError.stderr || execError.message || String(error));
|
||||
await fs.rm(mixedOutputPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -336,11 +335,35 @@ export async function finalizeStoredVideo(videoPath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
let validation: { fileSizeBytes: number; durationSeconds: number | null } | null = null;
|
||||
let validation: { fileSizeBytes: number; durationSeconds: number | null };
|
||||
try {
|
||||
validation = await validateRecordedVideo(videoPath);
|
||||
} catch (error) {
|
||||
console.warn("Video validation failed (proceeding anyway):", error);
|
||||
if (
|
||||
lastNativeCaptureDiagnostics?.backend === "mac-screencapturekit" ||
|
||||
lastNativeCaptureDiagnostics?.backend === "windows-wgc"
|
||||
) {
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: lastNativeCaptureDiagnostics.backend,
|
||||
phase: lastNativeCaptureDiagnostics.phase === "mux" ? "mux" : "stop",
|
||||
sourceId: lastNativeCaptureDiagnostics.sourceId ?? null,
|
||||
sourceType: lastNativeCaptureDiagnostics.sourceType ?? "unknown",
|
||||
displayId: lastNativeCaptureDiagnostics.displayId ?? null,
|
||||
displayBounds: lastNativeCaptureDiagnostics.displayBounds ?? null,
|
||||
windowHandle: lastNativeCaptureDiagnostics.windowHandle ?? null,
|
||||
helperPath: lastNativeCaptureDiagnostics.helperPath ?? null,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: lastNativeCaptureDiagnostics.systemAudioPath ?? null,
|
||||
microphonePath: lastNativeCaptureDiagnostics.microphonePath ?? null,
|
||||
osRelease: lastNativeCaptureDiagnostics.osRelease,
|
||||
supported: lastNativeCaptureDiagnostics.supported,
|
||||
helperExists: lastNativeCaptureDiagnostics.helperExists,
|
||||
processOutput: lastNativeCaptureDiagnostics.processOutput,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
snapshotCursorTelemetryForPersistence();
|
||||
@@ -351,10 +374,13 @@ export async function finalizeStoredVideo(videoPath: string) {
|
||||
await pruneAutoRecordings([videoPath]);
|
||||
}
|
||||
|
||||
if (lastNativeCaptureDiagnostics?.backend === "mac-screencapturekit") {
|
||||
if (
|
||||
lastNativeCaptureDiagnostics?.backend === "mac-screencapturekit" ||
|
||||
lastNativeCaptureDiagnostics?.backend === "windows-wgc"
|
||||
) {
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "mac-screencapturekit",
|
||||
phase: "stop",
|
||||
backend: lastNativeCaptureDiagnostics.backend,
|
||||
phase: lastNativeCaptureDiagnostics.phase === "mux" ? "mux" : "stop",
|
||||
sourceId: lastNativeCaptureDiagnostics.sourceId ?? null,
|
||||
sourceType: lastNativeCaptureDiagnostics.sourceType ?? "unknown",
|
||||
displayId: lastNativeCaptureDiagnostics.displayId ?? null,
|
||||
@@ -368,7 +394,7 @@ export async function finalizeStoredVideo(videoPath: string) {
|
||||
supported: lastNativeCaptureDiagnostics.supported,
|
||||
helperExists: lastNativeCaptureDiagnostics.helperExists,
|
||||
processOutput: lastNativeCaptureDiagnostics.processOutput,
|
||||
fileSizeBytes: validation?.fileSizeBytes ?? null,
|
||||
fileSizeBytes: validation.fileSizeBytes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -376,7 +402,7 @@ export async function finalizeStoredVideo(videoPath: string) {
|
||||
success: true,
|
||||
path: videoPath,
|
||||
message:
|
||||
validation?.durationSeconds !== null && validation !== null
|
||||
validation.durationSeconds !== null
|
||||
? `Video stored successfully (${validation.fileSizeBytes} bytes, ${validation.durationSeconds.toFixed(2)}s)`
|
||||
: `Video stored successfully`,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { execFile } from "node:child_process";
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { getFfmpegBinaryPath } from "../ffmpeg/binary";
|
||||
import {
|
||||
getAudioSyncAdjustment,
|
||||
appendSyncedAudioFilter,
|
||||
normalizePauseSegments,
|
||||
buildPausedAudioFilter,
|
||||
getAudioSyncAdjustment,
|
||||
normalizePauseSegments,
|
||||
} from "../ffmpeg/filters";
|
||||
import type { PauseSegment, AudioSyncAdjustment } from "../types";
|
||||
import { getWindowsCaptureExePath } from "../paths/binaries";
|
||||
import {
|
||||
setWindowsCaptureProcess,
|
||||
windowsCaptureOutputBuffer,
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
setWindowsCaptureStopRequested,
|
||||
selectedSource,
|
||||
} from "../state";
|
||||
import type { AudioSyncAdjustment, PauseSegment } from "../types";
|
||||
import { moveFileWithOverwrite } from "../utils";
|
||||
import { probeMediaDurationSeconds } from "./diagnostics";
|
||||
import { probeMediaDurationSeconds, validateRecordedVideo } from "./diagnostics";
|
||||
import { emitRecordingInterrupted } from "./events";
|
||||
import { getWindowsCaptureExePath } from "../paths/binaries";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -220,100 +220,110 @@ export async function muxNativeWindowsVideoWithAudio(
|
||||
durationDeltaMs: 0,
|
||||
};
|
||||
|
||||
if (audioInputs.length === 2) {
|
||||
const filterParts: string[] = [];
|
||||
const systemPauseFilter = buildPausedAudioFilter(
|
||||
"1:a",
|
||||
"system_trimmed",
|
||||
normalizedPauseSegments,
|
||||
);
|
||||
const micPauseFilter = buildPausedAudioFilter(
|
||||
"2:a",
|
||||
"mic_trimmed",
|
||||
normalizedPauseSegments,
|
||||
);
|
||||
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);
|
||||
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);
|
||||
filterParts.push("[s][m]amix=inputs=2:duration=longest:normalize=0[aout]");
|
||||
|
||||
await execFileAsync(
|
||||
ffmpegPath,
|
||||
[
|
||||
"-y",
|
||||
...inputs,
|
||||
"-filter_complex",
|
||||
filterParts.join(";"),
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-map",
|
||||
"[aout]",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"192k",
|
||||
"-shortest",
|
||||
mixedOutputPath,
|
||||
],
|
||||
{ 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,
|
||||
tempoRatio: 1,
|
||||
durationDeltaMs: 0,
|
||||
};
|
||||
|
||||
// Always route through the filter graph so that aresample=async=1 is
|
||||
// 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, "aout", singleAdjustment);
|
||||
|
||||
await execFileAsync(
|
||||
ffmpegPath,
|
||||
[
|
||||
"-y",
|
||||
...inputs,
|
||||
"-filter_complex",
|
||||
filterParts.join(";"),
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-map",
|
||||
"[aout]",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"192k",
|
||||
"-shortest",
|
||||
mixedOutputPath,
|
||||
],
|
||||
{ timeout: 120000, maxBuffer: 10 * 1024 * 1024 },
|
||||
);
|
||||
}
|
||||
|
||||
const systemLabel = systemPauseFilter ? "[system_trimmed]" : "[1:a]";
|
||||
const micLabel = micPauseFilter ? "[mic_trimmed]" : "[2:a]";
|
||||
|
||||
appendSyncedAudioFilter(filterParts, systemLabel, "s", systemAdjustment);
|
||||
appendSyncedAudioFilter(filterParts, micLabel, "m", micAdjustment);
|
||||
filterParts.push("[s][m]amix=inputs=2:duration=longest:normalize=0[aout]");
|
||||
|
||||
await execFileAsync(
|
||||
ffmpegPath,
|
||||
[
|
||||
"-y",
|
||||
...inputs,
|
||||
"-filter_complex",
|
||||
filterParts.join(";"),
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-map",
|
||||
"[aout]",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"192k",
|
||||
"-shortest",
|
||||
mixedOutputPath,
|
||||
],
|
||||
{ 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,
|
||||
tempoRatio: 1,
|
||||
durationDeltaMs: 0,
|
||||
};
|
||||
|
||||
// Always route through the filter graph so that aresample=async=1 is
|
||||
// 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, "aout", singleAdjustment);
|
||||
|
||||
await execFileAsync(
|
||||
ffmpegPath,
|
||||
[
|
||||
"-y",
|
||||
...inputs,
|
||||
"-filter_complex",
|
||||
filterParts.join(";"),
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-map",
|
||||
"[aout]",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"192k",
|
||||
"-shortest",
|
||||
mixedOutputPath,
|
||||
],
|
||||
{ timeout: 120000, maxBuffer: 10 * 1024 * 1024 },
|
||||
);
|
||||
await validateRecordedVideo(mixedOutputPath);
|
||||
await moveFileWithOverwrite(mixedOutputPath, videoPath);
|
||||
} catch (error) {
|
||||
await fs.rm(mixedOutputPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await moveFileWithOverwrite(mixedOutputPath, videoPath);
|
||||
|
||||
for (const audioPath of [systemAudioPath, micAudioPath]) {
|
||||
if (audioPath) {
|
||||
await fs.rm(audioPath, { force: true }).catch(() => undefined);
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
recordNativeCaptureDiagnostics,
|
||||
getFileSizeIfPresent,
|
||||
getCompanionAudioFallbackPaths,
|
||||
validateRecordedVideo,
|
||||
} from "../recording/diagnostics";
|
||||
import { rememberApprovedLocalReadPath } from "../project/manager";
|
||||
import {
|
||||
@@ -105,7 +106,6 @@ import {
|
||||
buildFfmpegCaptureArgs,
|
||||
waitForFfmpegCaptureStart,
|
||||
waitForFfmpegCaptureStop,
|
||||
getDisplayBoundsForSource,
|
||||
} from "../recording/ffmpeg";
|
||||
import { resolveWindowsCaptureDisplay } from "../windowsCaptureSelection";
|
||||
import {
|
||||
@@ -189,11 +189,21 @@ export function registerRecordingHandlers(
|
||||
const recordingsDir = await getRecordingsDir()
|
||||
const timestamp = Date.now()
|
||||
const outputPath = path.join(recordingsDir, `recording-${timestamp}.mp4`)
|
||||
const displayBounds = source?.id?.startsWith('window:') ? null : getDisplayBoundsForSource(source)
|
||||
const resolvedDisplay = resolveWindowsCaptureDisplay(
|
||||
source,
|
||||
getScreen().getAllDisplays(),
|
||||
getScreen().getPrimaryDisplay(),
|
||||
)
|
||||
const displayBounds = resolvedDisplay.bounds
|
||||
|
||||
const config: Record<string, unknown> = {
|
||||
outputPath,
|
||||
fps: 60,
|
||||
displayId: resolvedDisplay.displayId,
|
||||
displayX: Math.round(resolvedDisplay.bounds.x),
|
||||
displayY: Math.round(resolvedDisplay.bounds.y),
|
||||
displayW: Math.round(resolvedDisplay.bounds.width),
|
||||
displayH: Math.round(resolvedDisplay.bounds.height),
|
||||
}
|
||||
|
||||
if (options?.capturesSystemAudio) {
|
||||
@@ -213,25 +223,6 @@ export function registerRecordingHandlers(
|
||||
setWindowsMicAudioPath(micPath)
|
||||
}
|
||||
|
||||
const windowId = parseWindowId(source?.id)
|
||||
if (windowId && source?.id?.startsWith('window:')) {
|
||||
config.windowHandle = windowId
|
||||
} else {
|
||||
const resolvedDisplay = resolveWindowsCaptureDisplay(
|
||||
source,
|
||||
getScreen().getAllDisplays(),
|
||||
getScreen().getPrimaryDisplay(),
|
||||
)
|
||||
config.displayId = resolvedDisplay.displayId
|
||||
|
||||
// Monitor handle IDs can drift across Electron/Windows capture boundaries,
|
||||
// so also provide display bounds for a coordinate-based native fallback.
|
||||
config.displayX = Math.round(resolvedDisplay.bounds.x)
|
||||
config.displayY = Math.round(resolvedDisplay.bounds.y)
|
||||
config.displayW = Math.round(resolvedDisplay.bounds.width)
|
||||
config.displayH = Math.round(resolvedDisplay.bounds.height)
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'start',
|
||||
@@ -560,18 +551,19 @@ export function registerRecordingHandlers(
|
||||
setWindowsCaptureStopRequested(true)
|
||||
proc.stdin.write('stop\n')
|
||||
const tempVideoPath = await waitForWindowsCaptureStop(proc)
|
||||
|
||||
const finalVideoPath = preferredVideoPath ?? tempVideoPath
|
||||
if (tempVideoPath !== finalVideoPath) {
|
||||
await moveFileWithOverwrite(tempVideoPath, finalVideoPath)
|
||||
}
|
||||
const validation = await validateRecordedVideo(finalVideoPath)
|
||||
|
||||
setWindowsCaptureProcess(null)
|
||||
setWindowsNativeCaptureActive(false)
|
||||
setNativeScreenRecordingActive(false)
|
||||
setWindowsCaptureTargetPath(null)
|
||||
setWindowsCaptureStopRequested(false)
|
||||
setWindowsCapturePaused(false)
|
||||
|
||||
const finalVideoPath = preferredVideoPath ?? tempVideoPath
|
||||
if (tempVideoPath !== finalVideoPath) {
|
||||
await moveFileWithOverwrite(tempVideoPath, finalVideoPath)
|
||||
}
|
||||
|
||||
setWindowsPendingVideoPath(finalVideoPath)
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
@@ -580,7 +572,7 @@ export function registerRecordingHandlers(
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
fileSizeBytes: await getFileSizeIfPresent(finalVideoPath),
|
||||
fileSizeBytes: validation.fileSizeBytes,
|
||||
})
|
||||
return { success: true, path: finalVideoPath }
|
||||
} catch (error) {
|
||||
@@ -599,6 +591,7 @@ export function registerRecordingHandlers(
|
||||
if (fallbackPath) {
|
||||
try {
|
||||
await fs.access(fallbackPath)
|
||||
const validation = await validateRecordedVideo(fallbackPath)
|
||||
setWindowsPendingVideoPath(fallbackPath)
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
@@ -607,12 +600,12 @@ export function registerRecordingHandlers(
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
fileSizeBytes: await getFileSizeIfPresent(fallbackPath),
|
||||
fileSizeBytes: validation.fileSizeBytes,
|
||||
error: String(error),
|
||||
})
|
||||
return { success: true, path: fallbackPath }
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
// File is absent or failed validation.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,6 +616,7 @@ export function registerRecordingHandlers(
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
fileSizeBytes: await getFileSizeIfPresent(fallbackPath),
|
||||
error: String(error),
|
||||
})
|
||||
|
||||
@@ -922,10 +916,10 @@ export function registerRecordingHandlers(
|
||||
})
|
||||
setWindowsSystemAudioPath(null)
|
||||
setWindowsMicAudioPath(null)
|
||||
try {
|
||||
return await finalizeStoredVideo(videoPath)
|
||||
} catch {
|
||||
return { success: false, message: 'Failed to mux native Windows recording', error: String(error) }
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to finalize native Windows recording',
|
||||
error: String(error),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1055,15 +1049,24 @@ export function registerRecordingHandlers(
|
||||
return stat ? { path: fullPath, mtimeMs: stat.mtimeMs } : null
|
||||
}),
|
||||
)
|
||||
const latestVideo = candidates
|
||||
const sortedCandidates = candidates
|
||||
.filter((candidate): candidate is { path: string; mtimeMs: number } => candidate !== null)
|
||||
.sort((left, right) => right.mtimeMs - left.mtimeMs)[0]
|
||||
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
||||
|
||||
if (!latestVideo) {
|
||||
for (const candidate of sortedCandidates) {
|
||||
try {
|
||||
await validateRecordedVideo(candidate.path)
|
||||
return { success: true, path: candidate.path }
|
||||
} catch (error) {
|
||||
console.warn("Skipping unusable recovered recording candidate:", candidate.path, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (sortedCandidates.length === 0) {
|
||||
return { success: false, message: 'No recorded video found' }
|
||||
}
|
||||
|
||||
return { success: true, path: latestVideo.path }
|
||||
return { success: false, message: 'No usable recorded video found' }
|
||||
} catch (error) {
|
||||
console.error('Failed to get video path:', error)
|
||||
return { success: false, message: 'Failed to get video path', error: String(error) }
|
||||
|
||||
@@ -36,7 +36,7 @@ export function resolveWindowsCaptureDisplay(
|
||||
primaryDisplay;
|
||||
|
||||
return {
|
||||
displayId: Number(matchedDisplay.id),
|
||||
displayId: requestedOrPrimaryDisplayId,
|
||||
bounds: matchedDisplay.bounds,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -435,7 +435,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}, []);
|
||||
|
||||
const storeMicrophoneSidecar = useCallback(
|
||||
async (micFallbackBlobPromise: Promise<Blob | null> | null | undefined, finalPath: string) => {
|
||||
async (
|
||||
micFallbackBlobPromise: Promise<Blob | null> | null | undefined,
|
||||
finalPath: string,
|
||||
) => {
|
||||
const micFallbackBlob = await micFallbackBlobPromise;
|
||||
if (!micFallbackBlob) {
|
||||
return;
|
||||
@@ -649,9 +652,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
);
|
||||
void logNativeCaptureDiagnostics("stop-native-screen-recording");
|
||||
try {
|
||||
const recoveredPath = await recoverNativeRecordingSession(
|
||||
micFallbackBlobPromise,
|
||||
);
|
||||
const recoveredPath =
|
||||
await recoverNativeRecordingSession(micFallbackBlobPromise);
|
||||
if (recoveredPath) {
|
||||
return;
|
||||
}
|
||||
@@ -674,10 +676,17 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (isNativeWindows) {
|
||||
const muxResult =
|
||||
await window.electronAPI.muxNativeWindowsRecording(pauseSegments);
|
||||
if (!muxResult?.success) {
|
||||
if (!muxResult?.success || !muxResult.path) {
|
||||
void logNativeCaptureDiagnostics("mux-native-windows-recording");
|
||||
const failureMessage = await buildNativeCaptureFailureMessage(
|
||||
"mux-native-windows-recording",
|
||||
muxResult?.message ||
|
||||
"Failed to finalize the Windows recording, so the editor was not opened.",
|
||||
);
|
||||
await notifyRecordingFinalizationFailure(failureMessage);
|
||||
return;
|
||||
}
|
||||
finalPath = muxResult?.path ?? result.path;
|
||||
finalPath = muxResult.path;
|
||||
}
|
||||
|
||||
await storeMicrophoneSidecar(micFallbackBlobPromise, finalPath);
|
||||
|
||||
Reference in New Issue
Block a user