mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 02:12:30 +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:
parent
e5c81f0ea8
commit
9044f682fd
22
README.md
22
README.md
@ -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> (period) Tiny seek forward
|
||||
- <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)
|
||||
|
||||
## Development building / running
|
||||
@ -51,16 +54,23 @@ npm run build
|
||||
npm run package
|
||||
```
|
||||
|
||||
## TODO
|
||||
- more hotkeys
|
||||
- ffprobe show keyframes?
|
||||
- ffprobe format
|
||||
## TODO / ideas
|
||||
- About menu
|
||||
- icon
|
||||
- Visual feedback on button presses
|
||||
- ffprobe show keyframes
|
||||
- ffprobe format
|
||||
- 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
|
||||
- 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/issues/2538
|
||||
|
@ -40,6 +40,7 @@
|
||||
"lodash": "^4.16.4",
|
||||
"react": "^15.3.2",
|
||||
"react-dom": "^15.3.2",
|
||||
"react-hammerjs": "^0.5.0",
|
||||
"which": "^1.2.11"
|
||||
}
|
||||
}
|
||||
|
39
src/main.css
39
src/main.css
@ -34,10 +34,27 @@ input, button, textarea, :focus {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#play, #set-start, #set-end, #seek-start, #seek-end, #cut {
|
||||
.button {
|
||||
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 {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -60,7 +77,7 @@ input, button, textarea, :focus {
|
||||
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;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
@ -69,6 +86,7 @@ input, button, textarea, :focus {
|
||||
|
||||
.timeline-wrapper .current-time {
|
||||
background-color: red;
|
||||
z-index: 2;
|
||||
}
|
||||
.timeline-wrapper .cursor-time {
|
||||
background-color: black;
|
||||
@ -79,10 +97,6 @@ input, button, textarea, :focus {
|
||||
border-right: 1px solid black;
|
||||
z-index: 1;
|
||||
}
|
||||
.timeline-wrapper .cut-end-time {
|
||||
background-color: green;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#working {
|
||||
color: white;
|
||||
@ -97,10 +111,11 @@ input, button, textarea, :focus {
|
||||
}
|
||||
|
||||
#drag-drop-field {
|
||||
padding: 5rem;
|
||||
border: 1rem dashed #252525;
|
||||
color: #252525;
|
||||
margin: 3rem;
|
||||
font-size: 3em;
|
||||
text-align: center;
|
||||
padding: 15vw 0;
|
||||
border: 2vw dashed #252525;
|
||||
color: #252525;
|
||||
margin: 7vw;
|
||||
font-size: 9vw;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
182
src/renderer.jsx
182
src/renderer.jsx
@ -5,6 +5,7 @@ const ffmpeg = require('./ffmpeg');
|
||||
const _ = require('lodash');
|
||||
const captureFrame = require('capture-frame');
|
||||
const fs = require('fs');
|
||||
const Hammer = require('react-hammerjs');
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
@ -91,18 +92,51 @@ class App extends React.Component {
|
||||
keyboardJs.bind('right', () => seekRel(1));
|
||||
keyboardJs.bind('period', () => 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) {
|
||||
const $target = $('.timeline-wrapper'); // $(e.target);
|
||||
setCutStart() {
|
||||
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 relX = e.pageX - parentOffset.left;
|
||||
const relX = e.srcEvent.pageX - parentOffset.left;
|
||||
setCursor((relX / $target[0].offsetWidth) * this.state.duration);
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -125,6 +159,7 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
capture() {
|
||||
if (!this.state.filePath) return;
|
||||
const buf = captureFrame(getVideo(), 'jpg');
|
||||
const outPath = `${this.state.filePath}-${formatDuration(this.state.currentTime)}.jpg`;
|
||||
fs.writeFile(outPath, buf, (err) => {
|
||||
@ -149,70 +184,89 @@ class App extends React.Component {
|
||||
</div>
|
||||
|
||||
<div className="controls-wrapper">
|
||||
<div className="timeline-wrapper" onMouseDown={e => this.mouseDown(e)}>
|
||||
<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}%`,
|
||||
}}
|
||||
<Hammer
|
||||
onTap={e => this.handleTap(e)}
|
||||
onPan={e => this.handlePan(e)}
|
||||
options={{
|
||||
recognizers: {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
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"
|
||||
className="fa fa-camera"
|
||||
className="button fa fa-camera"
|
||||
aria-hidden="true"
|
||||
onClick={() => this.capture()}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user