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

improvements

- Show stream info
- add cute loader
This commit is contained in:
Mikael Finstad 2020-02-18 22:17:14 +08:00
parent 9973878175
commit 0cfe65e890
5 changed files with 90 additions and 33 deletions

View File

@ -75,12 +75,13 @@
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-hammerjs": "^1.0.1", "react-hammerjs": "^1.0.1",
"react-icons": "^3.9.0", "react-icons": "^3.9.0",
"react-lottie": "^1.2.3",
"react-sortable-hoc": "^1.5.3", "react-sortable-hoc": "^1.5.3",
"react-use": "^13.24.0", "react-use": "^13.24.0",
"read-chunk": "^2.0.0", "read-chunk": "^2.0.0",
"string-to-stream": "^1.1.1", "string-to-stream": "^1.1.1",
"strong-data-uri": "^1.0.5", "strong-data-uri": "^1.0.5",
"sweetalert2": "^8.0.1", "sweetalert2": "^9.7.2",
"sweetalert2-react-content": "^1.0.1", "sweetalert2-react-content": "^1.0.1",
"trash": "^6.1.1", "trash": "^6.1.1",
"uuid": "^3.3.2", "uuid": "^3.3.2",

1
src/7077-magic-flow.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,13 @@
import React, { memo, Fragment } from 'react'; import React, { memo, Fragment } from 'react';
import { FaVideo, FaVideoSlash, FaFileExport, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaTrashAlt } from 'react-icons/fa'; import { FaVideo, FaVideoSlash, FaFileExport, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaTrashAlt, FaInfoCircle } from 'react-icons/fa';
import { GoFileBinary } from 'react-icons/go'; import { GoFileBinary } from 'react-icons/go';
import { MdSubtitles } from 'react-icons/md'; import { MdSubtitles } from 'react-icons/md';
import Swal from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
const ReactSwal = withReactContent(Swal);
const { formatDuration } = require('./util'); const { formatDuration } = require('./util');
const { getStreamFps } = require('./ffmpeg'); const { getStreamFps } = require('./ffmpeg');
@ -25,12 +30,17 @@ const Stream = memo(({ stream, onToggle, copyStream }) => {
const streamFps = getStreamFps(stream); const streamFps = getStreamFps(stream);
function onInfoClick(s) {
ReactSwal.fire({
icon: 'info',
title: 'Stream info',
html: <div style={{ whiteSpace: 'pre', textAlign: 'left', overflow: 'auto' }}>{JSON.stringify(s, null, 2)}</div>,
});
}
return ( return (
<tr <tr style={{ opacity: copyStream ? undefined : 0.4 }}>
style={{ opacity: copyStream ? undefined : 0.4 }} <td><Icon size={20} style={{ padding: '0 5px', cursor: 'pointer' }} role="button" onClick={() => onToggle && onToggle(stream.index)} /></td>
onClick={() => onToggle && onToggle(stream.index)}
>
<td><Icon size={20} style={{ padding: '0 5px', cursor: 'pointer' }} /></td>
<td>{stream.index}</td> <td>{stream.index}</td>
<td>{stream.codec_type}</td> <td>{stream.codec_type}</td>
<td>{stream.codec_tag !== '0x0000' && stream.codec_tag_string}</td> <td>{stream.codec_tag !== '0x0000' && stream.codec_tag_string}</td>
@ -39,6 +49,7 @@ const Stream = memo(({ stream, onToggle, copyStream }) => {
<td>{stream.nb_frames}</td> <td>{stream.nb_frames}</td>
<td>{!Number.isNaN(bitrate) && `${(bitrate / 1e6).toFixed(1)}MBit/s`}</td> <td>{!Number.isNaN(bitrate) && `${(bitrate / 1e6).toFixed(1)}MBit/s`}</td>
<td>{stream.width && stream.height && `${stream.width}x${stream.height}`} {stream.channels && `${stream.channels}c`} {stream.channel_layout} {streamFps && `${streamFps.toFixed(1)}fps`}</td> <td>{stream.width && stream.height && `${stream.width}x${stream.height}`} {stream.channels && `${stream.channels}c`} {stream.channel_layout} {streamFps && `${streamFps.toFixed(1)}fps`}</td>
<td><FaInfoCircle role="button" onClick={() => onInfoClick(stream)} size={30} /></td>
</tr> </tr>
); );
}); });
@ -75,6 +86,7 @@ const StreamsSelector = memo(({
<td>Frames</td> <td>Frames</td>
<td>Bitrate</td> <td>Bitrate</td>
<td>Data</td> <td>Data</td>
<td />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -6,8 +6,9 @@ import { GiYinYang } from 'react-icons/gi';
import { FiScissors } from 'react-icons/fi'; import { FiScissors } from 'react-icons/fi';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import Lottie from 'react-lottie';
import { SideSheet, Button, Position } from 'evergreen-ui'; import { SideSheet, Button, Position } from 'evergreen-ui';
import fromPairs from 'lodash/fromPairs'; import fromPairs from 'lodash/fromPairs';
import clamp from 'lodash/clamp'; import clamp from 'lodash/clamp';
import clone from 'lodash/clone'; import clone from 'lodash/clone';
@ -20,6 +21,8 @@ import InverseCutSegment from './InverseCutSegment';
import StreamsSelector from './StreamsSelector'; import StreamsSelector from './StreamsSelector';
import { loadMifiLink } from './mifi'; import { loadMifiLink } from './mifi';
import loadingLottie from './7077-magic-flow.json';
const isDev = require('electron-is-dev'); const isDev = require('electron-is-dev');
const electron = require('electron'); // eslint-disable-line const electron = require('electron'); // eslint-disable-line
@ -460,7 +463,7 @@ const App = memo(() => {
return video.play().catch((err) => { return video.play().catch((err) => {
console.error(err); console.error(err);
if (err.name === 'NotSupportedError') { if (err.name === 'NotSupportedError') {
toast.fire({ type: 'error', title: 'This format/codec is not supported. Try to convert it to a friendly format/codec in the player from the "File" menu.', timer: 10000 }); toast.fire({ icon: 'error', title: 'This format/codec is not supported. Try to convert it to a friendly format/codec in the player from the "File" menu.', timer: 10000 });
} }
}); });
}, [playing]); }, [playing]);
@ -477,7 +480,7 @@ const App = memo(() => {
await trash(filePath); await trash(filePath);
if (html5FriendlyPath) await trash(html5FriendlyPath); if (html5FriendlyPath) await trash(html5FriendlyPath);
} catch (err) { } catch (err) {
toast.fire({ type: 'error', title: `Failed to trash source file: ${err.message}` }); toast.fire({ icon: 'error', title: `Failed to trash source file: ${err.message}` });
} finally { } finally {
resetState(); resetState();
} }
@ -494,6 +497,11 @@ const App = memo(() => {
return; return;
} }
if (numStreamsToCopy === 0) {
errorToast('No tracks to export!');
return;
}
const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments; const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
const ffmpegSegments = segments.map((seg) => ({ const ffmpegSegments = segments.map((seg) => ({
@ -532,7 +540,7 @@ const App = memo(() => {
}); });
} }
toast.fire({ timer: 10000, type: 'success', title: `Export completed! Output file(s) can be found at: ${getOutDir(customOutDir, filePath)}. You can change the output directory in settings` }); toast.fire({ timer: 10000, icon: 'success', title: `Export completed! Output file(s) can be found at: ${getOutDir(customOutDir, filePath)}. You can change the output directory in settings` });
} catch (err) { } catch (err) {
console.error('stdout:', err.stdout); console.error('stdout:', err.stdout);
console.error('stderr:', err.stderr); console.error('stderr:', err.stderr);
@ -549,11 +557,11 @@ const App = memo(() => {
}, [ }, [
effectiveRotation, apparentCutSegments, invertCutSegments, inverseCutSegments, effectiveRotation, apparentCutSegments, invertCutSegments, inverseCutSegments,
working, duration, filePath, keyframeCut, detectedFileFormat, working, duration, filePath, keyframeCut, detectedFileFormat,
autoMerge, customOutDir, fileFormat, haveInvalidSegs, copyStreamIds, autoMerge, customOutDir, fileFormat, haveInvalidSegs, copyStreamIds, numStreamsToCopy,
]); ]);
function showUnsupportedFileMessage() { function showUnsupportedFileMessage() {
toast.fire({ timer: 10000, type: 'warning', title: 'This video is not natively supported', text: 'This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!' }); toast.fire({ timer: 10000, icon: 'warning', title: 'This video is not natively supported', text: 'This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!' });
} }
// TODO use ffmpeg to capture frame // TODO use ffmpeg to capture frame
@ -719,7 +727,7 @@ const App = memo(() => {
try { try {
setWorking(true); setWorking(true);
await ffmpeg.extractAllStreams({ customOutDir, filePath }); await ffmpeg.extractAllStreams({ customOutDir, filePath });
toast.fire({ type: 'success', title: `All streams can be found as separate files at: ${getOutDir(customOutDir, filePath)}` }); toast.fire({ icon: 'success', title: `All streams can be found as separate files at: ${getOutDir(customOutDir, filePath)}` });
} catch (err) { } catch (err) {
errorToast('Failed to extract all streams'); errorToast('Failed to extract all streams');
console.error('Failed to extract all streams', err); console.error('Failed to extract all streams', err);
@ -755,6 +763,7 @@ const App = memo(() => {
} }
const { value } = await Swal.fire({ const { value } = await Swal.fire({
title: 'You opened a new file. What do you want to do?', title: 'You opened a new file. What do you want to do?',
icon: 'question',
input: 'radio', input: 'radio',
showCancelButton: true, showCancelButton: true,
inputOptions: { inputOptions: {
@ -1068,7 +1077,7 @@ const App = memo(() => {
const topBarHeight = '2rem'; const topBarHeight = '2rem';
const bottomBarHeight = '6rem'; const bottomBarHeight = '6rem';
const VolumeIcon = muted ? FaVolumeMute : FaVolumeUp; const VolumeIcon = muted || dummyVideoPath ? FaVolumeMute : FaVolumeUp;
const CutIcon = areWeCutting ? FiScissors : FaFileExport; const CutIcon = areWeCutting ? FiScissors : FaFileExport;
function renderInvertCutButton() { function renderInvertCutButton() {
@ -1090,6 +1099,8 @@ const App = memo(() => {
); );
} }
const primaryColor = 'hsl(194, 78%, 47%)';
return ( return (
<div> <div>
<div style={{ background: '#6b6b6b', height: topBarHeight, display: 'flex', alignItems: 'center', padding: '0 5px', justifyContent: 'space-between' }}> <div style={{ background: '#6b6b6b', height: topBarHeight, display: 'flex', alignItems: 'center', padding: '0 5px', justifyContent: 'space-between' }}>
@ -1169,19 +1180,38 @@ const App = memo(() => {
</div> </div>
)} )}
{working && ( <AnimatePresence>
<div style={{ {working && (
color: 'white', background: 'rgba(0, 0, 0, 0.3)', borderRadius: '.5em', margin: '1em', padding: '.2em .5em', position: 'absolute', zIndex: 1, top: topBarHeight, left: 0, <div style={{
}} position: 'absolute', zIndex: 1, bottom: bottomBarHeight, top: topBarHeight, left: 0, right: 0, display: 'flex', justifyContent: 'center', alignItems: 'center', pointerEvents: 'none',
> }}
<i className="fa fa-cog fa-spin fa-3x fa-fw" style={{ verticalAlign: 'middle', width: '1em', height: '1em' }} /> >
{cutProgress != null && ( <motion.div
<span style={{ color: 'rgba(255, 255, 255, 0.7)', paddingLeft: '.4em' }}> style={{ background: primaryColor, boxShadow: `${primaryColor} 0px 0px 20px 25px`, borderRadius: 20, paddingBottom: 15, color: 'white', textAlign: 'center', fontSize: 14 }}
{`${Math.floor(cutProgress * 100)} %`} initial={{ opacity: 0, scale: 0 }}
</span> animate={{ opacity: 1, scale: 1 }}
)} exit={{ opacity: 0, scale: 0 }}
</div> >
)} <div style={{ width: 150, height: 150 }}>
<Lottie
options={{ loop: true, autoplay: true, animationData: loadingLottie }}
style={{ width: '170%', height: '130%', marginLeft: '-35%', marginTop: '-29%' }}
/>
</div>
<div style={{ marginTop: 10 }}>
WORKING
</div>
{(cutProgress != null) && (
<div style={{ marginTop: 10 }}>
{`${Math.floor(cutProgress * 100)} %`}
</div>
)}
</motion.div>
</div>
)}
</AnimatePresence>
<div style={{ position: 'absolute', top: topBarHeight, left: 0, right: 0, bottom: bottomBarHeight }}> <div style={{ position: 'absolute', top: topBarHeight, left: 0, right: 0, bottom: bottomBarHeight }}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */} {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
@ -1403,7 +1433,7 @@ const App = memo(() => {
/> />
<span <span
style={{ background: 'hsl(194, 78%, 47%)', borderRadius: 5, padding: '3px 7px', fontSize: 14 }} style={{ background: primaryColor, borderRadius: 5, padding: '3px 7px', fontSize: 14 }}
onClick={cutClick} onClick={cutClick}
title={cutSegments.length > 1 ? 'Export all segments' : 'Export selection'} title={cutSegments.length > 1 ? 'Export all segments' : 'Export selection'}
role="button" role="button"

View File

@ -3896,6 +3896,11 @@ loose-envify@^1.4.0:
dependencies: dependencies:
js-tokens "^3.0.0 || ^4.0.0" js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.1.3:
version "5.6.4"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.6.4.tgz#8520f9e7e8daa9e0ab8343c628c3f470800088a4"
integrity sha512-eU+21Wo/RSi4i260S7fDUxfhNJ9PhfzUJMVQpip0yZd19oJ18jrNCoSQKVUzjC2TzOjqumlLZXR636ezKoWNQg==
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
@ -4805,6 +4810,14 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-lottie@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/react-lottie/-/react-lottie-1.2.3.tgz#8544b96939e088658072eea5e12d912cdaa3acc1"
integrity sha512-qLCERxUr8M+4mm1LU0Ruxw5Y5Fn/OmYkGfnA+JDM/dZb3oKwVAJCjwnjkj9TMHtzR2U6sMEUD3ZZ1RaHagM7kA==
dependencies:
babel-runtime "^6.26.0"
lottie-web "^5.1.3"
react-scrollbar-size@^2.0.2: react-scrollbar-size@^2.0.2:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/react-scrollbar-size/-/react-scrollbar-size-2.1.0.tgz#105e797135cab92b1f9e16f00071db7f29f80754" resolved "https://registry.yarnpkg.com/react-scrollbar-size/-/react-scrollbar-size-2.1.0.tgz#105e797135cab92b1f9e16f00071db7f29f80754"
@ -5752,10 +5765,10 @@ sweetalert2-react-content@^1.0.1:
resolved "https://registry.yarnpkg.com/sweetalert2-react-content/-/sweetalert2-react-content-1.0.1.tgz#0403145a1af819504e394df0dfc6194df67068a0" resolved "https://registry.yarnpkg.com/sweetalert2-react-content/-/sweetalert2-react-content-1.0.1.tgz#0403145a1af819504e394df0dfc6194df67068a0"
integrity sha512-wZxDGbF24jzNXGsr3hjkSa5wQlhgq4wOPPZShe4RMujGdDuPtniodQGZOW2Tn38dzpgXSZ4/sb7++zxwueberw== integrity sha512-wZxDGbF24jzNXGsr3hjkSa5wQlhgq4wOPPZShe4RMujGdDuPtniodQGZOW2Tn38dzpgXSZ4/sb7++zxwueberw==
sweetalert2@^8.0.1: sweetalert2@^9.7.2:
version "8.0.1" version "9.7.2"
resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-8.0.1.tgz#d59fa124a56595c1e799dd4c635689afa0a5f811" resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-9.7.2.tgz#ad11a75e765b543792b032c27a5ab981a7c8429e"
integrity sha512-N88+meCLt1t/5dQEAvBs31j6qcEVucE/bwMj8JiuPsMRvXhnWKMbvLAFPGNO7a/LECRVjT6o0/mccmIVZqQZSQ== integrity sha512-VmFmigf+rO20t5fOql21wIZlk60B8M02kXwPLmMA2qaBcVwvDQA+qGXuqtXipx1wsztTOiXgyDiBvdbHbIvQpw==
table@^5.2.3: table@^5.2.3:
version "5.4.6" version "5.4.6"