mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
add: 20 seconds -> 2 seconds fix, for now behind a flag to be sure to be able to revert.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Plan de Diagnostic de Performance : "Stop Recording"
|
||||
|
||||
L'objectif est d'identifier précisément les goulots d'étranglement qui causent un délai de 10-15 secondes après l'arrêt d'un enregistrement natif sur Windows.
|
||||
|
||||
## 1. Instrumentation Frontend (`src/hooks/useScreenRecorder.ts`)
|
||||
|
||||
Nous allons mesurer le temps total côté UI et le temps d'attente pour chaque appel IPC majeur.
|
||||
|
||||
- **Total Stop Sequence** : Du clic sur "Stop" à l'ouverture de l'éditeur.
|
||||
- **IPC: stopNativeScreenRecording** : Temps d'arrêt du processus de capture natif.
|
||||
- **IPC: muxNativeWindowsRecording** : Temps passé dans FFmpeg pour le padding et le muxing audio.
|
||||
- **Store Sidecar** : Temps d'écriture de l'audio du microphone enregistré par le navigateur.
|
||||
|
||||
## 2. Instrumentation Main Process IPC (`electron/ipc/register/recording.ts`)
|
||||
|
||||
Nous allons mesurer le temps passé dans les handlers IPC pour différencier le temps de traitement de l'overhead de communication.
|
||||
|
||||
- **Handler: stop-native-screen-recording**
|
||||
- **Handler: mux-native-windows-recording**
|
||||
|
||||
## 3. Instrumentation Logique Interne (`electron/ipc/recording/windows.ts`)
|
||||
|
||||
C'est ici que se trouvent les opérations FFmpeg suspectées.
|
||||
|
||||
- **extendNativeWindowsVideoToDuration** : Temps de re-encodage pour ajouter du padding (Suspect #1).
|
||||
- **muxNativeWindowsVideoWithAudio** : Temps de muxing audio (Suspect #2).
|
||||
- **Probing** : Temps passé dans `ffprobe`.
|
||||
|
||||
## Méthodologie d'analyse
|
||||
|
||||
Une fois les logs ajoutés :
|
||||
1. Lancer l'app en mode dev.
|
||||
2. Faire un enregistrement court (10s).
|
||||
3. Arrêter l'enregistrement.
|
||||
4. Récupérer les logs dans :
|
||||
- La **Console du navigateur** (pour le frontend).
|
||||
- Le **Terminal** (pour le processus main d'Electron).
|
||||
|
||||
Les logs seront préfixés par `[PERF:RENDERER]` et `[PERF:MAIN]`.
|
||||
@@ -38,3 +38,10 @@ export function shouldKeepRecordingAudioSidecars(env: NodeJS.ProcessEnv = proces
|
||||
const value = env[RECORDING_AUDIO_SIDECAR_DEBUG_ENV]?.trim().toLowerCase();
|
||||
return value === "1" || value === "true" || value === "yes" || value === "on";
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle for the recording finalization optimizations.
|
||||
* - true: Fast probe (no -count_frames), separate audio tracks (no muxing).
|
||||
* - false: slow probe (full decode), combined audio/video (heavy muxing).
|
||||
*/
|
||||
export const OPTIMIZE_RECORDING_FINALIZATION = true;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { COMPANION_AUDIO_LAYOUTS } from "../constants";
|
||||
import { OPTIMIZE_RECORDING_FINALIZATION } from "./audioFilters";
|
||||
import { getFfmpegBinaryPath, getFfprobeBinaryPath } from "../ffmpeg/binary";
|
||||
import { lastNativeCaptureDiagnostics, setLastNativeCaptureDiagnostics } from "../state";
|
||||
import type { CompanionAudioCandidate, NativeCaptureDiagnostics } from "../types";
|
||||
@@ -190,6 +191,7 @@ export function summarizeMicrophoneChunkTiming(
|
||||
|
||||
/** Probe the duration of a media file (in seconds) using the container header. */
|
||||
export async function probeMediaDurationSeconds(filePath: string): Promise<number> {
|
||||
const start = Date.now();
|
||||
const ffmpegPath = getFfmpegBinaryPath();
|
||||
try {
|
||||
await execFileAsync(ffmpegPath, ["-i", filePath, "-hide_banner"], { timeout: 5000 });
|
||||
@@ -199,6 +201,10 @@ export async function probeMediaDurationSeconds(filePath: string): Promise<numbe
|
||||
if (duration !== null) {
|
||||
return duration;
|
||||
}
|
||||
} finally {
|
||||
console.log(
|
||||
`[PERF:MAIN] probeMediaDurationSeconds: COMPLETED in ${Date.now() - start}ms`,
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -265,6 +271,7 @@ export function parseFfprobeVideoStreamDuration(output: string): VideoStreamDura
|
||||
export async function probeVideoStreamDuration(
|
||||
filePath: string,
|
||||
): Promise<VideoStreamDurationProbe | null> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result = await execFileAsync(
|
||||
getFfprobeBinaryPath(),
|
||||
@@ -273,7 +280,7 @@ export async function probeVideoStreamDuration(
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-count_frames",
|
||||
...(OPTIMIZE_RECORDING_FINALIZATION ? [] : ["-count_frames"]),
|
||||
"-show_entries",
|
||||
"stream=duration,nb_frames,nb_read_frames,avg_frame_rate,r_frame_rate",
|
||||
"-of",
|
||||
@@ -286,6 +293,10 @@ export async function probeVideoStreamDuration(
|
||||
return parseFfprobeVideoStreamDuration(stdout);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
console.log(
|
||||
`[PERF:MAIN] probeVideoStreamDuration: COMPLETED in ${Date.now() - start}ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "../state";
|
||||
import type { AudioSyncAdjustment } from "../types";
|
||||
import { isAutoRecordingPath, moveFileWithOverwrite } from "../utils";
|
||||
import { OPTIMIZE_RECORDING_FINALIZATION } from "./audioFilters";
|
||||
import {
|
||||
getFileSizeIfPresent,
|
||||
getRecordingAudioMuxTimeoutMs,
|
||||
@@ -318,7 +319,7 @@ export function attachNativeCaptureLifecycle(process: ChildProcessWithoutNullStr
|
||||
|
||||
export async function finalizeStoredVideo(videoPath: string) {
|
||||
// Safety net: if companion audio files still exist, the mux was skipped — attempt it now
|
||||
if (videoPath.endsWith(".mp4")) {
|
||||
if (!OPTIMIZE_RECORDING_FINALIZATION && videoPath.endsWith(".mp4")) {
|
||||
const companionCandidates = await getUsableCompanionAudioCandidates(videoPath);
|
||||
for (const { systemPath, micPath, platform } of companionCandidates) {
|
||||
if (platform === "mac" || platform === "win") {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import type { AudioSyncAdjustment } from "../types";
|
||||
import { moveFileWithOverwrite } from "../utils";
|
||||
import {
|
||||
OPTIMIZE_RECORDING_FINALIZATION,
|
||||
shouldKeepRecordingAudioSidecars,
|
||||
WINDOWS_NATIVE_MIC_PRE_FILTERS,
|
||||
} from "./audioFilters";
|
||||
@@ -193,6 +194,9 @@ export async function extendNativeWindowsVideoToDuration(
|
||||
videoPath: string,
|
||||
targetDurationMs: number | null | undefined,
|
||||
): Promise<NativeWindowsVideoPaddingResult> {
|
||||
const start = Date.now();
|
||||
console.log("[PERF:MAIN] extendNativeWindowsVideoToDuration: STARTED");
|
||||
try {
|
||||
if (!Number.isFinite(targetDurationMs) || (targetDurationMs ?? 0) <= 0) {
|
||||
return {
|
||||
padded: false,
|
||||
@@ -267,8 +271,13 @@ export async function extendNativeWindowsVideoToDuration(
|
||||
padDurationSeconds,
|
||||
};
|
||||
} catch (error) {
|
||||
await fs.rm(paddedOutputPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
await fs.rm(paddedOutputPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
console.log(
|
||||
`[PERF:MAIN] extendNativeWindowsVideoToDuration: COMPLETED in ${Date.now() - start}ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +286,9 @@ export async function muxNativeWindowsVideoWithAudio(
|
||||
systemAudioPath: string | null,
|
||||
micAudioPath: string | null,
|
||||
): Promise<NativeWindowsAudioMuxResult> {
|
||||
const start = Date.now();
|
||||
console.log("[PERF:MAIN] muxNativeWindowsVideoWithAudio: STARTED");
|
||||
try {
|
||||
const ffmpegPath = getFfmpegBinaryPath();
|
||||
const keepAudioSidecars = shouldKeepRecordingAudioSidecars();
|
||||
const inputs: string[] = ["-i", videoPath];
|
||||
@@ -385,6 +397,44 @@ export async function muxNativeWindowsVideoWithAudio(
|
||||
durationDeltaMs: 0,
|
||||
};
|
||||
|
||||
if (OPTIMIZE_RECORDING_FINALIZATION) {
|
||||
console.log("[mux-win] Optimization enabled: skipping heavy muxing, keeping tracks separate.");
|
||||
const videoPathWithoutExt = videoPath.replace(/\.[^.]+$/u, "");
|
||||
|
||||
// Move audio sidecars to final companion paths if they exist
|
||||
if (systemAudioPath) {
|
||||
const finalSystemPath = `${videoPathWithoutExt}.system.wav`;
|
||||
if (systemAudioPath !== finalSystemPath) {
|
||||
try {
|
||||
await moveFileWithOverwrite(systemAudioPath, finalSystemPath);
|
||||
if (audio.system) audio.system.path = finalSystemPath;
|
||||
} catch (err) {
|
||||
console.error(`[mux-win] Failed to move system audio to ${finalSystemPath}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (micAudioPath) {
|
||||
const finalMicPath = `${videoPathWithoutExt}.mic.wav`;
|
||||
if (micAudioPath !== finalMicPath) {
|
||||
try {
|
||||
await moveFileWithOverwrite(micAudioPath, finalMicPath);
|
||||
if (audio.mic) audio.mic.path = finalMicPath;
|
||||
} catch (err) {
|
||||
console.error(`[mux-win] Failed to move mic audio to ${finalMicPath}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
muxed: false,
|
||||
videoDurationSeconds: videoDuration,
|
||||
muxTimeoutMs,
|
||||
audioInputs,
|
||||
audio,
|
||||
keptAudioSidecars: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (audioInputs.length === 2) {
|
||||
const filterParts: string[] = [];
|
||||
@@ -469,17 +519,22 @@ export async function muxNativeWindowsVideoWithAudio(
|
||||
await validateRecordedVideo(mixedOutputPath);
|
||||
await moveFileWithOverwrite(mixedOutputPath, videoPath);
|
||||
} catch (error) {
|
||||
await fs.rm(mixedOutputPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
await fs.rm(mixedOutputPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
muxed: true,
|
||||
videoDurationSeconds: videoDuration,
|
||||
muxTimeoutMs,
|
||||
audioInputs,
|
||||
audio,
|
||||
outputPath: videoPath,
|
||||
keptAudioSidecars: true,
|
||||
};
|
||||
return {
|
||||
muxed: true,
|
||||
videoDurationSeconds: videoDuration,
|
||||
muxTimeoutMs,
|
||||
audioInputs,
|
||||
audio,
|
||||
outputPath: videoPath,
|
||||
keptAudioSidecars: true,
|
||||
};
|
||||
} finally {
|
||||
console.log(
|
||||
`[PERF:MAIN] muxNativeWindowsVideoWithAudio: COMPLETED in ${Date.now() - start}ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+139
-125
@@ -40,6 +40,7 @@ import {
|
||||
import { rememberApprovedLocalReadPath } from "../project/manager";
|
||||
import {
|
||||
getBrowserMicSidecarFilters,
|
||||
OPTIMIZE_RECORDING_FINALIZATION,
|
||||
shouldKeepRecordingAudioSidecars,
|
||||
} from "../recording/audioFilters";
|
||||
import {
|
||||
@@ -821,6 +822,9 @@ export function registerRecordingHandlers(
|
||||
);
|
||||
|
||||
ipcMain.handle("stop-native-screen-recording", async () => {
|
||||
const start = Date.now();
|
||||
console.log("[PERF:MAIN] Handler: stop-native-screen-recording: STARTED");
|
||||
try {
|
||||
// Windows native capture stop path
|
||||
if (process.platform === "win32" && windowsNativeCaptureActive) {
|
||||
try {
|
||||
@@ -1093,11 +1097,16 @@ export function registerRecordingHandlers(
|
||||
return recovered;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to stop native ScreenCaptureKit recording",
|
||||
error: String(error),
|
||||
};
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to stop native ScreenCaptureKit recording",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
console.log(
|
||||
`[PERF:MAIN] Handler: stop-native-screen-recording: COMPLETED in ${Date.now() - start}ms`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1258,139 +1267,144 @@ export function registerRecordingHandlers(
|
||||
});
|
||||
|
||||
ipcMain.handle("mux-native-windows-recording", async (_event, expectedDurationMs?: number) => {
|
||||
const videoPath = windowsPendingVideoPath;
|
||||
const orphanedMicAudioPath = windowsOrphanedMicAudioPath;
|
||||
const diagnosticsSystemAudioPath = windowsSystemAudioPath;
|
||||
const diagnosticsMicAudioPath = windowsMicAudioPath;
|
||||
setWindowsPendingVideoPath(null);
|
||||
setWindowsOrphanedMicAudioPath(null);
|
||||
|
||||
if (!videoPath) {
|
||||
return { success: false, message: "No native Windows video pending for mux" };
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
console.log("[PERF:MAIN] Handler: mux-native-windows-recording: STARTED");
|
||||
try {
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "mux-start",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
details: {
|
||||
hasSystemAudio: Boolean(diagnosticsSystemAudioPath),
|
||||
hasMicrophone: Boolean(diagnosticsMicAudioPath),
|
||||
hasOrphanedMicrophone: Boolean(orphanedMicAudioPath),
|
||||
},
|
||||
});
|
||||
try {
|
||||
const padding = await extendNativeWindowsVideoToDuration(
|
||||
videoPath,
|
||||
expectedDurationMs,
|
||||
);
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "pad",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
details: { ...padding },
|
||||
});
|
||||
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,
|
||||
);
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "pad",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
error: String(paddingError),
|
||||
});
|
||||
const videoPath = windowsPendingVideoPath;
|
||||
const orphanedMicAudioPath = windowsOrphanedMicAudioPath;
|
||||
const diagnosticsSystemAudioPath = windowsSystemAudioPath;
|
||||
const diagnosticsMicAudioPath = windowsMicAudioPath;
|
||||
setWindowsPendingVideoPath(null);
|
||||
setWindowsOrphanedMicAudioPath(null);
|
||||
|
||||
if (!videoPath) {
|
||||
return { success: false, message: "No native Windows video pending for mux" };
|
||||
}
|
||||
|
||||
let muxDetails: unknown = null;
|
||||
if (diagnosticsSystemAudioPath || diagnosticsMicAudioPath) {
|
||||
muxDetails = await muxNativeWindowsVideoWithAudio(
|
||||
videoPath,
|
||||
diagnosticsSystemAudioPath,
|
||||
diagnosticsMicAudioPath,
|
||||
);
|
||||
try {
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "mux-start",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
details: {
|
||||
hasSystemAudio: Boolean(diagnosticsSystemAudioPath),
|
||||
hasMicrophone: Boolean(diagnosticsMicAudioPath),
|
||||
hasOrphanedMicrophone: Boolean(orphanedMicAudioPath),
|
||||
},
|
||||
});
|
||||
if (!OPTIMIZE_RECORDING_FINALIZATION && expectedDurationMs) {
|
||||
try {
|
||||
const padding = await extendNativeWindowsVideoToDuration(
|
||||
videoPath,
|
||||
expectedDurationMs,
|
||||
);
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "pad",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
details: { ...padding },
|
||||
});
|
||||
} catch (paddingError) {
|
||||
console.warn(
|
||||
"[mux-win] Failed to extend native Windows video duration:",
|
||||
paddingError,
|
||||
);
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "pad",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
error: String(paddingError),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let muxDetails: unknown = null;
|
||||
if (diagnosticsSystemAudioPath || diagnosticsMicAudioPath) {
|
||||
muxDetails = await muxNativeWindowsVideoWithAudio(
|
||||
videoPath,
|
||||
diagnosticsSystemAudioPath,
|
||||
diagnosticsMicAudioPath,
|
||||
);
|
||||
setWindowsSystemAudioPath(null);
|
||||
setWindowsMicAudioPath(null);
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
});
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "mux-complete",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
details: {
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
mux: muxDetails,
|
||||
},
|
||||
});
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
return await finalizeStoredVideo(videoPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to mux native Windows recording:", error);
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
error: String(error),
|
||||
});
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "mux-error",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
error: String(error),
|
||||
details: {
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
},
|
||||
});
|
||||
setWindowsSystemAudioPath(null);
|
||||
setWindowsMicAudioPath(null);
|
||||
}
|
||||
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
});
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "mux-complete",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
details: {
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
mux: muxDetails,
|
||||
},
|
||||
});
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
return await finalizeStoredVideo(videoPath);
|
||||
} catch (error) {
|
||||
console.error("Failed to mux native Windows recording:", error);
|
||||
recordNativeCaptureDiagnostics({
|
||||
backend: "windows-wgc",
|
||||
phase: "mux",
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
error: String(error),
|
||||
});
|
||||
await writeWindowsRecordingDiagnostics(videoPath, {
|
||||
phase: "mux-error",
|
||||
expectedDurationMs,
|
||||
outputPath: videoPath,
|
||||
systemAudioPath: diagnosticsSystemAudioPath,
|
||||
microphonePath: diagnosticsMicAudioPath,
|
||||
error: String(error),
|
||||
details: {
|
||||
fileSizeBytes: await getFileSizeIfPresent(videoPath),
|
||||
},
|
||||
});
|
||||
setWindowsSystemAudioPath(null);
|
||||
setWindowsMicAudioPath(null);
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
try {
|
||||
return await finalizeStoredVideo(videoPath);
|
||||
} catch {
|
||||
await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath);
|
||||
try {
|
||||
await validateRecordedVideo(videoPath);
|
||||
return await finalizeStoredVideo(videoPath);
|
||||
} catch {
|
||||
try {
|
||||
await validateRecordedVideo(videoPath);
|
||||
return {
|
||||
success: false,
|
||||
path: videoPath,
|
||||
message: "Failed to mux native Windows recording",
|
||||
error: String(error),
|
||||
};
|
||||
} catch {
|
||||
// The fallback path is not safely playable; surface the original mux error.
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
path: videoPath,
|
||||
message: "Failed to mux native Windows recording",
|
||||
error: String(error),
|
||||
};
|
||||
} catch {
|
||||
// The fallback path is not safely playable; surface the original mux error.
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to mux native Windows recording",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
console.log(
|
||||
`[PERF:MAIN] Handler: mux-native-windows-recording: COMPLETED in ${Date.now() - start}ms`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+4
-2
@@ -696,6 +696,8 @@ function loadPackagedEditorWindow(win: BrowserWindow) {
|
||||
}
|
||||
|
||||
export function createEditorWindow(): BrowserWindow {
|
||||
const perfStart = Date.now();
|
||||
console.log("[PERF:MAIN] createEditorWindow: STARTED");
|
||||
const isMac = process.platform === "darwin";
|
||||
const { workArea, workAreaSize } = getScreen().getPrimaryDisplay();
|
||||
const initialWidth = isMac ? Math.round(workAreaSize.width * 0.85) : workArea.width;
|
||||
@@ -735,12 +737,12 @@ export function createEditorWindow(): BrowserWindow {
|
||||
});
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
console.log("[editor-window] ready-to-show");
|
||||
console.log(`[PERF:MAIN] Editor Window: ready-to-show in ${Date.now() - perfStart}ms`);
|
||||
win.show();
|
||||
});
|
||||
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
console.log("[editor-window] did-finish-load", win.webContents.getURL());
|
||||
console.log(`[PERF:MAIN] Editor Window: did-finish-load in ${Date.now() - perfStart}ms`);
|
||||
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
// Fallback for Linux/Wayland where `ready-to-show` may not fire reliably.
|
||||
if (!win.isDestroyed() && !win.isVisible()) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const skeletonVariants = cva(
|
||||
"relative overflow-hidden rounded-md transition-shadow",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted/40",
|
||||
glass: "bg-white/5 backdrop-blur-sm border border-white/10",
|
||||
dark: "bg-black/20",
|
||||
subtle: "bg-foreground/[0.03]",
|
||||
},
|
||||
animation: {
|
||||
none: "",
|
||||
pulse: "animate-pulse",
|
||||
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-foreground/[0.04] before:to-transparent",
|
||||
"shimmer-glass": "before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-white/[0.08] before:to-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
animation: "shimmer",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface SkeletonProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof skeletonVariants> {}
|
||||
|
||||
/**
|
||||
* A magnificent Skeleton component designed with Apple-inspired aesthetics.
|
||||
* Supports shimmer animations, pulse effects, and glassmorphism.
|
||||
*/
|
||||
const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
|
||||
({ className, variant, animation, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(skeletonVariants({ variant, animation, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Skeleton.displayName = "Skeleton";
|
||||
|
||||
export { Skeleton, skeletonVariants };
|
||||
@@ -620,6 +620,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
const finalizeRecordingSession = useCallback(
|
||||
async (videoPath: string, webcamPath: string | null) => {
|
||||
const start = performance.now();
|
||||
console.log("[PERF:RENDERER] Finalize Session & Switch to Editor: STARTED");
|
||||
const shouldHideOverlayCursor = hideEditorOverlayCursorByDefault.current;
|
||||
try {
|
||||
if (webcamPath) {
|
||||
@@ -648,6 +650,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
setFinalizing(false);
|
||||
await window.electronAPI.switchToEditor();
|
||||
console.log(
|
||||
`[PERF:RENDERER] Finalize Session & Switch to Editor: COMPLETED in ${(performance.now() - start).toFixed(2)}ms`,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -1004,6 +1009,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
setFinalizing(true);
|
||||
|
||||
void (async () => {
|
||||
const stopStart = performance.now();
|
||||
console.log("[PERF:RENDERER] Total Stop Sequence: STARTED");
|
||||
|
||||
const fallbackStartDelayMs = micFallbackStartDelayMs.current;
|
||||
const fallbackTrackSettings = micFallbackTrackSettings.current;
|
||||
const stoppedAtMs = Date.now();
|
||||
@@ -1014,7 +1022,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
const isNativeWindows = nativeWindowsRecording.current;
|
||||
nativeWindowsRecording.current = false;
|
||||
|
||||
const ipcStopStart = performance.now();
|
||||
console.log("[PERF:RENDERER] IPC: stopNativeScreenRecording: STARTED");
|
||||
const result = await window.electronAPI.stopNativeScreenRecording();
|
||||
console.log(
|
||||
`[PERF:RENDERER] IPC: stopNativeScreenRecording: COMPLETED in ${(performance.now() - ipcStopStart).toFixed(2)}ms`,
|
||||
);
|
||||
|
||||
await window.electronAPI?.setRecordingState(false);
|
||||
const webcamPath = await webcamPathPromise;
|
||||
|
||||
@@ -1030,6 +1044,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
fallbackStartDelayMs,
|
||||
);
|
||||
if (recoveredPath) {
|
||||
console.log(
|
||||
`[PERF:RENDERER] Total Stop Sequence (RECOVERED) in ${(performance.now() - stopStart).toFixed(2)}ms`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (recoveryError) {
|
||||
@@ -1049,8 +1066,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
let finalPath = result.path;
|
||||
|
||||
if (isNativeWindows) {
|
||||
const ipcMuxStart = performance.now();
|
||||
console.log("[PERF:RENDERER] IPC: muxNativeWindowsRecording: STARTED");
|
||||
const muxResult =
|
||||
await window.electronAPI.muxNativeWindowsRecording(expectedDurationMs);
|
||||
console.log(
|
||||
`[PERF:RENDERER] IPC: muxNativeWindowsRecording: COMPLETED in ${(performance.now() - ipcMuxStart).toFixed(2)}ms`,
|
||||
);
|
||||
|
||||
if (!muxResult?.success || !muxResult.path) {
|
||||
void logNativeCaptureDiagnostics("mux-native-windows-recording");
|
||||
const fallbackPath = muxResult?.path ?? finalPath;
|
||||
@@ -1068,14 +1091,23 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}
|
||||
}
|
||||
|
||||
const sidecarStart = performance.now();
|
||||
console.log("[PERF:RENDERER] Store Sidecar: STARTED");
|
||||
await storeMicrophoneSidecar(
|
||||
micFallbackBlobPromise,
|
||||
finalPath,
|
||||
fallbackStartDelayMs,
|
||||
fallbackTrackSettings,
|
||||
);
|
||||
console.log(
|
||||
`[PERF:RENDERER] Store Sidecar: COMPLETED in ${(performance.now() - sidecarStart).toFixed(2)}ms`,
|
||||
);
|
||||
|
||||
await finalizeRecordingSession(finalPath, webcamPath);
|
||||
|
||||
console.log(
|
||||
`[PERF:RENDERER] Total Stop Sequence: COMPLETED in ${(performance.now() - stopStart).toFixed(2)}ms`,
|
||||
);
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,16 @@ module.exports = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
shimmer: {
|
||||
"100%": {
|
||||
transform: "translateX(100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
shimmer: "shimmer 2s infinite",
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
||||
Reference in New Issue
Block a user