Improve export failure diagnostics

This commit is contained in:
Yusuf Mohsinally
2026-05-03 14:24:42 -07:00
parent 7e00cdb1a9
commit 156e9c1ec5
3 changed files with 111 additions and 22 deletions
+1 -1
View File
@@ -161,7 +161,7 @@ export function ExportDialog({
<div className="p-1 bg-red-500/20 rounded-full">
<X className="w-3 h-3 text-red-400" />
</div>
<p className="text-sm text-red-400 leading-relaxed">{error}</p>
<p className="whitespace-pre-wrap text-sm text-red-400 leading-relaxed">{error}</p>
</div>
</div>
)}
+101 -12
View File
@@ -76,6 +76,53 @@ import {
} from "./types";
import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback";
interface ExportDiagnostics {
formatLabel: "GIF" | "Video";
reason?: string;
sourcePath?: string | null;
width?: number;
height?: number;
frameRate?: number;
codec?: string;
bitrate?: number;
}
function getFileNameForDiagnostics(filePath?: string | null) {
if (!filePath) return "unknown";
try {
const url = new URL(filePath);
if (url.protocol === "file:") {
return decodeURIComponent(url.pathname).split(/[\\/]/).pop() || filePath;
}
} catch {
// Treat non-URL values as filesystem paths.
}
return filePath.split(/[\\/]/).pop() || filePath;
}
function buildExportDiagnosticMessage(diagnostics: ExportDiagnostics) {
const details = [
diagnostics.reason ? `Reason: ${diagnostics.reason}` : null,
`Source: ${getFileNameForDiagnostics(diagnostics.sourcePath)}`,
diagnostics.width && diagnostics.height
? `Output: ${diagnostics.width}x${diagnostics.height}${
diagnostics.frameRate ? ` @ ${diagnostics.frameRate} fps` : ""
}`
: null,
diagnostics.codec ? `Codec: ${diagnostics.codec}` : null,
diagnostics.bitrate ? `Bitrate: ${Math.round(diagnostics.bitrate / 1_000_000)} Mbps` : null,
`VideoEncoder: ${"VideoEncoder" in window ? "available" : "unavailable"}`,
].filter(Boolean);
return `${diagnostics.formatLabel} export failed\n${details.join("\n")}`;
}
function buildSaveDiagnosticMessage(formatLabel: "GIF" | "Video", reason?: string) {
return `${formatLabel} export save failed${reason ? `\nReason: ${reason}` : ""}`;
}
export default function VideoEditor() {
const {
state: editorState,
@@ -1332,11 +1379,21 @@ export default function VideoEditor() {
setUnsavedExport(null);
handleExportSaved(unsavedExport.format === "gif" ? "GIF" : "Video", saveResult.path);
} else {
toast.error(saveResult.message || "Failed to save export");
toast.error(
buildSaveDiagnosticMessage(
unsavedExport.format === "gif" ? "GIF" : "Video",
saveResult.message || "Failed to save export",
),
);
}
} catch (error) {
console.error("Error saving unsaved export:", error);
toast.error("Failed to save exported video");
toast.error(
buildSaveDiagnosticMessage(
unsavedExport.format === "gif" ? "GIF" : "Video",
error instanceof Error ? error.message : "Failed to save exported video",
),
);
}
}, [unsavedExport, handleExportSaved]);
@@ -1437,12 +1494,24 @@ export default function VideoEditor() {
setUnsavedExport(null);
handleExportSaved("GIF", saveResult.path);
} else {
setExportError(saveResult.message || "Failed to save GIF");
toast.error(saveResult.message || "Failed to save GIF");
const message = buildSaveDiagnosticMessage(
"GIF",
saveResult.message || "Failed to save GIF",
);
setExportError(message);
toast.error(message);
}
} else {
setExportError(result.error || "GIF export failed");
toast.error(result.error || "GIF export failed");
const message = buildExportDiagnosticMessage({
formatLabel: "GIF",
reason: result.error || "GIF export failed",
sourcePath: videoSourcePath ?? videoPath,
width: settings.gifConfig.width,
height: settings.gifConfig.height,
frameRate: settings.gifConfig.frameRate,
});
setExportError(message);
toast.error(message);
}
} else {
// MP4 Export
@@ -1579,12 +1648,26 @@ export default function VideoEditor() {
setUnsavedExport(null);
handleExportSaved("Video", saveResult.path);
} else {
setExportError(saveResult.message || "Failed to save video");
toast.error(saveResult.message || "Failed to save video");
const message = buildSaveDiagnosticMessage(
"Video",
saveResult.message || "Failed to save video",
);
setExportError(message);
toast.error(message);
}
} else {
setExportError(result.error || "Export failed");
toast.error(result.error || "Export failed");
const message = buildExportDiagnosticMessage({
formatLabel: "Video",
reason: result.error || "Export failed",
sourcePath: videoSourcePath ?? videoPath,
width: exportWidth,
height: exportHeight,
frameRate: 60,
codec: "avc1.640033",
bitrate,
});
setExportError(message);
toast.error(message);
}
}
@@ -1599,8 +1682,13 @@ export default function VideoEditor() {
toast.error(message);
} else {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
setExportError(errorMessage);
toast.error(t("errors.exportFailedWithError", { error: errorMessage }));
const message = buildExportDiagnosticMessage({
formatLabel: settings.format === "gif" ? "GIF" : "Video",
reason: errorMessage,
sourcePath: videoSourcePath ?? videoPath,
});
setExportError(message);
toast.error(t("errors.exportFailedWithError", { error: message }));
}
} finally {
setIsExporting(false);
@@ -1613,6 +1701,7 @@ export default function VideoEditor() {
},
[
videoPath,
videoSourcePath,
webcamVideoPath,
wallpaper,
zoomRegions,
+9 -9
View File
@@ -408,6 +408,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
});
const safeHideCountdownOverlay = useCallback(async (runId: number) => {
try {
await window.electronAPI.hideCountdownOverlay(runId);
} catch (error) {
console.warn("Failed to hide countdown overlay:", error);
}
}, []);
useEffect(() => {
let cleanup: (() => void) | undefined;
@@ -450,7 +458,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamRecorder.current = null;
teardownMedia();
};
}, [teardownMedia]);
}, [teardownMedia, safeHideCountdownOverlay]);
const safeShowCountdownOverlay = async (value: number, runId: number) => {
try {
@@ -477,14 +485,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
};
const safeHideCountdownOverlay = async (runId: number) => {
try {
await window.electronAPI.hideCountdownOverlay(runId);
} catch (error) {
console.warn("Failed to hide countdown overlay:", error);
}
};
const isCountdownRunActive = (runId?: number) =>
runId === undefined || countdownRunId.current === runId;