mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
feat(debug): add native capture diagnostics
This commit is contained in:
Vendored
+25
@@ -22,6 +22,27 @@ declare namespace NodeJS {
|
||||
}
|
||||
|
||||
// Used in Renderer process, expose in `preload.ts`
|
||||
interface NativeCaptureDiagnostics {
|
||||
backend: "windows-wgc" | "mac-screencapturekit" | "browser-store" | "ffmpeg";
|
||||
phase: "availability" | "start" | "stop" | "mux";
|
||||
timestamp: string;
|
||||
sourceId?: string | null;
|
||||
sourceType?: "screen" | "window" | "unknown";
|
||||
displayId?: number | null;
|
||||
displayBounds?: { x: number; y: number; width: number; height: number } | null;
|
||||
windowHandle?: number | null;
|
||||
helperPath?: string | null;
|
||||
outputPath?: string | null;
|
||||
systemAudioPath?: string | null;
|
||||
microphonePath?: string | null;
|
||||
osRelease?: string;
|
||||
supported?: boolean;
|
||||
helperExists?: boolean;
|
||||
fileSizeBytes?: number | null;
|
||||
processOutput?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
hudOverlayHide: () => void;
|
||||
@@ -55,6 +76,10 @@ interface Window {
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
getLastNativeCaptureDiagnostics: () => Promise<{
|
||||
success: boolean;
|
||||
diagnostics?: NativeCaptureDiagnostics | null;
|
||||
}>;
|
||||
pauseNativeScreenRecording: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
|
||||
+162
-4
@@ -84,6 +84,27 @@ type WindowBounds = {
|
||||
height: number
|
||||
}
|
||||
|
||||
type NativeCaptureDiagnostics = {
|
||||
backend: 'windows-wgc' | 'mac-screencapturekit' | 'browser-store' | 'ffmpeg'
|
||||
phase: 'availability' | 'start' | 'stop' | 'mux'
|
||||
timestamp: string
|
||||
sourceId?: string | null
|
||||
sourceType?: SelectedSource['sourceType'] | 'unknown'
|
||||
displayId?: number | null
|
||||
displayBounds?: WindowBounds | null
|
||||
windowHandle?: number | null
|
||||
helperPath?: string | null
|
||||
outputPath?: string | null
|
||||
systemAudioPath?: string | null
|
||||
microphonePath?: string | null
|
||||
osRelease?: string
|
||||
supported?: boolean
|
||||
helperExists?: boolean
|
||||
fileSizeBytes?: number | null
|
||||
processOutput?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
type RecordingSessionData = {
|
||||
videoPath: string
|
||||
webcamPath?: string | null
|
||||
@@ -129,6 +150,7 @@ let windowsCapturePaused = false
|
||||
let windowsSystemAudioPath: string | null = null
|
||||
let windowsMicAudioPath: string | null = null
|
||||
let windowsPendingVideoPath: string | null = null
|
||||
let lastNativeCaptureDiagnostics: NativeCaptureDiagnostics | null = null
|
||||
let ffmpegScreenRecordingActive = false
|
||||
let ffmpegCaptureProcess: ChildProcessWithoutNullStreams | null = null
|
||||
let ffmpegCaptureOutputBuffer = ''
|
||||
@@ -238,6 +260,30 @@ async function getRecordingsDir() {
|
||||
return targetDir
|
||||
}
|
||||
|
||||
function recordNativeCaptureDiagnostics(
|
||||
diagnostics: Omit<NativeCaptureDiagnostics, 'timestamp'>,
|
||||
) {
|
||||
lastNativeCaptureDiagnostics = {
|
||||
timestamp: new Date().toISOString(),
|
||||
...diagnostics,
|
||||
}
|
||||
|
||||
return lastNativeCaptureDiagnostics
|
||||
}
|
||||
|
||||
async function getFileSizeIfPresent(filePath: string | null | undefined) {
|
||||
if (!filePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(filePath)
|
||||
return stat.size
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function getProjectsDir() {
|
||||
const projectsDir = path.join(await getRecordingsDir(), PROJECTS_DIRECTORY_NAME)
|
||||
await fs.mkdir(projectsDir, { recursive: true })
|
||||
@@ -1698,15 +1744,38 @@ function getCursorMonitorExePath() {
|
||||
async function isNativeWindowsCaptureAvailable(): Promise<boolean> {
|
||||
if (process.platform !== 'win32') return false
|
||||
|
||||
const helperPath = getWindowsCaptureExePath()
|
||||
const os = await import('node:os')
|
||||
const [major, , build] = os.release().split('.').map(Number)
|
||||
const supported = major >= 10 && build >= 19041
|
||||
let helperExists = false
|
||||
|
||||
try {
|
||||
await fs.access(getWindowsCaptureExePath(), fsConstants.X_OK)
|
||||
await fs.access(helperPath, fsConstants.X_OK)
|
||||
helperExists = true
|
||||
} catch {
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'availability',
|
||||
helperPath,
|
||||
helperExists,
|
||||
osRelease: os.release(),
|
||||
supported,
|
||||
error: 'Native Windows capture helper is missing or not executable.',
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const os = await import('node:os')
|
||||
const [major, , build] = os.release().split('.').map(Number)
|
||||
return major >= 10 && build >= 19041
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'availability',
|
||||
helperPath,
|
||||
helperExists,
|
||||
osRelease: os.release(),
|
||||
supported,
|
||||
})
|
||||
|
||||
return supported
|
||||
}
|
||||
|
||||
function waitForWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) {
|
||||
@@ -3024,6 +3093,7 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
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 config: Record<string, unknown> = {
|
||||
outputPath,
|
||||
@@ -3070,6 +3140,20 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
}
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'start',
|
||||
sourceId: source?.id ?? null,
|
||||
sourceType: source?.sourceType ?? 'unknown',
|
||||
displayId: typeof config.displayId === 'number' ? config.displayId : null,
|
||||
displayBounds,
|
||||
windowHandle: typeof config.windowHandle === 'number' ? config.windowHandle : null,
|
||||
helperPath: exePath,
|
||||
outputPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
})
|
||||
|
||||
windowsCaptureOutputBuffer = ''
|
||||
windowsCaptureTargetPath = outputPath
|
||||
windowsCaptureStopRequested = false
|
||||
@@ -3090,8 +3174,34 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
await waitForWindowsCaptureStart(windowsCaptureProcess)
|
||||
windowsNativeCaptureActive = true
|
||||
nativeScreenRecordingActive = true
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'start',
|
||||
sourceId: source?.id ?? null,
|
||||
sourceType: source?.sourceType ?? 'unknown',
|
||||
displayId: typeof config.displayId === 'number' ? config.displayId : null,
|
||||
displayBounds,
|
||||
windowHandle: typeof config.windowHandle === 'number' ? config.windowHandle : null,
|
||||
helperPath: exePath,
|
||||
outputPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
})
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'start',
|
||||
sourceId: source?.id ?? null,
|
||||
sourceType: source?.sourceType ?? 'unknown',
|
||||
helperPath: windowsCaptureTargetPath ? getWindowsCaptureExePath() : null,
|
||||
outputPath: windowsCaptureTargetPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
error: String(error),
|
||||
})
|
||||
console.error('Failed to start native Windows capture:', error)
|
||||
try { windowsCaptureProcess?.kill() } catch { /* ignore */ }
|
||||
windowsNativeCaptureActive = false
|
||||
@@ -3255,6 +3365,15 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
}
|
||||
|
||||
windowsPendingVideoPath = finalVideoPath
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'stop',
|
||||
outputPath: finalVideoPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
fileSizeBytes: await getFileSizeIfPresent(finalVideoPath),
|
||||
})
|
||||
return { success: true, path: finalVideoPath }
|
||||
} catch (error) {
|
||||
console.error('Failed to stop native Windows capture:', error)
|
||||
@@ -3273,12 +3392,32 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
try {
|
||||
await fs.access(fallbackPath)
|
||||
windowsPendingVideoPath = fallbackPath
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'stop',
|
||||
outputPath: fallbackPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
fileSizeBytes: await getFileSizeIfPresent(fallbackPath),
|
||||
error: String(error),
|
||||
})
|
||||
return { success: true, path: fallbackPath }
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'stop',
|
||||
outputPath: fallbackPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
processOutput: windowsCaptureOutputBuffer.trim() || undefined,
|
||||
error: String(error),
|
||||
})
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to stop native Windows capture',
|
||||
@@ -3452,6 +3591,10 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
return { available: await isNativeWindowsCaptureAvailable() }
|
||||
})
|
||||
|
||||
ipcMain.handle('get-last-native-capture-diagnostics', async () => {
|
||||
return { success: true, diagnostics: lastNativeCaptureDiagnostics }
|
||||
})
|
||||
|
||||
ipcMain.handle('mux-native-windows-recording', async () => {
|
||||
const videoPath = windowsPendingVideoPath
|
||||
windowsPendingVideoPath = null
|
||||
@@ -3467,9 +3610,24 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
windowsMicAudioPath = null
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'mux',
|
||||
outputPath: videoPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
})
|
||||
return await finalizeStoredVideo(videoPath)
|
||||
} catch (error) {
|
||||
console.error('Failed to mux native Windows recording:', error)
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: 'windows-wgc',
|
||||
phase: 'mux',
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: windowsSystemAudioPath,
|
||||
microphonePath: windowsMicAudioPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
error: String(error),
|
||||
})
|
||||
windowsSystemAudioPath = null
|
||||
windowsMicAudioPath = null
|
||||
try {
|
||||
|
||||
@@ -68,6 +68,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
stopNativeScreenRecording: () => {
|
||||
return ipcRenderer.invoke("stop-native-screen-recording");
|
||||
},
|
||||
getLastNativeCaptureDiagnostics: () => {
|
||||
return ipcRenderer.invoke("get-last-native-capture-diagnostics");
|
||||
},
|
||||
pauseNativeScreenRecording: () => {
|
||||
return ipcRenderer.invoke("pause-native-screen-recording");
|
||||
},
|
||||
|
||||
@@ -94,6 +94,21 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
const accumulatedPausedDurationMs = useRef(0);
|
||||
const pauseStartedAtMs = useRef<number | null>(null);
|
||||
|
||||
const logNativeCaptureDiagnostics = useCallback(async (context: string) => {
|
||||
if (typeof window.electronAPI?.getLastNativeCaptureDiagnostics !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.getLastNativeCaptureDiagnostics();
|
||||
if (result.success && result.diagnostics) {
|
||||
console.warn(`[NativeCaptureDiagnostics:${context}]`, result.diagnostics);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load native capture diagnostics:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetRecordingClock = useCallback((startedAt: number) => {
|
||||
startTime.current = startedAt;
|
||||
accumulatedPausedDurationMs.current = 0;
|
||||
@@ -404,6 +419,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
if (!result.success || !result.path) {
|
||||
console.error("Failed to stop native screen recording:", result.error ?? result.message);
|
||||
void logNativeCaptureDiagnostics("stop-native-screen-recording");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,6 +427,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
if (isNativeWindows) {
|
||||
const muxResult = await window.electronAPI.muxNativeWindowsRecording();
|
||||
if (!muxResult?.success) {
|
||||
void logNativeCaptureDiagnostics("mux-native-windows-recording");
|
||||
}
|
||||
finalPath = muxResult?.path ?? result.path;
|
||||
}
|
||||
|
||||
@@ -546,6 +565,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
const nativeWindowsResult = await window.electronAPI.isNativeWindowsCaptureAvailable();
|
||||
useNativeWindowsCapture = nativeWindowsResult.available;
|
||||
if (!useNativeWindowsCapture && !hasShownNativeWindowsFallbackToast.current) {
|
||||
void logNativeCaptureDiagnostics("is-native-windows-capture-available");
|
||||
hasShownNativeWindowsFallbackToast.current = true;
|
||||
toast.info(
|
||||
"Native Windows capture is unavailable. Falling back to browser capture.",
|
||||
@@ -586,6 +606,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
if (!nativeResult.success) {
|
||||
if (useNativeWindowsCapture) {
|
||||
console.warn("Native Windows capture failed, falling back to browser capture:", nativeResult.error ?? nativeResult.message);
|
||||
void logNativeCaptureDiagnostics("start-native-screen-recording");
|
||||
if (!hasShownNativeWindowsFallbackToast.current) {
|
||||
hasShownNativeWindowsFallbackToast.current = true;
|
||||
toast.warning(
|
||||
|
||||
Reference in New Issue
Block a user