Implementation of cutting on keyframes

not working, because output videos get bugged
cut-on-keyframe
Mikael Finstad 2017-10-21 19:19:44 +02:00
rodzic 815fbc082a
commit 207650ade9
3 zmienionych plików z 96 dodań i 9 usunięć

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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 {
>
<div className="timeline-wrapper">
<div className="current-time" style={{ left: `${((this.state.currentTime || 0) / (this.state.duration || 1)) * 100}%` }} />
{this.state.keyframes.map(keyframeTime => (
<div key={keyframeTime.toString()} className="keyframe-marker" style={{ left: `${((keyframeTime || 0) / (this.state.duration || 1)) * 100}%` }} />
))}
<div
className="cut-start-time"
style={{