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>
<div><kbd>H</kbd> Show/hide this screen</div>
<h2>Playback</h2>
<div><kbd>SPACE</kbd>, <kbd>k</kbd> Play/pause</div>
<div><kbd>J</kbd> Slow down video</div>
<div><kbd>L</kbd> Speed up video</div>
<div><kbd>J</kbd> Slow down playback</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 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 forward 1% of timeline (at current zoom)</div>
<div><kbd>,</kbd> Seek backward 1 frame</div>
<div><kbd>.</kbd> Seek forward 1 frame</div>
<div><kbd>I</kbd> Mark in / cut start point</div>
<div><kbd>O</kbd> Mark out / cut end point</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>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>
<h2>Segments and cut points</h2>
<div><kbd>I</kbd> Mark in / cut start point for current segment</div>
<div><kbd>O</kbd> Mark out / cut end point for current 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>
<h1>Mouse actions</h1>
<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>
<h1 style={{ marginTop: 40 }}>Last ffmpeg commands</h1>
<div style={{ overflowY: 'scroll', height: 200 }}>
{ffmpegCommandLog.reverse().map(({ command }, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} style={{ whiteSpace: 'pre', margin: '5px 0' }}>
<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>
{ffmpegCommandLog.length > 0 ? (
<div style={{ overflowY: 'scroll', height: 200 }}>
{ffmpegCommandLog.reverse().map(({ command }, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} style={{ whiteSpace: 'pre', margin: '5px 0' }}>
<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>
) : (
<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>
)}
</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
// 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 isCloseTo = (time1, time2) => Math.abs(time1 - time2) < sigma;
@ -167,6 +167,15 @@ function getNextPrevKeyframe(frames, cutTime, nextMode) {
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({
filePath, outFormat, cutFrom, cutTo, videoDuration, rotation,
onProgress, copyStreamIds, keyframeCut, outPath, appendFfmpegCommandLog, shortestFlag,
@ -632,7 +641,8 @@ module.exports = {
isCuttingStart,
isCuttingEnd,
readFrames,
getNextPrevKeyframe,
getSafeCutTime,
findNearestKeyFrameTime,
renderWaveformPng,
renderThumbnails,
};

@ -33,6 +33,14 @@ body {
.help-sheet h1 {
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;
}

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

@ -178,8 +178,8 @@ const App = memo(() => {
}, 500, [filePath, commandedTime, duration, zoom, waveformEnabled, mainAudioStream]);
const [, cancelReadKeyframeDataDebounce] = useDebounce(() => {
setDebouncedReadKeyframesData({ keyframesEnabled, filePath, commandedTime, duration, zoom, mainVideoStream });
}, 500, [keyframesEnabled, filePath, commandedTime, duration, zoom, mainVideoStream]);
setDebouncedReadKeyframesData({ keyframesEnabled, filePath, commandedTime, mainVideoStream });
}, 500, [keyframesEnabled, filePath, commandedTime, mainVideoStream]);
// Preferences
const [captureFormat, setCaptureFormat] = useState(configStore.get('captureFormat'));
@ -450,7 +450,7 @@ const App = memo(() => {
currentTimeRef.current = playing ? playerTime : commandedTime;
}, [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(() => {
try {
@ -459,7 +459,7 @@ const App = memo(() => {
const suggestedStart = currentTimeRef.current;
/* if (keyframeCut) {
const keyframeAlignedStart = getNextPrevKeyframe(suggestedStart, true);
const keyframeAlignedStart = getSafeCutTime(suggestedStart, true);
if (keyframeAlignedStart != null) suggestedStart = keyframeAlignedStart;
} */
@ -467,7 +467,7 @@ const App = memo(() => {
if (suggestedEnd >= duration) {
suggestedEnd = undefined;
} /* else if (keyframeCut) {
const keyframeAlignedEnd = getNextPrevKeyframe(suggestedEnd, false);
const keyframeAlignedEnd = getSafeCutTime(suggestedEnd, false);
if (keyframeAlignedEnd != null) suggestedEnd = keyframeAlignedEnd;
} */
@ -498,7 +498,7 @@ const App = memo(() => {
try {
const startTime = currentTimeRef.current;
/* if (keyframeCut) {
const keyframeAlignedCutTo = getNextPrevKeyframe(startTime, true);
const keyframeAlignedCutTo = getSafeCutTime(startTime, true);
if (keyframeAlignedCutTo != null) startTime = keyframeAlignedCutTo;
} */
setCutTime('start', startTime);
@ -513,7 +513,7 @@ const App = memo(() => {
const endTime = currentTimeRef.current;
/* if (keyframeCut) {
const keyframeAlignedCutTo = getNextPrevKeyframe(endTime, false);
const keyframeAlignedCutTo = getSafeCutTime(endTime, false);
if (keyframeAlignedCutTo != null) endTime = keyframeAlignedCutTo;
} */
setCutTime('end', endTime);
@ -727,7 +727,8 @@ const App = memo(() => {
useEffect(() => {
async function run() {
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 {
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 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(() => {
Mousetrap.bind('space', () => playCommand());
Mousetrap.bind('k', () => playCommand());
@ -1055,6 +1062,8 @@ const App = memo(() => {
Mousetrap.bind('right', () => seekRel(1));
Mousetrap.bind(['ctrl+left', 'command+left'], () => { 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('down', () => jumpSeg(1));
Mousetrap.bind('.', () => shortStep(1));
@ -1077,6 +1086,8 @@ const App = memo(() => {
Mousetrap.unbind('right');
Mousetrap.unbind(['ctrl+left', 'command+left']);
Mousetrap.unbind(['ctrl+right', 'command+right']);
Mousetrap.unbind('alt+left');
Mousetrap.unbind('alt+right');
Mousetrap.unbind('up');
Mousetrap.unbind('down');
Mousetrap.unbind('.');
@ -1093,6 +1104,7 @@ const App = memo(() => {
}, [
addCutSegment, capture, changePlaybackRate, cutClick, playCommand, removeCutSegment,
setCutEnd, setCutStart, seekRel, seekRelPercent, shortStep, deleteSource, jumpSeg, toggleHelp,
seekClosestKeyframe,
]);
useEffect(() => {

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

Loading…
Cancel
Save