diff --git a/electron/main.ts b/electron/main.ts index c2d7a4d48..827e7725c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -126,6 +126,30 @@ if (hasSingleInstanceLock) { app.quit(); } +// Forward renderer console output to the main process so it appears in the +// terminal where `npm run dev` is running. Without this, `console.info` / +// `console.warn` / `console.error` calls in the renderer only show up in +// DevTools, which is invisible when the user is following terminal +// instructions (see issue #8 and PR #11). +app.on("browser-window-created", (_event, window) => { + window.webContents.on("console-message", (details) => { + // New API: details.level is "info" | "warning" | "error" | "debug". + // Skip debug to keep the terminal readable; the user can still inspect + // DevTools for that. + const { level, message } = details; + if (level === "debug") return; + const tag = level.toUpperCase(); + const line = `[renderer ${tag}] ${message}`; + if (level === "error") { + console.error(line); + } else if (level === "warning") { + console.warn(line); + } else { + console.log(line); + } + }); +}); + function isEditorWindow(window: BrowserWindow) { return window.webContents.getURL().includes("windowType=editor"); } diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 445a09060..3b7afb360 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -174,6 +174,31 @@ function buildSaveDiagnosticMessage(formatLabel: "GIF" | "Video", reason?: strin return `${formatLabel} export save failed${reason ? `\nReason: ${reason}` : ""}`; } +function getPreviewVideoDiagnostics(video: HTMLVideoElement | null) { + if (!video) { + return { present: false }; + } + + return { + present: true, + src: video.currentSrc || video.src, + readyState: video.readyState, + networkState: video.networkState, + error: video.error + ? { + code: video.error.code, + message: video.error.message, + } + : null, + currentTime: Number.isFinite(video.currentTime) ? video.currentTime : null, + duration: Number.isFinite(video.duration) ? video.duration : null, + paused: video.paused, + ended: video.ended, + videoWidth: video.videoWidth, + videoHeight: video.videoHeight, + }; +} + const CAPTION_WORD_CHOICES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const; export default function VideoEditor() { @@ -1855,6 +1880,33 @@ export default function VideoEditor() { setExportError(null); setExportedFilePath(null); + const previewBeforeExport = getPreviewVideoDiagnostics(video); + console.info( + `[VideoEditor] export started ${JSON.stringify({ + format: settings.format, + sourcePath: videoSourcePath ?? videoPath, + videoPath, + webcamVideoPath, + preview: previewBeforeExport, + settings: { + exportQuality: settings.quality || exportQuality, + aspectRatio, + padding, + borderRadius, + shadowIntensity, + showBlur, + motionBlurAmount, + cropRegion, + zoomRegions: zoomRegions.length, + trimRegions: trimRegions.length, + speedRegions: speedRegions.length, + annotations: annotationRegions.length, + effectiveShowCursor, + cursorSize, + }, + })}`, + ); + try { const wasPlaying = isPlaying; if (wasPlaying) { @@ -2080,6 +2132,14 @@ export default function VideoEditor() { toast.error(t("errors.exportFailedWithError", { error: message })); } } finally { + console.info( + `[VideoEditor] export finished ${JSON.stringify({ + sourcePath: videoSourcePath ?? videoPath, + videoPath, + previewBefore: previewBeforeExport, + previewAfter: getPreviewVideoDiagnostics(videoPlaybackRef.current?.video ?? null), + })}`, + ); setIsExporting(false); exporterRef.current = null; // Reset so the next export can reopen the dialog (second export diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 1b4ad5263..64967184f 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -215,6 +215,26 @@ function enableAllPreviewAudioTracks(video: HTMLVideoElement) { } } +function getVideoElementDiagnostics(video: HTMLVideoElement) { + return { + src: video.currentSrc || video.src, + readyState: video.readyState, + networkState: video.networkState, + error: video.error + ? { + code: video.error.code, + message: video.error.message, + } + : null, + currentTime: Number.isFinite(video.currentTime) ? video.currentTime : null, + duration: Number.isFinite(video.duration) ? video.duration : null, + paused: video.paused, + ended: video.ended, + videoWidth: video.videoWidth, + videoHeight: video.videoHeight, + }; +} + const VideoPlayback = forwardRef( ( { @@ -345,6 +365,18 @@ const VideoPlayback = forwardRef( const speedRegionsRef = useRef([]); const motionBlurAmountRef = useRef(motionBlurAmount); const cursorOverlayRef = useRef(null); + + const logPreviewVideoEvent = useCallback( + (event: React.SyntheticEvent) => { + console.info( + `[VideoPlayback] preview video ${event.type} ${JSON.stringify({ + videoPath, + diagnostics: getVideoElementDiagnostics(event.currentTarget), + })}`, + ); + }, + [videoPath], + ); const showCursorRef = useRef(showCursor); const cursorSizeRef = useRef(cursorSize); const cursorSmoothingRef = useRef(cursorSmoothing); @@ -2152,7 +2184,14 @@ const VideoPlayback = forwardRef( forceResolveDuration(e.currentTarget); } }} - onError={() => onError("Failed to load video")} + onEmptied={logPreviewVideoEvent} + onStalled={logPreviewVideoEvent} + onSuspend={logPreviewVideoEvent} + onAbort={logPreviewVideoEvent} + onError={(event) => { + logPreviewVideoEvent(event); + onError("Failed to load video"); + }} /> {supplementalAudioPath && (