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:
parent
cbf693b798
commit
364fb0df7a
82
src/App.jsx
82
src/App.jsx
@ -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>
|
||||
|
||||
|
@ -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>}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -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 }) => [
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user