1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-21 18:02:35 +01:00

Adds support for more formats by detecting format and using same format for output file (don't always use mp4)

Also:
Improve ffmpeg error handling
React enable production
Improve react rendering a bit
This commit is contained in:
Mikael Finstad 2016-11-03 22:31:13 +01:00
parent 3851cb264f
commit cce542af41
5 changed files with 119 additions and 43 deletions

View File

@ -1,5 +1,5 @@
# LosslessCut 🎥 [![Travis](https://img.shields.io/travis/mifi/lossless-cut.svg)]()
Simple, cross platform video editor for lossless trimming / cutting of videos. Great for rough processing of large video files taken from a video camera, GoPro, drone, etc. Lets you quickly extract the good parts from your videos. It doesn't do any decoding / encoding and is therefore extremely fast and has no quality loss. Also allows for taking JPEG snapshots of the video at the selected time. This app uses the awesome ffmpeg🙏 for doing the grunt work. ffmpeg is not included and must be installed separately.
Simple, cross platform video editor for lossless trimming / cutting of videos. Great for rough processing of large video files taken from a video camera, GoPro, drone, etc. Lets you quickly extract the good parts from your videos. It doesn't do any decoding / encoding and is therefore extremely fast and has no quality loss. Also allows for taking JPEG snapshots of the video at the selected time. This app uses the awesome ffmpeg🙏 for doing the grunt work. ffmpeg is not included and must be installed separately. Also supports lossless cutting in the most common audio formats.
![Demo](demo.gif)
@ -38,7 +38,7 @@ The original video files will not be modified. Instead it creates a lossless exp
## Development building / running
This app is made using Electron. [electron-compile](https://github.com/electron/electron-compile) is used for development. Make sure you have at least node v4 with npm 3.
This app is made using Electron. Make sure you have at least node v4 with npm 3.
```
git clone https://github.com/mifi/lossless-cut.git
cd lossless-cut
@ -46,6 +46,11 @@ npm install
```
### Running
In one terminal:
```
npm run watch
```
Then:
```
npm start
```
@ -59,14 +64,12 @@ npm run package
## TODO / ideas
- About menu
- icon
- Visual feedback on button presses
- ffprobe show keyframes
- ffprobe format
- improve ffmpeg error handling
- Slow scrub with modifier key
- show frame number
- Bundle ffmpeg
- support for loading other formats by streaming through ffmpeg?
- Visual feedback on button presses
- support for previewing other formats by streaming through ffmpeg?
- Slow scrub with modifier key
- show frame number (approx?)
- ffprobe show keyframes
- cutting out the commercials in a video file while saving the rest to a single file?
## Links

View File

@ -1,25 +1,74 @@
const execa = require('execa');
const bluebird = require('bluebird');
const which = bluebird.promisify(require('which'));
const path = require('path');
const util = require('./util');
const Configstore = require('configstore');
const configstore = new Configstore('lossless-cut', { ffmpegPath: '' });
module.exports.cut = (filePath, cutFrom, cutTo, outFile) => {
function showFfmpegFail(err) {
alert('Failed to run ffmpeg, make sure you have it installed and in available in your PATH or set its path (from the file menu)');
console.error(err.stack);
}
function getFfmpegPath() {
return which('ffmpeg')
.catch(() => configstore.get('ffmpegPath'));
}
function cut(filePath, format, cutFrom, cutTo) {
const ext = path.extname(filePath) || format;
const outFileAppend = `${util.formatDuration(cutFrom)}-${util.formatDuration(cutTo)}`;
const outFile = `${filePath}-${outFileAppend}.${ext}`;
console.log('Cutting from', cutFrom, 'to', cutTo);
return which('ffmpeg')
.catch(() => configstore.get('ffmpegPath'))
.then(ffmpegPath => execa(ffmpegPath, [
'-i', filePath, '-y', '-vcodec', 'copy', '-acodec', 'copy', '-ss', cutFrom, '-t', cutTo - cutFrom, outFile,
]))
const ffmpegArgs = [
'-i', filePath, '-y', '-vcodec', 'copy', '-acodec', 'copy',
'-ss', cutFrom, '-t', cutTo - cutFrom,
'-f', format,
outFile,
];
console.log('ffmpeg', ffmpegArgs.join(' '));
return getFfmpegPath()
.then(ffmpegPath => execa(ffmpegPath, ffmpegArgs))
.then((result) => {
console.log(result.stdout);
})
.catch((err) => {
console.error(err.stack);
alert(`Failed to run ffmpeg, make sure you have it installed and in available in your PATH or its path configured in ${configstore.path}`);
if (err.code === 1) {
alert('Whoops! ffmpeg was unable to cut this video. It may be of an unknown format or codec combination');
return;
}
showFfmpegFail(err);
});
}
function getFormats(filePath) {
console.log('getFormat', filePath);
return getFfmpegPath()
.then(ffmpegPath => path.join(path.dirname(ffmpegPath), 'ffprobe'))
.then(ffprobePath => execa(ffprobePath, [
'-of', 'json', '-show_format', '-i', filePath,
]))
.then((result) => {
const formatsStr = JSON.parse(result.stdout).format.format_name;
console.log('formats', formatsStr);
const formats = formatsStr.split(',');
return formats;
});
}
// '-of', 'json', '-select_streams', 'v', '-show_frames', filePath,
module.exports = {
cut,
getFormats,
showFfmpegFail,
};

View File

@ -10,6 +10,10 @@ const BrowserWindow = electron.BrowserWindow;
const dialog = electron.dialog;
const configstore = new Configstore('lossless-cut');
// http://stackoverflow.com/questions/39362292/how-do-i-set-node-env-production-on-electron-app-when-packaged-with-electron-pac
const isProd = process.execPath.search('electron-prebuilt') === -1;
if (isProd) process.env.NODE_ENV = 'production';
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

View File

@ -1,7 +1,6 @@
const electron = require('electron'); // eslint-disable-line
const $ = require('jquery');
const keyboardJs = require('keyboardjs');
const ffmpeg = require('./ffmpeg');
const _ = require('lodash');
const captureFrame = require('capture-frame');
const fs = require('fs');
@ -11,19 +10,8 @@ const React = require('react');
const ReactDOM = require('react-dom');
const classnames = require('classnames');
function formatDuration(_seconds) {
const seconds = _seconds || 0;
const minutes = seconds / 60;
const hours = minutes / 60;
const hoursPadded = _.padStart(Math.floor(hours), 2, '0');
const minutesPadded = _.padStart(Math.floor(minutes % 60), 2, '0');
const secondsPadded = _.padStart(Math.floor(seconds) % 60, 2, '0');
const msPadded = _.padStart(Math.floor((seconds - Math.floor(seconds)) * 1000), 3, '0');
// Be nice to filenames and use .
return `${hoursPadded}.${minutesPadded}.${secondsPadded}.${msPadded}`;
}
const ffmpeg = require('./ffmpeg');
const util = require('./util');
function getVideo() {
return $('#player video')[0];
@ -63,6 +51,7 @@ class App extends React.Component {
duration: undefined,
cutStartTime: 0,
cutEndTime: undefined,
fileFormat: undefined,
};
this.state = _.cloneDeep(defaultState);
@ -76,7 +65,19 @@ class App extends React.Component {
const load = (filePath) => {
resetState();
this.setState({ filePath });
ffmpeg.getFormats(filePath)
.then((formats) => {
if (formats.length < 1) return alert('Unsupported file');
return this.setState({ filePath, fileFormat: formats[0] });
})
.catch((err) => {
if (err.code === 1) {
alert('Unsupported file');
return;
}
ffmpeg.showFfmpegFail(err);
});
};
electron.ipcRenderer.on('file-opened', (event, message) => {
@ -180,17 +181,14 @@ class App extends React.Component {
}
this.setState({ working: true });
const ext = 'mp4';
const outFileAppend = `${formatDuration(cutStartTime)}-${formatDuration(cutEndTime)}`;
const outPath = `${filePath}-${outFileAppend}.${ext}`;
return ffmpeg.cut(filePath, cutStartTime, cutEndTime, outPath)
return ffmpeg.cut(filePath, this.state.fileFormat, cutStartTime, cutEndTime)
.finally(() => this.setState({ working: false }));
}
capture() {
if (!this.state.filePath) return;
const buf = captureFrame(getVideo(), 'jpg');
const outPath = `${this.state.filePath}-${formatDuration(this.state.currentTime)}.jpg`;
const outPath = `${this.state.filePath}-${util.formatDuration(this.state.currentTime)}.jpg`;
fs.writeFile(outPath, buf, (err) => {
if (err) alert(err);
});
@ -223,16 +221,16 @@ class App extends React.Component {
}}
>
<div className="timeline-wrapper">
<div className="current-time" style={{ left: `${(this.state.currentTime / this.state.duration) * 100}%` }} />
<div className="current-time" style={{ left: `${((this.state.currentTime || 0) / (this.state.duration || 1)) * 100}%` }} />
<div
className="cut-start-time"
style={{
left: `${(this.state.cutStartTime / this.state.duration) * 100}%`,
width: `${((this.state.cutEndTime - this.state.cutStartTime) / this.state.duration) * 100}%`,
left: `${((this.state.cutStartTime || 0) / (this.state.duration || 1)) * 100}%`,
width: `${(((this.state.cutEndTime || 0) - (this.state.cutStartTime || 0)) / (this.state.duration || 1)) * 100}%`,
}}
/>
<div id="current-time-display">{formatDuration(this.state.currentTime)}</div>
<div id="current-time-display">{util.formatDuration(this.state.currentTime)}</div>
</div>
</Hammer>
@ -267,7 +265,7 @@ class App extends React.Component {
<button
className="jump-cut-start" title="Cut start time"
onClick={() => this.jumpCutStart()}
>{formatDuration(this.state.cutStartTime || 0)}</button>
>{util.formatDuration(this.state.cutStartTime || 0)}</button>
<i
title="Set cut start"
className="button fa fa-angle-left"
@ -289,11 +287,14 @@ class App extends React.Component {
<button
className="jump-cut-end" title="Cut end time"
onClick={() => this.jumpCutEnd()}
>{formatDuration(this.state.cutEndTime || 0)}</button>
>{util.formatDuration(this.state.cutEndTime || 0)}</button>
</div>
</div>
<div className="right-menu">
<button className="file-format" title="Format">
{this.state.fileFormat || '-'}
</button>
<button className="playback-rate" title="Playback rate">
{_.round(this.state.playbackRate, 1) || 1}x
</button>

19
src/util.js Normal file
View File

@ -0,0 +1,19 @@
const _ = require('lodash');
function formatDuration(_seconds) {
const seconds = _seconds || 0;
const minutes = seconds / 60;
const hours = minutes / 60;
const hoursPadded = _.padStart(Math.floor(hours), 2, '0');
const minutesPadded = _.padStart(Math.floor(minutes % 60), 2, '0');
const secondsPadded = _.padStart(Math.floor(seconds) % 60, 2, '0');
const msPadded = _.padStart(Math.floor((seconds - Math.floor(seconds)) * 1000), 3, '0');
// Be nice to filenames and use .
return `${hoursPadded}.${minutesPadded}.${secondsPadded}.${msPadded}`;
}
module.exports = {
formatDuration,
};