diff --git a/public/configStore.js b/public/configStore.js index bd8c927e..40384185 100644 --- a/public/configStore.js +++ b/public/configStore.js @@ -106,7 +106,8 @@ const defaults = { outSegTemplate: undefined, keyboardSeekAccFactor: 1.03, keyboardNormalSeekSpeed: 1, - enableTransferTimestamps: true, + treatInputFileModifiedTimeAsStart: true, + treatOutputFileModifiedTimeAsStart: true, outFormatLocked: undefined, safeOutputFileName: true, windowBounds: undefined, @@ -150,6 +151,19 @@ async function getCustomStoragePath() { let store; +function get(key) { + return store.get(key); +} + +function set(key, val) { + if (val === undefined) store.delete(key); + else store.set(key, val); +} + +function reset(key) { + set(key, defaults[key]); +} + async function init() { const customStoragePath = await getCustomStoragePath(); if (customStoragePath) logger.info('customStoragePath', customStoragePath); @@ -165,22 +179,17 @@ async function init() { } } + // migrate old configs: + const enableTransferTimestamps = store.get('enableTransferTimestamps'); // todo remove after a while + if (enableTransferTimestamps != null) { + logger.info('Migrating enableTransferTimestamps'); + store.delete('enableTransferTimestamps'); + set('treatOutputFileModifiedTimeAsStart', enableTransferTimestamps ? true : undefined); + } + throw new Error('Timed out while creating config store'); } -function get(key) { - return store.get(key); -} - -function set(key, val) { - if (val === undefined) store.delete(key); - else store.set(key, val); -} - -function reset(key) { - set(key, defaults[key]); -} - module.exports = { init, get, diff --git a/src/App.jsx b/src/App.jsx index bf396ae7..e3b70c89 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -185,7 +185,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, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, + 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, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, } = allUserSettings; useEffect(() => { @@ -378,7 +378,7 @@ const App = memo(() => { return formatDuration({ seconds, shorten, fileNameFriendly }); }, [detectedFps, timecodeFormat, getFrameCount]); - const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ formatTimecode }); + const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ formatTimecode, treatOutputFileModifiedTimeAsStart }); // const getSafeCutTime = useCallback((cutTime, next) => ffmpeg.getSafeCutTime(neighbouringFrames, cutTime, next), [neighbouringFrames]); @@ -733,7 +733,7 @@ const App = memo(() => { const { concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration, - } = useFfmpegOperations({ filePath, enableTransferTimestamps, needSmartCut, enableOverwriteOutput }); + } = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput }); const html5ifyAndLoad = useCallback(async (cod, fp, speed, hv, ha) => { const usesDummyVideo = ['fastest-audio', 'fastest-audio-remux', 'fastest'].includes(speed); @@ -1251,15 +1251,15 @@ const App = memo(() => { const video = videoRef.current; const useFffmpeg = usingPreviewFile || captureFrameMethod === 'ffmpeg'; const outPath = useFffmpeg - ? await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, enableTransferTimestamps, quality: captureFrameQuality }) - : await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps, quality: captureFrameQuality }); + ? await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, quality: captureFrameQuality }) + : await captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, 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, getRelevantTime, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, enableTransferTimestamps, captureFrameQuality, captureFrameFromTag, hideAllNotifications]); + }, [filePath, getRelevantTime, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, hideAllNotifications]); const extractSegmentFramesAsImages = useCallback(async (index) => { if (!filePath || detectedFps == null || workingRef.current) return; @@ -1687,14 +1687,14 @@ const App = memo(() => { if (!filePath) return; try { const currentTime = getRelevantTime(); - const path = await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, enableTransferTimestamps, quality: captureFrameQuality }); + const path = await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, 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, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, enableTransferTimestamps, filePath, getRelevantTime, hideAllNotifications]); + }, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, filePath, getRelevantTime, hideAllNotifications]); const batchLoadPaths = useCallback((newPaths, append) => { setBatchFiles((existingFiles) => { diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx index eedaea1f..70d20b6b 100644 --- a/src/components/Settings.jsx +++ b/src/components/Settings.jsx @@ -38,7 +38,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, enableOverwriteOutput, setEnableOverwriteOutput, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors } = 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, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, enableOverwriteOutput, setEnableOverwriteOutput, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart } = useUserSettings(); const onLangChange = useCallback((e) => { const { value } = e.target; @@ -190,9 +190,21 @@ const Settings = memo(({ {t('Set file modification date/time of output files to:')} - + + + + + + {t('Treat source file modification date/time as:')} + + diff --git a/src/hooks/useFfmpegOperations.js b/src/hooks/useFfmpegOperations.js index 3b18d6f1..fad1e374 100644 --- a/src/hooks/useFfmpegOperations.js +++ b/src/hooks/useFfmpegOperations.js @@ -7,6 +7,7 @@ import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileMeta, cutEncodeSmartPart, getExperimentalArgs, html5ify as ffmpegHtml5ify, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat } from '../ffmpeg'; import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams'; import { getSmartCutParams } from '../smartcut'; +import { isDurationValid } from '../segments'; const { join, resolve, dirname } = window.require('path'); const { pathExists } = window.require('fs-extra'); @@ -55,11 +56,7 @@ const tryDeleteFiles = async (paths) => pMap(paths, (path) => { unlink(path).catch((err) => console.error('Failed to delete', path, err)); }, { concurrency: 5 }); -function useFfmpegOperations({ filePath, enableTransferTimestamps, needSmartCut, enableOverwriteOutput }) { - const optionalTransferTimestamps = useCallback(async (...args) => { - if (enableTransferTimestamps) await transferTimestamps(...args); - }, [enableTransferTimestamps]); - +function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput }) { const shouldSkipExistingFile = useCallback(async (path) => { const skip = !enableOverwriteOutput && await pathExists(path); if (skip) console.log('Not overwriting existing file', path); @@ -165,13 +162,13 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps, needSmartCut, const result = await runFfmpegConcat({ ffmpegArgs, concatTxt, totalDuration, onProgress }); logStdoutStderr(result); - await optionalTransferTimestamps(metadataFromPath, outPath); + await transferTimestamps({ inPath: metadataFromPath, outPath, treatOutputFileModifiedTimeAsStart }); return { haveExcludedStreams: excludedStreamIds.length > 0 }; } finally { if (chaptersPath) await tryDeleteFiles([chaptersPath]); } - }, [optionalTransferTimestamps, shouldSkipExistingFile]); + }, [shouldSkipExistingFile, treatOutputFileModifiedTimeAsStart]); const cutSingle = useCallback(async ({ keyframeCut: ssBeforeInput, avoidNegativeTs, copyFileStreams, cutFrom, cutTo, chaptersPath, onProgress, outPath, @@ -319,8 +316,8 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps, needSmartCut, const result = await runFfmpegWithProgress({ ffmpegArgs, duration: cutDuration, onProgress }); logStdoutStderr(result); - await optionalTransferTimestamps(filePath, outPath, cutFrom); - }, [filePath, optionalTransferTimestamps, shouldSkipExistingFile]); + await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(videoDuration) ? videoDuration : undefined, treatOutputFileModifiedTimeAsStart }); + }, [filePath, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]); const cutMultiple = useCallback(async ({ outputDir, customOutDir, segments, outSegFileNames, videoDuration, rotation, detectedFps, @@ -459,9 +456,9 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps, needSmartCut, const html5ify = useCallback(async ({ customOutDir, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }) => { const outPath = getHtml5ifiedPath(customOutDir, filePathArg, speed); await ffmpegHtml5ify({ filePath: filePathArg, outPath, speed, hasAudio, hasVideo, onProgress }); - await optionalTransferTimestamps(filePathArg, outPath); + await transferTimestamps({ inPath: filePathArg, outPath, treatOutputFileModifiedTimeAsStart }); return outPath; - }, [optionalTransferTimestamps]); + }, [treatOutputFileModifiedTimeAsStart]); // This is just used to load something into the player with correct length, // so user can seek and then we render frames using ffmpeg @@ -483,8 +480,8 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps, needSmartCut, const result = await runFfmpegWithProgress({ ffmpegArgs, duration, onProgress }); logStdoutStderr(result); - await optionalTransferTimestamps(filePathArg, outPath); - }, [optionalTransferTimestamps]); + await transferTimestamps({ inPath: filePathArg, outPath, treatOutputFileModifiedTimeAsStart }); + }, [treatOutputFileModifiedTimeAsStart]); // https://stackoverflow.com/questions/34118013/how-to-determine-webm-duration-using-ffprobe const fixInvalidDuration = useCallback(async ({ fileFormat, customOutDir, duration, onProgress }) => { @@ -508,10 +505,10 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps, needSmartCut, const result = await runFfmpegWithProgress({ ffmpegArgs, duration, onProgress }); logStdoutStderr(result); - await optionalTransferTimestamps(filePath, outPath); + await transferTimestamps({ inPath: filePath, outPath, treatOutputFileModifiedTimeAsStart }); return outPath; - }, [filePath, optionalTransferTimestamps]); + }, [filePath, treatOutputFileModifiedTimeAsStart]); return { cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, autoConcatCutSegments, diff --git a/src/hooks/useFrameCapture.js b/src/hooks/useFrameCapture.js index b5b20dd0..8c2ee3ac 100644 --- a/src/hooks/useFrameCapture.js +++ b/src/hooks/useFrameCapture.js @@ -23,7 +23,7 @@ function getFrameFromVideo(video, format, quality) { return dataUriToBuffer(dataUri); } -export default ({ formatTimecode }) => { +export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => { async function captureFramesRange({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) { const getSuffix = (prefix) => `${prefix}.${captureFormat}`; @@ -71,17 +71,17 @@ export default ({ formatTimecode }) => { return outPaths[0]; } - async function captureFrameFromFfmpeg({ customOutDir, filePath, fromTime, captureFormat, enableTransferTimestamps, quality }) { + async function captureFrameFromFfmpeg({ customOutDir, filePath, fromTime, captureFormat, quality }) { const time = formatTimecode({ seconds: fromTime, fileNameFriendly: true }); const nameSuffix = `${time}.${captureFormat}`; const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix }); await ffmpegCaptureFrame({ timestamp: fromTime, videoPath: filePath, outPath, quality }); - if (enableTransferTimestamps) await transferTimestamps(filePath, outPath, fromTime); + await transferTimestamps({ inPath: filePath, outPath, cutFrom: fromTime, treatOutputFileModifiedTimeAsStart }); return outPath; } - async function captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps, quality }) { + async function captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, quality }) { const buf = getFrameFromVideo(video, captureFormat, quality); const ext = mime.extension(buf.type); @@ -90,7 +90,7 @@ export default ({ formatTimecode }) => { const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `${time}.${ext}` }); await fs.writeFile(outPath, buf); - if (enableTransferTimestamps) await transferTimestamps(filePath, outPath, currentTime); + await transferTimestamps({ inPath: filePath, outPath, cutFrom: currentTime, treatOutputFileModifiedTimeAsStart }); return outPath; } diff --git a/src/hooks/useUserSettingsRoot.js b/src/hooks/useUserSettingsRoot.js index cccc239e..f2561ea4 100644 --- a/src/hooks/useUserSettingsRoot.js +++ b/src/hooks/useUserSettingsRoot.js @@ -97,8 +97,12 @@ export default () => { useEffect(() => safeSetConfig({ keyboardSeekAccFactor }), [keyboardSeekAccFactor]); const [keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed] = useState(safeGetConfigInitial('keyboardNormalSeekSpeed')); useEffect(() => safeSetConfig({ keyboardNormalSeekSpeed }), [keyboardNormalSeekSpeed]); - const [enableTransferTimestamps, setEnableTransferTimestamps] = useState(safeGetConfigInitial('enableTransferTimestamps')); - useEffect(() => safeSetConfig({ enableTransferTimestamps }), [enableTransferTimestamps]); + + const [treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart] = useState(safeGetConfigInitial('treatInputFileModifiedTimeAsStart')); + useEffect(() => safeSetConfig({ treatInputFileModifiedTimeAsStart }), [treatInputFileModifiedTimeAsStart]); + const [treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart] = useState(safeGetConfigInitial('treatOutputFileModifiedTimeAsStart')); + useEffect(() => safeSetConfig({ treatOutputFileModifiedTimeAsStart }), [treatOutputFileModifiedTimeAsStart]); + const [outFormatLocked, setOutFormatLocked] = useState(safeGetConfigInitial('outFormatLocked')); useEffect(() => safeSetConfig({ outFormatLocked }), [outFormatLocked]); const [safeOutputFileName, setSafeOutputFileName] = useState(safeGetConfigInitial('safeOutputFileName')); @@ -211,8 +215,10 @@ export default () => { setKeyboardSeekAccFactor, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, - enableTransferTimestamps, - setEnableTransferTimestamps, + treatInputFileModifiedTimeAsStart, + setTreatInputFileModifiedTimeAsStart, + treatOutputFileModifiedTimeAsStart, + setTreatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, diff --git a/src/util.js b/src/util.js index 41d44464..03d4ad5d 100644 --- a/src/util.js +++ b/src/util.js @@ -89,10 +89,27 @@ export async function dirExists(dirPath) { return (await pathExists(dirPath)) && (await fsExtra.lstat(dirPath)).isDirectory(); } -export async function transferTimestamps(inPath, outPath, offset = 0) { +export async function transferTimestamps({ inPath, outPath, cutFrom = 0, cutTo = 0, duration = 0, treatInputFileModifiedTimeAsStart = true, treatOutputFileModifiedTimeAsStart }) { + if (treatOutputFileModifiedTimeAsStart == null) return; // null means disabled; + + // see https://github.com/mifi/lossless-cut/issues/1017#issuecomment-1049097115 + function calculateTime(fileTime) { + if (treatInputFileModifiedTimeAsStart && treatOutputFileModifiedTimeAsStart) { + return fileTime + cutFrom; + } + if (!treatInputFileModifiedTimeAsStart && !treatOutputFileModifiedTimeAsStart) { + return fileTime - duration + cutTo; + } + if (treatInputFileModifiedTimeAsStart && !treatOutputFileModifiedTimeAsStart) { + return fileTime + cutTo; + } + // if (!treatInputFileModifiedTimeAsStart && treatOutputFileModifiedTimeAsStart) { + return fileTime - duration + cutFrom; + } + try { const { atime, mtime } = await stat(inPath); - await fsExtra.utimes(outPath, (atime.getTime() / 1000) + offset, (mtime.getTime() / 1000) + offset); + await fsExtra.utimes(outPath, calculateTime((atime.getTime() / 1000)), calculateTime((mtime.getTime() / 1000))); } catch (err) { console.error('Failed to set output file modified time', err); }