Skip to content

Commit

Permalink
implement customisable timestamp transfer #1017
Browse files Browse the repository at this point in the history
  • Loading branch information
mifi committed Aug 20, 2023
1 parent 9c633cb commit 5bc5715
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 52 deletions.
37 changes: 23 additions & 14 deletions public/configStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -165,20 +179,15 @@ async function init() {
}
}

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

function reset(key) {
set(key, defaults[key]);
throw new Error('Timed out while creating config store');
}

module.exports = {
Expand Down
16 changes: 8 additions & 8 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down
20 changes: 16 additions & 4 deletions src/components/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>

Expand Down
27 changes: 12 additions & 15 deletions src/hooks/useFfmpegOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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 }) => {
Expand All @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions src/hooks/useFrameCapture.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;

Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down
Loading

0 comments on commit 5bc5715

Please sign in to comment.