From 207650ade9ca0f7e1f07bc8053eca0cda74edb3a Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sat, 21 Oct 2017 19:19:44 +0200 Subject: [PATCH] Implementation of cutting on keyframes not working, because output videos get bugged --- src/ffmpeg.js | 50 +++++++++++++++++++++++++++++++++++++++++++++--- src/main.css | 12 ++++++++---- src/renderer.jsx | 43 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/ffmpeg.js b/src/ffmpeg.js index f712b10..24709db 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -55,7 +55,7 @@ function handleProgress(process, cutDuration, onProgress) { }); } -async function cut(customOutDir, filePath, format, cutFrom, cutTo, onProgress) { +async function cut(customOutDir, filePath, format, cutFrom, cutTo, onProgress, cutArgsFirst) { const extWithoutDot = path.extname(filePath) || `.${format}`; const ext = `.${extWithoutDot}`; const duration = `${util.formatDuration(cutFrom)}-${util.formatDuration(cutTo)}`; @@ -64,14 +64,24 @@ async function cut(customOutDir, filePath, format, cutFrom, cutTo, onProgress) { console.log('Cutting from', cutFrom, 'to', cutTo); - const ffmpegArgs = [ + // https://github.com/mifi/lossless-cut/pull/13 + const ffmpegCutArgs = ['-ss', cutFrom]; + const ffmpegArgs1 = [ '-i', filePath, '-y', '-vcodec', 'copy', '-acodec', 'copy', - '-ss', cutFrom, '-t', cutTo - cutFrom, + ]; + const ffmpegArgs2 = [ + '-t', cutTo - cutFrom, + // '-to', cutTo - cutFrom, '-map_metadata', '0', '-f', format, + '-avoid_negative_ts', 'make_zero', outPath, ]; + const ffmpegArgs = cutArgsFirst + ? [...ffmpegCutArgs, ...ffmpegArgs1, ...ffmpegArgs2] + : [...ffmpegArgs1, ...ffmpegCutArgs, ...ffmpegArgs2]; + console.log('ffmpeg', ffmpegArgs.join(' ')); onProgress(0); @@ -134,8 +144,42 @@ function getFormat(filePath) { }); } +function handleKeyFramesProcess(process, onKeyFrame) { + const rl = readline.createInterface({ input: process.stdout }); + rl.on('line', (line) => { + try { + // console.log(line); + // const match = line.match(/^packet,([.\d]+),K/); + // const match = line.match(/^frame,1,([.\d]+),./); + const match = line.match(/^frame,1,([.\d]+),./); + if (!match) return; + + const time = parseFloat(match[1]); + if (!isNaN(time)) onKeyFrame(time); + } catch (err) { + console.log('Failed to parse ffprobe keyframe line', err); + } + }); +} +async function getKeyFrames(filePath, onKeyFrame) { + console.log('Getting keyframes'); + const ffmpegPath = await getFfmpegPath(); + const ffprobePath = path.join(path.dirname(ffmpegPath), getWithExt('ffprobe')); + + // const args = ['-show_packets', '-show_entries', 'packet=pts_time,flags', + // '-of', 'csv', filePath]; + + // const args = ['-select_streams', 'v', '-show_frames', '-skip_frame', 'nokey', '-show_entries', 'frame=key_frame,pict_type,pkt_dts_time', '-of', 'csv', filePath]; + const args = ['-select_streams', 'v', '-show_frames', '-show_entries', 'frame=coded_picture_number,key_frame,pict_type,pkt_dts_time', '-of', 'csv', filePath]; + const process = execa(ffprobePath, args); + handleKeyFramesProcess(process, onKeyFrame); + return process; // promise + // process.then(result => console.log(result.stdout)); +} + module.exports = { cut, getFormat, showFfmpegFail, + getKeyFrames, }; diff --git a/src/main.css b/src/main.css index 79ed56b..6e201be 100644 --- a/src/main.css +++ b/src/main.css @@ -72,7 +72,7 @@ input, button, textarea, :focus { #current-time-display { text-align: center; - color: rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.7); padding: .5em; } @@ -82,22 +82,26 @@ input, button, textarea, :focus { background-color: #444; } -.timeline-wrapper .current-time, .timeline-wrapper .cut-start-time { +.timeline-wrapper .current-time, .timeline-wrapper .cut-start-time, .timeline-wrapper .keyframe-marker { position: absolute; bottom: 0; top: 0; width: 1px; } +.timeline-wrapper .keyframe-marker { + background-color: rgba(0, 0, 0, 0.3); + z-index: 1; +} .timeline-wrapper .current-time { background-color: red; - z-index: 2; + z-index: 3; } .timeline-wrapper .cut-start-time { background-color: rgba(0, 0, 0, 0.3); border-left: 1px solid black; border-right: 1px solid black; - z-index: 1; + z-index: 2; } #working { diff --git a/src/renderer.jsx b/src/renderer.jsx index 5a2f6ac..fec3a7c 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -92,6 +92,8 @@ class App extends React.Component { cutEndTime: undefined, fileFormat: undefined, captureFormat: 'jpeg', + keyframes: [], + keyframesLoaded: false, }; this.state = _.cloneDeep(defaultState); @@ -103,6 +105,23 @@ class App extends React.Component { this.setState(defaultState); }; + const loadKeyframes = async (filePath) => { + // TODO cancel already running process + this.setState({ keyframesLoaded: false }); + try { + ffmpeg.getKeyFrames(filePath, (time) => { + console.log('keyframe', time); + if (this.state.keyframes.includes(time)) return; + this.setState(state => ({ keyframes: state.keyframes.concat(time) })); + }); + console.log('Done reading keyframes'); + } catch (err) { + console.error('Failed to read keyframes', err); + } finally { + this.setState({ keyframesLoaded: true }); + } + }; + const load = (filePath) => { console.log('Load', filePath); if (this.state.working) return alert('I\'m busy'); @@ -114,6 +133,9 @@ class App extends React.Component { return ffmpeg.getFormat(filePath) .then((fileFormat) => { if (!fileFormat) return alert('Unsupported file'); + + loadKeyframes(filePath); + setFileNameTitle(filePath); return this.setState({ filePath, fileFormat }); }) @@ -144,6 +166,7 @@ class App extends React.Component { keyboardJs.bind('k', () => this.playCommand()); keyboardJs.bind('j', () => this.changePlaybackRate(-1)); keyboardJs.bind('l', () => this.changePlaybackRate(1)); + keyboardJs.bind('m', () => this.snapToKeyFrame()); keyboardJs.bind('left', () => seekRel(-1)); keyboardJs.bind('right', () => seekRel(1)); keyboardJs.bind('period', () => shortStep(1)); @@ -175,11 +198,11 @@ class App extends React.Component { } setCutStart() { - this.setState({ cutStartTime: this.state.currentTime }); + this.setState({ cutStartTime: this.findNearestKeyFrame(this.state.currentTime) }); } setCutEnd() { - this.setState({ cutEndTime: this.state.currentTime }); + this.setState({ cutEndTime: this.findNearestKeyFrame(this.state.currentTime) }); } setOutputDir() { @@ -205,6 +228,16 @@ class App extends React.Component { seekAbs(this.state.cutEndTime); } + findNearestKeyFrame(time) { + return this.state.keyframes.sort((a, b) => Math.abs(time - a) - Math.abs(time - b))[0]; + } + + snapToKeyFrame() { + const currentTime = getVideo().currentTime; + const nearestKeyFrame = this.findNearestKeyFrame(currentTime); + seekAbs(nearestKeyFrame); + } + handlePan(e) { _.throttle(e2 => this.handleTap(e2), 200)(e); } @@ -267,6 +300,7 @@ class App extends React.Component { cutStartTime, cutEndTime, progress => this.onCutProgress(progress), + true, ); } catch (err) { console.error('stdout:', err.stdout); @@ -329,6 +363,11 @@ class App extends React.Component { >
+ + {this.state.keyframes.map(keyframeTime => ( +
+ ))} +