diff --git a/public/configStore.js b/public/configStore.js
index 8ce0ba7cb9d..e28ca7e3561 100644
--- a/public/configStore.js
+++ b/public/configStore.js
@@ -113,6 +113,8 @@ const defaults = {
storeProjectInWorkingDir: true,
enableOverwriteOutput: true,
mouseWheelZoomModifierKey: 'ctrl',
+ captureFrameMethod: 'ffmpeg',
+ captureFrameQuality: 0.95,
};
// For portable app: https://github.com/mifi/lossless-cut/issues/645
diff --git a/src/App.jsx b/src/App.jsx
index d3223502896..6df6bbc1174 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -201,7 +201,7 @@ const App = memo(() => {
const allUserSettings = useUserSettingsRoot();
const {
- captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, enableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey,
+ captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, enableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality,
} = allUserSettings;
useEffect(() => {
@@ -1409,16 +1409,16 @@ const App = memo(() => {
try {
const currentTime = getCurrentTime();
const video = videoRef.current;
- const outPath = usingPreviewFile
- ? await captureFramesFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, enableTransferTimestamps, numFrames: 1 })
- : await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps });
+ const outPath = (usingPreviewFile || captureFrameMethod === 'ffmpeg')
+ ? await captureFramesFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, enableTransferTimestamps, numFrames: 1, quality: captureFrameQuality })
+ : await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps, quality: captureFrameQuality });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: `${i18n.t('Screenshot captured to:')} ${outPath}` });
} catch (err) {
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
}
- }, [filePath, getCurrentTime, usingPreviewFile, customOutDir, captureFormat, enableTransferTimestamps, hideAllNotifications]);
+ }, [filePath, getCurrentTime, usingPreviewFile, captureFrameMethod, customOutDir, captureFormat, enableTransferTimestamps, captureFrameQuality, hideAllNotifications]);
const extractSegmentFramesAsImages = useCallback(async (index) => {
if (!filePath) return;
@@ -1429,14 +1429,14 @@ const App = memo(() => {
try {
setWorking(i18n.t('Extracting frames'));
- const outPath = await captureFramesFfmpeg({ customOutDir, filePath, fromTime: start, captureFormat, enableTransferTimestamps, numFrames });
+ const outPath = await captureFramesFfmpeg({ customOutDir, filePath, fromTime: start, captureFormat, enableTransferTimestamps, numFrames, quality: captureFrameQuality });
if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: outPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) });
} catch (err) {
handleError(err);
} finally {
setWorking();
}
- }, [apparentCutSegments, captureFormat, customOutDir, enableTransferTimestamps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking]);
+ }, [apparentCutSegments, captureFormat, captureFrameQuality, customOutDir, enableTransferTimestamps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking]);
const extractCurrentSegmentFramesAsImages = useCallback(() => extractSegmentFramesAsImages(currentSegIndexSafe), [currentSegIndexSafe, extractSegmentFramesAsImages]);
@@ -1911,14 +1911,14 @@ const App = memo(() => {
if (!filePath) return;
try {
const currentTime = getCurrentTime();
- const path = await captureFramesFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, enableTransferTimestamps, numFrames: 1 });
+ const path = await captureFramesFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, enableTransferTimestamps, numFrames: 1, quality: captureFrameQuality });
if (!(await addFileAsCoverArt(path))) return;
if (!hideAllNotifications) toast.fire({ text: i18n.t('Current frame has been set as cover art') });
} catch (err) {
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
}
- }, [addFileAsCoverArt, captureFormat, customOutDir, enableTransferTimestamps, filePath, getCurrentTime, hideAllNotifications]);
+ }, [addFileAsCoverArt, captureFormat, captureFrameQuality, customOutDir, enableTransferTimestamps, filePath, getCurrentTime, hideAllNotifications]);
const batchLoadPaths = useCallback((newPaths, append) => {
setBatchFiles((existingFiles) => {
diff --git a/src/Settings.jsx b/src/Settings.jsx
index b5a8dd0c62d..1bef213901d 100644
--- a/src/Settings.jsx
+++ b/src/Settings.jsx
@@ -8,36 +8,11 @@ import AutoExportToggler from './components/AutoExportToggler';
import useUserSettings from './hooks/useUserSettings';
import { askForFfPath } from './dialogs';
import { isMasBuild } from './util';
+import { langNames } from './util/constants';
import { keyMap } from './hooks/useTimelineScroll';
-// https://www.electronjs.org/docs/api/locales
-// See i18n.js
-const langNames = {
- en: 'English',
- cs: 'Čeština',
- de: 'Deutsch',
- es: 'Español',
- fr: 'Français',
- it: 'Italiano',
- nl: 'Nederlands',
- nb: 'Norsk',
- pl: 'Polski',
- pt: 'Português',
- pt_BR: 'português do Brasil',
- fi: 'Suomi',
- ru: 'русский',
- // sr: 'Cрпски',
- tr: 'Türkçe',
- vi: 'Tiếng Việt',
- ja: '日本語',
- zh: '中文',
- zh_Hant: '繁體中文',
- zh_Hans: '简体中文',
- ko: '한국어',
-};
-
// eslint-disable-next-line react/jsx-props-no-spreading
const Row = (props) =>
;
// eslint-disable-next-line react/jsx-props-no-spreading
@@ -49,7 +24,7 @@ const Settings = memo(({
}) => {
const { t } = useTranslation();
- const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, enableTransferTimestamps, setEnableTransferTimestamps, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, setEnableOverwriteOutput, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey } = useUserSettings();
+ const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, enableTransferTimestamps, setEnableTransferTimestamps, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, setEnableOverwriteOutput, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality } = useUserSettings();
const onLangChange = useCallback((e) => {
const { value } = e.target;
@@ -210,6 +185,23 @@ const Settings = memo(({
+
+ {t('Snapshot capture method')}
+
+
+
+
+
+
+ {t('Snapshot capture quality')}
+
+ setCaptureFrameQuality(Math.max(Math.min(1, parseInt(e.target.value, 10) / 1000)), 0)} />
+ {Math.round(captureFrameQuality * 100)}%
+
+
+
{t('In timecode show')}
diff --git a/src/capture-frame.js b/src/capture-frame.js
index adcb088ca61..191beed8c85 100644
--- a/src/capture-frame.js
+++ b/src/capture-frame.js
@@ -3,24 +3,24 @@ import dataUriToBuffer from 'data-uri-to-buffer';
import { getSuffixedOutPath, transferTimestamps } from './util';
import { formatDuration } from './util/duration';
-import { captureFrame as ffmpegCaptureFrame } from './ffmpeg';
+import { captureFrames as ffmpegCaptureFrames } from './ffmpeg';
const fs = window.require('fs-extra');
const mime = window.require('mime-types');
-function getFrameFromVideo(video, format) {
+function getFrameFromVideo(video, format, quality) {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
- const dataUri = canvas.toDataURL(`image/${format}`);
+ const dataUri = canvas.toDataURL(`image/${format}`, quality);
return dataUriToBuffer(dataUri);
}
-export async function captureFramesFfmpeg({ customOutDir, filePath, fromTime, captureFormat, enableTransferTimestamps, numFrames }) {
+export async function captureFramesFfmpeg({ customOutDir, filePath, fromTime, captureFormat, enableTransferTimestamps, numFrames, quality }) {
const time = formatDuration({ seconds: fromTime, fileNameFriendly: true });
let nameSuffix;
if (numFrames > 1) {
@@ -30,14 +30,14 @@ export async function captureFramesFfmpeg({ customOutDir, filePath, fromTime, ca
nameSuffix = `${time}.${captureFormat}`;
}
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix });
- await ffmpegCaptureFrame({ timestamp: fromTime, videoPath: filePath, outPath, numFrames });
+ await ffmpegCaptureFrames({ timestamp: fromTime, videoPath: filePath, outPath, numFrames, quality });
if (enableTransferTimestamps && numFrames === 1) await transferTimestamps(filePath, outPath, fromTime);
return outPath;
}
-export async function captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps }) {
- const buf = getFrameFromVideo(video, captureFormat);
+export async function captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps, quality }) {
+ const buf = getFrameFromVideo(video, captureFormat, quality);
const ext = mime.extension(buf.type);
const time = formatDuration({ seconds: currentTime, fileNameFriendly: true });
diff --git a/src/ffmpeg.js b/src/ffmpeg.js
index 282f0a344e4..7df67d484d4 100644
--- a/src/ffmpeg.js
+++ b/src/ffmpeg.js
@@ -727,15 +727,17 @@ export async function extractWaveform({ filePath, outPath }) {
console.timeEnd('ffmpeg');
}
-const imageCaptureQuality = 3;
-
// See also capture-frame.js
-export async function captureFrame({ timestamp, videoPath, outPath, numFrames }) {
+export async function captureFrames({ timestamp, videoPath, outPath, numFrames, quality }) {
+ // Normal range for JPEG is 2-31 with 31 being the worst quality.
+ const min = 2;
+ const max = 31;
+ const ffmpegQuality = Math.min(Math.max(min, quality, Math.round((1 - quality) * (max - min) + min)), max);
await runFfmpeg([
'-ss', timestamp,
'-i', videoPath,
'-vframes', numFrames,
- '-q:v', imageCaptureQuality,
+ '-q:v', ffmpegQuality,
'-y', outPath,
]);
}
diff --git a/src/hooks/useUserSettingsRoot.js b/src/hooks/useUserSettingsRoot.js
index 2ab97f5aa59..dc6bdb38535 100644
--- a/src/hooks/useUserSettingsRoot.js
+++ b/src/hooks/useUserSettingsRoot.js
@@ -117,6 +117,10 @@ export default () => {
useEffect(() => safeSetConfig('enableOverwriteOutput', enableOverwriteOutput), [enableOverwriteOutput]);
const [mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey] = useState(safeGetConfigInitial('mouseWheelZoomModifierKey'));
useEffect(() => safeSetConfig('mouseWheelZoomModifierKey', mouseWheelZoomModifierKey), [mouseWheelZoomModifierKey]);
+ const [captureFrameMethod, setCaptureFrameMethod] = useState(safeGetConfigInitial('captureFrameMethod'));
+ useEffect(() => safeSetConfig('captureFrameMethod', captureFrameMethod), [captureFrameMethod]);
+ const [captureFrameQuality, setCaptureFrameQuality] = useState(safeGetConfigInitial('captureFrameQuality'));
+ useEffect(() => safeSetConfig('captureFrameQuality', captureFrameQuality), [captureFrameQuality]);
const resetKeyBindings = useCallback(() => {
configStore.reset('keyBindings');
@@ -213,5 +217,9 @@ export default () => {
setEnableOverwriteOutput,
mouseWheelZoomModifierKey,
setMouseWheelZoomModifierKey,
+ captureFrameMethod,
+ setCaptureFrameMethod,
+ captureFrameQuality,
+ setCaptureFrameQuality,
};
};
diff --git a/src/util/constants.js b/src/util/constants.js
index 22685dd3330..f7b09b03fd1 100644
--- a/src/util/constants.js
+++ b/src/util/constants.js
@@ -7,3 +7,29 @@ export const zoomMax = 2 ** 14;
export const rightBarWidth = 200;
export const leftBarWidth = 240;
+
+// https://www.electronjs.org/docs/api/locales
+// See i18n.js
+export const langNames = {
+ en: 'English',
+ cs: 'Čeština',
+ de: 'Deutsch',
+ es: 'Español',
+ fr: 'Français',
+ it: 'Italiano',
+ nl: 'Nederlands',
+ nb: 'Norsk',
+ pl: 'Polski',
+ pt: 'Português',
+ pt_BR: 'português do Brasil',
+ fi: 'Suomi',
+ ru: 'русский',
+ // sr: 'Cрпски',
+ tr: 'Türkçe',
+ vi: 'Tiếng Việt',
+ ja: '日本語',
+ zh: '中文',
+ zh_Hant: '繁體中文',
+ zh_Hans: '简体中文',
+ ko: '한국어',
+};