1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 02:12:30 +01:00

Build with create-react-app #108

This commit is contained in:
Mikael Finstad 2020-03-04 18:41:40 +08:00
parent 173e1bab94
commit 676f92c803
23 changed files with 8412 additions and 1613 deletions

View File

@ -1,4 +1,2 @@
dist
package
build
watch-build
/dist
/build

View File

@ -1,26 +1,25 @@
{
"name": "lossless-cut",
"productName": "LosslessCut",
"description": "Lossless video editor",
"copyright": "Copyright © 2019 ${author}",
"description": "The swiss army knife of lossless video/audio editing",
"copyright": "Copyright © 2020 ${author}",
"version": "3.13.0",
"main": "build/index.js",
"main": "public/electron.js",
"homepage": "./",
"scripts": {
"start": "electron watch-build",
"watch": "babel src -d watch-build --copy-files -w",
"start": "concurrently -k \"BROWSER=none PORT=3001 react-scripts start\" \"wait-on http://localhost:3001 && electron .\"",
"icon-gen": "mkdir -p icon-build && svg2png src/icon.svg -o ./icon-build/app-512.png -w 512 -h 512",
"build": "yarn icon-gen && rm -rf build && babel src -d build --copy-files",
"postinstall": "electron-builder install-app-deps",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint --ext .jsx --ext .js .",
"pack-mac": "electron-builder --mac",
"prepack-mac": "yarn build",
"pack-win": "electron-builder --win",
"prepack-win": "yarn build",
"postinstall": "electron-builder install-app-deps",
"pack-linux": "electron-builder --linux",
"prepack-linux": "yarn build",
"release": "electron-builder",
"prerelease": "yarn build",
"gifify": "gifify -p 405:299 -r 5@3 Untitled.mov-00.00.00.971-00.00.19.780.mp4",
"lint": "eslint --ext .jsx --ext .js ."
"prepack-linux": "yarn build"
},
"author": {
"name": "Mikael Finstad",
@ -33,12 +32,7 @@
},
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-eslint": "^10.0.3",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"concurrently": "^5.1.0",
"electron": "^8.0.0",
"electron-builder": "^22.3.2",
"electron-builder-notarize": "^1.1.2",
@ -49,8 +43,10 @@
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^1.7.0",
"react-scripts": "^3.4.0",
"svg2png": "^4.1.1",
"use-trace-update": "^1.3.0"
"use-trace-update": "^1.3.0",
"wait-on": "^4.0.1"
},
"dependencies": {
"axios": "^0.19.2",
@ -65,6 +61,7 @@
"ffmpeg-static": "^4.0.1",
"ffprobe-static": "^3.0.0",
"file-type": "^12.4.0",
"file-url": "^3.0.0",
"framer-motion": "^1.8.4",
"fs-extra": "^8.1.0",
"github-api": "^3.2.2",
@ -93,6 +90,9 @@
"uuid": "^3.3.2",
"which": "^1.2.11"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
"electron 8.0"

View File

@ -1,5 +1,6 @@
const electron = require('electron'); // eslint-disable-line
const isDev = require('electron-is-dev');
const path = require('path');
const menu = require('./menu');
@ -10,8 +11,6 @@ const { BrowserWindow } = electron;
app.name = 'LosslessCut';
if (!isDev) 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.
let mainWindow;
@ -23,9 +22,12 @@ function createWindow() {
darkTheme: true,
webPreferences: {
nodeIntegration: true,
// https://github.com/electron/electron/issues/5107
webSecurity: !isDev,
},
});
mainWindow.loadFile(isDev ? 'index.html' : 'build/index.html');
mainWindow.loadURL(isDev ? 'http://localhost:3001' : `file://${path.join(__dirname, '../build/index.html')}`);
if (isDev) {
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); // eslint-disable-line global-require,import/no-extraneous-dependencies
@ -38,7 +40,7 @@ function createWindow() {
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time

10
public/index.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>LosslessCut</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -1,4 +1,5 @@
const GitHub = require('github-api');
// eslint-disable-next-line import/no-extraneous-dependencies
const electron = require('electron');
const { app } = electron;

View File

@ -6,6 +6,10 @@ import Lottie from 'react-lottie';
import { SideSheet, Button, Position, SegmentedControl, Select } from 'evergreen-ui';
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
import useDebounce from 'react-use/lib/useDebounce';
import PQueue from 'p-queue';
import filePathToUrl from 'file-url';
import Mousetrap from 'mousetrap';
import uuid from 'uuid';
import fromPairs from 'lodash/fromPairs';
import clamp from 'lodash/clamp';
@ -26,37 +30,30 @@ import RightMenu from './RightMenu';
import TimelineControls from './TimelineControls';
import { loadMifiLink } from './mifi';
import { primaryColor, controlsBackground, waveformColor } from './colors';
import { showMergeDialog, showOpenAndMergeDialog } from './merge/merge';
import allOutFormats from './outFormats';
import captureFrame from './capture-frame';
import {
defaultProcessedCodecTypes, getStreamFps, isCuttingStart, isCuttingEnd,
getDefaultOutFormat, getFormatData, renderFrame, mergeAnyFiles, renderThumbnails as ffmpegRenderThumbnails,
readFrames, renderWaveformPng, html5ifyDummy, cutMultiple, extractStreams, autoMergeSegments, getAllStreams,
findNearestKeyFrameTime, html5ify as ffmpegHtml5ify,
} from './ffmpeg';
import configStore from './store';
import { save as edlStoreSave, load as edlStoreLoad } from './edlStore';
import {
getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle,
promptTimeOffset, generateColor, getOutDir, withBlur,
} from './util';
import loadingLottie from './7077-magic-flow.json';
const isDev = require('electron-is-dev');
const electron = require('electron'); // eslint-disable-line
const Mousetrap = require('mousetrap');
const trash = require('trash');
const uuid = require('uuid');
const ReactDOM = require('react-dom');
const { default: PQueue } = require('p-queue');
const { unlink, exists } = require('fs-extra');
const { showMergeDialog, showOpenAndMergeDialog } = require('./merge/merge');
const allOutFormats = require('./outFormats');
const captureFrame = require('./capture-frame');
const ffmpeg = require('./ffmpeg');
const configStore = require('./store');
const edlStore = require('./edlStore');
const {
defaultProcessedCodecTypes, getStreamFps, isCuttingStart, isCuttingEnd,
getDefaultOutFormat, getFormatData,
} = ffmpeg;
const {
getOutPath, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle,
promptTimeOffset, generateColor, getOutDir, withBlur,
} = require('./util');
// const isDev = window.require('electron-is-dev');
const electron = window.require('electron'); // eslint-disable-line
const trash = window.require('trash');
const { unlink, exists } = window.require('fs-extra');
const { dialog } = electron.remote;
@ -528,7 +525,9 @@ const App = memo(() => {
setCustomOutDir((filePaths && filePaths.length === 1) ? filePaths[0] : undefined);
}, []);
const fileUri = (dummyVideoPath || html5FriendlyPath || filePath || '').replace(/#/g, '%23');
const effectiveFilePath = dummyVideoPath || html5FriendlyPath || filePath;
const fileUri = effectiveFilePath ? filePathToUrl(effectiveFilePath) : '';
const outputDir = getOutDir(customOutDir, filePath);
@ -553,7 +552,7 @@ const App = memo(() => {
return;
} */
await edlStore.save(edlFilePath, debouncedCutSegments);
await edlStoreSave(edlFilePath, debouncedCutSegments);
lastSavedCutSegmentsRef.current = debouncedCutSegments;
} catch (err) {
errorToast('Failed to save CSV');
@ -576,7 +575,7 @@ const App = memo(() => {
if (playerTime == null || !filePath) return;
try {
const framePathNew = await ffmpeg.renderFrame(playerTime, filePath, effectiveRotation);
const framePathNew = await renderFrame(playerTime, filePath, effectiveRotation);
setFramePath(framePathNew);
} catch (err) {
console.error(err);
@ -624,7 +623,7 @@ const App = memo(() => {
setWorking(true);
// console.log('merge', paths);
await ffmpeg.mergeAnyFiles({
await mergeAnyFiles({
customOutDir, paths, allStreams,
});
} catch (err) {
@ -701,7 +700,7 @@ const App = memo(() => {
try {
setThumbnails([]);
const promise = ffmpeg.renderThumbnails({ filePath, from: zoomWindowStartTime, duration: zoomedDuration, onThumbnail: addThumbnail });
const promise = ffmpegRenderThumbnails({ filePath, from: zoomWindowStartTime, duration: zoomedDuration, onThumbnail: addThumbnail });
thumnailsRenderingPromiseRef.current = promise;
await promise;
} catch (err) {
@ -729,7 +728,7 @@ const App = memo(() => {
if (!d || !d.keyframesEnabled || !d.filePath || !d.mainVideoStream || d.commandedTime == null || readingKeyframesPromise.current) return;
try {
const promise = ffmpeg.readFrames({ filePath: d.filePath, aroundTime: d.commandedTime, stream: d.mainVideoStream.index, window: ffmpegExtractWindow });
const promise = readFrames({ filePath: d.filePath, aroundTime: d.commandedTime, stream: d.mainVideoStream.index, window: ffmpegExtractWindow });
readingKeyframesPromise.current = promise;
const newFrames = await promise;
// console.log(newFrames);
@ -748,7 +747,7 @@ const App = memo(() => {
const d = debouncedWaveformData;
if (!d || !d.filePath || !d.mainAudioStream || d.commandedTime == null || !calcShouldShowWaveform(d.zoomedDuration) || !d.waveformEnabled || creatingWaveformPromise.current) return;
try {
const promise = ffmpeg.renderWaveformPng({ filePath: d.filePath, aroundTime: d.commandedTime, window: ffmpegExtractWindow, color: waveformColor });
const promise = renderWaveformPng({ filePath: d.filePath, aroundTime: d.commandedTime, window: ffmpegExtractWindow, color: waveformColor });
creatingWaveformPromise.current = promise;
const wf = await promise;
setWaveform(wf);
@ -771,7 +770,7 @@ const App = memo(() => {
const createDummyVideo = useCallback(async (fp) => {
const html5ifiedDummyPathDummy = getOutPath(customOutDir, fp, 'html5ified-dummy.mkv');
await ffmpeg.html5ifyDummy(fp, html5ifiedDummyPathDummy);
await html5ifyDummy(fp, html5ifiedDummyPathDummy);
setDummyVideoPath(html5ifiedDummyPathDummy);
setHtml5FriendlyPath();
showUnsupportedFileMessage();
@ -858,7 +857,7 @@ const App = memo(() => {
try {
setWorking(true);
const outFiles = await ffmpeg.cutMultiple({
const outFiles = await cutMultiple({
customOutDir,
filePath,
outFormat: fileFormat,
@ -876,7 +875,7 @@ const App = memo(() => {
if (outFiles.length > 1 && autoMerge) {
setCutProgress(0); // TODO implement progress
await ffmpeg.autoMergeSegments({
await autoMergeSegments({
customOutDir,
sourceFile: filePath,
segmentPaths: outFiles,
@ -885,7 +884,7 @@ const App = memo(() => {
if (exportExtraStreams) {
try {
await ffmpeg.extractStreams({
await extractStreams({
filePath, customOutDir, streams: nonCopiedExtraStreams,
});
} catch (err) {
@ -954,7 +953,7 @@ const App = memo(() => {
const loadEdlFile = useCallback(async (edlPath) => {
try {
const storedEdl = await edlStore.load(edlPath);
const storedEdl = await edlStoreLoad(edlPath);
const allRowsValid = storedEdl
.every(row => row.start === undefined || row.end === undefined || row.start < row.end);
@ -992,7 +991,7 @@ const App = memo(() => {
return;
}
const { streams } = await ffmpeg.getAllStreams(fp);
const { streams } = await getAllStreams(fp);
// console.log('streams', streamsNew);
setMainStreams(streams);
setCopyStreamIdsForPath(fp, () => fromPairs(streams.map((stream) => [
@ -1046,7 +1045,7 @@ const App = memo(() => {
const jumpSeg = useCallback((val) => setCurrentSegIndex((old) => Math.max(Math.min(old + val, cutSegments.length - 1), 0)), [cutSegments.length]);
const seekClosestKeyframe = useCallback((direction) => {
const time = ffmpeg.findNearestKeyFrameTime({ frames: neighbouringFrames, time: commandedTime, direction, fps: detectedFps });
const time = findNearestKeyFrameTime({ frames: neighbouringFrames, time: commandedTime, direction, fps: detectedFps });
if (time == null) return;
seekAbs(time);
}, [commandedTime, neighbouringFrames, seekAbs, detectedFps]);
@ -1121,7 +1120,7 @@ const App = memo(() => {
try {
setWorking(true);
await ffmpeg.extractStreams({ customOutDir, filePath, streams: mainStreams });
await extractStreams({ customOutDir, filePath, streams: mainStreams });
toast.fire({ icon: 'success', title: `All streams can be found as separate files at: ${outputDir}` });
} catch (err) {
errorToast('Failed to extract all streams');
@ -1137,7 +1136,7 @@ const App = memo(() => {
const addStreamSourceFile = useCallback(async (path) => {
if (externalStreamFiles[path]) return;
const { streams } = await ffmpeg.getAllStreams(path);
const { streams } = await getAllStreams(path);
const formatData = await getFormatData(path);
// console.log('streams', streams);
setExternalStreamFiles(old => ({ ...old, [path]: { streams, formatData } }));
@ -1212,7 +1211,7 @@ const App = memo(() => {
const html5FriendlyPathNew = getHtml5ifiedPath(filePath, speed);
const encodeVideo = ['slow', 'slow-audio'].includes(speed);
const encodeAudio = speed === 'slow-audio';
await ffmpeg.html5ify(filePath, html5FriendlyPathNew, encodeVideo, encodeAudio);
await ffmpegHtml5ify(filePath, html5FriendlyPathNew, encodeVideo, encodeAudio);
load(filePath, html5FriendlyPathNew);
} else {
await createDummyVideo(filePath);
@ -1259,7 +1258,7 @@ const App = memo(() => {
errorToast('File exists, bailing');
return;
}
await edlStore.save(fp, cutSegments);
await edlStoreSave(fp, cutSegments);
} catch (err) {
errorToast('Failed to export CSV');
console.error('Failed to export CSV', err);
@ -1723,6 +1722,4 @@ const App = memo(() => {
);
});
ReactDOM.render(<App />, document.getElementById('app'));
console.log('Version', electron.remote.app.getVersion());
export default App;

View File

@ -3,10 +3,10 @@ import { IoIosCloseCircleOutline } from 'react-icons/io';
import { FaClipboard } from 'react-icons/fa';
import { motion, AnimatePresence } from 'framer-motion';
// eslint-disable-next-line import/no-extraneous-dependencies
const { clipboard } = require('electron');
import { toast } from './util';
const { clipboard } = window.require('electron');
const { toast } = require('./util');
const HelpSheet = memo(({
visible, onTogglePress, ffmpegCommandLog,

View File

@ -3,7 +3,7 @@ import { Select } from 'evergreen-ui';
import { motion } from 'framer-motion';
import { FaYinYang } from 'react-icons/fa';
const { withBlur, toast } = require('./util');
import { withBlur, toast } from './util';
const LeftMenu = memo(({ zoom, setZoom, invertCutSegments, setInvertCutSegments }) => {

View File

@ -5,13 +5,13 @@ import { GoFileBinary } from 'react-icons/go';
import { MdSubtitles } from 'react-icons/md';
import Swal from 'sweetalert2';
import { SegmentedControl } from 'evergreen-ui';
import withReactContent from 'sweetalert2-react-content';
import { formatDuration } from './util';
import { getStreamFps } from './ffmpeg';
const ReactSwal = withReactContent(Swal);
const { formatDuration } = require('./util');
const { getStreamFps } = require('./ffmpeg');
function onInfoClick(s, title) {
ReactSwal.fire({

View File

@ -140,7 +140,7 @@ const Timeline = memo(({
const nextThumbTime = nextThumbnail ? nextThumbnail.time : durationSafe;
const maxWidthPercent = ((nextThumbTime - thumbnail.time) / durationSafe) * 100 * 0.9;
return (
<img key={thumbnail.url} src={thumbnail.url} alt="" style={{ position: 'absolute', left: `${leftPercent}%`, height: timelineHeight * 1.5, zIndex: 1, maxWidth: `${maxWidthPercent}%`, objectFit: 'cover', border: '1px solid rgba(255, 255, 255, 0.5)', borderBottomRightRadius: 15, borderTopLeftRadius: 15, borderTopRightRadius: 15 }} />
<img key={thumbnail.url} src={thumbnail.url} alt="" style={{ position: 'absolute', left: `${leftPercent}%`, height: timelineHeight * 1.5, zIndex: 1, maxWidth: `${maxWidthPercent}%`, objectFit: 'cover', border: '1px solid rgba(255, 255, 255, 0.5)', borderBottomRightRadius: 15, borderTopLeftRadius: 15, borderTopRightRadius: 15, pointerEvents: 'none' }} />
);
})}
</div>

View File

@ -4,7 +4,7 @@ import { FaTrashAlt } from 'react-icons/fa';
import { mySpring } from './animations';
const { formatDuration } = require('./util');
import { formatDuration } from './util';
const TimelineSeg = memo(({

View File

@ -1,8 +1,9 @@
const fs = require('fs-extra');
const mime = require('mime-types');
const strongDataUri = require('strong-data-uri');
import strongDataUri from 'strong-data-uri';
const { formatDuration, getOutPath, transferTimestampsWithOffset } = require('./util');
import { formatDuration, getOutPath, transferTimestampsWithOffset } from './util';
const fs = window.require('fs-extra');
const mime = window.require('mime-types');
function getFrameFromVideo(video, format) {
const canvas = document.createElement('canvas');
@ -16,7 +17,7 @@ function getFrameFromVideo(video, format) {
return strongDataUri.decode(dataUri);
}
async function captureFrame(customOutDir, filePath, video, currentTime, captureFormat) {
export default async function captureFrame(customOutDir, filePath, video, currentTime, captureFormat) {
const buf = getFrameFromVideo(video, captureFormat);
const ext = mime.extension(buf.mimetype);
@ -27,5 +28,3 @@ async function captureFrame(customOutDir, filePath, video, currentTime, captureF
const offset = -video.duration + currentTime;
return transferTimestampsWithOffset(filePath, outPath, offset);
}
module.exports = captureFrame;

View File

@ -1,12 +1,13 @@
const fs = require('fs-extra');
const parse = require('csv-parse');
const stringify = require('csv-stringify');
const { promisify } = require('util');
import parse from 'csv-parse';
import stringify from 'csv-stringify';
const fs = window.require('fs-extra');
const { promisify } = window.require('util');
const stringifyAsync = promisify(stringify);
const parseAsync = promisify(parse);
async function load(path) {
export async function load(path) {
const str = await fs.readFile(path, 'utf-8');
const rows = await parseAsync(str, {});
if (rows.length === 0) throw new Error('No rows found');
@ -30,14 +31,9 @@ async function load(path) {
return mapped;
}
async function save(path, cutSegments) {
export async function save(path, cutSegments) {
console.log('Saving', path);
const rows = cutSegments.map(({ start, end, name }) => [start, end, name]);
const str = await stringifyAsync(rows);
await fs.writeFile(path, str);
}
module.exports = {
load,
save,
};

View File

@ -1,20 +1,22 @@
const execa = require('execa');
const pMap = require('p-map');
const { join, extname } = require('path');
const fileType = require('file-type');
const readChunk = require('read-chunk');
const flatMap = require('lodash/flatMap');
const flatMapDeep = require('lodash/flatMapDeep');
const sum = require('lodash/sum');
const sortBy = require('lodash/sortBy');
const readline = require('readline');
const moment = require('moment');
const stringToStream = require('string-to-stream');
const trash = require('trash');
const isDev = require('electron-is-dev');
const os = require('os');
import pMap from 'p-map';
import flatMap from 'lodash/flatMap';
import flatMapDeep from 'lodash/flatMapDeep';
import sum from 'lodash/sum';
import sortBy from 'lodash/sortBy';
import moment from 'moment';
import { formatDuration, getOutPath, transferTimestamps, filenamify } from './util';
const execa = window.require('execa');
const { join, extname } = window.require('path');
const fileType = window.require('file-type');
const readChunk = window.require('read-chunk');
const readline = window.require('readline');
const stringToStream = window.require('string-to-stream');
const trash = window.require('trash');
const isDev = window.require('electron-is-dev');
const os = window.require('os');
const { formatDuration, getOutPath, transferTimestamps, filenamify } = require('./util');
function getFfCommandLine(cmd, args) {
@ -80,11 +82,11 @@ function handleProgress(process, cutDuration, onProgress) {
});
}
function isCuttingStart(cutFrom) {
export function isCuttingStart(cutFrom) {
return cutFrom > 0;
}
function isCuttingEnd(cutTo, duration) {
export function isCuttingEnd(cutTo, duration) {
return cutTo < duration;
}
@ -104,7 +106,7 @@ function getIntervalAroundTime(time, window) {
};
}
async function readFrames({ filePath, aroundTime, window, stream }) {
export async function readFrames({ filePath, aroundTime, window, stream }) {
let intervalsArgs = [];
if (aroundTime != null) {
const { from, to } = getIntervalAroundTime(aroundTime, window);
@ -120,7 +122,7 @@ async function readFrames({ filePath, aroundTime, window, stream }) {
// https://stackoverflow.com/questions/14005110/how-to-split-a-video-using-ffmpeg-so-that-each-chunk-starts-with-a-key-frame
// http://kicherer.org/joomla/index.php/de/blog/42-avcut-frame-accurate-video-cutting-with-only-small-quality-loss
function getSafeCutTime(frames, cutTime, nextMode) {
export function getSafeCutTime(frames, cutTime, nextMode) {
const sigma = 0.01;
const isCloseTo = (time1, time2) => Math.abs(time1 - time2) < sigma;
@ -167,7 +169,7 @@ function getSafeCutTime(frames, cutTime, nextMode) {
return frames[index - 1].time;
}
function findNearestKeyFrameTime({ frames, time, direction, fps }) {
export function findNearestKeyFrameTime({ frames, time, direction, fps }) {
const sigma = fps ? (1 / fps) : 0.1;
const keyframes = frames.filter(f => f.keyframe && (direction > 0 ? f.time > time + sigma : f.time < time - sigma));
if (keyframes.length === 0) return undefined;
@ -244,7 +246,7 @@ async function cut({
await transferTimestamps(filePath, outPath);
}
async function cutMultiple({
export async function cutMultiple({
customOutDir, filePath, segments: segmentsUnsorted, videoDuration, rotation,
onProgress, keyframeCut, copyStreamIds, outFormat, isOutFormatUserSelected,
appendFfmpegCommandLog, shortestFlag,
@ -295,7 +297,7 @@ async function cutMultiple({
return outFiles;
}
async function html5ify(filePath, outPath, encodeVideo, encodeAudio) {
export async function html5ify(filePath, outPath, encodeVideo, encodeAudio) {
console.log('Making HTML5 friendly version', { filePath, outPath, encodeVideo });
const videoArgs = encodeVideo
@ -315,7 +317,7 @@ async function html5ify(filePath, outPath, encodeVideo, encodeAudio) {
await transferTimestamps(filePath, outPath);
}
async function getDuration(filePath) {
export async function getDuration(filePath) {
// https://superuser.com/questions/650291/how-to-get-video-duration-in-seconds
const { stdout } = await runFfprobe(['-i', filePath, '-show_entries', 'format=duration', '-print_format', 'json']);
return parseFloat(JSON.parse(stdout).format.duration);
@ -323,7 +325,7 @@ async function getDuration(filePath) {
// This is just used to load something into the player with correct length,
// so user can seek and then we render frames using ffmpeg
async function html5ifyDummy(filePath, outPath) {
export async function html5ifyDummy(filePath, outPath) {
console.log('Making HTML5 friendly dummy', { filePath, outPath });
const duration = await getDuration(filePath);
@ -376,14 +378,14 @@ async function mergeFiles({ paths, outPath, allStreams }) {
console.log(result.stdout);
}
async function mergeAnyFiles({ customOutDir, paths, allStreams }) {
export async function mergeAnyFiles({ customOutDir, paths, allStreams }) {
const firstPath = paths[0];
const ext = extname(firstPath);
const outPath = getOutPath(customOutDir, firstPath, `merged${ext}`);
return mergeFiles({ paths, outPath, allStreams });
}
async function autoMergeSegments({ customOutDir, sourceFile, segmentPaths }) {
export async function autoMergeSegments({ customOutDir, sourceFile, segmentPaths }) {
const ext = extname(sourceFile);
const outPath = getOutPath(customOutDir, sourceFile, `cut-merged-${new Date().getTime()}${ext}`);
await mergeFiles({ paths: segmentPaths, outPath });
@ -413,7 +415,7 @@ function determineOutputFormat(ffprobeFormats, ft) {
return ffprobeFormats[0] || undefined;
}
async function getFormatData(filePath) {
export async function getFormatData(filePath) {
console.log('getFormatData', filePath);
const { stdout } = await runFfprobe([
@ -422,7 +424,7 @@ async function getFormatData(filePath) {
return JSON.parse(stdout).format;
}
async function getDefaultOutFormat(filePath, formatData) {
export async function getDefaultOutFormat(filePath, formatData) {
const formatsStr = formatData.format_name;
console.log('formats', formatsStr);
const formats = (formatsStr || '').split(',');
@ -435,7 +437,7 @@ async function getDefaultOutFormat(filePath, formatData) {
return mapFormat(assumedFormat);
}
async function getAllStreams(filePath) {
export async function getAllStreams(filePath) {
const { stdout } = await runFfprobe([
'-of', 'json', '-show_entries', 'stream', '-i', filePath,
]);
@ -472,7 +474,7 @@ function getPreferredCodecFormat(codec, type) {
}
// https://stackoverflow.com/questions/32922226/extract-every-audio-and-subtitles-from-a-video-with-ffmpeg
async function extractStreams({ filePath, customOutDir, streams }) {
export async function extractStreams({ filePath, customOutDir, streams }) {
const outStreams = streams.map((s) => ({
index: s.index,
codec: s.codec_name || s.codec_tag_string || s.codec_type,
@ -517,7 +519,7 @@ async function renderThumbnail(filePath, timestamp) {
return URL.createObjectURL(blob);
}
async function renderThumbnails({ filePath, from, duration, onThumbnail }) {
export async function renderThumbnails({ filePath, from, duration, onThumbnail }) {
// Time first render to determine how many to render
const startTime = new Date().getTime() / 1000;
let url = await renderThumbnail(filePath, from);
@ -538,7 +540,7 @@ async function renderThumbnails({ filePath, from, duration, onThumbnail }) {
}
async function renderWaveformPng({ filePath, aroundTime, window, color }) {
export async function renderWaveformPng({ filePath, aroundTime, window, color }) {
const { from, to } = getIntervalAroundTime(aroundTime, window);
const args1 = [
@ -589,7 +591,7 @@ async function renderWaveformPng({ filePath, aroundTime, window, color }) {
}
}
async function renderFrame(timestamp, filePath, rotation) {
export async function renderFrame(timestamp, filePath, rotation) {
const transpose = {
90: 'transpose=2',
180: 'transpose=1,transpose=1',
@ -619,13 +621,13 @@ async function renderFrame(timestamp, filePath, rotation) {
}
// https://www.ffmpeg.org/doxygen/3.2/libavutil_2utils_8c_source.html#l00079
const defaultProcessedCodecTypes = [
export const defaultProcessedCodecTypes = [
'video',
'audio',
'subtitle',
];
function getStreamFps(stream) {
export function getStreamFps(stream) {
const match = typeof stream.avg_frame_rate === 'string' && stream.avg_frame_rate.match(/^([0-9]+)\/([0-9]+)$/);
if (stream.codec_type === 'video' && match) {
const num = parseInt(match[1], 10);
@ -634,26 +636,3 @@ function getStreamFps(stream) {
}
return undefined;
}
module.exports = {
cutMultiple,
getFormatData,
getDefaultOutFormat,
html5ify,
html5ifyDummy,
mergeAnyFiles,
autoMergeSegments,
extractStreams,
renderFrame,
getAllStreams,
defaultProcessedCodecTypes,
getStreamFps,
isCuttingStart,
isCuttingEnd,
readFrames,
getSafeCutTime,
findNearestKeyFrameTime,
renderWaveformPng,
renderThumbnails,
};

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
</body>
</html>

11
src/index.jsx Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './main.css';
import App from './App';
const electron = window.require('electron');
console.log('Version', electron.remote.app.getVersion());
ReactDOM.render(<App />, document.getElementById('root'));

View File

@ -1,14 +1,16 @@
const React = require('react');
const swal = require('sweetalert2');
const withReactContent = require('sweetalert2-react-content');
import React from 'react';
import swal from 'sweetalert2';
const SortableFiles = require('./SortableFiles').default;
import withReactContent from 'sweetalert2-react-content';
const { errorToast } = require('../util');
import SortableFiles from './SortableFiles';
import { errorToast } from '../util';
const MySwal = withReactContent(swal);
async function showMergeDialog(paths, onMergeClick) {
export async function showMergeDialog(paths, onMergeClick) {
if (!paths) return;
if (paths.length < 2) {
errorToast('More than one file must be selected');
@ -36,7 +38,7 @@ async function showMergeDialog(paths, onMergeClick) {
}
}
async function showOpenAndMergeDialog({ dialog, defaultPath, onMergeClick }) {
export async function showOpenAndMergeDialog({ dialog, defaultPath, onMergeClick }) {
const title = 'Please select files to be merged';
const message = 'Please select files to be merged. The files need to be of the exact same format and codecs';
const { canceled, filePaths } = await dialog.showOpenDialog({
@ -48,8 +50,3 @@ async function showOpenAndMergeDialog({ dialog, defaultPath, onMergeClick }) {
if (canceled) return;
showMergeDialog(filePaths, onMergeClick);
}
module.exports = {
showMergeDialog,
showOpenAndMergeDialog,
};

View File

@ -1,5 +1,5 @@
// Extracted from "ffmpeg -formats"
module.exports = {
export default {
'3g2': '3GP2 (3GPP2 file format)',
'3gp': '3GP (3GPP file format)',
a64: 'a64 - video for Commodore 64',

View File

@ -1,12 +1,12 @@
// https://github.com/mock-end/random-color/blob/master/index.js
/* eslint-disable */
const color = require('color');
import color from 'color';
var ratio = 0.618033988749895;
var hue = 0.65;
module.exports = function (saturation, value) {
export default (saturation, value) => {
hue += ratio;
hue %= 1;

View File

@ -1,6 +1,6 @@
const Store = require('electron-store');
const Store = window.require('electron-store');
const store = new Store({
export default new Store({
defaults: {
captureFormat: 'jpeg',
customOutDir: undefined,
@ -14,5 +14,3 @@ const store = new Store({
autoSaveProjectFile: true,
},
});
module.exports = store;

View File

@ -1,30 +1,31 @@
const _ = require('lodash');
const path = require('path');
const fs = require('fs-extra');
const Swal = require('sweetalert2');
import padStart from 'lodash/padStart';
import Swal from 'sweetalert2';
const randomColor = require('./random-color');
import randomColor from './random-color';
const path = window.require('path');
const fs = window.require('fs-extra');
function formatDuration({ seconds: _seconds, fileNameFriendly, fps }) {
export function formatDuration({ seconds: _seconds, fileNameFriendly, fps }) {
const seconds = _seconds || 0;
const minutes = seconds / 60;
const hours = minutes / 60;
const hoursPadded = _.padStart(Math.floor(hours), 2, '0');
const minutesPadded = _.padStart(Math.floor(minutes % 60), 2, '0');
const secondsPadded = _.padStart(Math.floor(seconds) % 60, 2, '0');
const hoursPadded = padStart(Math.floor(hours), 2, '0');
const minutesPadded = padStart(Math.floor(minutes % 60), 2, '0');
const secondsPadded = padStart(Math.floor(seconds) % 60, 2, '0');
const ms = seconds - Math.floor(seconds);
const msPadded = fps != null
? _.padStart(Math.floor(ms * fps), 2, '0')
: _.padStart(Math.floor(ms * 1000), 3, '0');
? padStart(Math.floor(ms * fps), 2, '0')
: padStart(Math.floor(ms * 1000), 3, '0');
// Be nice to filenames and use .
const delim = fileNameFriendly ? '.' : ':';
return `${hoursPadded}${delim}${minutesPadded}${delim}${secondsPadded}.${msPadded}`;
}
function parseDuration(str) {
export function parseDuration(str) {
if (!str) return undefined;
const match = str.trim().match(/^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/);
if (!match) return undefined;
@ -37,20 +38,20 @@ function parseDuration(str) {
return ((((hours * 60) + minutes) * 60) + seconds) + (ms / 1000);
}
function getOutDir(customOutDir, filePath) {
export function getOutDir(customOutDir, filePath) {
if (customOutDir) return customOutDir;
if (filePath) return path.dirname(filePath);
return undefined;
}
function getOutPath(customOutDir, filePath, nameSuffix) {
export function getOutPath(customOutDir, filePath, nameSuffix) {
if (!filePath) return undefined;
const parsed = path.parse(filePath);
return path.join(getOutDir(customOutDir, filePath), `${parsed.name}-${nameSuffix}`);
}
async function transferTimestamps(inPath, outPath) {
export async function transferTimestamps(inPath, outPath) {
try {
const stat = await fs.stat(inPath);
await fs.utimes(outPath, stat.atime.getTime() / 1000, stat.mtime.getTime() / 1000);
@ -59,7 +60,7 @@ async function transferTimestamps(inPath, outPath) {
}
}
async function transferTimestampsWithOffset(inPath, outPath, offset) {
export async function transferTimestampsWithOffset(inPath, outPath, offset) {
try {
const stat = await fs.stat(inPath);
const time = (stat.mtime.getTime() / 1000) + offset;
@ -69,33 +70,33 @@ async function transferTimestampsWithOffset(inPath, outPath, offset) {
}
}
const toast = Swal.mixin({
export const toast = Swal.mixin({
toast: true,
position: 'top',
showConfirmButton: false,
timer: 5000,
});
const errorToast = (title) => toast.fire({
export const errorToast = (title) => toast.fire({
icon: 'error',
title,
});
async function showFfmpegFail(err) {
export async function showFfmpegFail(err) {
console.error(err);
return errorToast(`Failed to run ffmpeg: ${err.stack}`);
}
function setFileNameTitle(filePath) {
export function setFileNameTitle(filePath) {
const appName = 'LosslessCut';
document.title = filePath ? `${appName} - ${path.basename(filePath)}` : appName;
}
function filenamify(name) {
export function filenamify(name) {
return name.replace(/[^0-9a-zA-Z_.]/g, '_');
}
async function promptTimeOffset(inputValue) {
export async function promptTimeOffset(inputValue) {
const { value } = await Swal.fire({
title: 'Set custom start time offset',
text: 'Instead of video apparently starting at 0, you can offset by a specified value (useful for viewing/cutting videos according to timecodes)',
@ -116,18 +117,18 @@ async function promptTimeOffset(inputValue) {
return duration;
}
function generateColor() {
export function generateColor() {
return randomColor(1, 0.95);
}
function withBlur(cb) {
export function withBlur(cb) {
return (e) => {
cb(e);
e.target.blur();
};
}
function getSegColors(seg) {
export function getSegColors(seg) {
if (!seg) return {};
const { color } = seg;
return {
@ -136,21 +137,3 @@ function getSegColors(seg) {
segBorderColor: color.lighten(0.5).string(),
};
}
module.exports = {
formatDuration,
parseDuration,
getOutPath,
getOutDir,
transferTimestamps,
transferTimestampsWithOffset,
toast,
errorToast,
showFfmpegFail,
setFileNameTitle,
promptTimeOffset,
generateColor,
filenamify,
withBlur,
getSegColors,
};

9594
yarn.lock

File diff suppressed because it is too large Load Diff