From ccf3e5ab047c66147a5ed900c458b8f47955f314 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Thu, 13 Mar 2025 21:57:22 +0900 Subject: [PATCH] filter out initial segment from export to prevent cutting when not needed (if duration changes) --- src/renderer/src/App.tsx | 12 +++--- src/renderer/src/SegmentList.tsx | 4 +- src/renderer/src/components/ExportConfirm.tsx | 17 ++++----- src/renderer/src/hooks/useSegments.ts | 38 ++++++++++--------- src/renderer/src/types.ts | 13 ++++--- 5 files changed, 44 insertions(+), 40 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 131253d0..6f8606db 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -337,7 +337,7 @@ function App() { }, [isFileOpened]); const { - cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, + cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, } = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta }); const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments }); @@ -1045,7 +1045,7 @@ function App() { // Special segments-to-chapters mode: let chaptersToAdd: Chapter[] | undefined; if (segmentsToChaptersOnly) { - const sortedSegments = sortSegments(selectedSegmentsOrInverse); + const sortedSegments = sortSegments(segmentsOrInverse.selected); if (hasAnySegmentOverlap(sortedSegments)) { errorToast(i18n.t('Make sure you have no overlapping segments.')); return; @@ -1191,7 +1191,7 @@ function App() { setWorking(undefined); setProgress(undefined); } - }, [filePath, numStreamsToCopy, segmentsToExport, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, fileDuration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, selectedSegmentsOrInverse, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, extractStreams, showOsNotification, handleExportFailed]); + }, [filePath, numStreamsToCopy, segmentsToExport, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, fileDuration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices.cleanupAfterExport, cleanupFilesWithDialog, segmentsOrInverse, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, extractStreams, showOsNotification, handleExportFailed]); const onExportPress = useCallback(async () => { if (!filePath) return; @@ -2440,7 +2440,7 @@ function App() { numStreamsToCopy={numStreamsToCopy} numStreamsTotal={numStreamsTotal} setStreamsSelectorShown={setStreamsSelectorShown} - selectedSegments={selectedSegmentsOrInverse} + selectedSegments={segmentsOrInverse.selected} />
@@ -2559,7 +2559,7 @@ function App() { toggleSegmentsList={toggleSegmentsList} splitCurrentSegment={splitCurrentSegment} isSegmentSelected={isSegmentSelected} - selectedSegments={selectedSegmentsOrInverse} + selectedSegments={segmentsOrInverse.selected} onSelectSingleSegment={selectOnlySegment} onToggleSegmentSelected={toggleSegmentSelected} onDeselectAllSegments={deselectAllSegments} @@ -2677,7 +2677,7 @@ function App() { {/* Dialogs */} - + setStreamsSelectorShown(false)} maxWidth={1000}> {mainStreams && filePath != null && ( diff --git a/src/renderer/src/SegmentList.tsx b/src/renderer/src/SegmentList.tsx index eec6d93d..e6db1066 100644 --- a/src/renderer/src/SegmentList.tsx +++ b/src/renderer/src/SegmentList.tsx @@ -17,7 +17,7 @@ import { useSegColors } from './contexts'; import { mySpring } from './animations'; import { getSegmentTags } from './segments'; import TagEditor from './components/TagEditor'; -import { ContextMenuTemplate, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentBase, SegmentTags, StateSegment } from './types'; +import { ContextMenuTemplate, DefiniteSegmentBase, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentBase, SegmentTags, StateSegment } from './types'; import { UseSegments } from './hooks/useSegments'; @@ -290,7 +290,7 @@ function SegmentList({ segmentAtCursor: StateSegment | undefined, toggleSegmentsList: () => void, splitCurrentSegment: UseSegments['splitCurrentSegment'], - selectedSegments: UseSegments['selectedSegmentsOrInverse'], + selectedSegments: DefiniteSegmentBase[], isSegmentSelected: UseSegments['isSegmentSelected'], onSelectSingleSegment: UseSegments['selectOnlySegment'], onToggleSegmentSelected: UseSegments['toggleSegmentSelected'], diff --git a/src/renderer/src/components/ExportConfirm.tsx b/src/renderer/src/components/ExportConfirm.tsx index 7cd1ab41..b27a5625 100644 --- a/src/renderer/src/components/ExportConfirm.tsx +++ b/src/renderer/src/components/ExportConfirm.tsx @@ -21,11 +21,12 @@ import { toast } from '../swal'; import { isMov as ffmpegIsMov } from '../util/streams'; import useUserSettings from '../hooks/useUserSettings'; import styles from './ExportConfirm.module.css'; -import { SegmentBase, SegmentToExport } from '../types'; +import { SegmentToExport } from '../types'; import { defaultMergedFileTemplate, defaultOutSegTemplate, GenerateOutFileNames } from '../util/outputNameTemplate'; import { FFprobeStream } from '../../../../ffprobe'; import { AvoidNegativeTs, PreserveMetadata } from '../../../../types'; import TextInput from './TextInput'; +import { UseSegments } from '../hooks/useSegments'; const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' }; @@ -52,7 +53,6 @@ function ShiftTimes({ values, num, setNum }: { values: number[], num: number, se function ExportConfirm({ areWeCutting, - selectedSegments, segmentsToExport, willMerge, visible, @@ -71,7 +71,7 @@ function ExportConfirm({ generateOutSegFileNames, generateMergedFileNames, currentSegIndexSafe, - nonFilteredSegmentsOrInverse, + segmentsOrInverse, mainCopiedThumbnailStreams, needSmartCut, smartCutBitrate, @@ -80,7 +80,6 @@ function ExportConfirm({ outputPlaybackRate, } : { areWeCutting: boolean, - selectedSegments: unknown[], segmentsToExport: SegmentToExport[], willMerge: boolean, visible: boolean, @@ -99,7 +98,7 @@ function ExportConfirm({ generateOutSegFileNames: GenerateOutFileNames, generateMergedFileNames: GenerateOutFileNames, currentSegIndexSafe: number, - nonFilteredSegmentsOrInverse: SegmentBase[], + segmentsOrInverse: UseSegments['segmentsOrInverse'], mainCopiedThumbnailStreams: FFprobeStream[], needSmartCut: boolean, smartCutBitrate: number | undefined, @@ -266,10 +265,10 @@ function ExportConfirm({ ))} - {selectedSegments.length !== nonFilteredSegmentsOrInverse.length && ( + {segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && ( - {t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: selectedSegments.length, nonFilteredSegments: nonFilteredSegmentsOrInverse.length })} + {t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: segmentsOrInverse.selected.length, nonFilteredSegments: segmentsOrInverse.all.length })} @@ -277,10 +276,10 @@ function ExportConfirm({ - {selectedSegments.length > 1 ? t('Export mode for {{segments}} segments', { segments: selectedSegments.length }) : t('Export mode')} + {segmentsOrInverse.selected.length > 1 ? t('Export mode for {{segments}} segments', { segments: segmentsOrInverse.selected.length }) : t('Export mode')} - + {effectiveExportMode === 'segments_to_chapters' ? ( diff --git a/src/renderer/src/hooks/useSegments.ts b/src/renderer/src/hooks/useSegments.ts index 5a173d8e..759cdc59 100644 --- a/src/renderer/src/hooks/useSegments.ts +++ b/src/renderer/src/hooks/useSegments.ts @@ -13,7 +13,7 @@ import { createNumSegments as createNumSegmentsDialog, createFixedByteSixedSegme import { createSegment, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, addSegmentColorIndex, filterNonMarkers, makeDurationSegments, isInitialSegment } from '../segments'; import { parameters as allFfmpegParameters, FfmpegDialog } from '../ffmpegParameters'; import { maxSegmentsAllowed } from '../util/constants'; -import { ParseTimecode, SegmentBase, segmentTagsSchema, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types'; +import { DefiniteSegmentBase, ParseTimecode, SegmentBase, segmentTagsSchema, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types'; import safeishEval from '../worker/eval'; import { ScopeSegment } from '../../../../types'; import { FFprobeFormat, FFprobeStream } from '../../../../ffprobe'; @@ -256,7 +256,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea if (haveInvalidSegs || !isDurationValid(fileDuration)) return []; // exclude segments that don't have a length (markers) - const sortedSegments = sortSegments(filterNonMarkers(cutSegments)); + // also exclude initial segment (will cause problems later on) + const sortedSegments = sortSegments(filterNonMarkers(cutSegments).filter((seg) => !seg.initial)); return invertSegments(sortedSegments, true, true, fileDuration).map(({ segId, end, ...rest }) => { // in order to please TS: @@ -701,26 +702,30 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }); }, [createIndexedSegment, setCutSegments]); - const selectedSegmentsOrInverse = useMemo<{ start: number, end: number }[]>(() => { + const segmentsOrInverse = useMemo<{ selected: DefiniteSegmentBase[], all: DefiniteSegmentBase[] }>(() => { // For invertCutSegments we do not support filtering (selecting) segments - if (invertCutSegments) return inverseCutSegments; - - // exclude markers (segments without end) - return filterNonMarkers(selectedSegments); - }, [inverseCutSegments, invertCutSegments, selectedSegments]); + if (invertCutSegments) { + return { + selected: inverseCutSegments, + all: inverseCutSegments, + }; + } - // non filtered includes also non selected - const nonFilteredSegmentsOrInverse = useMemo(() => (invertCutSegments - ? inverseCutSegments - : filterNonMarkers(cutSegments) - ), [invertCutSegments, inverseCutSegments, cutSegments]); + return { + // exclude markers (segments without any end) + // and exclude the initial segment, to prevent cutting when not really needed (if duration changes after the segment was created) + selected: filterNonMarkers(selectedSegments).filter((seg) => !seg.initial), + // `all` includes also all non selected segments: + all: filterNonMarkers(cutSegments).filter((seg) => !seg.initial), + }; + }, [cutSegments, inverseCutSegments, invertCutSegments, selectedSegments]); const segmentsToExport = useMemo(() => { // segmentsToChaptersOnly is a special mode where all segments will be simply written out as chapters to one file: https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037927595 // Chapters export mode: Emulate a single segment with no cuts (full timeline) if (segmentsToChaptersOnly) return []; - return selectedSegmentsOrInverse; - }, [segmentsToChaptersOnly, selectedSegmentsOrInverse]); + return segmentsOrInverse.selected; + }, [segmentsOrInverse.selected, segmentsToChaptersOnly]); const removeSelectedSegments = useCallback(() => removeSegments(selectedSegmentsRaw.map((seg) => seg.segId)), [removeSegments, selectedSegmentsRaw]); @@ -779,8 +784,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea loadCutSegments, isSegmentSelected, selectedSegments, - selectedSegmentsOrInverse, - nonFilteredSegmentsOrInverse, + segmentsOrInverse, segmentsToExport, maybeCreateFullLengthSegment, diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index e477ae87..cce697f0 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -53,6 +53,11 @@ export interface SegmentBase { name?: string | undefined, } +export interface DefiniteSegmentBase { + start: number, + end: number, +} + export interface SegmentColorIndex { segColorIndex: number, } @@ -64,16 +69,12 @@ export interface StateSegment extends SegmentBase, SegmentColorIndex { initial?: true, } -export interface SegmentToExport { - start: number, - end: number, +export interface SegmentToExport extends DefiniteSegmentBase { name?: string | undefined; tags?: SegmentTags | undefined; } -export interface InverseCutSegment { - start: number, - end: number, +export interface InverseCutSegment extends DefiniteSegmentBase { segId: string; }