1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 03:33:14 +01:00

Add new features

Add more hotkeys
More buttons
Show in/out point timestamps and click to jump to these
Scrubbing
Show error for unsupported files
This commit is contained in:
Mikael Finstad 2016-10-31 23:24:04 +01:00
parent e5c81f0ea8
commit 9044f682fd
4 changed files with 162 additions and 82 deletions

View File

@ -29,6 +29,9 @@ The original video files will not be modified. Instead it creates a lossless exp
- <kbd></kbd> Seek forward 1 sec - <kbd></kbd> Seek forward 1 sec
- <kbd>.</kbd> (period) Tiny seek forward - <kbd>.</kbd> (period) Tiny seek forward
- <kbd>,</kbd> (comma) Tiny seek backward - <kbd>,</kbd> (comma) Tiny seek backward
- <kbd>i</kbd> Mark in / cut start point
- <kbd>o</kbd> Mark out / cut end point
- <kbd>e</kbd> Export selection (in the same dir as the video)
- <kbd>c</kbd> Capture snapshot (in the same dir as the video) - <kbd>c</kbd> Capture snapshot (in the same dir as the video)
## Development building / running ## Development building / running
@ -51,16 +54,23 @@ npm run build
npm run package npm run package
``` ```
## TODO ## TODO / ideas
- more hotkeys
- ffprobe show keyframes?
- ffprobe format
- About menu - About menu
- icon
- Visual feedback on button presses
- ffprobe show keyframes
- ffprobe format
- improve ffmpeg error handling - improve ffmpeg error handling
- timeline scrub support - Slow scrub with modifier key
- show frame number
- Bundle ffmpeg
- support for loading other formats by streaming through ffmpeg?
- cutting out the commercials in a video file while saving the rest to a single file?
## Links ## Links
- http://apple.stackexchange.com/questions/117306/what-options-are-available-to-losslessly-trim-mp4-m4v-video-on-10-8-or-above - http://apple.stackexchange.com/questions/117306/what-options-are-available-to-losslessly-trim-mp4-m4v-video-on-10-8-or-above
- https://www.google.no/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=lossless%20cut%20video - 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/blob/master/docs/api/file-object.md
- https://github.com/electron/electron/issues/2538 - https://github.com/electron/electron/issues/2538

View File

@ -40,6 +40,7 @@
"lodash": "^4.16.4", "lodash": "^4.16.4",
"react": "^15.3.2", "react": "^15.3.2",
"react-dom": "^15.3.2", "react-dom": "^15.3.2",
"react-hammerjs": "^0.5.0",
"which": "^1.2.11" "which": "^1.2.11"
} }
} }

View File

@ -34,10 +34,27 @@ input, button, textarea, :focus {
object-fit: contain; object-fit: contain;
} }
#play, #set-start, #set-end, #seek-start, #seek-end, #cut { .button {
padding: .4em; padding: .4em;
} }
.jump-cut-start, .jump-cut-end {
background: white;
border-radius: .3em;
color: rgba(0, 0, 0, 0.7);
font-size: 60%;
vertical-align: middle;
padding: .2em;
margin: 0 .5em;
border: none;
}
.right-menu {
position: absolute;
right: 0;
bottom: 0;
}
.controls-wrapper { .controls-wrapper {
position: absolute; position: absolute;
left: 0; left: 0;
@ -60,7 +77,7 @@ input, button, textarea, :focus {
background-color: #444; background-color: #444;
} }
.timeline-wrapper .current-time, .timeline-wrapper .cursor-time, .timeline-wrapper .cut-start-time, .timeline-wrapper .cut-end-time { .timeline-wrapper .current-time, .timeline-wrapper .cursor-time, .timeline-wrapper .cut-start-time {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
top: 0; top: 0;
@ -69,6 +86,7 @@ input, button, textarea, :focus {
.timeline-wrapper .current-time { .timeline-wrapper .current-time {
background-color: red; background-color: red;
z-index: 2;
} }
.timeline-wrapper .cursor-time { .timeline-wrapper .cursor-time {
background-color: black; background-color: black;
@ -79,10 +97,6 @@ input, button, textarea, :focus {
border-right: 1px solid black; border-right: 1px solid black;
z-index: 1; z-index: 1;
} }
.timeline-wrapper .cut-end-time {
background-color: green;
z-index: 2;
}
#working { #working {
color: white; color: white;
@ -97,10 +111,11 @@ input, button, textarea, :focus {
} }
#drag-drop-field { #drag-drop-field {
padding: 5rem; padding: 15vw 0;
border: 1rem dashed #252525; border: 2vw dashed #252525;
color: #252525; color: #252525;
margin: 3rem; margin: 7vw;
font-size: 3em; font-size: 9vw;
text-align: center; text-align: center;
white-space: nowrap;
} }

View File

@ -5,6 +5,7 @@ const ffmpeg = require('./ffmpeg');
const _ = require('lodash'); const _ = require('lodash');
const captureFrame = require('capture-frame'); const captureFrame = require('capture-frame');
const fs = require('fs'); const fs = require('fs');
const Hammer = require('react-hammerjs');
const React = require('react'); const React = require('react');
const ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
@ -91,18 +92,51 @@ class App extends React.Component {
keyboardJs.bind('right', () => seekRel(1)); keyboardJs.bind('right', () => seekRel(1));
keyboardJs.bind('period', () => shortStep(1)); keyboardJs.bind('period', () => shortStep(1));
keyboardJs.bind('comma', () => shortStep(-1)); keyboardJs.bind('comma', () => shortStep(-1));
keyboardJs.bind('c', this.capture); keyboardJs.bind('c', () => this.capture());
keyboardJs.bind('e', () => this.cutClick());
keyboardJs.bind('i', () => this.setCutStart());
keyboardJs.bind('o', () => this.setCutEnd());
} }
mouseDown(e) { setCutStart() {
const $target = $('.timeline-wrapper'); // $(e.target); this.setState({ cutStartTime: this.state.currentTime });
}
setCutEnd() {
this.setState({ cutEndTime: this.state.currentTime });
}
jumpCutStart() {
seekAbs(this.state.cutStartTime);
}
jumpCutEnd() {
seekAbs(this.state.cutEndTime);
}
handlePan(e) {
_.throttle(e2 => this.handleTap(e2), 200)(e);
}
handleTap(e) {
const $target = $('.timeline-wrapper');
const parentOffset = $target.offset(); const parentOffset = $target.offset();
const relX = e.pageX - parentOffset.left; const relX = e.srcEvent.pageX - parentOffset.left;
setCursor((relX / $target[0].offsetWidth) * this.state.duration); setCursor((relX / $target[0].offsetWidth) * this.state.duration);
} }
playCommand() { playCommand() {
getVideo()[this.state.playing ? 'pause' : 'play'](); const video = getVideo();
if (this.state.playing) {
return video.pause();
}
return video.play().catch((err) => {
console.log(err);
if (err.name === 'NotSupportedError') {
alert('This video format is not supported, maybe you can re-format the file first using ffmpeg');
}
});
} }
cutClick() { cutClick() {
@ -125,6 +159,7 @@ class App extends React.Component {
} }
capture() { capture() {
if (!this.state.filePath) return;
const buf = captureFrame(getVideo(), 'jpg'); const buf = captureFrame(getVideo(), 'jpg');
const outPath = `${this.state.filePath}-${formatDuration(this.state.currentTime)}.jpg`; const outPath = `${this.state.filePath}-${formatDuration(this.state.currentTime)}.jpg`;
fs.writeFile(outPath, buf, (err) => { fs.writeFile(outPath, buf, (err) => {
@ -149,70 +184,89 @@ class App extends React.Component {
</div> </div>
<div className="controls-wrapper"> <div className="controls-wrapper">
<div className="timeline-wrapper" onMouseDown={e => this.mouseDown(e)}> <Hammer
<div className="current-time" style={{ left: `${(this.state.currentTime / this.state.duration) * 100}%` }} /> onTap={e => this.handleTap(e)}
<div onPan={e => this.handlePan(e)}
className="cut-start-time" options={{
style={{ recognizers: {
left: `${(this.state.cutStartTime / this.state.duration) * 100}%`, },
width: `${((this.state.cutEndTime - this.state.cutStartTime) / this.state.duration) * 100}%`, }}
}} >
<div className="timeline-wrapper">
<div className="current-time" style={{ left: `${(this.state.currentTime / this.state.duration) * 100}%` }} />
<div
className="cut-start-time"
style={{
left: `${(this.state.cutStartTime / this.state.duration) * 100}%`,
width: `${((this.state.cutEndTime - this.state.cutStartTime) / this.state.duration) * 100}%`,
}}
/>
<div id="current-time-display">{formatDuration(this.state.currentTime)}</div>
</div>
</Hammer>
<div>
<i
className="button fa fa-step-backward"
aria-hidden="true"
onClick={() => seekAbs(0)}
/>
<i
className="button fa fa-caret-left"
aria-hidden="true"
onClick={() => shortStep(-1)}
/>
<i
className={classnames({ button: true, fa: true, 'fa-pause': this.state.playing, 'fa-play': !this.state.playing })}
aria-hidden="true"
onClick={() => this.playCommand()}
/>
<i
className="button fa fa-caret-right"
aria-hidden="true"
onClick={() => shortStep(1)}
/>
<i
className="button fa fa-step-forward"
aria-hidden="true"
onClick={() => seekAbs(this.state.duration)}
/> />
<div id="current-time-display">{formatDuration(this.state.currentTime)}</div>
</div> </div>
<div>
<button
className="jump-cut-start" title="Cut start time"
onClick={() => this.jumpCutStart()}
>{formatDuration(this.state.cutStartTime || 0)}</button>
<i
title="Set cut start"
className="button fa fa-angle-left"
aria-hidden="true"
onClick={() => this.setCutStart()}
/>
<i
title="Export selection"
className="button fa fa-scissors"
aria-hidden="true"
onClick={() => this.cutClick()}
/>
<i
title="Set cut end"
className="button fa fa-angle-right"
aria-hidden="true"
onClick={() => this.setCutEnd()}
/>
<button
className="jump-cut-end" title="Cut end time"
onClick={() => this.jumpCutEnd()}
>{formatDuration(this.state.cutEndTime || 0)}</button>
</div>
</div>
<div className="right-menu">
<i <i
id="seek-start"
className="fa fa-step-backward"
aria-hidden="true"
onClick={() => seekAbs(0)}
/>
<i
id="play"
className={classnames({ fa: true, 'fa-pause': this.state.playing, 'fa-play': !this.state.playing })}
aria-hidden="true"
onClick={() => this.playCommand()}
/>
<i
id="seek-end"
className="fa fa-step-forward"
aria-hidden="true"
onClick={() => seekAbs(this.state.duration)}
/>
<i
id="short-step"
title="Short step"
className="fa fa-ellipsis-h"
aria-hidden="true"
onClick={() => shortStep(1)}
/>
<br />
<i
id="set-start"
title="Set cut start"
className="fa fa-angle-left"
aria-hidden="true"
onClick={() => this.setState({ cutStartTime: this.state.currentTime })}
/>
<i
id="cut"
title="Cut/export"
className="fa fa-scissors"
aria-hidden="true"
onClick={() => this.cutClick()}
/>
<i
id="set-end"
title="Set cut end"
className="fa fa-angle-right"
aria-hidden="true"
onClick={() => this.setState({ cutEndTime: this.state.currentTime })}
/>
<i
id="capture-frame"
title="Capture frame" title="Capture frame"
className="fa fa-camera" className="button fa fa-camera"
aria-hidden="true" aria-hidden="true"
onClick={() => this.capture()} onClick={() => this.capture()}
/> />