kopia lustrzana https://github.com/mifi/lossless-cut
- 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
pull/13/head
rodzic
cce542af41
commit
ec875b5c65
|
|
@ -3,3 +3,5 @@ node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
dist
|
dist
|
||||||
package
|
package
|
||||||
|
ffmpeg-tmp
|
||||||
|
icon-dist
|
||||||
|
|
|
||||||
50
README.md
50
README.md
|
|
@ -1,25 +1,28 @@
|
||||||
# LosslessCut 🎥 []()
|
# LosslessCut 🎥 []()
|
||||||
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.
|
|
||||||
|
|
||||||

|

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

|
||||||
|
|
||||||
## Installing / running
|
## Installing / running
|
||||||
|
|
||||||
- Install [ffmpeg](https://www.ffmpeg.org/download.html)
|
|
||||||
- Download [latest LosslessCut from releases](https://github.com/mifi/lossless-cut/releases)
|
- Download [latest LosslessCut from releases](https://github.com/mifi/lossless-cut/releases)
|
||||||
- Run app
|
- Run LosslessCut app/exe
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
### Typical flow
|
||||||
- Drag drop a video file into player to load or use <kbd>⌘</kbd>/<kbd>CTRL</kbd>+<kbd>O</kbd>.
|
- 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 <kbd>SPACE</kbd> to play/pause
|
||||||
- Press the scissors button to export a slice.
|
- Select the cut start and end time
|
||||||
- Press the camera button to take a snapshot.
|
- 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.
|
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
|
## 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
|
git clone https://github.com/mifi/lossless-cut.git
|
||||||
cd lossless-cut
|
cd lossless-cut
|
||||||
|
|
@ -57,25 +60,12 @@ npm start
|
||||||
|
|
||||||
### Building package
|
### Building package
|
||||||
```
|
```
|
||||||
|
npm run download-ffmpeg
|
||||||
|
npm run extract-ffmpeg
|
||||||
npm run build
|
npm run build
|
||||||
npm run package
|
npm run icon-gen
|
||||||
|
npm run package # builds all platforms
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO / ideas
|
## Credits
|
||||||
- About menu
|
- 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")
|
||||||
- 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
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
BIN
demo.gif
BIN
demo.gif
Plik binarny nie jest wyświetlany.
|
Przed Szerokość: | Wysokość: | Rozmiar: 4.8 MiB |
18
package.json
18
package.json
|
|
@ -7,8 +7,18 @@
|
||||||
"start": "electron dist",
|
"start": "electron dist",
|
||||||
"watch": "npm run build && babel src -d dist --copy-files -w",
|
"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/",
|
"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",
|
"download-ffmpeg": "bash ./scripts/ffmpeg-dl/dl.sh",
|
||||||
"zip": "(cd package && for f in LosslessCut-*; do zip -r $f; done)",
|
"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",
|
"gifify": "gifify -p 405:299 -r 5@3 Untitled.mov-00.00.00.971-00.00.19.780.mp4",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
|
|
@ -27,13 +37,13 @@
|
||||||
"eslint-config-airbnb": "^12.0.0",
|
"eslint-config-airbnb": "^12.0.0",
|
||||||
"eslint-plugin-import": "^1.16.0",
|
"eslint-plugin-import": "^1.16.0",
|
||||||
"eslint-plugin-jsx-a11y": "^2.2.3",
|
"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": {
|
"dependencies": {
|
||||||
"bluebird": "^3.4.6",
|
"bluebird": "^3.4.6",
|
||||||
"capture-frame": "^1.0.0",
|
"capture-frame": "^1.0.0",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"configstore": "^2.1.0",
|
|
||||||
"electron": "^1.4.5",
|
"electron": "^1.4.5",
|
||||||
"electron-default-menu": "^1.0.0",
|
"electron-default-menu": "^1.0.0",
|
||||||
"execa": "^0.5.0",
|
"execa": "^0.5.0",
|
||||||
|
|
|
||||||
Plik binarny nie jest wyświetlany.
|
Po Szerokość: | Wysokość: | Rozmiar: 220 KiB |
|
|
@ -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}")
|
||||||
|
|
@ -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 which = bluebird.promisify(require('which'));
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const util = require('./util');
|
const util = require('./util');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
const Configstore = require('configstore');
|
bluebird.promisifyAll(fs);
|
||||||
|
|
||||||
const configstore = new Configstore('lossless-cut', { ffmpegPath: '' });
|
|
||||||
|
|
||||||
|
|
||||||
function showFfmpegFail(err) {
|
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);
|
console.error(err.stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWithExt(name) {
|
||||||
|
return process.platform === 'win32' ? `${name}.exe` : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canExecuteFfmpeg(ffmpegPath) {
|
||||||
|
return execa(ffmpegPath, ['-version']);
|
||||||
|
}
|
||||||
|
|
||||||
function getFfmpegPath() {
|
function getFfmpegPath() {
|
||||||
return which('ffmpeg')
|
const internalFfmpeg = path.join(__dirname, '..', 'app.asar.unpacked', 'ffmpeg', getWithExt('ffmpeg'));
|
||||||
.catch(() => configstore.get('ffmpegPath'));
|
return canExecuteFfmpeg(internalFfmpeg)
|
||||||
|
.then(() => internalFfmpeg)
|
||||||
|
.catch(() => {
|
||||||
|
console.log('Internal ffmpeg unavail');
|
||||||
|
return which('ffmpeg');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cut(filePath, format, cutFrom, cutTo) {
|
function cut(filePath, format, cutFrom, cutTo) {
|
||||||
|
|
@ -53,7 +65,7 @@ function getFormats(filePath) {
|
||||||
console.log('getFormat', filePath);
|
console.log('getFormat', filePath);
|
||||||
|
|
||||||
return getFfmpegPath()
|
return getFfmpegPath()
|
||||||
.then(ffmpegPath => path.join(path.dirname(ffmpegPath), 'ffprobe'))
|
.then(ffmpegPath => path.join(path.dirname(ffmpegPath), getWithExt('ffprobe')))
|
||||||
.then(ffprobePath => execa(ffprobePath, [
|
.then(ffprobePath => execa(ffprobePath, [
|
||||||
'-of', 'json', '-show_format', '-i', filePath,
|
'-of', 'json', '-show_format', '-i', filePath,
|
||||||
]))
|
]))
|
||||||
|
|
@ -65,8 +77,6 @@ function getFormats(filePath) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// '-of', 'json', '-select_streams', 'v', '-show_frames', filePath,
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
cut,
|
cut,
|
||||||
getFormats,
|
getFormats,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
Po Szerokość: | Wysokość: | Rozmiar: 2.8 KiB |
50
src/index.js
50
src/index.js
|
|
@ -1,18 +1,14 @@
|
||||||
const electron = require('electron'); // eslint-disable-line
|
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 menu = require('./menu');
|
||||||
|
|
||||||
const app = electron.app;
|
const app = electron.app;
|
||||||
const BrowserWindow = electron.BrowserWindow;
|
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
|
app.setName('LosslessCut');
|
||||||
const isProd = process.execPath.search('electron-prebuilt') === -1;
|
|
||||||
if (isProd) process.env.NODE_ENV = 'production';
|
if (util.isPackaged()) process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// 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.
|
// 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
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
createWindow();
|
createWindow();
|
||||||
menu(app, mainWindow, changeFfmpegPath);
|
menu(app, mainWindow);
|
||||||
configureFfmpeg();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed.
|
// Quit when all windows are closed.
|
||||||
|
|
|
||||||
11
src/main.css
11
src/main.css
|
|
@ -38,17 +38,22 @@ input, button, textarea, :focus {
|
||||||
padding: .4em;
|
padding: .4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jump-cut-start, .jump-cut-end, .playback-rate {
|
.controls-wrapper button, .right-menu button {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
color: rgba(0, 0, 0, 0.7);
|
color: rgba(0, 0, 0, 0.7);
|
||||||
font-size: 60%;
|
font-size: 60%;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: .2em;
|
padding: .2em .4em;
|
||||||
margin: 0 .5em;
|
margin: 0 .5em;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls-wrapper button:active, .right-menu button:active {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.right-menu {
|
.right-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
@ -60,7 +65,7 @@ input, button, textarea, :focus {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 6rem;
|
height: 5.75rem;
|
||||||
background: #6b6b6b;
|
background: #6b6b6b;
|
||||||
text-align: center;
|
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';
|
const homepage = 'https://github.com/mifi/lossless-cut';
|
||||||
|
|
||||||
module.exports = (app, mainWindow, changeFfmpegPath) => {
|
module.exports = (app, mainWindow) => {
|
||||||
const menu = defaultMenu(app, electron.shell);
|
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',
|
label: 'File',
|
||||||
submenu: [
|
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, {
|
const helpIndex = menu.findIndex(item => item.role === 'help');
|
||||||
role: 'help',
|
if (helpIndex >= 0) {
|
||||||
submenu: [
|
menu.splice(helpIndex, 1, {
|
||||||
{
|
role: 'help',
|
||||||
label: 'Learn More',
|
submenu: [
|
||||||
click() { electron.shell.openExternal(homepage); },
|
{
|
||||||
},
|
label: 'Learn More',
|
||||||
],
|
click() { electron.shell.openExternal(homepage); },
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
|
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
const bluebird = require('bluebird');
|
||||||
const electron = require('electron'); // eslint-disable-line
|
const electron = require('electron'); // eslint-disable-line
|
||||||
const $ = require('jquery');
|
const $ = require('jquery');
|
||||||
const keyboardJs = require('keyboardjs');
|
const keyboardJs = require('keyboardjs');
|
||||||
|
|
@ -64,9 +65,12 @@ class App extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
const load = (filePath) => {
|
const load = (filePath) => {
|
||||||
|
if (this.state.working) return alert('I\'m busy');
|
||||||
|
|
||||||
resetState();
|
resetState();
|
||||||
|
|
||||||
ffmpeg.getFormats(filePath)
|
this.setState({ working: true });
|
||||||
|
return bluebird.resolve(ffmpeg.getFormats(filePath))
|
||||||
.then((formats) => {
|
.then((formats) => {
|
||||||
if (formats.length < 1) return alert('Unsupported file');
|
if (formats.length < 1) return alert('Unsupported file');
|
||||||
return this.setState({ filePath, fileFormat: formats[0] });
|
return this.setState({ filePath, fileFormat: formats[0] });
|
||||||
|
|
@ -77,7 +81,8 @@ class App extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ffmpeg.showFfmpegFail(err);
|
ffmpeg.showFfmpegFail(err);
|
||||||
});
|
})
|
||||||
|
.finally(() => this.setState({ working: false }));
|
||||||
};
|
};
|
||||||
|
|
||||||
electron.ipcRenderer.on('file-opened', (event, message) => {
|
electron.ipcRenderer.on('file-opened', (event, message) => {
|
||||||
|
|
@ -170,6 +175,8 @@ class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
cutClick() {
|
cutClick() {
|
||||||
|
if (this.state.working) return alert('I\'m busy');
|
||||||
|
|
||||||
const cutStartTime = this.state.cutStartTime;
|
const cutStartTime = this.state.cutStartTime;
|
||||||
const cutEndTime = this.state.cutEndTime;
|
const cutEndTime = this.state.cutEndTime;
|
||||||
const filePath = this.state.filePath;
|
const filePath = this.state.filePath;
|
||||||
|
|
@ -181,7 +188,7 @@ class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ working: true });
|
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 }));
|
.finally(() => this.setState({ working: false }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,7 +299,7 @@ class App extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="right-menu">
|
<div className="right-menu">
|
||||||
<button className="file-format" title="Format">
|
<button title="Format">
|
||||||
{this.state.fileFormat || '-'}
|
{this.state.fileFormat || '-'}
|
||||||
</button>
|
</button>
|
||||||
<button className="playback-rate" title="Playback rate">
|
<button className="playback-rate" title="Playback rate">
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ function formatDuration(_seconds) {
|
||||||
return `${hoursPadded}.${minutesPadded}.${secondsPadded}.${msPadded}`;
|
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 = {
|
module.exports = {
|
||||||
formatDuration,
|
formatDuration,
|
||||||
|
isPackaged,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fixtures are stored separately:
|
||||||
|
https://github.com/mifi/lossless-cut-fixtures
|
||||||
|
|
@ -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
|
||||||
Ładowanie…
Reference in New Issue