implement customisable timestamp transfer #1017

pull/1580/head
Mikael Finstad 2023-08-20 15:32:02 +02:00
rodzic 9c633cbbdf
commit 5bc5715bf9
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
7 zmienionych plików z 93 dodań i 52 usunięć

Wyświetl plik

@ -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,

Wyświetl plik

@ -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) => {

Wyświetl plik

@ -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(({
<Row>
<KeyCell>{t('Set file modification date/time of output files to:')}</KeyCell>
<td>
<Button iconBefore={enableTransferTimestamps ? DocumentIcon : TimeIcon} onClick={() => setEnableTransferTimestamps((v) => !v)}>
{enableTransferTimestamps ? t('Source file\'s time') : t('Current time')}
</Button>
<Select value={treatOutputFileModifiedTimeAsStart ?? 'disabled'} onChange={(e) => setTreatOutputFileModifiedTimeAsStart(e.target.value === 'disabled' ? null : (e.target.value === 'true'))}>
<option value="disabled">{t('Current time')}</option>
<option value="true">{t('Source file\'s time plus segment start cut time')}</option>
<option value="false">{t('Source file\'s time minus segment end cut time')}</option>
</Select>
</td>
</Row>
<Row>
<KeyCell>{t('Treat source file modification date/time as:')}</KeyCell>
<td>
<Select disabled={treatOutputFileModifiedTimeAsStart == null} value={treatInputFileModifiedTimeAsStart} onChange={(e) => setTreatInputFileModifiedTimeAsStart((e.target.value === 'true'))}>
<option value="true">{t('Start of video')}</option>
<option value="false">{t('End of video')}</option>
</Select>
</td>
</Row>

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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);
}