kopia lustrzana https://github.com/mifi/lossless-cut
Implementation of cutting on keyframes
not working, because output videos get buggedcut-on-keyframe
rodzic
815fbc082a
commit
207650ade9
|
@ -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,
|
||||
};
|
||||
|
|
12
src/main.css
12
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 {
|
||||
|
|
|
@ -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={{
|
||||
|
|
Ładowanie…
Reference in New Issue