1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 19:52:44 +01:00

improvemenst

remove frame rounding when seeking #585
also show video fps
improve split
cleanup refs and current time logic
This commit is contained in:
Mikael Finstad 2022-02-18 17:42:35 +08:00
parent cbf693b798
commit 364fb0df7a
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
4 changed files with 115 additions and 107 deletions

View File

@ -210,7 +210,6 @@ const App = memo(() => {
}, [language]);
const videoRef = useRef();
const currentTimeRef = useRef();
const isFileOpened = !!filePath;
@ -253,8 +252,6 @@ const App = memo(() => {
setFfmpegCommandLog(old => [...old, { command, time: new Date() }]);
}
const getCurrentTime = useCallback(() => currentTimeRef.current, []);
const setCopyStreamIdsForPath = useCallback((path, cb) => {
setCopyStreamIdsByFile((old) => {
const oldIds = old[path] || {};
@ -282,16 +279,13 @@ const App = memo(() => {
const seekAbs = useCallback((val) => {
const video = videoRef.current;
if (val == null || Number.isNaN(val)) return;
let valRounded = val;
if (detectedFps) valRounded = Math.round(detectedFps * val) / detectedFps; // Round to nearest frame
let outVal = valRounded;
let outVal = val;
if (outVal < 0) outVal = 0;
if (outVal > video.duration) outVal = video.duration;
video.currentTime = outVal;
setCommandedTime(outVal);
}, [detectedFps]);
}, []);
const commandedTimeRef = useRef(commandedTime);
useEffect(() => {
@ -307,9 +301,14 @@ const App = memo(() => {
seekRel(val * zoomedDuration);
}, [seekRel, zoomedDuration]);
const shortStep = useCallback((dir) => {
seekRel((1 / (detectedFps || 60)) * dir);
}, [seekRel, detectedFps]);
const shortStep = useCallback((direction) => {
if (!detectedFps) return;
// try to align with frame
const currentTimeNearestFrameNumber = getFrameCountRaw(detectedFps, videoRef.current.currentTime);
const nextFrame = currentTimeNearestFrameNumber + direction;
seekAbs(nextFrame / detectedFps);
}, [seekAbs, detectedFps]);
// 360 means we don't modify rotation
const isRotationSet = rotation !== 360;
@ -458,9 +457,7 @@ const App = memo(() => {
return formatDuration({ seconds, shorten });
}, [detectedFps, timecodeFormat, getFrameCount]);
useEffect(() => {
currentTimeRef.current = playing ? playerTime : commandedTime;
}, [commandedTime, playerTime, playing]);
const getCurrentTime = useCallback(() => (playing ? videoRef.current.currentTime : commandedTimeRef.current), [playing]);
// const getSafeCutTime = useCallback((cutTime, next) => ffmpeg.getSafeCutTime(neighbouringFrames, cutTime, next), [neighbouringFrames]);
@ -469,7 +466,7 @@ const App = memo(() => {
// Cannot add if prev seg is not finished
if (currentCutSeg.start === undefined && currentCutSeg.end === undefined) return;
const suggestedStart = currentTimeRef.current;
const suggestedStart = getCurrentTime();
/* if (keyframeCut) {
const keyframeAlignedStart = getSafeCutTime(suggestedStart, true);
if (keyframeAlignedStart != null) suggestedStart = keyframeAlignedStart;
@ -485,19 +482,20 @@ const App = memo(() => {
} catch (err) {
console.error(err);
}
}, [currentCutSeg.start, currentCutSeg.end, cutSegments, createSegmentAndIncrementCount, setCutSegments]);
}, [currentCutSeg.start, currentCutSeg.end, getCurrentTime, cutSegments, createSegmentAndIncrementCount, setCutSegments]);
const setCutStart = useCallback(() => {
if (!filePath) return;
const currentTime = getCurrentTime();
// https://github.com/mifi/lossless-cut/issues/168
// If current time is after the end of the current segment in the timeline,
// add a new segment that starts at playerTime
if (currentCutSeg.end != null && currentTimeRef.current > currentCutSeg.end) {
if (currentCutSeg.end != null && currentTime > currentCutSeg.end) {
addCutSegment();
} else {
try {
const startTime = currentTimeRef.current;
const startTime = currentTime;
/* if (keyframeCut) {
const keyframeAlignedCutTo = getSafeCutTime(startTime, true);
if (keyframeAlignedCutTo != null) startTime = keyframeAlignedCutTo;
@ -507,13 +505,13 @@ const App = memo(() => {
handleError(err);
}
}
}, [setCutTime, currentCutSeg, addCutSegment, filePath]);
}, [filePath, getCurrentTime, currentCutSeg.end, addCutSegment, setCutTime]);
const setCutEnd = useCallback(() => {
if (!filePath) return;
try {
const endTime = currentTimeRef.current;
const endTime = getCurrentTime();
/* if (keyframeCut) {
const keyframeAlignedCutTo = getSafeCutTime(endTime, false);
@ -523,7 +521,7 @@ const App = memo(() => {
} catch (err) {
handleError(err);
}
}, [setCutTime, filePath]);
}, [filePath, getCurrentTime, setCutTime]);
const outputDir = getOutDir(customOutDir, filePath);
@ -1221,7 +1219,7 @@ const App = memo(() => {
if (!filePath) return;
try {
const currentTime = currentTimeRef.current;
const currentTime = getCurrentTime();
const video = videoRef.current;
const outPath = previewFilePath
? await captureFrameFfmpeg({ customOutDir, filePath, currentTime, captureFormat, enableTransferTimestamps })
@ -1232,7 +1230,7 @@ const App = memo(() => {
console.error(err);
errorToast(i18n.t('Failed to capture frame'));
}
}, [filePath, captureFormat, customOutDir, previewFilePath, outputDir, enableTransferTimestamps, hideAllNotifications]);
}, [filePath, getCurrentTime, previewFilePath, customOutDir, captureFormat, enableTransferTimestamps, hideAllNotifications, outputDir]);
const changePlaybackRate = useCallback((dir, rateMultiplier) => {
if (canvasPlayerEnabled) {
@ -1250,35 +1248,34 @@ const App = memo(() => {
}
}, [playing, canvasPlayerEnabled]);
const firstSegmentAtCursorIndex = useMemo(() => {
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, commandedTime);
return segmentsAtCursorIndexes[0];
}, [apparentCutSegments, commandedTime]);
const segmentAtCursorRef = useRef();
const segmentAtCursor = useMemo(() => {
const segment = cutSegments[firstSegmentAtCursorIndex];
segmentAtCursorRef.current = segment;
return segment;
}, [cutSegments, firstSegmentAtCursorIndex]);
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, commandedTime);
const firstSegmentAtCursorIndex = segmentsAtCursorIndexes[0];
return cutSegments[firstSegmentAtCursorIndex];
}, [apparentCutSegments, commandedTime, cutSegments]);
const splitCurrentSegment = useCallback(() => {
const segmentAtCursor2 = segmentAtCursorRef.current;
if (!segmentAtCursor2) {
const currentTime = getCurrentTime();
const segmentsAtCursorIndexes = findSegmentsAtCursor(apparentCutSegments, currentTime);
if (segmentsAtCursorIndexes.length === 0) {
errorToast(i18n.t('No segment to split. Please move cursor over the segment you want to split'));
return;
}
const getNewName = (oldName, suffix) => oldName && `${segmentAtCursor2.name} ${suffix}`;
const firstSegmentAtCursorIndex = segmentsAtCursorIndexes[0];
const segment = cutSegments[firstSegmentAtCursorIndex];
const firstPart = createSegmentAndIncrementCount({ name: getNewName(segmentAtCursor2.name, '1'), start: segmentAtCursor2.start, end: currentTimeRef.current });
const secondPart = createSegmentAndIncrementCount({ name: getNewName(segmentAtCursor2.name, '2'), start: currentTimeRef.current, end: segmentAtCursor2.end });
const getNewName = (oldName, suffix) => oldName && `${segment.name} ${suffix}`;
const firstPart = createSegmentAndIncrementCount({ name: getNewName(segment.name, '1'), start: segment.start, end: currentTime });
const secondPart = createSegmentAndIncrementCount({ name: getNewName(segment.name, '2'), start: currentTime, end: segment.end });
const newSegments = [...cutSegments];
newSegments.splice(firstSegmentAtCursorIndex, 1, firstPart, secondPart);
setCutSegments(newSegments);
}, [createSegmentAndIncrementCount, cutSegments, firstSegmentAtCursorIndex, setCutSegments]);
}, [apparentCutSegments, createSegmentAndIncrementCount, cutSegments, getCurrentTime, setCutSegments]);
const loadCutSegments = useCallback((edl, append = false) => {
const validEdl = edl.filter((row) => (
@ -1425,10 +1422,10 @@ const App = memo(() => {
const jumpSeg = useCallback((val) => setCurrentSegIndex((old) => Math.max(Math.min(old + val, cutSegments.length - 1), 0)), [cutSegments.length]);
const seekClosestKeyframe = useCallback((direction) => {
const time = findNearestKeyFrameTime({ time: currentTimeRef.current, direction });
const time = findNearestKeyFrameTime({ time: getCurrentTime(), direction });
if (time == null) return;
seekAbs(time);
}, [findNearestKeyFrameTime, seekAbs]);
}, [findNearestKeyFrameTime, getCurrentTime, seekAbs]);
const seekAccelerationRef = useRef(1);
@ -2403,6 +2400,7 @@ const App = memo(() => {
hasAudio={hasAudio}
keyframesEnabled={keyframesEnabled}
toggleKeyframesEnabled={toggleKeyframesEnabled}
detectedFps={detectedFps}
/>
</motion.div>

View File

@ -34,7 +34,7 @@ const BottomBar = memo(({
setCurrentSegIndex, cutStartTimeManual, setCutStartTimeManual, cutEndTimeManual, setCutEndTimeManual,
duration, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime, currentApparentCutSeg,
playing, shortStep, togglePlay, setTimelineMode, hasAudio, timelineMode,
keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe,
keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe, detectedFps,
}) => {
const { t } = useTranslation();
@ -151,32 +151,36 @@ const BottomBar = memo(({
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', flexBasis: leftRightWidth }}>
{hasAudio && !simpleMode && (
<GiSoundWaves
size={24}
style={{ padding: '0 5px', color: timelineMode === 'waveform' ? primaryTextColor : undefined }}
role="button"
title={t('Show waveform')}
onClick={() => setTimelineMode('waveform')}
/>
)}
{hasVideo && !simpleMode && (
{!simpleMode && (
<>
<FaImages
size={20}
style={{ padding: '0 5px', color: timelineMode === 'thumbnails' ? primaryTextColor : undefined }}
role="button"
title={t('Show thumbnails')}
onClick={() => setTimelineMode('thumbnails')}
/>
{hasAudio && (
<GiSoundWaves
size={24}
style={{ padding: '0 5px', color: timelineMode === 'waveform' ? primaryTextColor : undefined }}
role="button"
title={t('Show waveform')}
onClick={() => setTimelineMode('waveform')}
/>
)}
{hasVideo && (
<>
<FaImages
size={20}
style={{ padding: '0 5px', color: timelineMode === 'thumbnails' ? primaryTextColor : undefined }}
role="button"
title={t('Show thumbnails')}
onClick={() => setTimelineMode('thumbnails')}
/>
<FaKey
size={16}
style={{ padding: '0 5px', color: keyframesEnabled ? primaryTextColor : undefined }}
role="button"
title={t('Show keyframes')}
onClick={toggleKeyframesEnabled}
/>
<FaKey
size={16}
style={{ padding: '0 5px', color: keyframesEnabled ? primaryTextColor : undefined }}
role="button"
title={t('Show keyframes')}
onClick={toggleKeyframesEnabled}
/>
</>
)}
</>
)}
</div>
@ -184,17 +188,20 @@ const BottomBar = memo(({
<div style={{ flexGrow: 1 }} />
{!simpleMode && (
<FaStepBackward
size={16}
title={t('Jump to start of video')}
role="button"
onClick={() => seekAbs(0)}
/>
<>
<FaStepBackward
size={16}
title={t('Jump to start of video')}
role="button"
onClick={() => seekAbs(0)}
/>
{renderJumpCutpointButton(-1)}
<SegmentCutpointButton currentCutSeg={currentCutSeg} side="start" Icon={FaStepBackward} onClick={jumpCutStart} title={t('Jump to cut start')} style={{ marginRight: 5 }} />
</>
)}
{!simpleMode && renderJumpCutpointButton(-1)}
{!simpleMode && <SegmentCutpointButton currentCutSeg={currentCutSeg} side="start" Icon={FaStepBackward} onClick={jumpCutStart} title={t('Jump to cut start')} style={{ marginRight: 5 }} />}
<SetCutpointButton currentCutSeg={currentCutSeg} side="start" onClick={setCutStart} title={t('Set cut start to current position')} style={{ marginRight: 5 }} />
{!simpleMode && renderCutTimeInput('start')}
@ -247,17 +254,20 @@ const BottomBar = memo(({
{!simpleMode && renderCutTimeInput('end')}
<SetCutpointButton currentCutSeg={currentCutSeg} side="end" onClick={setCutEnd} title={t('Set cut end to current position')} style={{ marginLeft: 5 }} />
{!simpleMode && <SegmentCutpointButton currentCutSeg={currentCutSeg} side="end" Icon={FaStepForward} onClick={jumpCutEnd} title={t('Jump to cut end')} style={{ marginLeft: 5 }} />}
{!simpleMode && renderJumpCutpointButton(1)}
{!simpleMode && (
<FaStepForward
size={16}
title={t('Jump to end of video')}
role="button"
onClick={() => seekAbs(duration)}
/>
<>
<SegmentCutpointButton currentCutSeg={currentCutSeg} side="end" Icon={FaStepForward} onClick={jumpCutEnd} title={t('Jump to cut end')} style={{ marginLeft: 5 }} />
{renderJumpCutpointButton(1)}
<FaStepForward
size={16}
title={t('Jump to end of video')}
role="button"
onClick={() => seekAbs(duration)}
/>
</>
)}
<div style={{ flexGrow: 1 }} />
@ -273,25 +283,23 @@ const BottomBar = memo(({
{simpleMode && <div role="button" onClick={toggleSimpleMode} style={{ marginLeft: 5, fontSize: '90%' }}>{t('Toggle advanced view')}</div>}
{!simpleMode && (
<div style={{ marginLeft: 5 }}>
<motion.div
style={{ width: 24, height: 24 }}
animate={{ rotateX: invertCutSegments ? 0 : 180 }}
transition={{ duration: 0.3 }}
>
<FaYinYang
size={24}
role="button"
title={invertCutSegments ? t('Discard selected segments') : t('Keep selected segments')}
onClick={onYinYangClick}
/>
</motion.div>
</div>
)}
{!simpleMode && (
<>
<div style={{ marginLeft: 5 }}>
<motion.div
style={{ width: 24, height: 24 }}
animate={{ rotateX: invertCutSegments ? 0 : 180 }}
transition={{ duration: 0.3 }}
>
<FaYinYang
size={24}
role="button"
title={invertCutSegments ? t('Discard selected segments') : t('Keep selected segments')}
onClick={onYinYangClick}
/>
</motion.div>
</div>
<div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={toggleComfortZoom}>{Math.floor(zoom)}x</div>
<Select height={20} style={{ flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur(e => setZoom(parseInt(e.target.value, 10)))}>
@ -300,6 +308,8 @@ const BottomBar = memo(({
<option key={val} value={String(val)}>{t('Zoom')} {val}x</option>
))}
</Select>
{detectedFps != null && <div title={t('Video FPS')} style={{ color: 'rgba(255,255,255,0.6)', fontSize: '.7em', marginLeft: 6 }}>{detectedFps.toFixed(3)}</div>}
</>
)}

View File

@ -17,7 +17,7 @@ export const getTimeFromFrameNum = (detectedFps, frameNum) => frameNum / detecte
export function getFrameCountRaw(detectedFps, sec) {
if (detectedFps == null) return undefined;
return Math.floor(sec * detectedFps);
return Math.round(sec * detectedFps);
}
export async function parseCsv(csvStr, processTime = (t) => t) {
@ -183,7 +183,7 @@ export function formatYouTube(segments) {
}).join('\n');
}
// because null/undefined is also a valid value (start/end of timeline)
// because null/undefined is also valid values (start/end of timeline)
const safeFormatDuration = (duration) => (duration != null ? formatDuration({ seconds: duration }) : '');
export const formatSegmentsTimes = (cutSegments) => cutSegments.map(({ start, end, name }) => [

View File

@ -165,9 +165,9 @@ export function findNearestKeyFrameTime({ frames, time, direction, fps }) {
const sigma = fps ? (1 / fps) : 0.1;
const keyframes = frames.filter(f => f.keyframe && (direction > 0 ? f.time > time + sigma : f.time < time - sigma));
if (keyframes.length === 0) return undefined;
const nearestFrame = sortBy(keyframes, keyframe => (direction > 0 ? keyframe.time - time : time - keyframe.time))[0];
if (!nearestFrame) return undefined;
return nearestFrame.time;
const nearestKeyFrame = sortBy(keyframes, keyframe => (direction > 0 ? keyframe.time - time : time - keyframe.time))[0];
if (!nearestKeyFrame) return undefined;
return nearestKeyFrame.time;
}
export async function tryMapChaptersToEdl(chapters) {