Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
- Icon
  • Loading branch information
mifi committed Nov 5, 2016
1 parent cce542a commit ec875b5
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 111 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -3,3 +3,5 @@ node_modules
npm-debug.log
dist
package
ffmpeg-tmp
icon-dist
50 changes: 20 additions & 30 deletions README.md
@@ -1,25 +1,28 @@
# 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. Also supports lossless cutting in the most common audio formats.

![Demo](demo.gif)
![Screenshot](screenshot.jpg)

## Download
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. Also supports lossless cutting in the most common audio formats.

<b>ffmpeg is now included in the app! 🎉</b>

For an indication of supported formats / codecs, see https://www.chromium.org/audio-video

![Demo](https://giant.gfycat.com/HighAcclaimedAnaconda.gif)

## Installing / running

- Install [ffmpeg](https://www.ffmpeg.org/download.html)
- Download [latest LosslessCut from releases](https://github.com/mifi/lossless-cut/releases)
- Run app
- If ffmpeg is available in <b>$PATH</b>/<b>%PATH%</b> it will just work
- If not, a dialog will pop up to select ffmpeg executable path.
- Run LosslessCut app/exe

## Documentation

### Typical flow
- Drag drop a video file into player to load or use <kbd>⌘</kbd>/<kbd>CTRL</kbd>+<kbd>O</kbd>.
- Select the start and end time
- Press the scissors button to export a slice.
- Press the camera button to take a snapshot.
- Press <kbd>SPACE</kbd> to play/pause
- Select the cut start and end time
- Press the scissors button to export the slice
- Press the camera button to take a snapshot

The original video files will not be modified. Instead it creates a lossless export in the same directory as the original file with from/to timestamps. Note that the cut is currently not precise around the cutpoints, so video before/after the nearest keyframe will be lost. EXIF data is preserved.

Expand All @@ -38,7 +41,7 @@ The original video files will not be modified. Instead it creates a lossless exp

## Development building / running

This app is made using Electron. Make sure you have at least node v4 with npm 3.
This app is built using Electron. Make sure you have at least node v4 with npm 3. The app uses ffmpeg from PATH when developing.
```
git clone https://github.com/mifi/lossless-cut.git
cd lossless-cut
Expand All @@ -57,25 +60,12 @@ npm start

### Building package
```
npm run download-ffmpeg
npm run extract-ffmpeg
npm run build
npm run package
npm run icon-gen
npm run package # builds all platforms
```

## TODO / ideas
- About menu
- icon
- Bundle 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
- http://apple.stackexchange.com/questions/117306/what-options-are-available-to-losslessly-trim-mp4-m4v-video-on-10-8-or-above
- http://superuser.com/questions/554620/how-to-get-time-stamp-of-closest-keyframe-before-a-given-timestamp-with-ffmpeg/554679#554679
- http://www.fame-ring.com/smart_cutter.html
- http://electron.atom.io/apps/
- https://github.com/electron/electron/blob/master/docs/api/file-object.md
- https://github.com/electron/electron/issues/2538
## Credits
- App icon made by [Dimi Kazak](http://www.flaticon.com/authors/dimi-kazak "Dimi Kazak") from [www.flaticon.com](http://www.flaticon.com "Flaticon") is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/ "Creative Commons BY 3.0")
21 changes: 21 additions & 0 deletions TODO.md
@@ -0,0 +1,21 @@
## TODO / ideas
- 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 (pprobe -of json -select_streams v -show_frames file.mp4)
- cutting out the commercials in a video file while saving the rest to a single file?
- With the GOP structure of h.264 you could run into some pretty nasty playback issues without re-encoding if you cut the wrong frames out.
- Shortcut Cmd+o also triggers o (cut end)
- implement electron app event "open-file"
- Travis github deploys https://docs.travis-ci.com/user/deployment
- react video ref="video" this.refs.video.play()
- A dedicated "Options" menu where the users can set a default output folder for captured frames and for cut videos will also be handy, now lossless-cut uses the input folder.

## Links
- http://apple.stackexchange.com/questions/117306/what-options-are-available-to-losslessly-trim-mp4-m4v-video-on-10-8-or-above
- http://superuser.com/questions/554620/how-to-get-time-stamp-of-closest-keyframe-before-a-given-timestamp-with-ffmpeg/554679#554679
- http://www.fame-ring.com/smart_cutter.html
- http://electron.atom.io/apps/
- https://github.com/electron/electron/blob/master/docs/api/file-object.md
- https://github.com/electron/electron/issues/2538
Binary file removed demo.gif
Binary file not shown.
18 changes: 14 additions & 4 deletions package.json
Expand Up @@ -7,8 +7,18 @@
"start": "electron dist",
"watch": "npm run build && babel src -d dist --copy-files -w",
"build": "rm -rf dist && babel src -d dist --copy-files && ln -s ../node_modules dist/ && ln -s ../package.json ./dist/",
"package": "electron-packager dist LosslessCut --out=package --asar --overwrite --all --version 1.3.8",
"zip": "(cd package && for f in LosslessCut-*; do zip -r $f; done)",
"download-ffmpeg": "bash ./scripts/ffmpeg-dl/dl.sh",
"extract-ffmpeg": "bash ./scripts/ffmpeg-dl/extract.sh",
"copy-ffmpeg": "rm -rf dist/ffmpeg && mkdir dist/ffmpeg && cp ffmpeg-tmp/binaries/${PLATFORM}_${ARCH}/* dist/ffmpeg",
"package-single": "npm run copy-ffmpeg && electron-packager dist LosslessCut --out=package --asar.unpackDir=ffmpeg --overwrite --platform=${PLATFORM} --arch=${ARCH} --icon=icon-dist/${ICON}",
"package:darwin_x64": "PLATFORM=darwin ARCH=x64 ICON=app.icns npm run package-single",
"package:win32_ia32": "PLATFORM=win32 ARCH=ia32 ICON=app.ico npm run package-single",
"package:win32_x64": "PLATFORM=win32 ARCH=x64 ICON=app.ico npm run package-single",
"package:linux_ia32": "PLATFORM=linux ARCH=ia32 ICON=app.ico npm run package-single",
"package:linux_x64": "PLATFORM=linux ARCH=x64 ICON=app.ico npm run package-single",
"zip": "(cd package && rm -f LosslessCut-*.zip && for f in LosslessCut-*; do zip -r \"$f\".zip \"$f\"; done)",
"icon-gen": "icon-gen -i src/icon.svg -o ./icon-dist -r",
"package": "npm run package:darwin_x64 && npm run package:win32_ia32 && npm run package:win32_x64 && npm run package:linux_ia32 && npm run package:linux_x64 && npm run zip",
"gifify": "gifify -p 405:299 -r 5@3 Untitled.mov-00.00.00.971-00.00.19.780.mp4",
"lint": "eslint ."
},
Expand All @@ -27,13 +37,13 @@
"eslint-config-airbnb": "^12.0.0",
"eslint-plugin-import": "^1.16.0",
"eslint-plugin-jsx-a11y": "^2.2.3",
"eslint-plugin-react": "^6.4.1"
"eslint-plugin-react": "^6.4.1",
"icon-gen": "git+https://github.com/mifi/npm-icon-gen.git#ca9a098482d09bd378328bc1810ec2846429d109"
},
"dependencies": {
"bluebird": "^3.4.6",
"capture-frame": "^1.0.0",
"classnames": "^2.2.5",
"configstore": "^2.1.0",
"electron": "^1.4.5",
"electron-default-menu": "^1.0.0",
"execa": "^0.5.0",
Expand Down
Binary file added screenshot.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions scripts/ffmpeg-dl/dl.sh
@@ -0,0 +1,15 @@
ffmpeg_linux_ia32=https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-32bit-static.tar.xz
ffmpeg_linux_x64=https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz
ffmpeg_darwin_x64=http://evermeet.cx/ffmpeg/ffmpeg-3.2.7z
ffmpeg_win32_ia32=https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-3.1.5-win32-static.zip
ffmpeg_win32_x64=https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-3.1.5-win64-static.zip
ffprobe_darwin_x64=http://evermeet.cx/ffmpeg/ffprobe-3.2.7z

mkdir -p ffmpeg-tmp/archives &&
(cd ffmpeg-tmp/archives &&
wget -O ffmpeg_linux_ia32.tar.xz "${ffmpeg_linux_ia32}" &&
wget -O ffmpeg_linux_x64.tar.xz "${ffmpeg_linux_x64}" &&
wget -O ffmpeg_darwin_x64.7z "${ffmpeg_darwin_x64}" &&
wget -O ffmpeg_win32_ia32.zip "${ffmpeg_win32_ia32}" &&
wget -O ffmpeg_win32_x64.zip "${ffmpeg_win32_x64}" &&
wget -O ffprobe_darwin_x64.7z "${ffprobe_darwin_x64}")
32 changes: 32 additions & 0 deletions scripts/ffmpeg-dl/extract.sh
@@ -0,0 +1,32 @@
(
mkdir -p ffmpeg-tmp/extracted &&
cd ffmpeg-tmp/extracted &&
(mkdir -p linux_ia32 && cd linux_ia32 &&
7z x ../../archives/ffmpeg_linux_ia32.tar.xz && tar xvfp ffmpeg_linux_ia32.tar) &&
(mkdir -p linux_x64 && cd linux_x64 &&
7z x ../../archives/ffmpeg_linux_x64.tar.xz && tar xvfp ffmpeg_linux_x64.tar) &&
(mkdir -p win32_ia32 && cd win32_ia32 &&
unzip ../../archives/ffmpeg_win32_ia32.zip) &&
(mkdir -p win32_x64 && cd win32_x64 &&
unzip ../../archives/ffmpeg_win32_x64.zip) &&
(mkdir -p darwin_x64 && cd darwin_x64 &&
7z x ../../archives/ffmpeg_darwin_x64.7z &&
7z x ../../archives/ffprobe_darwin_x64.7z)
) &&
cd ffmpeg-tmp &&
mkdir -p binaries/linux_ia32 &&
mkdir -p binaries/linux_x64 &&
mkdir -p binaries/win32_ia32 &&
mkdir -p binaries/win32_x64 &&
mkdir -p binaries/darwin_x64 &&
mv extracted/linux_ia32/ffmpeg-3.2-32bit-static/ffmpeg binaries/linux_ia32 &&
mv extracted/linux_ia32/ffmpeg-3.2-32bit-static/ffprobe binaries/linux_ia32 &&
mv extracted/linux_x64/ffmpeg-3.2-64bit-static/ffmpeg binaries/linux_x64 &&
mv extracted/linux_x64/ffmpeg-3.2-64bit-static/ffprobe binaries/linux_x64 &&
mv extracted/win32_ia32/ffmpeg-3.1.5-win32-static/bin/ffmpeg.exe binaries/win32_ia32 &&
mv extracted/win32_ia32/ffmpeg-3.1.5-win32-static/bin/ffprobe.exe binaries/win32_ia32 &&
mv extracted/win32_x64/ffmpeg-3.1.5-win64-static/bin/ffmpeg.exe binaries/win32_x64 &&
mv extracted/win32_x64/ffmpeg-3.1.5-win64-static/bin/ffprobe.exe binaries/win32_x64 &&
mv extracted/darwin_x64/ffmpeg binaries/darwin_x64 &&
mv extracted/darwin_x64/ffprobe binaries/darwin_x64 &&
echo Done
28 changes: 19 additions & 9 deletions src/ffmpeg.js
Expand Up @@ -3,20 +3,32 @@ const bluebird = require('bluebird');
const which = bluebird.promisify(require('which'));
const path = require('path');
const util = require('./util');
const fs = require('fs');

const Configstore = require('configstore');

const configstore = new Configstore('lossless-cut', { ffmpegPath: '' });
bluebird.promisifyAll(fs);


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)');
alert(`Failed to run ffmpeg:\n${err.stack}`);
console.error(err.stack);
}

function getWithExt(name) {
return process.platform === 'win32' ? `${name}.exe` : name;
}

function canExecuteFfmpeg(ffmpegPath) {
return execa(ffmpegPath, ['-version']);
}

function getFfmpegPath() {
return which('ffmpeg')
.catch(() => configstore.get('ffmpegPath'));
const internalFfmpeg = path.join(__dirname, '..', 'app.asar.unpacked', 'ffmpeg', getWithExt('ffmpeg'));
return canExecuteFfmpeg(internalFfmpeg)
.then(() => internalFfmpeg)
.catch(() => {
console.log('Internal ffmpeg unavail');
return which('ffmpeg');
});
}

function cut(filePath, format, cutFrom, cutTo) {
Expand Down Expand Up @@ -53,7 +65,7 @@ function getFormats(filePath) {
console.log('getFormat', filePath);

return getFfmpegPath()
.then(ffmpegPath => path.join(path.dirname(ffmpegPath), 'ffprobe'))
.then(ffmpegPath => path.join(path.dirname(ffmpegPath), getWithExt('ffprobe')))
.then(ffprobePath => execa(ffprobePath, [
'-of', 'json', '-show_format', '-i', filePath,
]))
Expand All @@ -65,8 +77,6 @@ function getFormats(filePath) {
});
}

// '-of', 'json', '-select_streams', 'v', '-show_frames', filePath,

module.exports = {
cut,
getFormats,
Expand Down
59 changes: 59 additions & 0 deletions src/icon.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 5 additions & 45 deletions src/index.js
@@ -1,18 +1,14 @@
const electron = require('electron'); // eslint-disable-line
const Configstore = require('configstore');
const bluebird = require('bluebird');
const which = bluebird.promisify(require('which'));

const util = require('./util');
const menu = require('./menu');

const app = electron.app;
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';
app.setName('LosslessCut');

if (util.isPackaged()) 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.
Expand All @@ -32,48 +28,12 @@ function createWindow() {
});
}

function showFfmpegDialog() {
console.log('Show ffmpeg dialog');
return new Promise(resolve => dialog.showOpenDialog({
defaultPath: '/usr/local/bin/ffmpeg',
properties: ['openFile', 'showHiddenFiles'],
}, ffmpegPath => resolve(ffmpegPath !== undefined ? ffmpegPath[0] : undefined)));
}

function changeFfmpegPath() {
return showFfmpegDialog()
.then((ffmpegPath) => {
if (ffmpegPath !== undefined) configstore.set('ffmpegPath', ffmpegPath);
});
}

function configureFfmpeg() {
return which('ffmpeg')
.then(() => true)
.catch(() => {
if (configstore.get('ffmpegPath') !== undefined) {
return undefined;
}

console.log('Show first time dialog');
return new Promise(resolve => dialog.showMessageBox({
buttons: ['OK'],
message: 'This is the first time you run LosslessCut and ffmpeg path was not auto detected. Please close this dialog and then select the path to the ffmpeg executable.',
}, resolve))
.then(showFfmpegDialog)
.then((ffmpegPath) => {
configstore.set('ffmpegPath', ffmpegPath !== undefined ? ffmpegPath : '');
});
});
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
createWindow();
menu(app, mainWindow, changeFfmpegPath);
configureFfmpeg();
menu(app, mainWindow);
});

// Quit when all windows are closed.
Expand Down

0 comments on commit ec875b5

Please sign in to comment.