mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-28 08:45:41 +00:00
Fix Windows audio fallback cleanup and path matching
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user