Improvements:

- Implement jump prev/next keyframe
- improve help screen
- improve menu
pull/276/head
Mikael Finstad 6 years ago
parent b9a924f275
commit 3b22fb3852

@ -23,38 +23,52 @@ const HelpSheet = memo(({
<h1>Keyboard shortcuts</h1> <h1>Keyboard shortcuts</h1>
<div><kbd>H</kbd> Show/hide this screen</div> <div><kbd>H</kbd> Show/hide this screen</div>
<h2>Playback</h2>
<div><kbd>SPACE</kbd>, <kbd>k</kbd> Play/pause</div> <div><kbd>SPACE</kbd>, <kbd>k</kbd> Play/pause</div>
<div><kbd>J</kbd> Slow down video</div> <div><kbd>J</kbd> Slow down playback</div>
<div><kbd>L</kbd> Speed up video</div> <div><kbd>L</kbd> Speed up playback</div>
<h2>Seeking</h2>
<div><kbd>,</kbd> Step backward 1 frame</div>
<div><kbd>.</kbd> Step forward 1 frame</div>
<div><kbd>ALT</kbd> / <kbd>OPT</kbd> + <kbd></kbd> Seek to previous keyframe</div>
<div><kbd>ALT</kbd> / <kbd>OPT</kbd> + <kbd></kbd> Seek to next keyframe</div>
<div><kbd></kbd> Seek backward 1 sec</div> <div><kbd></kbd> Seek backward 1 sec</div>
<div><kbd></kbd> Seek forward 1 sec</div> <div><kbd></kbd> Seek forward 1 sec</div>
<div><kbd>CTRL</kbd>/<kbd>CMD</kbd>+<kbd></kbd> Seek backward 1% of timeline (at current zoom)</div> <div><kbd>CTRL</kbd> / <kbd>CMD</kbd> + <kbd></kbd> Seek backward 1% of timeline at current zoom</div>
<div><kbd>CTRL</kbd>/<kbd>CMD</kbd>+<kbd></kbd> Seek forward 1% of timeline (at current zoom)</div> <div><kbd>CTRL</kbd> / <kbd>CMD</kbd> + <kbd></kbd> Seek forward 1% of timeline at current zoom</div>
<div><kbd>,</kbd> Seek backward 1 frame</div>
<div><kbd>.</kbd> Seek forward 1 frame</div> <h2>Segments and cut points</h2>
<div><kbd>I</kbd> Mark in / cut start point</div> <div><kbd>I</kbd> Mark in / cut start point for current segment</div>
<div><kbd>O</kbd> Mark out / cut end point</div> <div><kbd>O</kbd> Mark out / cut end point for current segment</div>
<div><kbd>E</kbd> Cut (export selection in the same directory)</div>
<div><kbd>C</kbd> Capture snapshot (in the same directory)</div>
<div><kbd>+</kbd> Add cut segment</div> <div><kbd>+</kbd> Add cut segment</div>
<div><kbd>BACKSPACE</kbd> Remove current cut segment</div> <div><kbd>BACKSPACE</kbd> Remove current segment</div>
<h2>File system actions</h2>
<div><kbd>E</kbd> Export segment(s)</div>
<div><kbd>C</kbd> Capture snapshot</div>
<div><kbd>D</kbd> Delete source file</div> <div><kbd>D</kbd> Delete source file</div>
<h1>Mouse actions</h1> <h1>Mouse actions</h1>
<div><i>Mouse scroll up/down/left/right</i> - Seek / pan timeline</div> <div><i>Mouse scroll up/down/left/right</i> - Seek / pan timeline</div>
<div><kbd>CTRL</kbd><i>+Mouse scroll up/down</i> - Zoom in/out timeline</div> <div><kbd>CTRL</kbd><i> + Mouse scroll up/down</i> - Zoom in/out timeline</div>
<p style={{ fontWeight: 'bold' }}>Hover mouse over buttons in the main interface to see which function they have.</p> <p style={{ fontWeight: 'bold' }}>Hover mouse over buttons in the main interface to see which function they have.</p>
<h1 style={{ marginTop: 40 }}>Last ffmpeg commands</h1> <h1 style={{ marginTop: 40 }}>Last ffmpeg commands</h1>
<div style={{ overflowY: 'scroll', height: 200 }}> {ffmpegCommandLog.length > 0 ? (
{ffmpegCommandLog.reverse().map(({ command }, i) => ( <div style={{ overflowY: 'scroll', height: 200 }}>
// eslint-disable-next-line react/no-array-index-key {ffmpegCommandLog.reverse().map(({ command }, i) => (
<div key={i} style={{ whiteSpace: 'pre', margin: '5px 0' }}> // eslint-disable-next-line react/no-array-index-key
<FaClipboard style={{ cursor: 'pointer' }} title="Copy to clipboard" onClick={() => { clipboard.writeText(command); toast.fire({ timer: 2000, icon: 'success', title: 'Copied to clipboard' }); }} /> {command} <div key={i} style={{ whiteSpace: 'pre', margin: '5px 0' }}>
</div> <FaClipboard style={{ cursor: 'pointer' }} title="Copy to clipboard" onClick={() => { clipboard.writeText(command); toast.fire({ timer: 2000, icon: 'success', title: 'Copied to clipboard' }); }} /> {command}
))} </div>
</div> ))}
</div>
) : (
<p>Here the last run ffmpeg commands will show up after you ran an operation. You can copy them to clipboard and modify them to your needs before running on your command line.</p>
)}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

@ -120,7 +120,7 @@ async function readFrames({ filePath, aroundTime, window, stream }) {
// https://stackoverflow.com/questions/14005110/how-to-split-a-video-using-ffmpeg-so-that-each-chunk-starts-with-a-key-frame // 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 // http://kicherer.org/joomla/index.php/de/blog/42-avcut-frame-accurate-video-cutting-with-only-small-quality-loss
function getNextPrevKeyframe(frames, cutTime, nextMode) { function getSafeCutTime(frames, cutTime, nextMode) {
const sigma = 0.01; const sigma = 0.01;
const isCloseTo = (time1, time2) => Math.abs(time1 - time2) < sigma; const isCloseTo = (time1, time2) => Math.abs(time1 - time2) < sigma;
@ -167,6 +167,15 @@ function getNextPrevKeyframe(frames, cutTime, nextMode) {
return frames[index - 1].time; return frames[index - 1].time;
} }
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;
}
async function cut({ async function cut({
filePath, outFormat, cutFrom, cutTo, videoDuration, rotation, filePath, outFormat, cutFrom, cutTo, videoDuration, rotation,
onProgress, copyStreamIds, keyframeCut, outPath, appendFfmpegCommandLog, shortestFlag, onProgress, copyStreamIds, keyframeCut, outPath, appendFfmpegCommandLog, shortestFlag,
@ -632,7 +641,8 @@ module.exports = {
isCuttingStart, isCuttingStart,
isCuttingEnd, isCuttingEnd,
readFrames, readFrames,
getNextPrevKeyframe, getSafeCutTime,
findNearestKeyFrameTime,
renderWaveformPng, renderWaveformPng,
renderThumbnails, renderThumbnails,
}; };

@ -33,6 +33,14 @@ body {
.help-sheet h1 { .help-sheet h1 {
font-size: 1.2em; font-size: 1.2em;
margin-bottom: .5em;
margin-top: 1.3em;
text-transform: uppercase;
}
.help-sheet h2 {
font-size: 1em;
margin-bottom: .5em;
text-transform: uppercase; text-transform: uppercase;
} }

@ -29,6 +29,7 @@ module.exports = (app, mainWindow, newVersion) => {
mainWindow.webContents.send('close-file'); mainWindow.webContents.send('close-file');
}, },
}, },
{ type: 'separator' },
{ {
label: 'Import CSV cut file', label: 'Import CSV cut file',
click() { click() {
@ -41,6 +42,7 @@ module.exports = (app, mainWindow, newVersion) => {
mainWindow.webContents.send('exportEdlFile'); mainWindow.webContents.send('exportEdlFile');
}, },
}, },
{ type: 'separator' },
{ {
label: 'Convert to friendly format (fastest)', label: 'Convert to friendly format (fastest)',
click() { click() {
@ -65,6 +67,7 @@ module.exports = (app, mainWindow, newVersion) => {
mainWindow.webContents.send('html5ify', 'slow-audio'); mainWindow.webContents.send('html5ify', 'slow-audio');
}, },
}, },
{ type: 'separator' },
{ {
label: 'Extract all streams', label: 'Extract all streams',
click() { click() {

@ -178,8 +178,8 @@ const App = memo(() => {
}, 500, [filePath, commandedTime, duration, zoom, waveformEnabled, mainAudioStream]); }, 500, [filePath, commandedTime, duration, zoom, waveformEnabled, mainAudioStream]);
const [, cancelReadKeyframeDataDebounce] = useDebounce(() => { const [, cancelReadKeyframeDataDebounce] = useDebounce(() => {
setDebouncedReadKeyframesData({ keyframesEnabled, filePath, commandedTime, duration, zoom, mainVideoStream }); setDebouncedReadKeyframesData({ keyframesEnabled, filePath, commandedTime, mainVideoStream });
}, 500, [keyframesEnabled, filePath, commandedTime, duration, zoom, mainVideoStream]); }, 500, [keyframesEnabled, filePath, commandedTime, mainVideoStream]);
// Preferences // Preferences
const [captureFormat, setCaptureFormat] = useState(configStore.get('captureFormat')); const [captureFormat, setCaptureFormat] = useState(configStore.get('captureFormat'));
@ -450,7 +450,7 @@ const App = memo(() => {
currentTimeRef.current = playing ? playerTime : commandedTime; currentTimeRef.current = playing ? playerTime : commandedTime;
}, [commandedTime, playerTime, playing]); }, [commandedTime, playerTime, playing]);
// const getNextPrevKeyframe = useCallback((cutTime, next) => ffmpeg.getNextPrevKeyframe(neighbouringFrames, cutTime, next), [neighbouringFrames]); // const getSafeCutTime = useCallback((cutTime, next) => ffmpeg.getSafeCutTime(neighbouringFrames, cutTime, next), [neighbouringFrames]);
const addCutSegment = useCallback(() => { const addCutSegment = useCallback(() => {
try { try {
@ -459,7 +459,7 @@ const App = memo(() => {
const suggestedStart = currentTimeRef.current; const suggestedStart = currentTimeRef.current;
/* if (keyframeCut) { /* if (keyframeCut) {
const keyframeAlignedStart = getNextPrevKeyframe(suggestedStart, true); const keyframeAlignedStart = getSafeCutTime(suggestedStart, true);
if (keyframeAlignedStart != null) suggestedStart = keyframeAlignedStart; if (keyframeAlignedStart != null) suggestedStart = keyframeAlignedStart;
} */ } */
@ -467,7 +467,7 @@ const App = memo(() => {
if (suggestedEnd >= duration) { if (suggestedEnd >= duration) {
suggestedEnd = undefined; suggestedEnd = undefined;
} /* else if (keyframeCut) { } /* else if (keyframeCut) {
const keyframeAlignedEnd = getNextPrevKeyframe(suggestedEnd, false); const keyframeAlignedEnd = getSafeCutTime(suggestedEnd, false);
if (keyframeAlignedEnd != null) suggestedEnd = keyframeAlignedEnd; if (keyframeAlignedEnd != null) suggestedEnd = keyframeAlignedEnd;
} */ } */
@ -498,7 +498,7 @@ const App = memo(() => {
try { try {
const startTime = currentTimeRef.current; const startTime = currentTimeRef.current;
/* if (keyframeCut) { /* if (keyframeCut) {
const keyframeAlignedCutTo = getNextPrevKeyframe(startTime, true); const keyframeAlignedCutTo = getSafeCutTime(startTime, true);
if (keyframeAlignedCutTo != null) startTime = keyframeAlignedCutTo; if (keyframeAlignedCutTo != null) startTime = keyframeAlignedCutTo;
} */ } */
setCutTime('start', startTime); setCutTime('start', startTime);
@ -513,7 +513,7 @@ const App = memo(() => {
const endTime = currentTimeRef.current; const endTime = currentTimeRef.current;
/* if (keyframeCut) { /* if (keyframeCut) {
const keyframeAlignedCutTo = getNextPrevKeyframe(endTime, false); const keyframeAlignedCutTo = getSafeCutTime(endTime, false);
if (keyframeAlignedCutTo != null) endTime = keyframeAlignedCutTo; if (keyframeAlignedCutTo != null) endTime = keyframeAlignedCutTo;
} */ } */
setCutTime('end', endTime); setCutTime('end', endTime);
@ -727,7 +727,8 @@ const App = memo(() => {
useEffect(() => { useEffect(() => {
async function run() { async function run() {
const d = debouncedReadKeyframesData; const d = debouncedReadKeyframesData;
if (!d || !d.keyframesEnabled || !d.filePath || !d.mainVideoStream || d.commandedTime == null || !calcShouldShowKeyframes(d.duration, d.zoom) || readingKeyframesPromise.current) return; // We still want to calculate keyframes even if not shouldShowKeyframes because maybe we want to step to closest keyframe
if (!d || !d.keyframesEnabled || !d.filePath || !d.mainVideoStream || d.commandedTime == null || readingKeyframesPromise.current) return;
try { try {
const promise = ffmpeg.readFrames({ filePath: d.filePath, aroundTime: d.commandedTime, stream: d.mainVideoStream.index, window: ffmpegExtractWindow }); const promise = ffmpeg.readFrames({ filePath: d.filePath, aroundTime: d.commandedTime, stream: d.mainVideoStream.index, window: ffmpegExtractWindow });
@ -1046,6 +1047,12 @@ const App = memo(() => {
const jumpSeg = useCallback((val) => setCurrentSegIndex((old) => Math.max(Math.min(old + val, cutSegments.length - 1), 0)), [cutSegments.length]); const jumpSeg = useCallback((val) => setCurrentSegIndex((old) => Math.max(Math.min(old + val, cutSegments.length - 1), 0)), [cutSegments.length]);
const seekClosestKeyframe = useCallback((direction) => {
const time = ffmpeg.findNearestKeyFrameTime({ frames: neighbouringFrames, time: commandedTime, direction, fps: detectedFps });
if (time == null) return;
seekAbs(time);
}, [commandedTime, neighbouringFrames, seekAbs, detectedFps]);
useEffect(() => { useEffect(() => {
Mousetrap.bind('space', () => playCommand()); Mousetrap.bind('space', () => playCommand());
Mousetrap.bind('k', () => playCommand()); Mousetrap.bind('k', () => playCommand());
@ -1055,6 +1062,8 @@ const App = memo(() => {
Mousetrap.bind('right', () => seekRel(1)); Mousetrap.bind('right', () => seekRel(1));
Mousetrap.bind(['ctrl+left', 'command+left'], () => { seekRelPercent(-0.01); return false; }); Mousetrap.bind(['ctrl+left', 'command+left'], () => { seekRelPercent(-0.01); return false; });
Mousetrap.bind(['ctrl+right', 'command+right'], () => { seekRelPercent(0.01); return false; }); Mousetrap.bind(['ctrl+right', 'command+right'], () => { seekRelPercent(0.01); return false; });
Mousetrap.bind('alt+left', () => seekClosestKeyframe(-1));
Mousetrap.bind('alt+right', () => seekClosestKeyframe(1));
Mousetrap.bind('up', () => jumpSeg(-1)); Mousetrap.bind('up', () => jumpSeg(-1));
Mousetrap.bind('down', () => jumpSeg(1)); Mousetrap.bind('down', () => jumpSeg(1));
Mousetrap.bind('.', () => shortStep(1)); Mousetrap.bind('.', () => shortStep(1));
@ -1077,6 +1086,8 @@ const App = memo(() => {
Mousetrap.unbind('right'); Mousetrap.unbind('right');
Mousetrap.unbind(['ctrl+left', 'command+left']); Mousetrap.unbind(['ctrl+left', 'command+left']);
Mousetrap.unbind(['ctrl+right', 'command+right']); Mousetrap.unbind(['ctrl+right', 'command+right']);
Mousetrap.unbind('alt+left');
Mousetrap.unbind('alt+right');
Mousetrap.unbind('up'); Mousetrap.unbind('up');
Mousetrap.unbind('down'); Mousetrap.unbind('down');
Mousetrap.unbind('.'); Mousetrap.unbind('.');
@ -1093,6 +1104,7 @@ const App = memo(() => {
}, [ }, [
addCutSegment, capture, changePlaybackRate, cutClick, playCommand, removeCutSegment, addCutSegment, capture, changePlaybackRate, cutClick, playCommand, removeCutSegment,
setCutEnd, setCutStart, seekRel, seekRelPercent, shortStep, deleteSource, jumpSeg, toggleHelp, setCutEnd, setCutStart, seekRel, seekRelPercent, shortStep, deleteSource, jumpSeg, toggleHelp,
seekClosestKeyframe,
]); ]);
useEffect(() => { useEffect(() => {

@ -137,7 +137,6 @@ function getSegColors(seg) {
}; };
} }
module.exports = { module.exports = {
formatDuration, formatDuration,
parseDuration, parseDuration,

Loading…
Cancel
Save