Fix Windows audio fallback cleanup and path matching

This commit is contained in:
webadderall
2026-04-24 11:19:47 +10:00
parent 285f591238
commit 6d8ab9df45
6 changed files with 121 additions and 15 deletions
+2
View File
@@ -9,6 +9,7 @@ import {
setWindowsCapturePaused,
setWindowsSystemAudioPath,
setWindowsMicAudioPath,
setWindowsOrphanedMicAudioPath,
setWindowsPendingVideoPath,
selectedSource,
} from "./state";
@@ -43,6 +44,7 @@ export function killWindowsCaptureProcess() {
setWindowsCapturePaused(false);
setWindowsSystemAudioPath(null);
setWindowsMicAudioPath(null);
setWindowsOrphanedMicAudioPath(null);
setWindowsPendingVideoPath(null);
}
}
+28 -4
View File
@@ -38,6 +38,8 @@ import {
setWindowsSystemAudioPath,
windowsMicAudioPath,
setWindowsMicAudioPath,
windowsOrphanedMicAudioPath,
setWindowsOrphanedMicAudioPath,
windowsPendingVideoPath,
setWindowsPendingVideoPath,
lastNativeCaptureDiagnostics,
@@ -162,6 +164,14 @@ function normalizeDesktopSourceName(value: string) {
return value.trim().replace(/\s+/g, " ").toLowerCase();
}
async function cleanupWindowsOrphanedMicAudioPath(filePath: string | null) {
if (!filePath) {
return;
}
await fs.rm(filePath, { force: true }).catch(() => undefined);
}
export function registerRecordingHandlers(
onRecordingStateChange?: (recording: boolean, sourceName: string) => void,
) {
@@ -193,12 +203,14 @@ export function registerRecordingHandlers(
let captureOutput = ''
let systemAudioPath: string | null = null
let microphonePath: string | null = null
let orphanedMicAudioPath: string | null = null
const resolvedDisplay = resolveWindowsCaptureDisplay(
source,
getScreen().getAllDisplays(),
getScreen().getPrimaryDisplay(),
)
const displayBounds = resolvedDisplay.bounds
setWindowsOrphanedMicAudioPath(null)
const config: Record<string, unknown> = {
outputPath,
@@ -267,6 +279,8 @@ export function registerRecordingHandlers(
options,
)
if (microphoneFallbackRequired) {
orphanedMicAudioPath = microphonePath
setWindowsOrphanedMicAudioPath(orphanedMicAudioPath)
microphonePath = null
setWindowsMicAudioPath(null)
}
@@ -562,6 +576,7 @@ export function registerRecordingHandlers(
const proc = windowsCaptureProcess
const preferredVideoPath = windowsCaptureTargetPath
const preferredOrphanedMicAudioPath = windowsOrphanedMicAudioPath
setWindowsCaptureStopRequested(true)
proc.stdin.write('stop\n')
const tempVideoPath = await waitForWindowsCaptureStop(proc)
@@ -578,6 +593,8 @@ export function registerRecordingHandlers(
setWindowsCaptureTargetPath(null)
setWindowsCaptureStopRequested(false)
setWindowsCapturePaused(false)
setWindowsOrphanedMicAudioPath(null)
await cleanupWindowsOrphanedMicAudioPath(preferredOrphanedMicAudioPath)
setWindowsPendingVideoPath(finalVideoPath)
recordNativeCaptureDiagnostics({
backend: 'windows-wgc',
@@ -592,6 +609,7 @@ export function registerRecordingHandlers(
} catch (error) {
console.error('Failed to stop native Windows capture:', error)
const fallbackPath = windowsCaptureTargetPath
const fallbackOrphanedMicAudioPath = windowsOrphanedMicAudioPath
setWindowsNativeCaptureActive(false)
setNativeScreenRecordingActive(false)
setWindowsCaptureProcess(null)
@@ -600,7 +618,9 @@ export function registerRecordingHandlers(
setWindowsCapturePaused(false)
setWindowsSystemAudioPath(null)
setWindowsMicAudioPath(null)
setWindowsOrphanedMicAudioPath(null)
setWindowsPendingVideoPath(null)
await cleanupWindowsOrphanedMicAudioPath(fallbackOrphanedMicAudioPath)
if (fallbackPath) {
try {
@@ -897,7 +917,9 @@ export function registerRecordingHandlers(
ipcMain.handle('mux-native-windows-recording', async (_event, pauseSegments?: PauseSegment[]) => {
const videoPath = windowsPendingVideoPath
const orphanedMicAudioPath = windowsOrphanedMicAudioPath
setWindowsPendingVideoPath(null)
setWindowsOrphanedMicAudioPath(null)
if (!videoPath) {
return { success: false, message: 'No native Windows video pending for mux' }
@@ -916,6 +938,7 @@ export function registerRecordingHandlers(
outputPath: videoPath,
fileSizeBytes: await getFileSizeIfPresent(videoPath),
})
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath)
return await finalizeStoredVideo(videoPath)
} catch (error) {
console.error('Failed to mux native Windows recording:', error)
@@ -930,10 +953,11 @@ export function registerRecordingHandlers(
})
setWindowsSystemAudioPath(null)
setWindowsMicAudioPath(null)
return {
success: false,
message: 'Failed to finalize native Windows recording',
error: String(error),
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath)
try {
return await finalizeStoredVideo(videoPath)
} catch {
return { success: false, message: 'Failed to mux native Windows recording', error: String(error) }
}
}
})
+2
View File
@@ -44,6 +44,7 @@ export let windowsCaptureStopRequested = false;
export let windowsCapturePaused = false;
export let windowsSystemAudioPath: string | null = null;
export let windowsMicAudioPath: string | null = null;
export let windowsOrphanedMicAudioPath: string | null = null;
export let windowsPendingVideoPath: string | null = null;
// ── Diagnostics ───────────────────────────────────────────────────────────────
@@ -127,6 +128,7 @@ export function setWindowsCaptureStopRequested(v: boolean) { windowsCaptureStopR
export function setWindowsCapturePaused(v: boolean) { windowsCapturePaused = v; }
export function setWindowsSystemAudioPath(v: string | null) { windowsSystemAudioPath = v; }
export function setWindowsMicAudioPath(v: string | null) { windowsMicAudioPath = v; }
export function setWindowsOrphanedMicAudioPath(v: string | null) { windowsOrphanedMicAudioPath = v; }
export function setWindowsPendingVideoPath(v: string | null) { windowsPendingVideoPath = v; }
export function setLastNativeCaptureDiagnostics(v: NativeCaptureDiagnostics | null) { lastNativeCaptureDiagnostics = v; }
+30 -2
View File
@@ -85,7 +85,29 @@ function getErrorMessage(error: unknown) {
return error.message;
}
return String(error);
if (typeof error === "string" && error.trim().length > 0) {
return error;
}
if (typeof error === "object" && error !== null) {
try {
const serialized = JSON.stringify(error);
if (serialized && serialized !== "{}") {
return serialized;
}
} catch {
// Ignore stringify failures and fall through to a generic message.
}
if (typeof (error as { toString?: () => string }).toString === "function") {
const stringified = (error as { toString: () => string }).toString();
if (stringified && stringified !== "[object Object]") {
return stringified;
}
}
}
return "An unexpected error occurred";
}
export function useScreenRecorder(): UseScreenRecorderReturn {
@@ -1046,8 +1068,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
micFallbackRecorder.current = recorder;
} catch (micError) {
console.warn("Browser microphone fallback failed:", micError);
const permissionDenied =
micError instanceof DOMException &&
(micError.name === "NotAllowedError" ||
micError.name === "SecurityError");
toast.error(
`${getErrorMessage(micError)}. Recording will continue without microphone audio.`,
permissionDenied
? "Microphone permission denied. Recording will continue without microphone audio."
: `${getErrorMessage(micError)}. Recording will continue without microphone audio.`,
{ id: MICROPHONE_FALLBACK_ERROR_TOAST_ID, duration: 10000 },
);
}
+25 -1
View File
@@ -40,4 +40,28 @@ describe("resolveSourceAudioFallbackPaths", () => {
externalAudioPaths: ["/tmp/recording.mic.wav"],
});
});
});
it("normalizes Windows file URLs and local paths when checking embedded audio", () => {
expect(
resolveSourceAudioFallbackPaths("file:///C:/Users/Egg/Videos/recording.mp4", [
"C:\\Users\\Egg\\Videos\\recording.mp4",
"C:\\Users\\Egg\\Videos\\recording.mic.wav",
]),
).toEqual({
hasEmbeddedSourceAudio: true,
externalAudioPaths: ["C:\\Users\\Egg\\Videos\\recording.mic.wav"],
});
});
it("matches Windows paths case-insensitively for embedded audio detection", () => {
expect(
resolveSourceAudioFallbackPaths("file:///C:/Users/Egg/Videos/recording.mp4", [
"c:\\users\\egg\\videos\\recording.mp4",
"c:\\users\\egg\\videos\\recording.mic.wav",
]),
).toEqual({
hasEmbeddedSourceAudio: true,
externalAudioPaths: ["c:\\users\\egg\\videos\\recording.mic.wav"],
});
});
});
+34 -8
View File
@@ -1,22 +1,48 @@
import { getLocalFilePathFromResource } from "./mediaResource";
function normalizeSourceAudioFallbackPath(resourceOrPath: string): string | null {
if (typeof resourceOrPath !== "string") {
return null;
}
const resolvedPath = getLocalFilePathFromResource(resourceOrPath) ?? resourceOrPath;
const trimmedPath = resolvedPath.trim();
if (!trimmedPath) {
return null;
}
const isWindowsPath =
/^[A-Za-z]:[\\/]/.test(trimmedPath) || /^\\\\[^\\]+\\[^\\]+/.test(trimmedPath);
if (isWindowsPath) {
return trimmedPath.replace(/\//g, "\\").toLowerCase();
}
return trimmedPath.replace(/\\/g, "/");
}
export function resolveSourceAudioFallbackPaths(
videoResource: string | null | undefined,
sourceAudioFallbackPaths: string[] | null | undefined,
) {
const normalizedPaths = (sourceAudioFallbackPaths ?? []).filter(
(audioPath) => typeof audioPath === "string" && audioPath.trim().length > 0,
);
const normalizedPaths = (sourceAudioFallbackPaths ?? [])
.filter((audioPath) => typeof audioPath === "string" && audioPath.trim().length > 0)
.map((audioPath) => ({
audioPath,
normalizedPath: normalizeSourceAudioFallbackPath(audioPath),
}));
const localVideoSourcePath = videoResource
? getLocalFilePathFromResource(videoResource)
? normalizeSourceAudioFallbackPath(videoResource)
: null;
const hasEmbeddedSourceAudio =
Boolean(localVideoSourcePath) && normalizedPaths.includes(localVideoSourcePath);
Boolean(localVideoSourcePath) &&
normalizedPaths.some(({ normalizedPath }) => normalizedPath === localVideoSourcePath);
return {
hasEmbeddedSourceAudio,
externalAudioPaths: hasEmbeddedSourceAudio
? normalizedPaths.filter((audioPath) => audioPath !== localVideoSourcePath)
: normalizedPaths,
? normalizedPaths
.filter(({ normalizedPath }) => normalizedPath !== localVideoSourcePath)
.map(({ audioPath }) => audioPath)
: normalizedPaths.map(({ audioPath }) => audioPath),
};
}
}