mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 02:12:30 +01:00
- Icon
- Bundle ffmpeg
- Fix menus on windows
- fs-methods seem to hang on windows, so exec instead
Resources:
https://discuss.atom.io/t/information-about-bundled-ffmpeg/28456/6
27730fd269/app/src/scripts/mp4-to-gif.js
https://github.com/orionhealth/electron-packager-plugin-non-proprietary-codecs-ffmpeg
https://github.com/konsumer/easy-ffmpeg
https://github.com/eugeneware/ffmpeg-static
This commit is contained in:
parent
cce542af41
commit
ec875b5c65
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@ node_modules
|
||||
npm-debug.log
|
||||
dist
|
||||
package
|
||||
ffmpeg-tmp
|
||||
icon-dist
|
||||
|
50
README.md
50
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.
|
||||
|
||||
@ -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
|
||||
@ -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
TODO.md
Normal file
21
TODO.md
Normal file
@ -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
|
18
package.json
18
package.json
@ -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 ."
|
||||
},
|
||||
@ -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",
|
||||
|
BIN
screenshot.jpg
Normal file
BIN
screenshot.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 220 KiB |
15
scripts/ffmpeg-dl/dl.sh
Executable file
15
scripts/ffmpeg-dl/dl.sh
Executable file
@ -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
scripts/ffmpeg-dl/extract.sh
Executable file
32
scripts/ffmpeg-dl/extract.sh
Executable file
@ -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
|
@ -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) {
|
||||
@ -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,
|
||||
]))
|
||||
@ -65,8 +77,6 @@ function getFormats(filePath) {
|
||||
});
|
||||
}
|
||||
|
||||
// '-of', 'json', '-select_streams', 'v', '-show_frames', filePath,
|
||||
|
||||
module.exports = {
|
||||
cut,
|
||||
getFormats,
|
||||
|
59
src/icon.svg
Normal file
59
src/icon.svg
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<circle style="fill:#14B0BF;" cx="256" cy="256" r="256"/>
|
||||
<path style="fill:#3387B5;" d="M437.018,437.018c59.699-59.699,83.722-141.548,72.12-219.09L401.833,109.86L96.691,163.666
|
||||
l8.771,49.731l2.463-0.435l0.077,0.435h-2.54v168.397c0,6.38,3.036,12.032,7.67,15.77c0.916,1.137,112.067,112.067,112.579,112.579
|
||||
C300.913,519.05,379.31,494.725,437.018,437.018z"/>
|
||||
<path style="fill:#0F303F;" d="M415.309,381.788c0,11.192-9.16,20.352-20.352,20.352H125.809c-11.192,0-20.352-9.16-20.352-20.352
|
||||
V233.743c0-11.192,9.16-20.352,20.352-20.352h269.148c11.192,0,20.352,9.16,20.352,20.352V381.788z"/>
|
||||
<rect x="105.462" y="213.396" style="fill:#FFFFFF;" width="309.862" height="50.499"/>
|
||||
<path style="fill:#0F303F;" d="M156.001,220.314l-36.874,37.366h37.366l36.874-37.366H156.001z M311.322,220.314l-36.874,37.366
|
||||
h37.366l36.874-37.366H311.322z M207.775,220.314L170.9,257.679h37.366l36.874-37.366H207.775z M259.548,220.314l-36.874,37.366
|
||||
h37.366l36.874-37.366H259.548z M363.095,220.314l-36.874,37.366h37.366l36.874-37.366H363.095z M414.868,220.314l-36.874,37.366
|
||||
h37.315v-37.366H414.868z M105.462,220.314v37.366h0.246l36.874-37.366H105.462z"/>
|
||||
|
||||
<rect x="98.721" y="136.353" transform="matrix(0.9848 -0.1736 0.1736 0.9848 -24.203 46.4881)" style="fill:#FFFFFF;" width="309.857" height="50.498"/>
|
||||
<path style="fill:#0F303F;" d="M316.467,131.937l42.803,30.397l36.797-6.487l-42.803-30.397L316.467,131.937z M163.502,158.904
|
||||
l42.803,30.397l36.797-6.487L200.3,152.417L163.502,158.904z M265.477,140.928l42.803,30.397l36.797-6.487l-42.803-30.397
|
||||
L265.477,140.928z M214.492,149.919l42.803,30.397l36.797-6.487l-42.803-30.397L214.492,149.919z M112.517,167.895l42.803,30.397
|
||||
l36.797-6.487l-42.803-30.397L112.517,167.895z M97.894,170.476l6.487,36.797l36.746-6.482l-42.803-30.397L97.894,170.476z
|
||||
M366.479,123.116l42.803,30.397l0.241-0.041l-6.487-36.797L366.479,123.116z"/>
|
||||
<path style="fill:#FDC00F;" d="M394.634,317.379h-0.072v-36.552h-269.44v103.235h269.44v-62.172h0.072V317.379z M390.497,284.897
|
||||
v32.481h-101.13v-32.481L390.497,284.897L390.497,284.897z M284.856,284.897v32.481H177.05v-32.481L284.856,284.897
|
||||
L284.856,284.897z M129.193,379.991v-95.094h43.341v95.094H129.193z M177.05,379.991v-58.102h107.807v58.102L177.05,379.991
|
||||
L177.05,379.991z M390.497,379.991h-101.13v-58.102h101.125v58.102H390.497z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
50
src/index.js
50
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.
|
||||
@ -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.
|
||||
|
11
src/main.css
11
src/main.css
@ -38,17 +38,22 @@ input, button, textarea, :focus {
|
||||
padding: .4em;
|
||||
}
|
||||
|
||||
.jump-cut-start, .jump-cut-end, .playback-rate {
|
||||
.controls-wrapper button, .right-menu button {
|
||||
background: white;
|
||||
border-radius: .3em;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
font-size: 60%;
|
||||
vertical-align: middle;
|
||||
padding: .2em;
|
||||
padding: .2em .4em;
|
||||
margin: 0 .5em;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.controls-wrapper button:active, .right-menu button:active {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
|
||||
.right-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@ -60,7 +65,7 @@ input, button, textarea, :focus {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 6rem;
|
||||
height: 5.75rem;
|
||||
background: #6b6b6b;
|
||||
text-align: center;
|
||||
}
|
||||
|
32
src/menu.js
32
src/menu.js
@ -6,12 +6,13 @@ const dialog = electron.dialog;
|
||||
|
||||
const homepage = 'https://github.com/mifi/lossless-cut';
|
||||
|
||||
module.exports = (app, mainWindow, changeFfmpegPath) => {
|
||||
module.exports = (app, mainWindow) => {
|
||||
const menu = defaultMenu(app, electron.shell);
|
||||
|
||||
menu.splice(1, 1);
|
||||
const editMenuIndex = menu.findIndex(item => item.Label === 'Edit');
|
||||
if (editMenuIndex >= 0) menu.splice(editMenuIndex, 1);
|
||||
|
||||
menu.splice(1, 0, {
|
||||
menu.splice((process.platform === 'darwin' ? 1 : 0), 0, {
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
@ -23,22 +24,21 @@ module.exports = (app, mainWindow, changeFfmpegPath) => {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Change ffmpeg path',
|
||||
click: changeFfmpegPath,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
menu.splice(menu.findIndex(item => item.role === 'help'), 1, {
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
click() { electron.shell.openExternal(homepage); },
|
||||
},
|
||||
],
|
||||
});
|
||||
const helpIndex = menu.findIndex(item => item.role === 'help');
|
||||
if (helpIndex >= 0) {
|
||||
menu.splice(helpIndex, 1, {
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
click() { electron.shell.openExternal(homepage); },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
const bluebird = require('bluebird');
|
||||
const electron = require('electron'); // eslint-disable-line
|
||||
const $ = require('jquery');
|
||||
const keyboardJs = require('keyboardjs');
|
||||
@ -64,9 +65,12 @@ class App extends React.Component {
|
||||
};
|
||||
|
||||
const load = (filePath) => {
|
||||
if (this.state.working) return alert('I\'m busy');
|
||||
|
||||
resetState();
|
||||
|
||||
ffmpeg.getFormats(filePath)
|
||||
this.setState({ working: true });
|
||||
return bluebird.resolve(ffmpeg.getFormats(filePath))
|
||||
.then((formats) => {
|
||||
if (formats.length < 1) return alert('Unsupported file');
|
||||
return this.setState({ filePath, fileFormat: formats[0] });
|
||||
@ -77,7 +81,8 @@ class App extends React.Component {
|
||||
return;
|
||||
}
|
||||
ffmpeg.showFfmpegFail(err);
|
||||
});
|
||||
})
|
||||
.finally(() => this.setState({ working: false }));
|
||||
};
|
||||
|
||||
electron.ipcRenderer.on('file-opened', (event, message) => {
|
||||
@ -170,6 +175,8 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
cutClick() {
|
||||
if (this.state.working) return alert('I\'m busy');
|
||||
|
||||
const cutStartTime = this.state.cutStartTime;
|
||||
const cutEndTime = this.state.cutEndTime;
|
||||
const filePath = this.state.filePath;
|
||||
@ -181,7 +188,7 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({ working: true });
|
||||
return ffmpeg.cut(filePath, this.state.fileFormat, cutStartTime, cutEndTime)
|
||||
return bluebird.resolve(ffmpeg.cut(filePath, this.state.fileFormat, cutStartTime, cutEndTime))
|
||||
.finally(() => this.setState({ working: false }));
|
||||
}
|
||||
|
||||
@ -292,7 +299,7 @@ class App extends React.Component {
|
||||
</div>
|
||||
|
||||
<div className="right-menu">
|
||||
<button className="file-format" title="Format">
|
||||
<button title="Format">
|
||||
{this.state.fileFormat || '-'}
|
||||
</button>
|
||||
<button className="playback-rate" title="Playback rate">
|
||||
|
@ -14,6 +14,12 @@ function formatDuration(_seconds) {
|
||||
return `${hoursPadded}.${minutesPadded}.${secondsPadded}.${msPadded}`;
|
||||
}
|
||||
|
||||
function isPackaged() {
|
||||
// http://stackoverflow.com/questions/39362292/how-do-i-set-node-env-production-on-electron-app-when-packaged-with-electron-pac
|
||||
return process.execPath.search('electron-prebuilt') === -1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatDuration,
|
||||
isPackaged,
|
||||
};
|
||||
|
2
test/README.md
Normal file
2
test/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
Fixtures are stored separately:
|
||||
https://github.com/mifi/lossless-cut-fixtures
|
1
test/formats.sh
Executable file
1
test/formats.sh
Executable file
@ -0,0 +1 @@
|
||||
for f in sample-videos/*; do echo -n "$f: "; ffprobe -show_format -of json -i "$f" | json format.format_name; done 2> /dev/null
|
Loading…
Reference in New Issue
Block a user