refactor away apparentCutSegments

also:
- don't show invert selected segments when inverse cut mode
- fix i18n string
- simplify types
pull/2350/head
Mikael Finstad 1 year ago
parent 250217344f
commit 25021774bb
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -82,7 +82,7 @@ import { askForHtml5ifySpeed } from './dialogs/html5ify';
import { askForOutDir, askForImportChapters, promptTimecode, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, CleanupChoicesType, showOutputNotWritable } from './dialogs';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
import { findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment, getSegmentTags } from './segments';
import { findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, getPlaybackMode, getSegmentTags, getSegApparentStart } from './segments';
import { generateOutSegFileNames as generateOutSegFileNamesRaw, generateMergedFileNames as generateMergedFileNamesRaw, defaultOutSegTemplate, defaultCutMergedFileTemplate } from './util/outputNameTemplate';
import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './util/constants';
import BigWaveform from './components/BigWaveform';
@ -335,7 +335,7 @@ function App() {
}, [isFileOpened]);
const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onMutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex,
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onMutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, getCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, getSegApparentEnd, getApparentCutSegments,
} = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog });
const { getEdlFilePath, getEdlFilePathOld, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments });
@ -343,12 +343,12 @@ function App() {
const { nonCopiedExtraStreams, exportExtraStreams, mainCopiedThumbnailStreams, numStreamsToCopy, toggleStripAudio, toggleStripThumbnail, copyAnyAudioTrack, copyStreamIdsByFile, setCopyStreamIdsByFile, copyFileStreams, mainCopiedStreams, setCopyStreamIdsForPath, toggleCopyStreamId, isCopyingStreamId } = useStreamsMeta({ mainStreams, filePath, autoExportExtraStreams });
const segmentAtCursor = useMemo(() => {
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, commandedTime);
const segmentsAtCursorIndexes = findSegmentsAtCursor(cutSegments, commandedTime);
const firstSegmentAtCursorIndex = segmentsAtCursorIndexes[0];
if (firstSegmentAtCursorIndex == null) return undefined;
return cutSegments[firstSegmentAtCursorIndex];
}, [apparentCutSegments, commandedTime, cutSegments]);
}, [cutSegments, commandedTime]);
const segmentAtCursorRef = useRef<StateSegment>();
useEffect(() => {
@ -371,8 +371,8 @@ function App() {
seekAbs(nextFrame / fps);
}, [detectedFps, seekAbs, videoRef]);
const jumpSegStart = useCallback((index: number) => seekAbs(apparentCutSegments[index]!.start), [apparentCutSegments, seekAbs]);
const jumpSegEnd = useCallback((index: number) => seekAbs(apparentCutSegments[index]!.end), [apparentCutSegments, seekAbs]);
const jumpSegStart = useCallback((index: number) => seekAbs(getSegApparentStart(cutSegments[index]!)), [cutSegments, seekAbs]);
const jumpSegEnd = useCallback((index: number) => seekAbs(getSegApparentEnd(cutSegments[index]!)), [cutSegments, getSegApparentEnd, seekAbs]);
const jumpCutStart = useCallback(() => jumpSegStart(currentSegIndexSafe), [currentSegIndexSafe, jumpSegStart]);
const jumpCutEnd = useCallback(() => jumpSegEnd(currentSegIndexSafe), [currentSegIndexSafe, jumpSegEnd]);
const jumpTimelineStart = useCallback(() => seekAbs(0), [seekAbs]);
@ -700,21 +700,21 @@ function App() {
if (playbackModeRef.current != null) {
const selectedSegmentAtCursor = selectedSegments.find((selectedSegment) => selectedSegment.segId === segmentAtCursorRef.current?.segId);
const isSomeSegmentAtCursor = selectedSegmentAtCursor != null && commandedTimeRef.current != null && selectedSegmentAtCursor.end - commandedTimeRef.current > 0.1;
const isSomeSegmentAtCursor = selectedSegmentAtCursor != null && commandedTimeRef.current != null && getSegApparentEnd(selectedSegmentAtCursor) - commandedTimeRef.current > 0.1;
if (!isSomeSegmentAtCursor) { // if a segment is already at cursor, don't do anything
if (playbackModeRef.current === 'loop-selected-segments') {
const firstSelectedSegment = selectedSegments[0];
if (firstSelectedSegment == null) throw new Error();
const index = apparentCutSegments.indexOf(firstSelectedSegment);
const index = cutSegments.indexOf(firstSelectedSegment);
if (index >= 0) setCurrentSegIndex(index);
seekAbs(firstSelectedSegment.start);
} else {
seekAbs(currentApparentCutSeg.start);
seekAbs(getSegApparentStart(currentCutSeg));
}
}
}
play(resetPlaybackRate);
}, [playbackModeRef, playingRef, play, pause, selectedSegments, commandedTimeRef, apparentCutSegments, setCurrentSegIndex, seekAbs, currentApparentCutSeg.start]);
}, [playbackModeRef, playingRef, play, pause, selectedSegments, commandedTimeRef, getSegApparentEnd, cutSegments, setCurrentSegIndex, seekAbs, currentCutSeg]);
const onTimeUpdate = useCallback<ReactEventHandler<HTMLVideoElement>>((e) => {
const { currentTime } = e.currentTarget;
@ -723,28 +723,30 @@ function App() {
const playbackMode = playbackModeRef.current;
if (playbackMode != null && segmentAtCursorRef.current != null) { // todo and is currently playing?
const playingSegment = getApparentCutSegmentById(segmentAtCursorRef.current.segId);
const playingSegment = getCutSegmentById(segmentAtCursorRef.current.segId);
if (playingSegment != null) {
const nextAction = playOnlyCurrentSegment({ playbackMode, currentTime, playingSegment });
// console.log(nextAction);
if (nextAction.nextSegment) {
const index = selectedSegments.indexOf(playingSegment);
let newIndex = getNewJumpIndex(index >= 0 ? index : 0, 1);
if (newIndex > selectedSegments.length - 1) newIndex = 0; // have reached end of last segment, start over
const nextSelectedSegment = selectedSegments[newIndex];
if (nextSelectedSegment != null) seekAbs(nextSelectedSegment.start);
}
if (nextAction.seekTo != null) {
seekAbs(nextAction.seekTo);
}
if (nextAction.exit) {
playbackModeRef.current = undefined;
pause();
const nextAction = getPlaybackMode({ playbackMode, currentTime, playingSegment: { start: getSegApparentStart(playingSegment), end: getSegApparentEnd(playingSegment) } });
if (nextAction != null) {
console.log(nextAction);
if (nextAction.nextSegment) {
const index = selectedSegments.indexOf(playingSegment);
let newIndex = getNewJumpIndex(index >= 0 ? index : 0, 1);
if (newIndex > selectedSegments.length - 1) newIndex = 0; // have reached end of last segment, start over
const nextSelectedSegment = selectedSegments[newIndex];
if (nextSelectedSegment != null) seekAbs(nextSelectedSegment.start);
}
if (nextAction.seekTo != null) {
seekAbs(nextAction.seekTo);
}
if (nextAction.exit) {
playbackModeRef.current = undefined;
pause();
}
}
}
}
}, [getApparentCutSegmentById, pause, playbackModeRef, playerTime, seekAbs, selectedSegments, setPlayerTime]);
}, [getCutSegmentById, getSegApparentEnd, pause, playbackModeRef, playerTime, seekAbs, selectedSegments, setPlayerTime]);
const closeFileWithConfirm = useCallback(() => {
if (!isFileOpened || workingRef.current) return;
@ -992,7 +994,7 @@ function App() {
// Special segments-to-chapters mode:
let chaptersToAdd: Chapter[] | undefined;
if (segmentsToChaptersOnly) {
const sortedSegments = sortSegments(selectedSegmentsOrInverse);
const sortedSegments = sortSegments(getApparentCutSegments(selectedSegmentsOrInverse));
if (hasAnySegmentOverlap(sortedSegments)) {
errorToast(i18n.t('Make sure you have no overlapping segments.'));
return;
@ -1138,7 +1140,7 @@ function App() {
setWorking(undefined);
setProgress(undefined);
}
}, [filePath, numStreamsToCopy, segmentsToExport, haveInvalidSegs, workingRef, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, cutMultiple, outputDir, customOutDir, fileFormat, duration, 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, duration, 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, getApparentCutSegments, selectedSegmentsOrInverse, t, mergedFileTemplateOrDefault, segmentsToChapters, invertCutSegments, generateMergedFileNames, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, extractStreams, showOsNotification, handleExportFailed]);
const onExportPress = useCallback(async () => {
if (!filePath) return;
@ -1169,7 +1171,7 @@ function App() {
const extractSegmentFramesAsImages = useCallback(async (segIds: string[]) => {
if (!filePath || detectedFps == null || workingRef.current) return;
const segments = apparentCutSegments.filter((seg) => segIds.includes(seg.segId));
const segments = getApparentCutSegments(cutSegments.filter((seg) => segIds.includes(seg.segId)));
const segmentsNumFrames = segments.reduce((acc, { start, end }) => acc + (getFrameCount(end - start) ?? 0), 0);
const captureFramesResponse = await askExtractFramesAsImages({ segmentsNumFrames, plural: segments.length > 1, fps: detectedFps });
if (captureFramesResponse == null) return;
@ -1207,7 +1209,7 @@ function App() {
setWorking(undefined);
setProgress(undefined);
}
}, [apparentCutSegments, captureFormat, captureFrameFileNameFormat, captureFrameQuality, captureFramesRange, customOutDir, detectedFps, filePath, getFrameCount, hideAllNotifications, outputDir, setWorking, showOsNotification, workingRef]);
}, [filePath, detectedFps, workingRef, getApparentCutSegments, cutSegments, getFrameCount, setWorking, hideAllNotifications, captureFramesRange, customOutDir, captureFormat, captureFrameQuality, captureFrameFileNameFormat, showOsNotification, outputDir]);
const extractCurrentSegmentFramesAsImages = useCallback(() => extractSegmentFramesAsImages([currentCutSeg?.segId]), [currentCutSeg?.segId, extractSegmentFramesAsImages]);
const extractSelectedSegmentsFramesAsImages = useCallback(() => extractSegmentFramesAsImages(selectedSegments.map((seg) => seg.segId)), [extractSegmentFramesAsImages, selectedSegments]);
@ -1863,10 +1865,10 @@ function App() {
const onEditSegmentTags = useCallback((index: number) => {
setEditingSegmentTagsSegmentIndex(index);
const seg = apparentCutSegments[index];
if (seg == null) throw new Error();
const seg = cutSegments[index];
invariant(seg != null);
setEditingSegmentTags(getSegmentTags(seg));
}, [apparentCutSegments]);
}, [cutSegments]);
const editCurrentSegmentTags = useCallback(() => {
onEditSegmentTags(currentSegIndexSafe);
@ -1895,7 +1897,7 @@ function App() {
const mainActions = useMemo(() => {
async function exportYouTube() {
if (!checkFileOpened()) return;
await openYouTubeChaptersDialog(formatYouTube(apparentCutSegments));
await openYouTubeChaptersDialog(formatYouTube(getApparentCutSegments(cutSegments)));
}
function seekReset() {
@ -2037,7 +2039,7 @@ function App() {
};
return ret;
}, [toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, increaseRotation, goToTimecode, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripThumbnail, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, checkFileOpened, apparentCutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, setCurrentSegIndex, cutSegments.length, zoomRel, batchFileJump, removeCutSegment, currentSegIndexSafe, cutSegmentsHistory, onLabelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, setPlaybackVolume, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges]);
}, [toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, increaseRotation, goToTimecode, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripThumbnail, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, checkFileOpened, getApparentCutSegments, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, setCurrentSegIndex, zoomRel, batchFileJump, removeCutSegment, currentSegIndexSafe, cutSegmentsHistory, onLabelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, setPlaybackVolume, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges]);
const getKeyboardAction = useCallback((action: MainKeyboardAction) => mainActions[action], [mainActions]);
@ -2477,7 +2479,8 @@ function App() {
<SegmentList
width={rightBarWidth}
currentSegIndex={currentSegIndexSafe}
apparentCutSegments={apparentCutSegments}
cutSegments={cutSegments}
getSegApparentEnd={getSegApparentEnd}
inverseCutSegments={inverseCutSegments}
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
@ -2535,7 +2538,7 @@ function App() {
zoom={zoom}
seekAbs={seekAbs}
durationSafe={durationSafe}
apparentCutSegments={apparentCutSegments}
cutSegments={cutSegments}
setCurrentSegIndex={setCurrentSegIndex}
currentSegIndexSafe={currentSegIndexSafe}
inverseCutSegments={inverseCutSegments}
@ -2550,6 +2553,7 @@ function App() {
goToTimecode={goToTimecode}
isSegmentSelected={isSegmentSelected}
darkMode={darkMode}
getSegApparentEnd={getSegApparentEnd}
/>
<BottomBar
@ -2579,7 +2583,7 @@ function App() {
jumpTimelineEnd={jumpTimelineEnd}
startTimeOffset={startTimeOffset}
setCutTime={setCutTime}
currentApparentCutSeg={currentApparentCutSeg}
getSegApparentEnd={getSegApparentEnd}
playing={playing}
shortStep={shortStep}
seekClosestKeyframe={seekClosestKeyframe}

@ -23,8 +23,10 @@ import { useSegColors } from './contexts';
import { isExactDurationMatch } from './util/duration';
import useUserSettings from './hooks/useUserSettings';
import { askForPlaybackRate } from './dialogs';
import { ApparentCutSegment, FormatTimecode, ParseTimecode, SegmentToExport, StateSegment } from './types';
import { FormatTimecode, ParseTimecode, SegmentBase, SegmentColorIndex, SegmentToExport, StateSegment } from './types';
import { WaveformMode } from '../../../types';
import { GetSegApparentEnd } from './hooks/useSegments';
import { getSegApparentStart } from './segments';
const { clipboard } = window.require('electron');
@ -74,7 +76,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see
startTimeOffset: number,
seekAbs: (a: number) => void,
currentCutSeg: StateSegment,
currentApparentCutSeg: ApparentCutSegment,
currentApparentCutSeg: SegmentBase,
isStart?: boolean,
formatTimecode: FormatTimecode,
parseTimecode: ParseTimecode,
@ -187,13 +189,14 @@ function BottomBar({
captureSnapshot, onExportPress, segmentsToExport, hasVideo,
seekAbs, currentSegIndexSafe, cutSegments, currentCutSeg, setCutStart, setCutEnd,
setCurrentSegIndex,
jumpTimelineStart, jumpTimelineEnd, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime, currentApparentCutSeg,
jumpTimelineStart, jumpTimelineEnd, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime,
playing, shortStep, togglePlay, toggleLoopSelectedSegments, hasAudio,
keyframesEnabled, toggleShowKeyframes, seekClosestKeyframe, detectedFps, isFileOpened, selectedSegments,
darkMode, setDarkMode,
toggleShowThumbnails, toggleWaveformMode, waveformMode, showThumbnails,
outputPlaybackRate, setOutputPlaybackRate,
formatTimecode, parseTimecode, playbackRate,
getSegApparentEnd,
}: {
zoom: number,
setZoom: (fn: (z: number) => number) => void,
@ -220,7 +223,6 @@ function BottomBar({
jumpCutStart: () => void,
startTimeOffset: number,
setCutTime: (type: 'start' | 'end', v: number) => void,
currentApparentCutSeg: ApparentCutSegment,
playing: boolean,
shortStep: (a: number) => void,
togglePlay: () => void,
@ -231,7 +233,7 @@ function BottomBar({
seekClosestKeyframe: (a: number) => void,
detectedFps: number | undefined,
isFileOpened: boolean,
selectedSegments: ApparentCutSegment[],
selectedSegments: SegmentColorIndex[],
darkMode: boolean,
setDarkMode: Dispatch<SetStateAction<boolean>>,
toggleShowThumbnails: () => void,
@ -243,6 +245,7 @@ function BottomBar({
formatTimecode: FormatTimecode,
parseTimecode: ParseTimecode,
playbackRate: number,
getSegApparentEnd: GetSegApparentEnd,
}) {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
@ -378,7 +381,7 @@ function BottomBar({
<SetCutpointButton currentCutSeg={currentCutSeg} side="start" onClick={setCutStart} title={t('Start current segment at current time')} style={{ marginRight: 5 }} />
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.start} setCutTime={setCutTime} isStart formatTimecode={formatTimecode} parseTimecode={parseTimecode} />}
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={getSegApparentStart(currentCutSeg)} setCutTime={setCutTime} isStart formatTimecode={formatTimecode} parseTimecode={parseTimecode} />}
<IoMdKey
size={25}
@ -423,7 +426,7 @@ function BottomBar({
onClick={() => seekClosestKeyframe(1)}
/>
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.end} setCutTime={setCutTime} formatTimecode={formatTimecode} parseTimecode={parseTimecode} />}
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={getSegApparentEnd(currentCutSeg)} setCutTime={setCutTime} formatTimecode={formatTimecode} parseTimecode={parseTimecode} />}
<SetCutpointButton currentCutSeg={currentCutSeg} side="end" onClick={setCutEnd} title={t('End current segment at current time')} style={{ marginLeft: 5 }} />

@ -15,9 +15,9 @@ import useUserSettings from './hooks/useUserSettings';
import { saveColor, controlsBackground, primaryTextColor, darkModeTransition } from './colors';
import { useSegColors } from './contexts';
import { mySpring } from './animations';
import { getSegmentTags } from './segments';
import { getSegApparentStart, getSegmentTags } from './segments';
import TagEditor from './components/TagEditor';
import { ApparentCutSegment, ContextMenuTemplate, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentTags, StateSegment } from './types';
import { ContextMenuTemplate, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentTags, StateSegment } from './types';
import { UseSegments } from './hooks/useSegments';
const buttonBaseStyle = {
@ -55,8 +55,9 @@ const Segment = memo(({
onExtractSegmentFramesAsImages,
onInvertSelectedSegments,
onDuplicateSegmentClick,
getSegApparentEnd,
}: {
seg: ApparentCutSegment | InverseCutSegment,
seg: StateSegment | InverseCutSegment,
index: number,
currentSegIndex: number,
formatTimecode: FormatTimecode,
@ -83,6 +84,7 @@ const Segment = memo(({
onExtractSegmentFramesAsImages: (segIds: string[]) => Promise<void>,
onInvertSelectedSegments: UseSegments['invertSelectedSegments'],
onDuplicateSegmentClick: UseSegments['duplicateSegment'],
getSegApparentEnd: UseSegments['getSegApparentEnd'],
}) => {
const { invertCutSegments, darkMode } = useUserSettings();
const { t } = useTranslation();
@ -136,7 +138,7 @@ const Segment = memo(({
useContextMenu(ref, contextMenuTemplate);
const duration = seg.end - seg.start;
const duration = useMemo(() => getSegApparentEnd(seg) - getSegApparentStart(seg), [getSegApparentEnd, seg]);
const durationMs = duration * 1000;
const isActive = !invertCutSegments && currentSegIndex === index;
@ -156,7 +158,7 @@ const Segment = memo(({
return <b style={{ cursor: 'grab', color: 'white', padding: '0 4px', marginRight: 3, marginLeft: -3, background: color.string(), border: `1px solid ${isActive ? borderColor.string() : 'transparent'}`, borderRadius: 10, fontSize: 12 }}>{index + 1}</b>;
}
const timeStr = useMemo(() => `${formatTimecode({ seconds: seg.start })} - ${formatTimecode({ seconds: seg.end })}`, [seg.start, seg.end, formatTimecode]);
const timeStr = useMemo(() => `${formatTimecode({ seconds: getSegApparentStart(seg) })} - ${formatTimecode({ seconds: getSegApparentEnd(seg) })}`, [formatTimecode, seg, getSegApparentEnd]);
const onDoubleClick = useCallback(() => {
if (invertCutSegments) return;
@ -225,7 +227,7 @@ const Segment = memo(({
function SegmentList({
width,
formatTimecode,
apparentCutSegments,
cutSegments,
inverseCutSegments,
getFrameCount,
onSegClick,
@ -261,10 +263,11 @@ function SegmentList({
setEditingSegmentTags,
setEditingSegmentTagsSegmentIndex,
onEditSegmentTags,
getSegApparentEnd,
}: {
width: number,
formatTimecode: FormatTimecode,
apparentCutSegments: ApparentCutSegment[],
cutSegments: StateSegment[],
inverseCutSegments: InverseCutSegment[],
getFrameCount: GetFrameCount,
onSegClick: (index: number) => void,
@ -300,39 +303,40 @@ function SegmentList({
setEditingSegmentTags: Dispatch<SetStateAction<SegmentTags | undefined>>,
setEditingSegmentTagsSegmentIndex: Dispatch<SetStateAction<number | undefined>>,
onEditSegmentTags: (index: number) => void,
getSegApparentEnd: UseSegments['getSegApparentEnd'],
}) {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
const { invertCutSegments, simpleMode, darkMode } = useUserSettings();
const segments: (InverseCutSegment | ApparentCutSegment)[] = invertCutSegments ? inverseCutSegments : apparentCutSegments;
const segmentsOrInverse: (InverseCutSegment | StateSegment)[] = invertCutSegments ? inverseCutSegments : cutSegments;
const sortableList = useMemo(() => segments.map((seg) => ({ id: seg.segId, seg })), [segments]);
const sortableList = useMemo(() => segmentsOrInverse.map((seg) => ({ id: seg.segId, seg })), [segmentsOrInverse]);
const setSortableList = useCallback((newList: typeof sortableList) => {
if (isEqual(segments.map((s) => s.segId), newList.map((l) => l.id))) return; // No change
if (isEqual(segmentsOrInverse.map((s) => s.segId), newList.map((l) => l.id))) return; // No change
updateSegOrders(newList.map((list) => list.id));
}, [segments, updateSegOrders]);
}, [segmentsOrInverse, updateSegOrders]);
let header: ReactNode = t('Segments to export:');
if (segments.length === 0) {
if (segmentsOrInverse.length === 0) {
header = invertCutSegments ? (
<Trans>You have enabled the &quot;invert segments&quot; mode <FaYinYang style={{ verticalAlign: 'middle' }} /> which will cut away selected segments instead of keeping them. But there is no space between any segments, or at least two segments are overlapping. This would not produce any output. Either make room between segments or click the Yinyang <FaYinYang style={{ verticalAlign: 'middle', color: primaryTextColor }} /> symbol below to disable this mode. Alternatively you may combine overlapping segments from the menu.</Trans>
) : t('No segments to export.');
}
const onReorderSegs = useCallback(async (index: number) => {
if (apparentCutSegments.length < 2) return;
if (cutSegments.length < 2) return;
const { value } = await Swal.fire({
title: `${t('Change order of segment')} ${index + 1}`,
text: `Please enter a number from 1 to ${apparentCutSegments.length} to be the new order for the current segment`,
text: t('Please enter a number from 1 to {{n}} to be the new order for the current segment', { n: cutSegments.length}),
input: 'text',
inputValue: index + 1,
showCancelButton: true,
inputValidator: (v) => {
const parsed = parseInt(v, 10);
return Number.isNaN(parsed) || parsed > apparentCutSegments.length || parsed < 1 ? t('Invalid number entered') : undefined;
return Number.isNaN(parsed) || parsed > cutSegments.length || parsed < 1 ? t('Invalid number entered') : undefined;
},
});
@ -340,14 +344,14 @@ function SegmentList({
const newOrder = parseInt(value, 10);
updateSegOrder(index, newOrder - 1);
}
}, [apparentCutSegments.length, t, updateSegOrder]);
}, [cutSegments.length, t, updateSegOrder]);
function renderFooter() {
const getButtonColor = (seg: StateSegment | undefined) => getSegColor(seg).desaturate(0.3).lightness(darkMode ? 45 : 55).string();
const currentSegColor = getButtonColor(currentCutSeg);
const segAtCursorColor = getButtonColor(segmentAtCursor);
const segmentsTotal = selectedSegments.reduce((acc, { start, end }) => (end - start) + acc, 0);
const segmentsTotal = selectedSegments.reduce((acc, seg) => (getSegApparentEnd(seg) - getSegApparentStart(seg)) + acc, 0);
return (
<>
@ -362,7 +366,7 @@ function SegmentList({
<FaMinus
size={24}
style={{ ...buttonBaseStyle, background: apparentCutSegments.length >= 2 ? currentSegColor : neutralButtonColor }}
style={{ ...buttonBaseStyle, background: cutSegments.length >= 2 ? currentSegColor : neutralButtonColor }}
role="button"
title={`${t('Remove segment')} ${currentSegIndex + 1}`}
onClick={() => removeCutSegment(currentSegIndex)}
@ -396,13 +400,15 @@ function SegmentList({
onClick={splitCurrentSegment}
/>
<FaRegCheckCircle
size={22}
title={t('Invert selected segments')}
role="button"
style={{ ...buttonBaseStyle, padding: 1, background: segmentAtCursor ? segAtCursorColor : neutralButtonColor }}
onClick={onInvertSelectedSegments}
/>
{!invertCutSegments && (
<FaRegCheckCircle
size={22}
title={t('Invert selected segments')}
role="button"
style={{ ...buttonBaseStyle, padding: 1, background: segmentAtCursor ? segAtCursorColor : neutralButtonColor }}
onClick={onInvertSelectedSegments}
/>
)}
</div>
<div style={{ padding: '5px 10px', boxSizing: 'border-box', borderBottom: '1px solid var(gray6)', borderTop: '1px solid var(gray6)', display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
@ -507,13 +513,14 @@ function SegmentList({
onLabelSelectedSegments={onLabelSelectedSegments}
onInvertSelectedSegments={onInvertSelectedSegments}
onDuplicateSegmentClick={onDuplicateSegmentClick}
getSegApparentEnd={getSegApparentEnd}
/>
);
})}
</ReactSortable>
</div>
{segments.length > 0 && renderFooter()}
{segmentsOrInverse.length > 0 && renderFooter()}
</motion.div>
</>
);

@ -15,7 +15,8 @@ import styles from './Timeline.module.css';
import { timelineBackground, darkModeTransition } from './colors';
import { Frame } from './ffmpeg';
import { ApparentCutSegment, FormatTimecode, InverseCutSegment, RenderableWaveform, Thumbnail } from './types';
import { FormatTimecode, InverseCutSegment, RenderableWaveform, StateSegment, Thumbnail } from './types';
import { UseSegments } from './hooks/useSegments';
type CalculateTimelinePercent = (time: number) => string | undefined;
@ -88,7 +89,7 @@ function Timeline({
zoom,
neighbouringKeyFrames,
seekAbs,
apparentCutSegments,
cutSegments,
setCurrentSegIndex,
currentSegIndexSafe,
inverseCutSegments,
@ -110,6 +111,7 @@ function Timeline({
goToTimecode,
isSegmentSelected,
darkMode,
getSegApparentEnd,
} : {
durationSafe: number,
startTimeOffset: number,
@ -119,7 +121,7 @@ function Timeline({
zoom: number,
neighbouringKeyFrames: Frame[],
seekAbs: (a: number) => void,
apparentCutSegments: ApparentCutSegment[],
cutSegments: StateSegment[],
setCurrentSegIndex: (a: number) => void,
currentSegIndexSafe: number,
inverseCutSegments: InverseCutSegment[],
@ -141,6 +143,7 @@ function Timeline({
goToTimecode: () => void,
isSegmentSelected: (a: { segId: string }) => boolean,
darkMode: boolean,
getSegApparentEnd: UseSegments['getSegApparentEnd'],
}) {
const { t } = useTranslation();
@ -379,7 +382,7 @@ function Timeline({
style={{ height: timelineHeight, width: `${zoom * 100}%`, position: 'relative', backgroundColor: timelineBackground, transition: darkModeTransition }}
ref={timelineWrapperRef}
>
{apparentCutSegments.map((seg, i) => {
{cutSegments.map((seg, i) => {
if (seg.start === 0 && seg.end === 0) return null; // No video loaded
const selected = invertCutSegments || isSegmentSelected({ segId: seg.segId });
@ -395,6 +398,7 @@ function Timeline({
invertCutSegments={invertCutSegments}
formatTimecode={formatTimecode}
selected={selected}
getSegApparentEnd={getSegApparentEnd}
/>
);
})}

@ -5,13 +5,15 @@ import { FaTrashAlt } from 'react-icons/fa';
import { mySpring } from './animations';
import useUserSettings from './hooks/useUserSettings';
import { useSegColors } from './contexts';
import { ApparentCutSegment, FormatTimecode } from './types';
import { FormatTimecode, StateSegment } from './types';
import { getSegApparentStart } from './segments';
import { UseSegments } from './hooks/useSegments';
function TimelineSeg({
seg, duration, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected,
seg, duration, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected, getSegApparentEnd,
} : {
seg: ApparentCutSegment,
seg: StateSegment,
duration: number,
isActive: boolean,
segNum: number,
@ -19,13 +21,17 @@ function TimelineSeg({
invertCutSegments: boolean,
formatTimecode: FormatTimecode,
selected: boolean,
getSegApparentEnd: UseSegments['getSegApparentEnd'],
}) {
const { darkMode } = useUserSettings();
const { getSegColor } = useSegColors();
const segColor = useMemo(() => getSegColor(seg), [getSegColor, seg]);
const { name, start: cutStart, end: cutEnd } = seg;
const { name } = seg;
const cutStart = useMemo(() => getSegApparentStart(seg), [seg]);
const cutEnd = useMemo(() => getSegApparentEnd(seg), [getSegApparentEnd, seg]);
const cutSectionWidth = `${((cutEnd - cutStart) / duration) * 100}%`;

@ -10,7 +10,6 @@ import ExportModeButton from './components/ExportModeButton';
import { withBlur } from './util';
import { primaryTextColor, controlsBackground, darkModeTransition } from './colors';
import useUserSettings from './hooks/useUserSettings';
import { InverseCutSegment } from './types';
const { stat } = window.require('fs/promises');
@ -42,7 +41,7 @@ function TopMenu({
numStreamsTotal: number,
setStreamsSelectorShown: (v: boolean) => void,
toggleSettings: () => void,
selectedSegments: InverseCutSegment[],
selectedSegments: unknown[],
isCustomFormatSelected: boolean,
clearOutDir: () => void,
}) {

@ -21,7 +21,7 @@ import { toast } from '../swal';
import { isMov as ffmpegIsMov } from '../util/streams';
import useUserSettings from '../hooks/useUserSettings';
import styles from './ExportConfirm.module.css';
import { InverseCutSegment, SegmentToExport } from '../types';
import { SegmentToExport } from '../types';
import { defaultMergedFileTemplate, defaultOutSegTemplate, GenerateOutFileNames } from '../util/outputNameTemplate';
import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs, PreserveMetadata } from '../../../../types';
@ -78,7 +78,7 @@ function ExportConfirm({
outputPlaybackRate,
} : {
areWeCutting: boolean,
selectedSegments: InverseCutSegment[],
selectedSegments: unknown[],
segmentsToExport: SegmentToExport[],
willMerge: boolean,
visible: boolean,
@ -97,7 +97,7 @@ function ExportConfirm({
generateOutSegFileNames: GenerateOutFileNames,
generateMergedFileNames: GenerateOutFileNames,
currentSegIndexSafe: number,
nonFilteredSegmentsOrInverse: InverseCutSegment[],
nonFilteredSegmentsOrInverse: unknown[],
mainCopiedThumbnailStreams: FFprobeStream[],
needSmartCut: boolean,
smartCutBitrate: number | undefined,

@ -67,16 +67,16 @@ describe('parseYouTube', () => {
it('formatYouTube', () => {
expect(formatYouTube([
{ start: 1, end: 2, name: 'Label 🎉' },
{ start: 3, end: 5 },
{ start: 10000, end: 10001, name: '' },
{ start: 1, name: 'Label 🎉' },
{ start: 3 },
{ start: 10000, name: '' },
]).split('\n')).toEqual([
'0:01 Label 🎉',
'0:03',
'2:46:40',
]);
expect(formatYouTube([
{ start: 0, end: 100 },
{ start: 0 },
]).split('\n')).toEqual([
'0:00',
]);

@ -10,7 +10,7 @@ import type { ICueSheet, ITrack } from 'cue-parser/lib/types';
import { formatDuration } from './util/duration';
import { invertSegments, sortSegments } from './segments';
import { GetFrameCount, Segment, SegmentBase } from './types';
import { GetFrameCount, SegmentBase } from './types';
import parseCmx3600 from './cmx3600';
export const getTimeFromFrameNum = (detectedFps: number, frameNum: number) => frameNum / detectedFps;
@ -222,7 +222,7 @@ export function parsePbf(buf: Buffer) {
return undefined;
}).filter(Boolean);
const out: Segment[] = [];
const out: SegmentBase[] = [];
for (let i = 0; i < bookmarks.length;) {
const bookmark = bookmarks[i]!;
@ -316,7 +316,7 @@ export function parseYouTube(str: string) {
return edl.filter((ed) => ed.start !== ed.end);
}
export function formatYouTube(segments: Segment[]) {
export function formatYouTube(segments: { start: number, name?: string }[]) {
return segments.map((segment) => {
const timeStr = formatDuration({ seconds: segment.start, showFraction: false, shorten: true });
const namePart = segment.name ? ` ${segment.name}` : '';
@ -327,13 +327,13 @@ export function formatYouTube(segments: Segment[]) {
// because null/undefined is also valid values (start/end of timeline)
const safeFormatDuration = (duration: number | undefined) => (duration != null ? formatDuration({ seconds: duration }) : '');
export const formatSegmentsTimes = (cutSegments: Segment[]) => cutSegments.map(({ start, end, name }) => [
export const formatSegmentsTimes = (cutSegments: SegmentBase[]) => cutSegments.map(({ start, end, name }) => [
safeFormatDuration(start),
safeFormatDuration(end),
name,
]);
export async function formatCsvFrames({ cutSegments, getFrameCount }: { cutSegments: Segment[], getFrameCount: GetFrameCount }) {
export async function formatCsvFrames({ cutSegments, getFrameCount }: { cutSegments: SegmentBase[], getFrameCount: GetFrameCount }) {
const safeFormatFrameCount = (seconds: number | undefined) => (seconds != null ? getFrameCount(seconds) : '');
const formatted = cutSegments.map(({ start, end, name }) => [
@ -345,16 +345,16 @@ export async function formatCsvFrames({ cutSegments, getFrameCount }: { cutSegme
return csvStringify(formatted);
}
export async function formatCsvSeconds(cutSegments: Segment[]) {
export async function formatCsvSeconds(cutSegments: SegmentBase[]) {
const rows = cutSegments.map(({ start, end, name }) => [start, end, name]);
return csvStringify(rows);
}
export async function formatCsvHuman(cutSegments: Segment[]) {
export async function formatCsvHuman(cutSegments: SegmentBase[]) {
return csvStringify(formatSegmentsTimes(cutSegments));
}
export async function formatTsv(cutSegments: Segment[]) {
export async function formatTsv(cutSegments: SegmentBase[]) {
return csvStringify(formatSegmentsTimes(cutSegments), { delimiter: '\t' });
}
@ -445,7 +445,7 @@ export function parseSrtToSegments(text: string) {
}));
}
export function formatSrt(segments: Segment[]) {
export function formatSrt(segments: SegmentBase[]) {
return segments.reduce((acc, segment, index) => `${acc}${index > 0 ? '\r\n' : ''}${index + 1}\r\n${formatDuration({ seconds: segment.start }).replaceAll('.', ',')} --> ${formatDuration({ seconds: segment.end }).replaceAll('.', ',')}\r\n${segment.name || '-'}\r\n`, '');
}

@ -5,7 +5,7 @@ import invariant from 'tiny-invariant';
import { parseSrtToSegments, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parseCutlist, parsePbf, parseEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats';
import { askForYouTubeInput, showOpenDialog } from './dialogs';
import { getOutPath } from './util';
import { EdlExportType, EdlFileType, EdlImportType, GetFrameCount, Segment, StateSegment } from './types';
import { EdlExportType, EdlFileType, EdlImportType, GetFrameCount, SegmentBase, StateSegment } from './types';
const { readFile, writeFile } = window.require('fs/promises');
const cueParser = window.require('cue-parser');
@ -54,27 +54,27 @@ async function loadSrt(path: string) {
return parseSrtToSegments(await readFile(path, 'utf8'));
}
export async function saveCsv(path: string, cutSegments: Segment[]) {
export async function saveCsv(path: string, cutSegments: SegmentBase[]) {
await writeFile(path, await formatCsvSeconds(cutSegments));
}
export async function saveCsvHuman(path: string, cutSegments: Segment[]) {
export async function saveCsvHuman(path: string, cutSegments: SegmentBase[]) {
await writeFile(path, await formatCsvHuman(cutSegments));
}
export async function saveCsvFrames({ path, cutSegments, getFrameCount }: {
path: string,
cutSegments: Segment[],
cutSegments: SegmentBase[],
getFrameCount: GetFrameCount,
}) {
await writeFile(path, await formatCsvFrames({ cutSegments, getFrameCount }));
}
export async function saveTsv(path: string, cutSegments: Segment[]) {
export async function saveTsv(path: string, cutSegments: SegmentBase[]) {
await writeFile(path, await formatTsv(cutSegments));
}
export async function saveSrt(path: string, cutSegments: Segment[]) {
export async function saveSrt(path: string, cutSegments: SegmentBase[]) {
await writeFile(path, formatSrt(cutSegments));
}

@ -13,7 +13,7 @@ import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegmen
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments';
import { parameters as allFfmpegParameters, FfmpegDialog } from '../ffmpegParameters';
import { maxSegmentsAllowed } from '../util/constants';
import { ApparentCutSegment, ParseTimecode, SegmentBase, segmentTagsSchema, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
import { ParseTimecode, SegmentBase, segmentTagsSchema, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
import safeishEval from '../worker/eval';
import { ScopeSegment } from '../../../../types';
import { FFprobeStream } from '../../../../ffprobe';
@ -142,18 +142,15 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
return { index, label: seg.name, start, end, duration: end - start, tags: { ...seg.tags } };
}, [getSegApparentEnd]);
const getApparentCutSegments = useCallback((segments: StateSegment[]) => segments.map((cutSegment) => ({
const getApparentCutSegments = useCallback(<T extends SegmentBase>(segments: T[]) => segments.map((cutSegment) => ({
...cutSegment,
start: getSegApparentStart(cutSegment),
end: getSegApparentEnd(cutSegment),
})), [getSegApparentEnd]);
// These are segments guaranteed to have a start and end time
const apparentCutSegments = useMemo(() => getApparentCutSegments(cutSegments), [cutSegments, getApparentCutSegments]);
const getCutSegmentById = useCallback((id: string) => cutSegments.find((s) => s.segId === id), [cutSegments]);
const getApparentCutSegmentById = useCallback((id: string) => apparentCutSegments.find((s) => s.segId === id), [apparentCutSegments]);
const haveInvalidSegs = useMemo(() => apparentCutSegments.some((cutSegment) => cutSegment.start >= cutSegment.end), [apparentCutSegments]);
const haveInvalidSegs = useMemo(() => cutSegments.some((cutSegment) => getSegApparentStart(cutSegment) >= getSegApparentEnd(cutSegment)), [cutSegments, getSegApparentEnd]);
const currentSegIndexSafe = Math.min(currentSegIndex, cutSegments.length - 1);
const currentCutSeg = useMemo(() => {
@ -161,13 +158,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
if (ret == null) throw new Error('currentCutSeg was nullish, this shouldn\'t happen');
return ret;
}, [currentSegIndexSafe, cutSegments]);
const currentApparentCutSeg = useMemo(() => {
const ret = apparentCutSegments[currentSegIndexSafe];
if (ret == null) throw new Error('currentApparentCutSeg was nullish, this shouldn\'t happen');
return ret;
}, [apparentCutSegments, currentSegIndexSafe]);
const selectedSegmentsRaw = useMemo(() => apparentCutSegments.filter((segment) => isSegmentSelected(segment)), [apparentCutSegments, isSegmentSelected]);
const selectedSegmentsRaw = useMemo(() => cutSegments.filter((segment) => isSegmentSelected(segment)), [cutSegments, isSegmentSelected]);
const getFfmpegParameters = useCallback((key: FfmpegDialog) => {
const parameters = ffmpegParameters[key];
@ -183,8 +175,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
setFfmpegParametersForDialog(dialogType, parameters);
invariant(mode === '1' || mode === '2');
invariant(filePath != null);
await detectSegments({ name: 'blackScenes', workingText: i18n.t('Detecting black scenes'), errorText: i18n.t('Failed to detect black scenes'), fn: async () => blackDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, setProgress, currentApparentCutSeg.start, currentApparentCutSeg.end]);
await detectSegments({ name: 'blackScenes', workingText: i18n.t('Detecting black scenes'), errorText: i18n.t('Failed to detect black scenes'), fn: async () => blackDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, from: getSegApparentStart(currentCutSeg), to: getSegApparentEnd(currentCutSeg) }) });
}, [getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, setProgress, currentCutSeg, getSegApparentEnd]);
const detectSilentScenes = useCallback(async () => {
const dialogType = 'silencedetect';
@ -194,8 +186,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const { mode, ...filterOptions } = parameters;
invariant(mode === '1' || mode === '2');
invariant(filePath != null);
await detectSegments({ name: 'silentScenes', workingText: i18n.t('Detecting silent scenes'), errorText: i18n.t('Failed to detect silent scenes'), fn: async () => silenceDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
await detectSegments({ name: 'silentScenes', workingText: i18n.t('Detecting silent scenes'), errorText: i18n.t('Failed to detect silent scenes'), fn: async () => silenceDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, from: getSegApparentStart(currentCutSeg), to: getSegApparentEnd(currentCutSeg) }) });
}, [currentCutSeg, detectSegments, filePath, getFfmpegParameters, getSegApparentEnd, setFfmpegParametersForDialog, setProgress]);
const detectSceneChanges = useCallback(async () => {
const dialogType = 'sceneChange';
@ -206,16 +198,16 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
// eslint-disable-next-line prefer-destructuring
const minChange = parameters['minChange'];
invariant(minChange != null);
await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async () => ffmpegDetectSceneChanges({ filePath, minChange, onProgress: setProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async () => ffmpegDetectSceneChanges({ filePath, minChange, onProgress: setProgress, from: getSegApparentStart(currentCutSeg), to: getSegApparentEnd(currentCutSeg) }) });
}, [currentCutSeg, detectSegments, filePath, getFfmpegParameters, getSegApparentEnd, setFfmpegParametersForDialog, setProgress]);
const createSegmentsFromKeyframes = useCallback(async () => {
if (!videoStream) return;
invariant(filePath != null);
const keyframes = (await readFrames({ filePath, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end, streamIndex: videoStream.index })).filter((frame) => frame.keyframe);
const keyframes = (await readFrames({ filePath, from: getSegApparentStart(currentCutSeg), to: getSegApparentEnd(currentCutSeg), streamIndex: videoStream.index })).filter((frame) => frame.keyframe);
const newSegments = mapTimesToSegments(keyframes.map((keyframe) => keyframe.time), true);
loadCutSegments(newSegments, true);
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, filePath, loadCutSegments, videoStream]);
}, [currentCutSeg, filePath, getSegApparentEnd, loadCutSegments, videoStream]);
const removeSegments = useCallback((removeSegmentIds: string[]) => {
setCutSegments((existingSegments) => {
@ -239,7 +231,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const inverseCutSegments = useMemo(() => {
if (haveInvalidSegs || !isDurationValid(duration)) return [];
return invertSegments(sortSegments(apparentCutSegments), true, true, duration).map(({ segId, start, end }) => {
return invertSegments(sortSegments(getApparentCutSegments(cutSegments)), true, true, duration).map(({ segId, start, end }) => {
// this is workaround to please TS
if (segId == null || start == null || end == null) throw new Error(`Encountered inverted segment with nullish value ${JSON.stringify({ segId, start, end })}`);
return {
@ -248,7 +240,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
end,
};
});
}, [apparentCutSegments, duration, haveInvalidSegs]);
}, [cutSegments, duration, getApparentCutSegments, haveInvalidSegs]);
const invertAllSegments = useCallback(() => {
if (inverseCutSegments.length === 0) {
@ -316,15 +308,15 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
setCutSegments(safeNewSegments.length > 0 ? safeNewSegments : createInitialCutSegments());
}, [clampValue, createInitialCutSegments, setCutSegments]);
const modifySelectedSegmentTimes = useCallback(async (transformSegment: <T extends ApparentCutSegment>(s: T) => Promise<T> | T, concurrency = 5) => {
const newSegments = await pMap(apparentCutSegments, async (segment) => {
const modifySelectedSegmentTimes = useCallback(async (transformSegment: <T extends { start: number, end: number }>(s: T) => Promise<T> | T, concurrency = 5) => {
const newSegments = await pMap(getApparentCutSegments(cutSegments), async (segment) => {
if (!isSegmentSelected(segment)) return segment; // pass thru non-selected segments
return transformSegment(segment);
}, { concurrency });
safeSetCutSegments(newSegments);
}, [apparentCutSegments, isSegmentSelected, safeSetCutSegments]);
}, [cutSegments, getApparentCutSegments, isSegmentSelected, safeSetCutSegments]);
const shiftAllSegmentTimes = useCallback(async () => {
const shift = await askForShiftSegments({ inputPlaceholder: timecodePlaceholder, parseTimecode });
@ -485,14 +477,14 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const focusSegmentAtCursor = useCallback(() => {
const relevantTime = getRelevantTime();
const [firstSegmentAtCursorIndex] = findSegmentsAtCursor(apparentCutSegments, relevantTime);
const [firstSegmentAtCursorIndex] = findSegmentsAtCursor(getApparentCutSegments(cutSegments), relevantTime);
if (firstSegmentAtCursorIndex == null) return;
setCurrentSegIndex(firstSegmentAtCursorIndex);
}, [apparentCutSegments, getRelevantTime]);
}, [cutSegments, getApparentCutSegments, getRelevantTime]);
const splitCurrentSegment = useCallback(() => {
const relevantTime = getRelevantTime();
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, relevantTime);
const segmentsAtCursorIndexes = findSegmentsAtCursor(getApparentCutSegments(cutSegments), relevantTime);
const firstSegmentAtCursorIndex = segmentsAtCursorIndexes[0];
@ -512,7 +504,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const newSegments = [...cutSegments];
newSegments.splice(firstSegmentAtCursorIndex, 1, firstPart, secondPart);
setCutSegments(newSegments);
}, [apparentCutSegments, createIndexedSegment, cutSegments, getRelevantTime, setCutSegments]);
}, [createIndexedSegment, cutSegments, getApparentCutSegments, getRelevantTime, setCutSegments]);
const createNumSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
@ -637,19 +629,19 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
}, [maxLabelLength, selectedSegmentsRaw, setCutSegments]);
// Guaranteed to have at least one segment (if user has selected none to export (selectedSegments empty), it makes no sense so select all instead.)
const selectedSegments = useMemo(() => (selectedSegmentsRaw.length > 0 ? selectedSegmentsRaw : apparentCutSegments), [apparentCutSegments, selectedSegmentsRaw]);
const selectedSegments = useMemo(() => (selectedSegmentsRaw.length > 0 ? selectedSegmentsRaw : cutSegments), [cutSegments, selectedSegmentsRaw]);
// For invertCutSegments we do not support filtering (selecting) segments
const selectedSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegments), [inverseCutSegments, invertCutSegments, selectedSegments]);
const nonFilteredSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments), [invertCutSegments, inverseCutSegments, apparentCutSegments]);
const selectedSegmentsOrInverse = useMemo<Pick<StateSegment, 'start' | 'end'>[]>(() => (invertCutSegments ? inverseCutSegments : selectedSegments), [inverseCutSegments, invertCutSegments, selectedSegments]);
const nonFilteredSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : cutSegments), [invertCutSegments, inverseCutSegments, cutSegments]);
const segmentsToExport = useMemo<SegmentToExport[]>(() => {
// 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 [{ start: 0, end: getSegApparentEnd({}) }];
return selectedSegmentsOrInverse;
}, [selectedSegmentsOrInverse, getSegApparentEnd, segmentsToChaptersOnly]);
return getApparentCutSegments(selectedSegmentsOrInverse);
}, [segmentsToChaptersOnly, getSegApparentEnd, getApparentCutSegments, selectedSegmentsOrInverse]);
const removeSelectedSegments = useCallback(() => removeSegments(selectedSegmentsRaw.map((seg) => seg.segId)), [removeSegments, selectedSegmentsRaw]);
@ -691,12 +683,12 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
createNumSegments,
createFixedDurationSegments,
createRandomSegments,
apparentCutSegments,
getApparentCutSegmentById,
getCutSegmentById,
getApparentCutSegments,
getSegApparentEnd,
haveInvalidSegs,
currentSegIndexSafe,
currentCutSeg,
currentApparentCutSeg,
inverseCutSegments,
clearSegments,
loadCutSegments,
@ -726,6 +718,8 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
};
}
export type GetSegApparentEnd = (seg: SegmentBase) => number;
export type UseSegments = ReturnType<typeof useSegments>;
export default useSegments;

@ -4,7 +4,7 @@ import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy';
import invariant from 'tiny-invariant';
import { ApparentSegmentBase, PlaybackMode, SegmentBase, SegmentTags, StateSegment } from './types';
import { PlaybackMode, SegmentBase, SegmentTags, StateSegment } from './types';
export const isDurationValid = (duration?: number): duration is number => duration != null && Number.isFinite(duration) && duration > 0;
@ -48,10 +48,10 @@ export const getCleanCutSegments = (cs: Pick<StateSegment, 'start' | 'end' | 'na
tags: seg.tags,
}));
export function findSegmentsAtCursor(apparentSegments: ApparentSegmentBase[], currentTime: number) {
export function findSegmentsAtCursor(segments: SegmentBase[], currentTime: number) {
const indexes: number[] = [];
apparentSegments.forEach((segment, index) => {
if (segment.start <= currentTime && segment.end >= currentTime) indexes.push(index);
segments.forEach((segment, index) => {
if ((segment.start == null || segment.start <= currentTime) && (segment.end == null || segment.end >= currentTime)) indexes.push(index);
});
return indexes;
}
@ -61,10 +61,10 @@ export const getSegmentTags = (segment: { tags?: SegmentTags | undefined }) => (
Object.fromEntries(Object.entries(segment.tags || {}).flatMap(([tag, value]) => (value != null ? [[tag, String(value)]] : [])))
);
export const sortSegments = <T>(segments: T[]) => sortBy(segments, 'start');
export const sortSegments = <T extends { start: number }>(segments: T[]) => sortBy(segments, 'start');
// https://stackoverflow.com/a/30472982/6519037
export function partitionIntoOverlappingRanges<T extends SegmentBase | ApparentSegmentBase>(
export function partitionIntoOverlappingRanges<T extends SegmentBase>(
array: T[],
getSegmentStart = getSegApparentStart,
getSegmentEnd = (seg: T) => {
@ -217,7 +217,11 @@ export function convertSegmentsToChapters(sortedSegments: { start: number, end:
return sortSegments([...sortedSegments, ...invertedSegments]);
}
export function playOnlyCurrentSegment({ playbackMode, currentTime, playingSegment }: { playbackMode: PlaybackMode, currentTime: number, playingSegment: ApparentSegmentBase }) {
export function getPlaybackMode({ playbackMode, currentTime, playingSegment }: {
playbackMode: PlaybackMode,
currentTime: number,
playingSegment: { start: number, end: number },
}) {
switch (playbackMode) {
case 'loop-segment-start-end': {
const maxSec = 3; // max time each side (start/end)
@ -259,7 +263,7 @@ export function playOnlyCurrentSegment({ playbackMode, currentTime, playingSegme
default:
}
return {};
return undefined; // no action
}
export const getNumDigits = (value: number) => Math.floor(value > 0 ? Math.log10(value) : 0) + 1;

@ -10,23 +10,6 @@ export interface ChromiumHTMLAudioElement extends HTMLAudioElement {
audioTracks?: { id: string, enabled: boolean }[]
}
export interface SegmentBase {
start?: number | undefined,
end?: number | undefined,
}
export interface SegmentColorIndex {
segColorIndex: number,
}
export interface ApparentSegmentBase {
start: number,
end: number,
}
export interface ApparentSegmentWithColorIndex extends ApparentSegmentBase, SegmentColorIndex {}
export const openFilesActionArgsSchema = z.tuple([z.string().array()]);
export type OpenFilesActionArgs = z.infer<typeof openFilesActionArgsSchema>
@ -39,19 +22,19 @@ export type SegmentTags = z.infer<typeof segmentTagsSchema>
export type EditingSegmentTags = Record<string, SegmentTags>
export interface StateSegment extends SegmentBase, SegmentColorIndex {
name: string;
segId: string;
tags?: SegmentTags | undefined;
export interface SegmentBase {
start?: number | undefined,
end?: number | undefined,
name?: string | undefined,
}
export interface Segment extends SegmentBase {
name?: string | undefined,
export interface SegmentColorIndex {
segColorIndex: number,
}
export interface ApparentCutSegment extends ApparentSegmentWithColorIndex {
export interface StateSegment extends SegmentBase, SegmentColorIndex {
name: string;
segId: string,
segId: string;
tags?: SegmentTags | undefined;
}
@ -59,7 +42,6 @@ export interface SegmentToExport {
start: number,
end: number,
name?: string | undefined;
segId?: string | undefined;
tags?: SegmentTags | undefined;
}

@ -1,7 +1,11 @@
import padStart from 'lodash/padStart';
export function formatDuration({ seconds: totalSecondsIn, fileNameFriendly, showFraction = true, shorten = false, fps }: {
seconds?: number | undefined, fileNameFriendly?: boolean | undefined, showFraction?: boolean | undefined, shorten?: boolean | undefined, fps?: number | undefined,
seconds?: number | undefined,
fileNameFriendly?: boolean | undefined,
showFraction?: boolean | undefined,
shorten?: boolean | undefined,
fps?: number | undefined,
}) {
const totalSeconds = totalSecondsIn || 0;
const totalSecondsAbs = Math.abs(totalSeconds);

Loading…
Cancel
Save