mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-21 18:02:35 +01:00
parent
b5028dc142
commit
096db54f11
@ -165,6 +165,7 @@ function App() {
|
|||||||
const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState<number>();
|
const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState<number>();
|
||||||
const [editingSegmentTags, setEditingSegmentTags] = useState<SegmentTags>();
|
const [editingSegmentTags, setEditingSegmentTags] = useState<SegmentTags>();
|
||||||
const [mediaSourceQuality, setMediaSourceQuality] = useState(0);
|
const [mediaSourceQuality, setMediaSourceQuality] = useState(0);
|
||||||
|
const [smartCutBitrate, setSmartCutBitrate] = useState<number | undefined>();
|
||||||
|
|
||||||
const incrementMediaSourceQuality = useCallback(() => setMediaSourceQuality((v) => (v + 1) % mediaSourceQualities.length), []);
|
const incrementMediaSourceQuality = useCallback(() => setMediaSourceQuality((v) => (v + 1) % mediaSourceQualities.length), []);
|
||||||
|
|
||||||
@ -836,7 +837,7 @@ function App() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration,
|
concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration,
|
||||||
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog });
|
} = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate: smartCutBitrate });
|
||||||
|
|
||||||
const html5ifyAndLoad = useCallback(async (cod: string | undefined, fp: string, speed: Html5ifyMode, hv: boolean, ha: boolean) => {
|
const html5ifyAndLoad = useCallback(async (cod: string | undefined, fp: string, speed: Html5ifyMode, hv: boolean, ha: boolean) => {
|
||||||
const usesDummyVideo = speed === 'fastest';
|
const usesDummyVideo = speed === 'fastest';
|
||||||
@ -2733,7 +2734,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ExportConfirm areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} />
|
<ExportConfirm areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} smartCutBitrate={smartCutBitrate} setSmartCutBitrate={setSmartCutBitrate} />
|
||||||
|
|
||||||
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>
|
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>
|
||||||
{mainStreams && filePath != null && (
|
{mainStreams && filePath != null && (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CSSProperties, memo, useCallback, useMemo } from 'react';
|
import { CSSProperties, Dispatch, SetStateAction, memo, useCallback, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { WarningSignIcon, CrossIcon } from 'evergreen-ui';
|
import { WarningSignIcon, CrossIcon } from 'evergreen-ui';
|
||||||
import { FaRegCheckCircle } from 'react-icons/fa';
|
import { FaRegCheckCircle } from 'react-icons/fa';
|
||||||
@ -28,6 +28,7 @@ import { InverseCutSegment, SegmentToExport } from '../types';
|
|||||||
import { GenerateOutSegFileNames } from '../util/outputNameTemplate';
|
import { GenerateOutSegFileNames } from '../util/outputNameTemplate';
|
||||||
import { FFprobeStream } from '../../../../ffprobe';
|
import { FFprobeStream } from '../../../../ffprobe';
|
||||||
import { AvoidNegativeTs } from '../../../../types';
|
import { AvoidNegativeTs } from '../../../../types';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
|
||||||
|
|
||||||
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
|
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
|
||||||
@ -61,6 +62,8 @@ const ExportConfirm = memo(({
|
|||||||
needSmartCut,
|
needSmartCut,
|
||||||
mergedOutFileName,
|
mergedOutFileName,
|
||||||
setMergedOutFileName,
|
setMergedOutFileName,
|
||||||
|
smartCutBitrate,
|
||||||
|
setSmartCutBitrate,
|
||||||
} : {
|
} : {
|
||||||
areWeCutting: boolean,
|
areWeCutting: boolean,
|
||||||
selectedSegments: InverseCutSegment[],
|
selectedSegments: InverseCutSegment[],
|
||||||
@ -84,6 +87,8 @@ const ExportConfirm = memo(({
|
|||||||
needSmartCut: boolean,
|
needSmartCut: boolean,
|
||||||
mergedOutFileName: string | undefined,
|
mergedOutFileName: string | undefined,
|
||||||
setMergedOutFileName: (a: string) => void,
|
setMergedOutFileName: (a: string) => void,
|
||||||
|
smartCutBitrate: number | undefined,
|
||||||
|
setSmartCutBitrate: Dispatch<SetStateAction<number | undefined>>,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -165,6 +170,16 @@ const ExportConfirm = memo(({
|
|||||||
|
|
||||||
const canEditTemplate = !willMerge || !autoDeleteMergedSegments;
|
const canEditTemplate = !willMerge || !autoDeleteMergedSegments;
|
||||||
|
|
||||||
|
const handleSmartCutBitrateToggle = useCallback((checked: boolean) => {
|
||||||
|
setSmartCutBitrate(() => (checked ? undefined : 10000));
|
||||||
|
}, [setSmartCutBitrate]);
|
||||||
|
|
||||||
|
const handleSmartCutBitrateChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const v = parseInt(e.target.value, 10);
|
||||||
|
if (Number.isNaN(v) || v <= 0) return;
|
||||||
|
setSmartCutBitrate(v);
|
||||||
|
}, [setSmartCutBitrate]);
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container
|
// https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@ -347,6 +362,26 @@ const ExportConfirm = memo(({
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{needSmartCut && (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{t('Smart cut auto detect bitrate')}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||||
|
{smartCutBitrate != null && (
|
||||||
|
<>
|
||||||
|
<TextInput value={smartCutBitrate} onChange={handleSmartCutBitrateChange} style={{ width: '4em', flexGrow: 0, marginRight: '.3em' }} />
|
||||||
|
<span style={{ marginRight: '.3em' }}>{t('kbit/s')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span><Switch checked={smartCutBitrate == null} onCheckedChange={handleSmartCutBitrateToggle} /></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
{!needSmartCut && (
|
{!needSmartCut && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
@ -60,7 +60,7 @@ async function tryDeleteFiles(paths: string[]) {
|
|||||||
return pMap(paths, (path) => unlinkWithRetry(path).catch((err) => console.error('Failed to delete', path, err)), { concurrency: 5 });
|
return pMap(paths, (path) => unlinkWithRetry(path).catch((err) => console.error('Failed to delete', path, err)), { concurrency: 5 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog }: {
|
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate }: {
|
||||||
filePath: string | undefined,
|
filePath: string | undefined,
|
||||||
treatInputFileModifiedTimeAsStart: boolean | null | undefined,
|
treatInputFileModifiedTimeAsStart: boolean | null | undefined,
|
||||||
treatOutputFileModifiedTimeAsStart: boolean | null | undefined,
|
treatOutputFileModifiedTimeAsStart: boolean | null | undefined,
|
||||||
@ -69,6 +69,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
|
|||||||
outputPlaybackRate: number,
|
outputPlaybackRate: number,
|
||||||
cutFromAdjustmentFrames: number,
|
cutFromAdjustmentFrames: number,
|
||||||
appendLastCommandsLog: (a: string) => void,
|
appendLastCommandsLog: (a: string) => void,
|
||||||
|
smartCutCustomBitrate: number | undefined,
|
||||||
}) {
|
}) {
|
||||||
const appendFfmpegCommandLog = useCallback((args: string[]) => appendLastCommandsLog(getFfCommandLine('ffmpeg', args)), [appendLastCommandsLog]);
|
const appendFfmpegCommandLog = useCallback((args: string[]) => appendLastCommandsLog(getFfCommandLine('ffmpeg', args)), [appendLastCommandsLog]);
|
||||||
|
|
||||||
@ -412,7 +413,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
|
|||||||
return match ? [match] : [];
|
return match ? [match] : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const { losslessCutFrom, segmentNeedsSmartCut, videoCodec, videoBitrate, videoStreamIndex, videoTimebase } = await getSmartCutParams({ path: filePath, videoDuration, desiredCutFrom, streams: streamsToCopyFromMainFile });
|
const { losslessCutFrom, segmentNeedsSmartCut, videoCodec, videoBitrate: detectedVideoBitrate, videoStreamIndex, videoTimebase } = await getSmartCutParams({ path: filePath, videoDuration, desiredCutFrom, streams: streamsToCopyFromMainFile });
|
||||||
|
|
||||||
if (segmentNeedsSmartCut && !detectedFps) throw new Error('Smart cut is not possible when FPS is unknown');
|
if (segmentNeedsSmartCut && !detectedFps) throw new Error('Smart cut is not possible when FPS is unknown');
|
||||||
|
|
||||||
@ -430,10 +431,10 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
|
|||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }) {
|
async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }) {
|
||||||
if (await shouldSkipExistingFile(outPath)) return;
|
if (await shouldSkipExistingFile(outPath)) return;
|
||||||
if (videoCodec == null || videoBitrate == null || videoTimebase == null) throw new Error();
|
if (videoCodec == null || detectedVideoBitrate == null || videoTimebase == null) throw new Error();
|
||||||
invariant(filePath != null);
|
invariant(filePath != null);
|
||||||
invariant(outFormat != null);
|
invariant(outFormat != null);
|
||||||
const args = await cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoStreamIndex, videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental });
|
const args = await cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate: smartCutCustomBitrate != null ? smartCutCustomBitrate * 1000 : detectedVideoBitrate, videoStreamIndex, videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental });
|
||||||
appendFfmpegCommandLog(args);
|
appendFfmpegCommandLog(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,7 +498,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
|
|||||||
} finally {
|
} finally {
|
||||||
if (chaptersPath) await tryDeleteFiles([chaptersPath]);
|
if (chaptersPath) await tryDeleteFiles([chaptersPath]);
|
||||||
}
|
}
|
||||||
}, [needSmartCut, filePath, losslessCutSingle, shouldSkipExistingFile, concatFiles]);
|
}, [needSmartCut, filePath, losslessCutSingle, shouldSkipExistingFile, smartCutCustomBitrate, appendFfmpegCommandLog, concatFiles]);
|
||||||
|
|
||||||
const autoConcatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, mergedOutFilePath }) => {
|
const autoConcatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, mergedOutFilePath }) => {
|
||||||
const outDir = getOutDir(customOutDir, filePath);
|
const outDir = getOutDir(customOutDir, filePath);
|
||||||
@ -576,7 +577,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
|
|||||||
await transferTimestamps({ inPath: filePath, outPath, treatOutputFileModifiedTimeAsStart });
|
await transferTimestamps({ inPath: filePath, outPath, treatOutputFileModifiedTimeAsStart });
|
||||||
|
|
||||||
return outPath;
|
return outPath;
|
||||||
}, [filePath, treatOutputFileModifiedTimeAsStart]);
|
}, [appendFfmpegCommandLog, filePath, treatOutputFileModifiedTimeAsStart]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, autoConcatCutSegments,
|
cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, autoConcatCutSegments,
|
||||||
|
Loading…
Reference in New Issue
Block a user