add notice when exporting without cutting

also refactor
pull/2350/head
Mikael Finstad 1 year ago
parent 250218d93e
commit 25021840ab
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -178,13 +178,13 @@ function App() {
} = allUserSettings;
const { working, setWorking, workingRef, abortWorking } = useLoading();
const { videoRef, videoContainerRef, playbackRate, setPlaybackRate, outputPlaybackRate, setOutputPlaybackRate, commandedTime, seekAbs, playingRef, getRelevantTime, setPlaying, onSeeked, relevantTime, onStartPlaying, setCommandedTime, setCompatPlayerEventId, compatPlayerEventId, setOutputPlaybackRateState, commandedTimeRef, onStopPlaying, onVideoAbort, duration, setDuration, playerTime, setPlayerTime, playbackModeRef, onDurationChange, playing, play, pause, seekRel } = useVideo({ filePath });
const { videoRef, videoContainerRef, playbackRate, setPlaybackRate, outputPlaybackRate, setOutputPlaybackRate, commandedTime, seekAbs, playingRef, getRelevantTime, setPlaying, onSeeked, relevantTime, onStartPlaying, setCommandedTime, setCompatPlayerEventId, compatPlayerEventId, setOutputPlaybackRateState, commandedTimeRef, onStopPlaying, onVideoAbort, fileDuration, setFileDuration, playerTime, setPlayerTime, playbackModeRef, onDurationChange, playing, play, pause, seekRel } = useVideo({ filePath });
const { timecodePlaceholder, formatTimecode, formatTimeAndFrames, parseTimecode, getFrameCount } = useTimecode({ detectedFps, timecodeFormat });
const { loadSubtitle, subtitlesByStreamId, setSubtitlesByStreamId } = useSubtitles();
const durationSafe = isDurationValid(duration) ? duration : 1;
const fileDurationNonZero = isDurationValid(fileDuration) ? fileDuration : 1;
const zoom = Math.floor(zoomUnrounded);
const zoomedDuration = isDurationValid(duration) ? duration / zoom : undefined;
const zoomedDuration = isDurationValid(fileDuration) ? fileDuration / zoom : undefined;
const zoomWindowEndTime = useMemo(() => (zoomedDuration != null ? zoomWindowStartTime + zoomedDuration : undefined), [zoomedDuration, zoomWindowStartTime]);
useEffect(() => setDocumentTitle({ filePath, working: working?.text, progress }), [progress, filePath, working?.text]);
@ -315,7 +315,7 @@ function App() {
if (compatPlayerEnabled) setHideMediaSourcePlayer(false);
}, [compatPlayerEnabled]);
const comfortZoom = isDurationValid(duration) ? Math.max(duration / 100, 1) : undefined;
const comfortZoom = isDurationValid(fileDuration) ? Math.max(fileDuration / 100, 1) : undefined;
const timelineToggleComfortZoom = useCallback(() => {
if (!comfortZoom) return;
@ -336,7 +336,7 @@ function App() {
const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, 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,
} = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, durationSafe, mainFileMeta });
} = 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 });
@ -379,7 +379,7 @@ function App() {
const jumpCutStart = useCallback(() => jumpSegStart(currentSegIndexSafe), [currentSegIndexSafe, jumpSegStart]);
const jumpCutEnd = useCallback(() => jumpSegEnd(currentSegIndexSafe), [currentSegIndexSafe, jumpSegEnd]);
const jumpTimelineStart = useCallback(() => seekAbs(0), [seekAbs]);
const jumpTimelineEnd = useCallback(() => seekAbs(durationSafe), [durationSafe, seekAbs]);
const jumpTimelineEnd = useCallback(() => seekAbs(fileDuration), [fileDuration, seekAbs]);
// const getSafeCutTime = useCallback((cutTime, next) => ffmpeg.getSafeCutTime(neighbouringFrames, cutTime, next), [neighbouringFrames]);
@ -542,7 +542,7 @@ function App() {
const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration);
const { neighbouringKeyFrames, findNearestKeyFrameTime } = useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream: activeVideoStream, detectedFps, ffmpegExtractWindow });
const { waveforms } = useWaveform({ filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, ffmpegExtractWindow, duration });
const { waveforms } = useWaveform({ filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, ffmpegExtractWindow, fileDuration });
const resetState = useCallback(() => {
console.log('State reset');
@ -558,7 +558,7 @@ function App() {
playingRef.current = false;
playbackModeRef.current = undefined;
setCompatPlayerEventId(0);
setDuration(undefined);
setFileDuration(undefined);
cutSegmentsHistory.go(0);
clearSegments();
setFileFormat(undefined);
@ -587,7 +587,7 @@ function App() {
setExportConfirmVisible(false);
setOutputPlaybackRateState(1);
setCurrentFileExportCount(0);
}, [videoRef, setCommandedTime, setPlaybackRate, setPlaying, playingRef, playbackModeRef, setCompatPlayerEventId, setDuration, cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setCopyStreamIdsByFile, setThumbnails, setDeselectedSegmentIds, setSubtitlesByStreamId, setOutputPlaybackRateState]);
}, [videoRef, setCommandedTime, setPlaybackRate, setPlaying, playingRef, playbackModeRef, setCompatPlayerEventId, setFileDuration, cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setCopyStreamIdsByFile, setThumbnails, setDeselectedSegmentIds, setSubtitlesByStreamId, setOutputPlaybackRateState]);
const showUnsupportedFileMessage = useCallback(() => {
@ -598,7 +598,7 @@ function App() {
showNotification({ icon: 'info', text: i18n.t('Loaded existing preview file: {{ fileName }}', { fileName }) });
}, [showNotification]);
const areWeCutting = useMemo(() => segmentsToExport.some(({ start, end }) => isCuttingStart(start) || isCuttingEnd(end, duration)), [duration, segmentsToExport]);
const areWeCutting = useMemo(() => segmentsToExport.some(({ start, end }) => isCuttingStart(start) || isCuttingEnd(end, fileDuration)), [fileDuration, segmentsToExport]);
const needSmartCut = !!(areWeCutting && enableSmartCut);
const {
@ -982,8 +982,8 @@ function App() {
const generateOutSegFileNames = useCallback(async ({ segments = segmentsToExport, template }: { segments?: SegmentToExport[], template: string }) => {
invariant(fileFormat != null && outputDir != null && filePath != null);
return generateOutSegFileNamesRaw({ exportCount, currentFileExportCount, segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding });
}, [currentFileExportCount, exportCount, fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]);
return generateOutSegFileNamesRaw({ fileDuration, exportCount, currentFileExportCount, segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding });
}, [currentFileExportCount, exportCount, fileDuration, fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]);
const generateMergedFileNames = useCallback(async ({ template }: { template: string }) => {
invariant(fileFormat != null && filePath != null);
@ -1002,11 +1002,6 @@ function App() {
return;
}
if (segmentsToExport.length === 0) {
console.warn('Nothing to export');
return;
}
if (haveInvalidSegs) {
errorToast(i18n.t('Start time must be before end time'));
return;
@ -1046,7 +1041,7 @@ function App() {
outputDir,
customOutDir,
outFormat: fileFormat,
videoDuration: duration,
fileDuration,
rotation: isRotationSet ? effectiveRotation : undefined,
copyFileStreams,
allFilesMeta,
@ -1168,7 +1163,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, 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]);
const onExportPress = useCallback(async () => {
if (!filePath) return;
@ -2461,7 +2456,7 @@ function App() {
{filePath != null && compatPlayerEnabled && <MediaSourcePlayer rotate={effectiveRotation} filePath={filePath} videoStream={activeVideoStream} audioStream={activeAudioStream} playerTime={playerTime ?? 0} commandedTime={commandedTime} playing={playing} eventId={compatPlayerEventId} masterVideoRef={videoRef} mediaSourceQuality={mediaSourceQuality} playbackVolume={playbackVolume} />}
</div>
{bigWaveformEnabled && <BigWaveform waveforms={waveforms} relevantTime={relevantTime} playing={playing} durationSafe={durationSafe} zoom={zoomUnrounded} seekRel={seekRel} darkMode={darkMode} />}
{bigWaveformEnabled && <BigWaveform waveforms={waveforms} relevantTime={relevantTime} playing={playing} fileDurationNonZero={fileDurationNonZero} zoom={zoomUnrounded} seekRel={seekRel} darkMode={darkMode} />}
{compatPlayerEnabled && (
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, marginTop: '1em', marginLeft: '1em', color: 'white', opacity: 0.7, display: 'flex', alignItems: 'center', pointerEvents: 'none' }}>
@ -2572,7 +2567,7 @@ function App() {
startTimeOffset={startTimeOffset}
zoom={zoom}
seekAbs={seekAbs}
durationSafe={durationSafe}
fileDurationNonZero={fileDurationNonZero}
cutSegments={cutSegments}
setCurrentSegIndex={setCurrentSegIndex}
currentSegIndexSafe={currentSegIndexSafe}

@ -7,8 +7,13 @@ import { saveColor } from './colors';
import useUserSettings from './hooks/useUserSettings';
function BetweenSegments({ start, end, duration, invertCutSegments }: { start: number, end: number, duration: number, invertCutSegments: boolean }) {
const left = `${(start / duration) * 100}%`;
function BetweenSegments({ start, end, fileDurationNonZero, invertCutSegments }: {
start: number,
end: number,
fileDurationNonZero: number,
invertCutSegments: boolean,
}) {
const left = `${(start / fileDurationNonZero) * 100}%`;
const { effectiveExportMode } = useUserSettings();
@ -28,7 +33,7 @@ function BetweenSegments({ start, end, duration, invertCutSegments }: { start: n
}}
animate={{
left,
width: `${((end - start) / duration) * 100}%`,
width: `${((end - start) / fileDurationNonZero) * 100}%`,
}}
layout
transition={mySpring}

@ -23,19 +23,19 @@ type CalculateTimelinePercent = (time: number) => string | undefined;
const currentTimeWidth = 1;
// eslint-disable-next-line react/display-name
const Waveform = memo(({ waveform, calculateTimelinePercent, durationSafe, darkMode }: {
const Waveform = memo(({ waveform, calculateTimelinePercent, fileDurationNonZero, darkMode }: {
waveform: RenderableWaveform,
calculateTimelinePercent: CalculateTimelinePercent,
durationSafe: number,
fileDurationNonZero: number,
darkMode: boolean,
}) => {
const leftPos = calculateTimelinePercent(waveform.from);
const toTruncated = Math.min(waveform.to, durationSafe);
const toTruncated = Math.min(waveform.to, fileDurationNonZero);
const style = useMemo<CSSProperties>(() => ({
position: 'absolute', height: '100%', left: leftPos, width: `${((toTruncated - waveform.from) / durationSafe) * 100}%`, filter: darkMode ? undefined : 'invert(1)',
}), [darkMode, durationSafe, leftPos, toTruncated, waveform.from]);
position: 'absolute', height: '100%', left: leftPos, width: `${((toTruncated - waveform.from) / fileDurationNonZero) * 100}%`, filter: darkMode ? undefined : 'invert(1)',
}), [darkMode, fileDurationNonZero, leftPos, toTruncated, waveform.from]);
if (waveform.url == null) {
return <div style={{ ...style }} className={styles['loading-bg']} />;
@ -47,9 +47,9 @@ const Waveform = memo(({ waveform, calculateTimelinePercent, durationSafe, darkM
});
// eslint-disable-next-line react/display-name
const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoom, height, darkMode }: {
const Waveforms = memo(({ calculateTimelinePercent, fileDurationNonZero, waveforms, zoom, height, darkMode }: {
calculateTimelinePercent: CalculateTimelinePercent,
durationSafe: number,
fileDurationNonZero: number,
waveforms: RenderableWaveform[],
zoom: number,
height: number,
@ -57,7 +57,7 @@ const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoo
}) => (
<div style={{ height, width: `${zoom * 100}%`, position: 'relative' }}>
{waveforms.map((waveform) => (
<Waveform key={`${waveform.from}-${waveform.to}`} waveform={waveform} calculateTimelinePercent={calculateTimelinePercent} durationSafe={durationSafe} darkMode={darkMode} />
<Waveform key={`${waveform.from}-${waveform.to}`} waveform={waveform} calculateTimelinePercent={calculateTimelinePercent} fileDurationNonZero={fileDurationNonZero} darkMode={darkMode} />
))}
</div>
));
@ -80,7 +80,7 @@ const timelineHeight = 36;
const timeWrapperStyle: CSSProperties = { height: timelineHeight };
function Timeline({
durationSafe,
fileDurationNonZero,
startTimeOffset,
playerTime,
commandedTime,
@ -111,7 +111,7 @@ function Timeline({
isSegmentSelected,
darkMode,
} : {
durationSafe: number,
fileDurationNonZero: number,
startTimeOffset: number,
playerTime: number | undefined,
commandedTime: number,
@ -154,7 +154,7 @@ function Timeline({
const [hoveringTime, setHoveringTime] = useState<number>();
const displayTime = (hoveringTime != null && isFileOpened && !playing ? hoveringTime : relevantTime) + startTimeOffset;
const displayTimePercent = useMemo(() => `${Math.round((displayTime / durationSafe) * 100)}%`, [displayTime, durationSafe]);
const displayTimePercent = useMemo(() => `${Math.round((displayTime / fileDurationNonZero) * 100)}%`, [displayTime, fileDurationNonZero]);
const isZoomed = zoom > 1;
@ -164,7 +164,7 @@ function Timeline({
// See https://github.com/mifi/lossless-cut/issues/259
const areKeyframesTooClose = keyFramesInZoomWindow.length > zoom * 200;
const calculateTimelinePos = useCallback((time: number | undefined) => (time !== undefined ? Math.min(time / durationSafe, 1) : undefined), [durationSafe]);
const calculateTimelinePos = useCallback((time: number | undefined) => (time !== undefined ? Math.min(time / fileDurationNonZero, 1) : undefined), [fileDurationNonZero]);
const calculateTimelinePercent = useCallback((time: number | undefined) => {
const pos = calculateTimelinePos(time);
return pos !== undefined ? `${pos * 100}%` : undefined;
@ -181,8 +181,8 @@ function Timeline({
}, [calculateTimelinePos, relevantTime, zoom]);
const calcZoomWindowStartTime = useCallback(() => (timelineScrollerRef.current
? (timelineScrollerRef.current.scrollLeft / (timelineScrollerRef.current!.offsetWidth * zoom)) * durationSafe
: 0), [durationSafe, zoom]);
? (timelineScrollerRef.current.scrollLeft / (timelineScrollerRef.current!.offsetWidth * zoom)) * fileDurationNonZero
: 0), [fileDurationNonZero, zoom]);
// const zoomWindowStartTime = calcZoomWindowStartTime(duration, zoom);
@ -231,11 +231,11 @@ function Timeline({
invariant(timelineScrollerRef.current != null);
const zoomedTargetWidth = timelineScrollerRef.current.offsetWidth * zoom;
const scrollLeft = Math.max((commandedTimeRef.current / durationSafe) * zoomedTargetWidth - timelineScrollerRef.current.offsetWidth / 2, 0);
const scrollLeft = Math.max((commandedTimeRef.current / fileDurationNonZero) * zoomedTargetWidth - timelineScrollerRef.current.offsetWidth / 2, 0);
scrollLeftMotion.set(scrollLeft);
timelineScrollerRef.current.scrollLeft = scrollLeft;
}
}, [zoom, durationSafe, commandedTimeRef, scrollLeftMotion, isZoomed]);
}, [zoom, fileDurationNonZero, commandedTimeRef, scrollLeftMotion, isZoomed]);
useEffect(() => {
@ -269,8 +269,8 @@ function Timeline({
invariant(target != null);
const rect = target.getBoundingClientRect();
const relX = e.pageX - (rect.left + document.body.scrollLeft);
return (relX / target.offsetWidth) * durationSafe;
}, [durationSafe]);
return (relX / target.offsetWidth) * fileDurationNonZero;
}, [fileDurationNonZero]);
const mouseDownRef = useRef<unknown>();
@ -353,7 +353,7 @@ function Timeline({
{waveformEnabled && shouldShowWaveform && waveforms.length > 0 && (
<Waveforms
calculateTimelinePercent={calculateTimelinePercent}
durationSafe={durationSafe}
fileDurationNonZero={fileDurationNonZero}
waveforms={waveforms}
zoom={zoom}
height={40}
@ -364,10 +364,10 @@ function Timeline({
{showThumbnails && (
<div style={{ height: 60, width: `${zoom * 100}%`, position: 'relative', marginBottom: 3 }}>
{thumbnails.map((thumbnail, i) => {
const leftPercent = (thumbnail.time / durationSafe) * 100;
const leftPercent = (thumbnail.time / fileDurationNonZero) * 100;
const nextThumbnail = thumbnails[i + 1];
const nextThumbTime = nextThumbnail ? nextThumbnail.time : durationSafe;
const maxWidthPercent = ((nextThumbTime - thumbnail.time) / durationSafe) * 100 * 0.9;
const nextThumbTime = nextThumbnail ? nextThumbnail.time : fileDurationNonZero;
const maxWidthPercent = ((nextThumbTime - thumbnail.time) / fileDurationNonZero) * 100 * 0.9;
return (
<img key={thumbnail.url} src={thumbnail.url} alt="" style={{ position: 'absolute', left: `${leftPercent}%`, height: '100%', boxSizing: 'border-box', maxWidth: `${maxWidthPercent}%`, objectFit: 'cover', border: '1px solid rgba(255, 255, 255, 0.5)', borderBottomRightRadius: 15, borderTopLeftRadius: 15, borderTopRightRadius: 15, pointerEvents: 'none' }} />
);
@ -389,7 +389,7 @@ function Timeline({
segNum={i}
onSegClick={setCurrentSegIndex}
isActive={i === currentSegIndexSafe}
duration={durationSafe}
fileDurationNonZero={fileDurationNonZero}
invertCutSegments={invertCutSegments}
formatTimecode={formatTimecode}
selected={selected}
@ -402,13 +402,13 @@ function Timeline({
key={seg.segId}
start={seg.start}
end={seg.end}
duration={durationSafe}
fileDurationNonZero={fileDurationNonZero}
invertCutSegments={invertCutSegments}
/>
))}
{shouldShowKeyframes && !areKeyframesTooClose && keyFramesInZoomWindow.map((f) => (
<div key={f.time} style={{ position: 'absolute', top: 0, bottom: 0, left: `${(f.time / durationSafe) * 100}%`, marginLeft: -1, width: 1, background: 'var(--gray11)', pointerEvents: 'none' }} />
<div key={f.time} style={{ position: 'absolute', top: 0, bottom: 0, left: `${(f.time / fileDurationNonZero) * 100}%`, marginLeft: -1, width: 1, background: 'var(--gray11)', pointerEvents: 'none' }} />
))}
{currentTimePercent !== undefined && (

@ -70,10 +70,10 @@ function Marker({
}
function TimelineSeg({
seg, duration, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected,
seg, fileDurationNonZero, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected,
} : {
seg: StateSegment,
duration: number,
fileDurationNonZero: number,
isActive: boolean,
segNum: number,
onSegClick: (a: number) => void,
@ -88,7 +88,7 @@ function TimelineSeg({
const { name } = seg;
const getTimePercent = (t: number) => `${(t / duration) * 100}%`;
const getTimePercent = (t: number) => `${(t / fileDurationNonZero) * 100}%`;
const vertBorder = useMemo(() => {
if (!isActive) return '2px solid transparent';
@ -114,7 +114,7 @@ function TimelineSeg({
);
}
const cutSectionWidth = `${((seg.end - seg.start) / duration) * 100}%`;
const cutSectionWidth = `${((seg.end - seg.start) / fileDurationNonZero) * 100}%`;
const wrapperStyle: MotionStyle = {
position: 'absolute',

@ -5,11 +5,11 @@ import { ffmpegExtractWindow } from '../util/constants';
import { RenderableWaveform } from '../types';
function BigWaveform({ waveforms, relevantTime, playing, durationSafe, zoom, seekRel, darkMode }: {
function BigWaveform({ waveforms, relevantTime, playing, fileDurationNonZero, zoom, seekRel, darkMode }: {
waveforms: RenderableWaveform[],
relevantTime: number,
playing: boolean,
durationSafe: number,
fileDurationNonZero: number,
zoom: number,
seekRel: (a: number) => void,
darkMode: boolean,
@ -105,7 +105,7 @@ function BigWaveform({ waveforms, relevantTime, playing, durationSafe, zoom, see
width: widthPercent,
left: leftPercent,
borderLeft: waveform.from === 0 ? '1px solid var(--gray11)' : undefined,
borderRight: waveform.to >= durationSafe ? '1px solid var(--gray11)' : undefined,
borderRight: waveform.to >= fileDurationNonZero ? '1px solid var(--gray11)' : undefined,
filter: darkMode ? undefined : 'invert(1)',
};

@ -1,6 +1,6 @@
import { CSSProperties, Dispatch, SetStateAction, memo, useCallback, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { WarningSignIcon, CrossIcon } from 'evergreen-ui';
import { WarningSignIcon, CrossIcon, InfoSignIcon } from 'evergreen-ui';
import { FaRegCheckCircle } from 'react-icons/fa';
import i18n from 'i18next';
import { useTranslation, Trans } from 'react-i18next';
@ -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 { SegmentToExport } from '../types';
import { SegmentBase, SegmentToExport } from '../types';
import { defaultMergedFileTemplate, defaultOutSegTemplate, GenerateOutFileNames } from '../util/outputNameTemplate';
import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs, PreserveMetadata } from '../../../../types';
@ -34,6 +34,8 @@ const outDirStyle: CSSProperties = { ...highlightedTextStyle, wordBreak: 'break-
const warningStyle: CSSProperties = { color: 'var(--orange8)', fontSize: '80%', marginBottom: '.5em' };
const infoStyle: CSSProperties = { color: 'white', fontSize: '80%', marginBottom: '.5em' };
const adjustCutFromValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const adjustCutToValues = [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
@ -97,7 +99,7 @@ function ExportConfirm({
generateOutSegFileNames: GenerateOutFileNames,
generateMergedFileNames: GenerateOutFileNames,
currentSegIndexSafe: number,
nonFilteredSegmentsOrInverse: unknown[],
nonFilteredSegmentsOrInverse: SegmentBase[],
mainCopiedThumbnailStreams: FFprobeStream[],
needSmartCut: boolean,
smartCutBitrate: number | undefined,
@ -121,14 +123,17 @@ function ExportConfirm({
// some thumbnail streams (png,jpg etc) cannot always be cut correctly, so we warn if they try to.
const areWeCuttingProblematicStreams = areWeCutting && mainCopiedThumbnailStreams.length > 0;
const warnings = useMemo(() => {
const ret: string[] = [];
const notices = useMemo(() => {
const ret: { warning?: true, text: string }[] = [];
if (!areWeCutting) {
ret.push({ text: t('Exporting whole file without cutting, because there are no segments to export.') });
}
// https://github.com/mifi/lossless-cut/issues/1809
if (areWeCutting && outFormat === 'flac') {
ret.push(t('There is a known issue in FFmpeg with cutting FLAC files. The file will be re-encoded, which is still lossless, but the export may be slower.'));
ret.push({ warning: true, text: t('There is a known issue in FFmpeg with cutting FLAC files. The file will be re-encoded, which is still lossless, but the export may be slower.') });
}
if (areWeCutting && outputPlaybackRate !== 1) {
ret.push(t('Adjusting the output FPS and cutting at the same time will cause incorrect cuts. Consider instead doing it in two separate steps.'));
ret.push({ warning: true, text: t('Adjusting the output FPS and cutting at the same time will cause incorrect cuts. Consider instead doing it in two separate steps.') });
}
return ret;
}, [areWeCutting, outFormat, outputPlaybackRate, t]);
@ -246,14 +251,21 @@ function ExportConfirm({
<table className={styles['options']}>
<tbody>
{warnings.map((warning) => (
<tr key={warning}>
{notices.map(({ warning, text }) => (
<tr key={text}>
<td colSpan={2}>
<div style={{ ...warningStyle, display: 'flex', alignItems: 'center', gap: '0 .5em' }}><WarningSignIcon verticalAlign="middle" color="warning" flexShrink="0" /> {warning}</div>
<div style={{ ...(warning ? warningStyle : infoStyle), display: 'flex', alignItems: 'center', gap: '0 .5em' }}>
{warning ? (
<WarningSignIcon verticalAlign="middle" color="warning" flexShrink="0" />
) : (
<InfoSignIcon verticalAlign="middle" color="info" flexShrink="0" />
)} {text}
</div>
</td>
<td />
</tr>
))}
{selectedSegments.length !== nonFilteredSegmentsOrInverse.length && (
<tr>
<td colSpan={2}>

@ -51,9 +51,9 @@ export function isCuttingStart(cutFrom: number) {
return cutFrom > 0;
}
export function isCuttingEnd(cutTo: number, duration: number | undefined) {
if (!isDurationValid(duration)) return true;
return cutTo < duration;
export function isCuttingEnd(cutTo: number, fileDuration: number | undefined) {
if (!isDurationValid(fileDuration)) return true;
return cutTo < fileDuration;
}
function getIntervalAroundTime(time: number, window: number) {

@ -233,7 +233,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const losslessCutSingle = useCallback(async ({
keyframeCut: ssBeforeInput, avoidNegativeTs, copyFileStreams, cutFrom, cutTo, chaptersPath, onProgress, outPath,
videoDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, detectedFps,
fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, detectedFps,
}: {
keyframeCut: boolean,
avoidNegativeTs: AvoidNegativeTs | undefined,
@ -243,7 +243,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
chaptersPath: string | undefined,
onProgress: (p: number) => void,
outPath: string,
videoDuration: number | undefined,
fileDuration: number | undefined,
rotation: number | undefined,
allFilesMeta: AllFilesMeta,
outFormat: string,
@ -265,7 +265,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const cuttingStart = isCuttingStart(cutFrom);
const cutFromWithAdjustment = cutFrom + cutFromAdjustmentFrames * frameDuration;
const cutToWithAdjustment = cutTo + cutToAdjustmentFrames * frameDuration;
const cuttingEnd = isCuttingEnd(cutTo, videoDuration);
const cuttingEnd = isCuttingEnd(cutTo, fileDuration);
const areWeCutting = cuttingStart || cuttingEnd;
if (areWeCutting) console.log('Cutting from', cuttingStart ? `${cutFrom} (${cutFromWithAdjustment} adjusted ${cutFromAdjustmentFrames} frames)` : 'start', 'to', cuttingEnd ? `${cutTo} (adjusted ${cutToAdjustmentFrames} frames)` : 'end');
@ -416,7 +416,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const result = await runFfmpegWithProgress({ ffmpegArgs, duration: cutDuration, onProgress });
logStdoutStderr(result);
await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(videoDuration) ? videoDuration : undefined, treatOutputFileModifiedTimeAsStart });
await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(fileDuration) ? fileDuration : undefined, treatOutputFileModifiedTimeAsStart });
}, [appendFfmpegCommandLog, cutFromAdjustmentFrames, cutToAdjustmentFrames, filePath, getOutputPlaybackRateArgs, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
// inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e
@ -474,13 +474,13 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
}, [appendFfmpegCommandLog, filePath]);
const cutMultiple = useCallback(async ({
outputDir, customOutDir, segments, outSegFileNames, videoDuration, rotation, detectedFps, onProgress: onTotalProgress, keyframeCut, copyFileStreams, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, chapters,
outputDir, customOutDir, segments: segmentsIn, outSegFileNames, fileDuration, rotation, detectedFps, onProgress: onTotalProgress, keyframeCut, copyFileStreams, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMetadataOnMerge, preserveMovData, preserveChapters, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, chapters,
}: {
outputDir: string,
customOutDir: string | undefined,
segments: SegmentToExport[],
outSegFileNames: string[],
videoDuration: number | undefined,
fileDuration: number | undefined,
rotation: number | undefined,
detectedFps: number | undefined,
onProgress: (p: number) => void,
@ -503,6 +503,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
console.log('customTagsByFile', customTagsByFile);
console.log('paramsByStreamId', paramsByStreamId);
const segments = segmentsIn.length > 0 ? segmentsIn : [{ start: 0, end: fileDuration ?? 0, name: '' }];
const singleProgresses: Record<number, number> = {};
function onSingleProgress(id: number, singleProgress: number) {
singleProgresses[id] = singleProgress;
@ -528,7 +530,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const outPath = await makeSegmentOutPath();
invariant(outFormat != null);
await losslessCutSingle({
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath, copyFileStreams, keyframeCut, avoidNegativeTs, videoDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, onProgress: (progress) => onSingleProgress(i, progress),
cutFrom: desiredCutFrom, cutTo, chaptersPath, outPath, copyFileStreams, keyframeCut, avoidNegativeTs, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, onProgress: (progress) => onSingleProgress(i, progress),
});
return outPath;
}
@ -543,7 +545,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
return match ? [match] : [];
});
const { losslessCutFrom, segmentNeedsSmartCut, videoCodec, videoBitrate: detectedVideoBitrate, videoStreamIndex, videoTimebase } = await getSmartCutParams({ path: filePath, videoDuration, desiredCutFrom, streams: streamsToCopyFromMainFile });
const { losslessCutFrom, segmentNeedsSmartCut, videoCodec, videoBitrate: detectedVideoBitrate, videoStreamIndex, videoTimebase } = await getSmartCutParams({ path: filePath, fileDuration, desiredCutFrom, streams: streamsToCopyFromMainFile });
if (segmentNeedsSmartCut && !detectedFps) throw new Error('Smart cut is not possible when FPS is unknown');
@ -590,7 +592,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// for smart cut we need to use keyframe cut here, and no avoid_negative_ts
await losslessCutSingle({
cutFrom: losslessCutFrom, cutTo, chaptersPath, outPath: losslessPartOutPath, copyFileStreams: copyFileStreamsFiltered, keyframeCut: true, avoidNegativeTs: undefined, videoDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, onProgress,
cutFrom: losslessCutFrom, cutTo, chaptersPath, outPath: losslessPartOutPath, copyFileStreams: copyFileStreamsFiltered, keyframeCut: true, avoidNegativeTs: undefined, fileDuration, rotation, allFilesMeta, outFormat, shortestFlag, ffmpegExperimental, preserveMetadata, preserveMovData, preserveChapters, movFastStart, customTagsByFile, paramsByStreamId, videoTimebase, onProgress,
});
// OK, just return the single cut file (we may need smart cut in other segments though)

@ -21,13 +21,13 @@ import { FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
const { ffmpeg: { blackDetect, silenceDetect } } = window.require('@electron/remote').require('./index.js');
function useSegments({ filePath, workingRef, setWorking, setProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, durationSafe, mainFileMeta }: {
function useSegments({ filePath, workingRef, setWorking, setProgress, videoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta }: {
filePath?: string | undefined,
workingRef: MutableRefObject<boolean>,
setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void,
setProgress: (a: number | undefined) => void,
videoStream: FFprobeStream | undefined,
duration?: number | undefined,
fileDuration?: number | undefined,
getRelevantTime: () => number,
maxLabelLength: number,
checkFileOpened: () => boolean,
@ -36,7 +36,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
timecodePlaceholder: string,
parseTimecode: ParseTimecode,
appendFfmpegCommandLog: (args: string[]) => void,
durationSafe: number,
fileDurationNonZero: number,
mainFileMeta: { formatData: FFprobeFormat } | undefined,
}) {
// Segment related state
@ -100,7 +100,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
if (validEdl.length > maxSegmentsAllowed) throw new Error(i18n.t('Tried to create too many segments (max {{maxSegmentsAllowed}}.)', { maxSegmentsAllowed }));
setCutSegments((existingSegments) => {
const needToAppend = append && !(existingSegments.length === 1 && isInitialSegment(existingSegments[0]!));
const needToAppend = append && !isInitialSegment(existingSegments);
let newSegments = validEdl.map((segment, i) => createIndexedSegment({ segment, incrementCount: needToAppend || i > 0 }));
if (needToAppend) newSegments = [...existingSegments, ...newSegments];
return newSegments;
@ -157,7 +157,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
}, [ffmpegParameters]);
const detectBlackScenes = useCallback(async () => {
const { start, end = durationSafe } = currentCutSeg;
const { start, end = fileDurationNonZero } = currentCutSeg;
const dialogType = 'blackdetect';
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' });
if (parameters == null) return;
@ -166,10 +166,10 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
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 (onSegmentDetected) => blackDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end }) });
}, [durationSafe, currentCutSeg, getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, setProgress]);
}, [fileDurationNonZero, currentCutSeg, getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, setProgress]);
const detectSilentScenes = useCallback(async () => {
const { start, end = durationSafe } = currentCutSeg;
const { start, end = fileDurationNonZero } = currentCutSeg;
const dialogType = 'silencedetect';
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#silencedetect' });
if (parameters == null) return;
@ -178,10 +178,10 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
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 (onSegmentDetected) => silenceDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, onSegmentDetected, from: start, to: end }) });
}, [currentCutSeg, detectSegments, durationSafe, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
}, [currentCutSeg, detectSegments, fileDurationNonZero, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
const detectSceneChanges = useCallback(async () => {
const { start, end = durationSafe } = currentCutSeg;
const { start, end = fileDurationNonZero } = currentCutSeg;
const dialogType = 'sceneChange';
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType) });
if (parameters == null) return;
@ -191,20 +191,20 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
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 (onSegmentDetected) => ffmpegDetectSceneChanges({ filePath, minChange, onProgress: setProgress, onSegmentDetected, from: start, to: end }) });
}, [currentCutSeg, detectSegments, durationSafe, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
}, [currentCutSeg, detectSegments, fileDurationNonZero, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
const createSegmentsFromKeyframes = useCallback(async () => {
const { start, end = durationSafe } = currentCutSeg;
const { start, end = fileDurationNonZero } = currentCutSeg;
if (!videoStream) return;
invariant(filePath != null);
const keyframes = (await readFrames({ filePath, from: start, to: end, streamIndex: videoStream.index })).filter((frame) => frame.keyframe);
const newSegments = mapTimesToSegments(keyframes.map((keyframe) => keyframe.time), true);
loadCutSegments(newSegments, true);
}, [currentCutSeg, durationSafe, filePath, loadCutSegments, videoStream]);
}, [currentCutSeg, fileDurationNonZero, filePath, loadCutSegments, videoStream]);
const removeSegments = useCallback((removeSegmentIds: string[]) => {
setCutSegments((existingSegments) => {
if (existingSegments.length === 1 && isInitialSegment(existingSegments[0]!)) {
if (isInitialSegment(existingSegments)) {
return existingSegments; // We are at initial segment, nothing more we can do (it cannot be removed)
}
@ -223,12 +223,12 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
}, [cutSegments, removeSegments]);
const inverseCutSegments = useMemo(() => {
if (haveInvalidSegs || !isDurationValid(duration)) return [];
if (haveInvalidSegs || !isDurationValid(fileDuration)) return [];
// exclude segments that don't have a length (markers)
const sortedSegments = sortSegments(filterNonMarkers(cutSegments));
return invertSegments(sortedSegments, true, true, duration).map(({ segId, end, ...rest }) => {
return invertSegments(sortedSegments, true, true, fileDuration).map(({ segId, end, ...rest }) => {
// in order to please TS:
invariant(segId != null && end != null);
return {
@ -237,7 +237,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
...rest,
};
});
}, [cutSegments, duration, haveInvalidSegs]);
}, [cutSegments, fileDuration, haveInvalidSegs]);
// 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 : cutSegments), [cutSegments, selectedSegmentsRaw]);
@ -246,7 +246,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
// treat markers as 0 length
const sortedSegments = sortSegments(selectedSegments.map(({ end, ...rest }) => ({ ...rest, end: end ?? rest.start })));
const inverseSegmentsAndMarkers = invertSegments(sortedSegments, true, true, duration);
const inverseSegmentsAndMarkers = invertSegments(sortedSegments, true, true, fileDuration);
if (inverseSegmentsAndMarkers.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.'));
@ -255,13 +255,13 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
// preserve segColorIndex (which represent colors) when inverting
const newInverseCutSegments = inverseSegmentsAndMarkers.map((inverseSegment, index) => addSegmentColorIndex(createSegment(inverseSegment), index));
setCutSegments(newInverseCutSegments);
}, [duration, selectedSegments, setCutSegments]);
}, [fileDuration, selectedSegments, setCutSegments]);
const fillSegmentsGaps = useCallback(() => {
// treat markers as 0 length
const sortedSegments = sortSegments(selectedSegments.map(({ end, ...rest }) => ({ ...rest, end: end ?? rest.start })));
const inverseSegmentsAndMarkers = invertSegments(sortedSegments, true, true, duration);
const inverseSegmentsAndMarkers = invertSegments(sortedSegments, true, true, fileDuration);
if (inverseSegmentsAndMarkers.length === 0) {
errorToast(i18n.t('Make sure you have no overlapping segments.'));
@ -269,7 +269,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
}
const newInverseCutSegments = inverseSegmentsAndMarkers.map((segment) => createIndexedSegment({ segment, incrementCount: true }));
setCutSegments((existing) => ([...existing, ...newInverseCutSegments]));
}, [createIndexedSegment, duration, selectedSegments, setCutSegments]);
}, [createIndexedSegment, fileDuration, selectedSegments, setCutSegments]);
const combineOverlappingSegments = useCallback(() => {
setCutSegments((existingSegments) => combineOverlappingSegments2(existingSegments));
@ -289,29 +289,29 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
}, [setCutSegments, cutSegments]);
const setCutTime = useCallback((type: 'start' | 'end', time: number | undefined) => {
if (!isDurationValid(duration)) return;
if (!isDurationValid(fileDuration)) return;
if (type === 'start') {
invariant(time != null);
if (currentCutSeg.end != null && time >= currentCutSeg.end) {
throw new Error('Start time must precede end time');
}
updateSegAtIndex(currentSegIndexSafe, { start: Math.min(Math.max(time, 0), duration) });
updateSegAtIndex(currentSegIndexSafe, { start: Math.min(Math.max(time, 0), fileDuration) });
}
if (type === 'end') {
if (time != null && time <= currentCutSeg.start) {
throw new Error('Start time must precede end time');
}
updateSegAtIndex(currentSegIndexSafe, { end: time != null ? Math.min(Math.max(time, 0), duration) : undefined });
updateSegAtIndex(currentSegIndexSafe, { end: time != null ? Math.min(Math.max(time, 0), fileDuration) : undefined });
}
}, [currentSegIndexSafe, currentCutSeg, duration, updateSegAtIndex]);
}, [currentSegIndexSafe, currentCutSeg, fileDuration, updateSegAtIndex]);
const clampValue = useCallback((val: number | undefined) => {
if (val == null || Number.isNaN(val)) return undefined;
const clamped = Math.max(val, 0);
if (duration == null) return clamped;
return Math.min(clamped, duration);
}, [duration]);
if (fileDuration == null) return clamped;
return Math.min(clamped, fileDuration);
}, [fileDuration]);
const safeSetCutSegments = useCallback((newSegments: StateSegment[]) => {
const safeNewSegments = newSegments.map(({ start, end, ...rest }) => ({
@ -406,7 +406,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
if (keyframeAlignedStart != null) suggestedStart = keyframeAlignedStart;
} */
if (duration == null || suggestedStart >= duration) return;
if (fileDuration == null || suggestedStart >= fileDuration) return;
const newSegment = createIndexedSegment({ segment: { start: suggestedStart }, incrementCount: true });
@ -424,7 +424,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
} catch (err) {
console.error(err);
}
}, [getRelevantTime, duration, cutSegments, createIndexedSegment, setCutSegments, setCurrentSegIndex]);
}, [getRelevantTime, fileDuration, cutSegments, createIndexedSegment, setCutSegments, setCurrentSegIndex]);
const duplicateSegment = useCallback((segment: Pick<StateSegment, 'start' | 'end'> & Partial<Pick<StateSegment, 'name'>>) => {
try {
@ -536,32 +536,32 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
}, [createIndexedSegment, cutSegments, findSegmentsAtCursor, getRelevantTime, setCutSegments]);
const createNumSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
const segments = await createNumSegmentsDialog(duration);
if (!checkFileOpened() || !isDurationValid(fileDuration)) return;
const segments = await createNumSegmentsDialog(fileDuration);
if (segments) loadCutSegments(segments);
}, [checkFileOpened, duration, loadCutSegments]);
}, [checkFileOpened, fileDuration, loadCutSegments]);
const createFixedDurationSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
const segmentDuration = await askForSegmentDuration({ fileDuration: duration, inputPlaceholder: timecodePlaceholder, parseTimecode });
if (!checkFileOpened() || !isDurationValid(fileDuration)) return;
const segmentDuration = await askForSegmentDuration({ fileDuration, inputPlaceholder: timecodePlaceholder, parseTimecode });
if (segmentDuration == null) return;
loadCutSegments(makeDurationSegments(segmentDuration, duration));
}, [checkFileOpened, duration, loadCutSegments, parseTimecode, timecodePlaceholder]);
loadCutSegments(makeDurationSegments(segmentDuration, fileDuration));
}, [checkFileOpened, fileDuration, loadCutSegments, parseTimecode, timecodePlaceholder]);
const createFixedByteSizedSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
if (!checkFileOpened() || !isDurationValid(fileDuration)) return;
const fileSize = mainFileMeta && parseInt(mainFileMeta.formatData.size, 10);
invariant(fileSize != null && !Number.isNaN(fileSize));
const segmentDuration = await createFixedByteSixedSegmentsDialog({ fileDuration: duration, fileSize });
const segmentDuration = await createFixedByteSixedSegmentsDialog({ fileDuration, fileSize });
if (segmentDuration == null) return;
loadCutSegments(makeDurationSegments(segmentDuration, duration));
}, [checkFileOpened, duration, loadCutSegments, mainFileMeta]);
loadCutSegments(makeDurationSegments(segmentDuration, fileDuration));
}, [checkFileOpened, fileDuration, loadCutSegments, mainFileMeta]);
const createRandomSegments = useCallback(async () => {
if (!checkFileOpened() || !isDurationValid(duration)) return;
const segments = await createRandomSegmentsDialog(duration);
if (!checkFileOpened() || !isDurationValid(fileDuration)) return;
const segments = await createRandomSegmentsDialog(fileDuration);
if (segments) loadCutSegments(segments);
}, [checkFileOpened, duration, loadCutSegments]);
}, [checkFileOpened, fileDuration, loadCutSegments]);
const selectSegments = useCallback((segments: { segId: string }[]) => {
if (segments.length === 0 || segments.length === cutSegments.length) return; // no point in selecting none or all
@ -689,10 +689,9 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
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: durationSafe }];
if (selectedSegmentsOrInverse.length > 0) return selectedSegmentsOrInverse;
return [{ start: 0, end: durationSafe }]; // e.g. only markers
}, [segmentsToChaptersOnly, durationSafe, selectedSegmentsOrInverse]);
if (segmentsToChaptersOnly) return [];
return selectedSegmentsOrInverse;
}, [segmentsToChaptersOnly, selectedSegmentsOrInverse]);
const removeSelectedSegments = useCallback(() => removeSegments(selectedSegmentsRaw.map((seg) => seg.segId)), [removeSegments, selectedSegmentsRaw]);

@ -9,7 +9,7 @@ export default ({ filePath }: { filePath: string | undefined }) => {
const [playbackRate, setPlaybackRateState] = useState(1);
const [outputPlaybackRate, setOutputPlaybackRateState] = useState(1);
const [playerTime, setPlayerTime] = useState<number>();
const [duration, setDuration] = useState<number>();
const [fileDuration, setFileDuration] = useState<number>();
const playbackModeRef = useRef<PlaybackMode>();
const videoRef = useRef<ChromiumHTMLVideoElement>(null);
@ -105,7 +105,7 @@ export default ({ filePath }: { filePath: string | undefined }) => {
// Sometimes after seeking to end of file, duration might change
const { duration: durationNew } = e.currentTarget;
console.log('onDurationChange', durationNew);
if (isDurationValid(durationNew)) setDuration(durationNew);
if (isDurationValid(durationNew)) setFileDuration(durationNew);
}, []);
const pause = useCallback(() => {
@ -154,8 +154,8 @@ export default ({ filePath }: { filePath: string | undefined }) => {
pause,
relevantTime,
getRelevantTime,
duration,
setDuration,
fileDuration,
setFileDuration,
onDurationChange,
onVideoAbort,
compatPlayerEventId,

@ -10,10 +10,10 @@ import { FFprobeStream } from '../../../../ffprobe';
const maxWaveforms = 100;
// const maxWaveforms = 3; // testing
export default ({ filePath, relevantTime, duration, waveformEnabled, audioStream, ffmpegExtractWindow }: {
export default ({ filePath, relevantTime, fileDuration, waveformEnabled, audioStream, ffmpegExtractWindow }: {
filePath: string | undefined,
relevantTime: number,
duration: number | undefined,
fileDuration: number | undefined,
waveformEnabled: boolean,
audioStream: FFprobeStream | undefined,
ffmpegExtractWindow: number,
@ -32,7 +32,7 @@ export default ({ filePath, relevantTime, duration, waveformEnabled, audioStream
}, [filePath, audioStream, setWaveforms]);
const waveformStartTime = Math.floor(relevantTime / ffmpegExtractWindow) * ffmpegExtractWindow;
const safeExtractDuration = duration != null ? Math.min(waveformStartTime + ffmpegExtractWindow, duration) - waveformStartTime : undefined;
const safeExtractDuration = fileDuration != null ? Math.min(waveformStartTime + ffmpegExtractWindow, fileDuration) - waveformStartTime : undefined;
const waveformStartTimeThrottled = useThrottle(waveformStartTime, 1000);

@ -262,4 +262,4 @@ export function makeDurationSegments(segmentDuration: number, fileDuration: numb
return edl;
}
export const isInitialSegment = (seg: SegmentBase) => seg.start === 0 && seg.end == null;
export const isInitialSegment = (segments: SegmentBase[]) => segments.length === 1 && segments[0]!.start === 0 && segments[0]!.end == null;

@ -9,8 +9,11 @@ const { stat } = window.require('fs-extra');
const mapVideoCodec = (codec: string) => ({ av1: 'libsvtav1' }[codec] ?? codec);
// eslint-disable-next-line import/prefer-default-export
export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, streams }: {
path: string, videoDuration: number | undefined, desiredCutFrom: number, streams: Pick<FFprobeStream, 'time_base' | 'codec_type' | 'disposition' | 'index' | 'bit_rate' | 'codec_name'>[],
export async function getSmartCutParams({ path, fileDuration, desiredCutFrom, streams }: {
path: string,
fileDuration: number | undefined,
desiredCutFrom: number,
streams: Pick<FFprobeStream, 'time_base' | 'codec_type' | 'disposition' | 'index' | 'bit_rate' | 'codec_name'>[],
}) {
const videoStreams = getRealVideoStreams(streams);
if (videoStreams.length > 1) throw new Error('Can only smart cut video with exactly one video stream');
@ -49,8 +52,8 @@ export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, s
if (Number.isNaN(videoBitrate)) {
console.warn('Unable to detect input bitrate');
const stats = await stat(path);
if (videoDuration == null) throw new Error('Video duration is unknown, cannot estimate bitrate');
videoBitrate = (stats.size * 8) / videoDuration;
if (fileDuration == null) throw new Error('Video duration is unknown, cannot estimate bitrate');
videoBitrate = (stats.size * 8) / fileDuration;
}
// to account for inaccuracies and quality loss

@ -164,7 +164,8 @@ function maybeTruncatePath(fileName: string, truncate: boolean) {
].join(pathSep);
}
export async function generateOutSegFileNames({ segments, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding, exportCount, currentFileExportCount }: {
export async function generateOutSegFileNames({ fileDuration, segments: segmentsIn, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding, exportCount, currentFileExportCount }: {
fileDuration: number | undefined,
segments: SegmentToExport[],
template: string,
formatTimecode: FormatTimecode,
@ -181,6 +182,8 @@ export async function generateOutSegFileNames({ segments, template: desiredTempl
async function generate({ template, forceSafeOutputFileName }: { template: string, forceSafeOutputFileName: boolean }) {
const epochMs = Date.now();
const segments = segmentsIn.length > 0 ? segmentsIn : [{ start: 0, end: fileDuration ?? 0, name: '' }];
return pMap(segments, async (segment, i) => {
const { start, end, name = '' } = segment;
const segNum = i + 1;

Loading…
Cancel
Save