diff --git a/locales/en/translation.json b/locales/en/translation.json index 168fcf42..7488aefb 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -740,6 +740,7 @@ "Some extra tracks have been discarded. You can change this option before merging.": "Some extra tracks have been discarded. You can change this option before merging.", "Something went wrong": "Something went wrong", "Sort items": "Sort items", + "Source code": "Source code", "Source file no longer exists or is not accessible. Please check that it is still in its original location and that you have permission to access it.": "Source file no longer exists or is not accessible. Please check that it is still in its original location and that you have permission to access it.", "Source file's time minus segment end cut time": "Source file's time minus segment end cut time", "Source file's time plus segment start cut time": "Source file's time plus segment start cut time", @@ -892,6 +893,7 @@ "You can customize the file name of the output segment(s) using special variables._one": "You can customize the file name of the output using special variables.", "You can customize the file name of the output segment(s) using special variables._other": "You can customize the file name of the output segments using special variables.", "You do not have permission to access this file": "You do not have permission to access this file", + "You have configured a custom FFmpeg directory. You may change or reset it in Settings.": "You have configured a custom FFmpeg directory. You may change or reset it in Settings.", "You have enabled the \"invert segments\" mode <1> which will cut away selected segments instead of keeping them. But there is no space between any segments, or at least two segments are overlapping. This would not produce any output. Either make room between segments or click the Yinyang <3> symbol below to disable this mode. Alternatively you may combine overlapping segments from the menu.": "You have enabled the \"invert segments\" mode <1> which will cut away selected segments instead of keeping them. But there is no space between any segments, or at least two segments are overlapping. This would not produce any output. Either make room between segments or click the Yinyang <3> symbol below to disable this mode. Alternatively you may combine overlapping segments from the menu.", "You have no write access to the directory of this file": "You have no write access to the directory of this file", "You have no write access to the directory of this file, please select a custom working dir": "You have no write access to the directory of this file, please select a custom working dir", diff --git a/src/main/ffmpeg.ts b/src/main/ffmpeg.ts index 86154c47..b86be90f 100644 --- a/src/main/ffmpeg.ts +++ b/src/main/ffmpeg.ts @@ -1,4 +1,5 @@ import { join } from 'node:path'; +import { access } from 'node:fs/promises'; import readline from 'node:readline'; import stringToStream from 'string-to-stream'; import type { Options as ExecaOptions, ResultPromise } from 'execa'; @@ -66,6 +67,15 @@ function getFfPath(cmd: FfCommand) { ); } +// Used by the startup check to fail fast with a proper ENOENT if the executable doesn't exist. +// This is because on Windows, cross-spawn (used by execa) falls back to running the command through +// cmd.exe when it cannot resolve the executable path (e.g. custom FFmpeg directory pointing to a +// location without ffmpeg.exe), which instead fails with a confusing exit code 1: +// "'...' is not recognized as an internal or external command" +export async function checkFfExists(cmd: FfCommand) { + await access(getFfPath(cmd)); // throws with code ENOENT if it doesn't exist +} + const getFfprobePath = () => getFfPath('ffprobe'); /** * ⚠️ Do not use directly when running ffmpeg, because we need to add certain options before running, like `LD_LIBRARY_PATH` on linux diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 35e2d023..35cf77de 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2483,7 +2483,7 @@ function App() { useEffect(() => { (async () => { - setFfmpegInfo(await runStartupCheck({ onError: ({ title, message }) => setGenericError({ title, err: message }) })); + setFfmpegInfo(await runStartupCheck({ customFfPath, onError: ({ title, message }) => setGenericError({ title, err: message }) })); })(); }, [customFfPath, setGenericError]); diff --git a/src/renderer/src/ffmpeg.ts b/src/renderer/src/ffmpeg.ts index 69d9e880..7444438e 100644 --- a/src/renderer/src/ffmpeg.ts +++ b/src/renderer/src/ffmpeg.ts @@ -18,7 +18,7 @@ import { parseFfprobeDuration } from '../../common/util'; const { ffmpeg } = window.require('@electron/remote').require('./index.js'); -const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath } = ffmpeg; +const { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, runFfprobe, getFfmpegPath, setCustomFfPath, checkFfExists } = ffmpeg; export { renderWaveformPng, mapTimesToSegments, detectSceneChanges, captureFrames, captureFrameToFile, captureFrameToClipboard, getFfCommandLine, runFfmpegConcat, runFfmpegWithProgress, getDuration, abortFfmpegs, runFfmpeg, getFfmpegPath, setCustomFfPath }; @@ -550,6 +550,10 @@ const ffprobeVersionSchema = z.object({ }); export async function runFfmpegStartupCheck() { + // will throw ENOENT if the executables don't exist (e.g. custom FFmpeg directory pointing to a location without them) + await checkFfExists('ffmpeg'); + await checkFfExists('ffprobe'); + // will throw if exit code != 0 const { stderr: ffmpegStderr } = await runFfmpeg(['-f', 'lavfi', '-i', 'nullsrc=s=256x256:d=1', '-f', 'null', '-']); console.log('FFmpeg startup check output:', new TextDecoder().decode(ffmpegStderr)); diff --git a/src/renderer/src/mifi.ts b/src/renderer/src/mifi.ts index 8f099003..241954af 100644 --- a/src/renderer/src/mifi.ts +++ b/src/renderer/src/mifi.ts @@ -18,7 +18,7 @@ export async function loadMifiLink() { } } -export async function runStartupCheck({ onError }: { onError: (error: { title: string, message: string }) => void }) { +export async function runStartupCheck({ customFfPath, onError }: { customFfPath: string | undefined, onError: (error: { title: string, message: string }) => void }) { try { return await runFfmpegStartupCheck(); } catch (err) { @@ -26,7 +26,15 @@ export async function runStartupCheck({ onError }: { onError: (error: { title: s if ('code' in err && err.code === 'ENOENT') { onError({ title: i18n.t('Fatal: FFmpeg executable not found'), - message: `${i18n.t('Make sure that the FFmpeg executable exists:')}\n\n${getFfmpegPath()}`, + message: [ + i18n.t('Make sure that the FFmpeg executable exists:'), + '', + getFfmpegPath(), + ...(customFfPath != null ? [ + '', + i18n.t('You have configured a custom FFmpeg directory. You may change or reset it in Settings.'), + ] : []), + ].join('\n'), }); return undefined; }