always use timecode format setting

also when exporting files
pull/1255/head
Mikael Finstad 2023-01-06 22:48:52 +08:00
rodzic 572a0caf1a
commit 75f768dad7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
3 zmienionych plików z 114 dodań i 104 usunięć

Wyświetl plik

@ -23,6 +23,7 @@ import useKeyframes from './hooks/useKeyframes';
import useWaveform from './hooks/useWaveform';
import useKeyboard from './hooks/useKeyboard';
import useFileFormatState from './hooks/useFileFormatState';
import useFrameCapture from './hooks/useFrameCapture';
import UserSettingsContext from './contexts/UserSettingsContext';
@ -48,7 +49,6 @@ import OutputFormatSelect from './components/OutputFormatSelect';
import { loadMifiLink, runStartupCheck } from './mifi';
import { controlsBackground } from './colors';
import { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } from './capture-frame';
import {
getStreamFps, isCuttingStart, isCuttingEnd,
readFileMeta, getSmarterOutFormat, renderThumbnails as ffmpegRenderThumbnails,
@ -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, captureFrameMethod, captureFrameQuality,
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, captureFrameFileNameFormat,
} = allUserSettings;
useEffect(() => {
@ -489,17 +489,19 @@ const App = memo(() => {
const getFrameCount = useCallback((sec) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
const formatTimecode = useCallback(({ seconds, shorten }) => {
const formatTimecode = useCallback(({ seconds, shorten, fileNameFriendly }) => {
if (timecodeFormat === 'frameCount') {
const frameCount = getFrameCount(seconds);
return frameCount != null ? frameCount : '';
}
if (timecodeFormat === 'timecodeWithFramesFraction') {
return formatDuration({ seconds, fps: detectedFps, shorten });
return formatDuration({ seconds, fps: detectedFps, shorten, fileNameFriendly });
}
return formatDuration({ seconds, shorten });
return formatDuration({ seconds, shorten, fileNameFriendly });
}, [detectedFps, timecodeFormat, getFrameCount]);
const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ formatTimecode });
const getCurrentTime = useCallback(() => (playing ? videoRef.current.currentTime : commandedTimeRef.current), [playing]);
// const getSafeCutTime = useCallback((cutTime, next) => ffmpeg.getSafeCutTime(neighbouringFrames, cutTime, next), [neighbouringFrames]);
@ -1208,8 +1210,8 @@ const App = memo(() => {
const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template, forceSafeOutputFileName }) => (
segments.map((segment, i) => {
const { start, end, name = '' } = segment;
const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true });
const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true });
const cutFromStr = formatTimecode({ seconds: start, fileNameFriendly: true });
const cutToStr = formatTimecode({ seconds: end, fileNameFriendly: true });
const segNum = i + 1;
const filenamifyOrNot = (fileName) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).substr(0, maxLabelLength);
@ -1230,7 +1232,7 @@ const App = memo(() => {
const generated = generateSegFileName({ template, segSuffix, inputFileNameWithoutExt: fileNameWithoutExt, ext, segNum, segLabel: nameSanitized, cutFrom: cutFromStr, cutTo: cutToStr, tags: tagsSanitized });
return safeOutputFileName ? generated.substring(0, 200) : generated; // If sanitation is enabled, make sure filename is not too long
})
), [segmentsToExport, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName, maxLabelLength]);
), [segmentsToExport, formatTimecode, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName, maxLabelLength]);
const getOutSegError = useCallback((fileNames) => getOutSegErrorRaw({ fileNames, filePath, outputDir }), [outputDir, filePath]);
@ -1419,7 +1421,7 @@ const App = memo(() => {
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
}
}, [filePath, getCurrentTime, usingPreviewFile, captureFrameMethod, customOutDir, captureFormat, enableTransferTimestamps, captureFrameQuality, hideAllNotifications]);
}, [filePath, getCurrentTime, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, enableTransferTimestamps, captureFrameQuality, captureFrameFromTag, hideAllNotifications]);
const extractSegmentFramesAsImages = useCallback(async (index) => {
if (!filePath || detectedFps == null || workingRef.current) return;
@ -1440,7 +1442,7 @@ const App = memo(() => {
setWorking();
setCutProgress();
}
}, [apparentCutSegments, captureFormat, captureFrameFileNameFormat, captureFrameQuality, customOutDir, detectedFps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking]);
}, [apparentCutSegments, captureFormat, captureFrameFileNameFormat, captureFrameQuality, captureFramesRange, customOutDir, detectedFps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking]);
const extractCurrentSegmentFramesAsImages = useCallback(() => extractSegmentFramesAsImages(currentSegIndexSafe), [currentSegIndexSafe, extractSegmentFramesAsImages]);
@ -1922,7 +1924,7 @@ const App = memo(() => {
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
}
}, [addFileAsCoverArt, captureFormat, captureFrameQuality, customOutDir, enableTransferTimestamps, filePath, getCurrentTime, hideAllNotifications]);
}, [addFileAsCoverArt, captureFormat, captureFrameFromFfmpeg, captureFrameQuality, customOutDir, enableTransferTimestamps, filePath, getCurrentTime, hideAllNotifications]);
const batchLoadPaths = useCallback((newPaths, append) => {
setBatchFiles((existingFiles) => {

Wyświetl plik

@ -1,93 +0,0 @@
import dataUriToBuffer from 'data-uri-to-buffer';
import pMap from 'p-map';
import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName, getOutPath, escapeRegExp } from './util';
import { formatDuration } from './util/duration';
import { captureFrame as ffmpegCaptureFrame, captureFrames as ffmpegCaptureFrames } from './ffmpeg';
const fs = window.require('fs-extra');
const mime = window.require('mime-types');
const { rename, readdir } = window.require('fs/promises');
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}`, quality);
return dataUriToBuffer(dataUri);
}
export async function captureFramesRange({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) {
const getSuffix = (prefix) => `${prefix}.${captureFormat}`;
if (!outputTimestamps) {
const numDigits = Math.floor(Math.log10(estimatedMaxNumFiles)) + 1;
const nameTemplateSuffix = getSuffix(`%0${numDigits}d`);
const nameSuffix = getSuffix(`${'1'.padStart(numDigits, '0')}`); // mimic ffmpeg output
const outPathTemplate = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: nameTemplateSuffix });
const firstFileOutPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix });
await ffmpegCaptureFrames({ from: fromTime, to: toTime, videoPath: filePath, outPathTemplate, quality, filter, onProgress });
return firstFileOutPath;
}
// see https://github.com/mifi/lossless-cut/issues/1139
const tmpSuffix = 'llc-tmp-frame-capture-';
const outPathTemplate = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: getSuffix(`${tmpSuffix}%d`) });
await ffmpegCaptureFrames({ from: fromTime, to: toTime, videoPath: filePath, outPathTemplate, quality, filter, framePts: true, onProgress });
const outDir = getOutDir(customOutDir, filePath);
const files = await readdir(outDir);
// https://github.com/mifi/lossless-cut/issues/1139
const matches = files.map((fileName) => {
const escapedRegexp = escapeRegExp(getSuffixedFileName(filePath, tmpSuffix));
const regexp = `^${escapedRegexp}(\\d+)`;
console.log(regexp);
const match = fileName.match(new RegExp(regexp));
if (!match) return undefined;
const frameNum = parseInt(match[1], 10);
if (Number.isNaN(frameNum) || frameNum < 0) return undefined;
return { fileName, frameNum };
}).filter((it) => it != null);
console.log('Renaming temp files...');
const outPaths = await pMap(matches, async ({ fileName, frameNum }) => {
const duration = formatDuration({ seconds: fromTime + (frameNum / fps), fileNameFriendly: true });
const renameFromPath = getOutPath({ customOutDir, filePath, fileName });
const renameToPath = getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, getSuffix(duration, captureFormat)) });
await rename(renameFromPath, renameToPath);
return renameToPath;
}, { concurrency: 1 });
return outPaths[0];
}
export async function captureFrameFromFfmpeg({ customOutDir, filePath, fromTime, captureFormat, enableTransferTimestamps, quality }) {
const time = formatDuration({ 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);
return outPath;
}
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 });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `${time}.${ext}` });
await fs.writeFile(outPath, buf);
if (enableTransferTimestamps) await transferTimestamps(filePath, outPath, currentTime);
return outPath;
}

Wyświetl plik

@ -0,0 +1,101 @@
import dataUriToBuffer from 'data-uri-to-buffer';
import pMap from 'p-map';
import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName, getOutPath, escapeRegExp } from '../util';
import { captureFrame as ffmpegCaptureFrame, captureFrames as ffmpegCaptureFrames } from '../ffmpeg';
const fs = window.require('fs-extra');
const mime = window.require('mime-types');
const { rename, readdir } = window.require('fs/promises');
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}`, quality);
return dataUriToBuffer(dataUri);
}
export default ({ formatTimecode }) => {
async function captureFramesRange({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) {
const getSuffix = (prefix) => `${prefix}.${captureFormat}`;
if (!outputTimestamps) {
const numDigits = Math.floor(Math.log10(estimatedMaxNumFiles)) + 1;
const nameTemplateSuffix = getSuffix(`%0${numDigits}d`);
const nameSuffix = getSuffix(`${'1'.padStart(numDigits, '0')}`); // mimic ffmpeg output
const outPathTemplate = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: nameTemplateSuffix });
const firstFileOutPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix });
await ffmpegCaptureFrames({ from: fromTime, to: toTime, videoPath: filePath, outPathTemplate, quality, filter, onProgress });
return firstFileOutPath;
}
// see https://github.com/mifi/lossless-cut/issues/1139
const tmpSuffix = 'llc-tmp-frame-capture-';
const outPathTemplate = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: getSuffix(`${tmpSuffix}%d`) });
await ffmpegCaptureFrames({ from: fromTime, to: toTime, videoPath: filePath, outPathTemplate, quality, filter, framePts: true, onProgress });
const outDir = getOutDir(customOutDir, filePath);
const files = await readdir(outDir);
// https://github.com/mifi/lossless-cut/issues/1139
const matches = files.map((fileName) => {
const escapedRegexp = escapeRegExp(getSuffixedFileName(filePath, tmpSuffix));
const regexp = `^${escapedRegexp}(\\d+)`;
console.log(regexp);
const match = fileName.match(new RegExp(regexp));
if (!match) return undefined;
const frameNum = parseInt(match[1], 10);
if (Number.isNaN(frameNum) || frameNum < 0) return undefined;
return { fileName, frameNum };
}).filter((it) => it != null);
console.log('Renaming temp files...');
const outPaths = await pMap(matches, async ({ fileName, frameNum }) => {
const duration = formatTimecode({ seconds: fromTime + (frameNum / fps), fileNameFriendly: true });
const renameFromPath = getOutPath({ customOutDir, filePath, fileName });
const renameToPath = getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, getSuffix(duration, captureFormat)) });
await rename(renameFromPath, renameToPath);
return renameToPath;
}, { concurrency: 1 });
return outPaths[0];
}
async function captureFrameFromFfmpeg({ customOutDir, filePath, fromTime, captureFormat, enableTransferTimestamps, 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);
return outPath;
}
async function captureFrameFromTag({ customOutDir, filePath, currentTime, captureFormat, video, enableTransferTimestamps, quality }) {
const buf = getFrameFromVideo(video, captureFormat, quality);
const ext = mime.extension(buf.type);
const time = formatTimecode({ seconds: currentTime, fileNameFriendly: true });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `${time}.${ext}` });
await fs.writeFile(outPath, buf);
if (enableTransferTimestamps) await transferTimestamps(filePath, outPath, currentTime);
return outPath;
}
return {
captureFramesRange,
captureFrameFromFfmpeg,
captureFrameFromTag,
};
};