diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 23d6ad4a..13a325df 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -575,9 +575,15 @@ function App() { const { thumbnailsSorted, setThumbnails } = useThumbnails({ filePath, zoomedDuration, zoomWindowStartTime, showThumbnails }); - const { neighbouringKeyFrames, findNearestKeyFrameTime } = useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream: activeVideoStream, detectedFps, ffmpegExtractWindow }); + const { neighbouringKeyFrames, findNearestKeyFrameTime, keyframeByNumber } = useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream: activeVideoStream, detectedFps, ffmpegExtractWindow }); const { waveforms, overviewWaveform, renderOverviewWaveform } = useWaveform({ filePath, relevantTime, waveformEnabled, audioStream: activeAudioStreams[0], ffmpegExtractWindow, fileDuration }); + const currentFrame = useMemo(() => { + const frameNum = getFrameCount(commandedTime); + if (frameNum == null) return undefined; + return keyframeByNumber[frameNum]; + }, [commandedTime, getFrameCount, keyframeByNumber]); + const onGenerateOverviewWaveformClick = useCallback(async () => { if (working) return; try { @@ -2734,6 +2740,7 @@ function App() { formatTimecode={formatTimecode} parseTimecode={parseTimecode} playbackRate={playbackRate} + currentFrame={currentFrame} /> diff --git a/src/renderer/src/BottomBar.tsx b/src/renderer/src/BottomBar.tsx index 89288dd9..f92513b1 100644 --- a/src/renderer/src/BottomBar.tsx +++ b/src/renderer/src/BottomBar.tsx @@ -26,6 +26,7 @@ import useUserSettings from './hooks/useUserSettings'; import { askForPlaybackRate } from './dialogs'; import { FormatTimecode, ParseTimecode, SegmentColorIndex, SegmentToExport, StateSegment } from './types'; import { WaveformMode } from '../../../types'; +import { Frame } from './ffmpeg'; const { clipboard } = window.require('electron'); @@ -150,10 +151,10 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see trySetTime(timeWithOffset); }, [isEmptyEndTime, parseTimecode, trySetTime]); - function handleCutTimeInput(text: string) { + const handleCutTimeInput = useCallback((text: string) => { if (isExactDurationMatch(text) || isEmptyEndTime(text)) parseAndSetCutTime(text); else setCutTimeManual(text); - } + }, [isEmptyEndTime, parseAndSetCutTime]); const tryPaste = useCallback((clipboardText: string) => { try { @@ -190,7 +191,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see return (
handleCutTimeInput(e.target.value)} @@ -216,6 +217,7 @@ function BottomBar({ toggleShowThumbnails, toggleWaveformMode, waveformMode, showThumbnails, outputPlaybackRate, setOutputPlaybackRate, formatTimecode, parseTimecode, playbackRate, + currentFrame, }: { zoom: number, setZoom: (fn: (z: number) => number) => void, @@ -264,6 +266,7 @@ function BottomBar({ formatTimecode: FormatTimecode, parseTimecode: ParseTimecode, playbackRate: number, + currentFrame: Frame | undefined, }) { const { t } = useTranslation(); const { getSegColor } = useSegColors(); @@ -302,6 +305,10 @@ function BottomBar({ }; }, [selectedSegments]); + const keyframeStyle = useMemo(() => ({ + color: currentFrame != null && currentFrame.keyframe ? primaryTextColor : undefined, + }), [currentFrame]); + const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode, exportConfirmEnabled } = useUserSettings(); const rotationStr = `${rotation}°`; @@ -414,7 +421,7 @@ function BottomBar({ size={25} role="button" title={t('Seek previous keyframe')} - style={{ flexShrink: 0, marginRight: 2, transform: mirrorTransform }} + style={{ flexShrink: 0, marginRight: 2, transform: mirrorTransform, ...keyframeStyle }} onClick={() => seekClosestKeyframe(-1)} /> @@ -446,7 +453,7 @@ function BottomBar({ )} frame.keyframe); } -export const findKeyframeAtExactTime = (keyframes: Keyframe[], time: number) => keyframes.find((keyframe) => Math.abs(keyframe.time - time) < 0.000001); -export const findNextKeyframe = (keyframes: Keyframe[], time: number) => keyframes.find((keyframe) => keyframe.time >= time); // (assume they are already sorted) -const findPreviousKeyframe = (keyframes: Keyframe[], time: number) => keyframes.findLast((keyframe) => keyframe.time <= time); -const findNearestKeyframe = (keyframes: Keyframe[], time: number) => minBy(keyframes, (keyframe) => Math.abs(keyframe.time - time)); +export const findKeyframeAtExactTime = (keyframes: Frame[], time: number) => keyframes.find((keyframe) => Math.abs(keyframe.time - time) < 0.000001); +export const findNextKeyframe = (keyframes: Frame[], time: number) => keyframes.find((keyframe) => keyframe.time >= time); // (assume they are already sorted) +const findPreviousKeyframe = (keyframes: Frame[], time: number) => keyframes.findLast((keyframe) => keyframe.time <= time); +const findNearestKeyframe = (keyframes: Frame[], time: number) => minBy(keyframes, (keyframe) => Math.abs(keyframe.time - time)); export type FindKeyframeMode = 'nearest' | 'before' | 'after'; -function findKeyframe(keyframes: Keyframe[], time: number, mode: FindKeyframeMode) { +function findKeyframe(keyframes: Frame[], time: number, mode: FindKeyframeMode) { switch (mode) { case 'nearest': { return findNearestKeyframe(keyframes, time); @@ -139,7 +136,7 @@ export async function findKeyframeNearTime({ filePath, streamIndex, time, mode } // todo this is not in use // https://stackoverflow.com/questions/14005110/how-to-split-a-video-using-ffmpeg-so-that-each-chunk-starts-with-a-key-frame // http://kicherer.org/joomla/index.php/de/blog/42-avcut-frame-accurate-video-cutting-with-only-small-quality-loss -export function getSafeCutTime(frames: (Frame & { time: number })[], cutTime: number, nextMode: boolean) { +export function getSafeCutTime(frames: Frame[], cutTime: number, nextMode: boolean) { const sigma = 0.01; const isCloseTo = (time1: number, time2: number) => Math.abs(time1 - time2) < sigma; diff --git a/src/renderer/src/hooks/useKeyframes.ts b/src/renderer/src/hooks/useKeyframes.ts index deaf3f0f..77c2db2d 100644 --- a/src/renderer/src/hooks/useKeyframes.ts +++ b/src/renderer/src/hooks/useKeyframes.ts @@ -4,6 +4,7 @@ import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out thi import { readFramesAroundTime, findNearestKeyFrameTime as ffmpegFindNearestKeyFrameTime, Frame } from '../ffmpeg'; import { FFprobeStream } from '../../../../ffprobe'; +import { getFrameCountRaw } from '../edlFormats'; const maxKeyframes = 1000; // const maxKeyframes = 100; @@ -20,6 +21,16 @@ function useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream, const [neighbouringKeyFramesMap, setNeighbouringKeyFrames] = useState>({}); const neighbouringKeyFrames = useMemo(() => Object.values(neighbouringKeyFramesMap), [neighbouringKeyFramesMap]); + const keyframeByNumber = useMemo(() => { + const map: Record = {}; + if (detectedFps != null) { + neighbouringKeyFrames.forEach((frame) => { + map[getFrameCountRaw(detectedFps, frame.time)!] = frame; + }); + } + return map; + }, [detectedFps, neighbouringKeyFrames]); + const findNearestKeyFrameTime = useCallback(({ time, direction }: { time: number, direction: number }) => ffmpegFindNearestKeyFrameTime({ frames: neighbouringKeyFrames, time, direction, fps: detectedFps }), [neighbouringKeyFrames, detectedFps]); useEffect(() => setNeighbouringKeyFrames({}), [filePath, videoStream]); @@ -64,7 +75,7 @@ function useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream, }, 500, [keyframesEnabled, filePath, commandedTime, videoStream, ffmpegExtractWindow]); return { - neighbouringKeyFrames, findNearestKeyFrameTime, + neighbouringKeyFrames, findNearestKeyFrameTime, keyframeByNumber, }; }