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
pull/13/head
Mikael Finstad 9 years ago
parent 3851cb264f
commit cce542af41

@ -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
- Bundle ffmpeg
- Visual feedback on button presses
- ffprobe show keyframes
- ffprobe format
- improve ffmpeg error handling
- support for previewing other formats by streaming through ffmpeg?
- Slow scrub with modifier key
- show frame number
- Bundle ffmpeg
- support for loading other formats by streaming through ffmpeg?
- show frame number (approx?)
- ffprobe show keyframes
- cutting out the commercials in a video file while saving the rest to a single file?
## Links

@ -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) => {
console.log('Cutting from', cutFrom, 'to', cutTo);
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'))
.then(ffmpegPath => execa(ffmpegPath, [
'-i', filePath, '-y', '-vcodec', 'copy', '-acodec', 'copy', '-ss', cutFrom, '-t', cutTo - cutFrom, outFile,
]))
.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);
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,
};

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

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

@ -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,
};
Loading…
Cancel
Save