1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 02:12:30 +01:00
Mikael Finstad 2016-11-05 21:22:31 +01:00
parent cce542af41
commit ec875b5c65
17 changed files with 231 additions and 111 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ node_modules
npm-debug.log
dist
package
ffmpeg-tmp
icon-dist

View File

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

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

15
scripts/ffmpeg-dl/dl.sh Executable file
View 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
View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,2 @@
Fixtures are stored separately:
https://github.com/mifi/lossless-cut-fixtures

1
test/formats.sh Executable file
View 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