diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 9a8d5f81..3b18d532 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -165,6 +165,7 @@ function App() { const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState(); const [editingSegmentTags, setEditingSegmentTags] = useState(); const [mediaSourceQuality, setMediaSourceQuality] = useState(0); + const [smartCutBitrate, setSmartCutBitrate] = useState(); const incrementMediaSourceQuality = useCallback(() => setMediaSourceQuality((v) => (v + 1) % mediaSourceQualities.length), []); @@ -836,7 +837,7 @@ function App() { const { 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 usesDummyVideo = speed === 'fastest'; @@ -2733,7 +2734,7 @@ function App() { /> - + setStreamsSelectorShown(false)} maxWidth={1000}> {mainStreams && filePath != null && ( diff --git a/src/renderer/src/components/ExportConfirm.tsx b/src/renderer/src/components/ExportConfirm.tsx index 56ef360a..02678e13 100644 --- a/src/renderer/src/components/ExportConfirm.tsx +++ b/src/renderer/src/components/ExportConfirm.tsx @@ -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 { WarningSignIcon, CrossIcon } from 'evergreen-ui'; import { FaRegCheckCircle } from 'react-icons/fa'; @@ -28,6 +28,7 @@ import { InverseCutSegment, SegmentToExport } from '../types'; import { GenerateOutSegFileNames } from '../util/outputNameTemplate'; import { FFprobeStream } from '../../../../ffprobe'; 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' }; @@ -61,6 +62,8 @@ const ExportConfirm = memo(({ needSmartCut, mergedOutFileName, setMergedOutFileName, + smartCutBitrate, + setSmartCutBitrate, } : { areWeCutting: boolean, selectedSegments: InverseCutSegment[], @@ -84,6 +87,8 @@ const ExportConfirm = memo(({ needSmartCut: boolean, mergedOutFileName: string | undefined, setMergedOutFileName: (a: string) => void, + smartCutBitrate: number | undefined, + setSmartCutBitrate: Dispatch>, }) => { const { t } = useTranslation(); @@ -165,6 +170,16 @@ const ExportConfirm = memo(({ const canEditTemplate = !willMerge || !autoDeleteMergedSegments; + const handleSmartCutBitrateToggle = useCallback((checked: boolean) => { + setSmartCutBitrate(() => (checked ? undefined : 10000)); + }, [setSmartCutBitrate]); + + const handleSmartCutBitrateChange = useCallback((e: React.ChangeEvent) => { + 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 return ( @@ -347,6 +362,26 @@ const ExportConfirm = memo(({ + {needSmartCut && ( + + + {t('Smart cut auto detect bitrate')} + + +
+ {smartCutBitrate != null && ( + <> + + {t('kbit/s')} + + )} + +
+ + + + )} + {!needSmartCut && ( diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts index 4d7f68a6..1853eb16 100644 --- a/src/renderer/src/hooks/useFfmpegOperations.ts +++ b/src/renderer/src/hooks/useFfmpegOperations.ts @@ -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 }); } -function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog }: { +function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate }: { filePath: string | undefined, treatInputFileModifiedTimeAsStart: boolean | null | undefined, treatOutputFileModifiedTimeAsStart: boolean | null | undefined, @@ -69,6 +69,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea outputPlaybackRate: number, cutFromAdjustmentFrames: number, appendLastCommandsLog: (a: string) => void, + smartCutCustomBitrate: number | undefined, }) { const appendFfmpegCommandLog = useCallback((args: string[]) => appendLastCommandsLog(getFfCommandLine('ffmpeg', args)), [appendLastCommandsLog]); @@ -412,7 +413,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea 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'); @@ -430,10 +431,10 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea // eslint-disable-next-line no-shadow async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }) { 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(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); } @@ -497,7 +498,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea } finally { 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 outDir = getOutDir(customOutDir, filePath); @@ -576,7 +577,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea await transferTimestamps({ inPath: filePath, outPath, treatOutputFileModifiedTimeAsStart }); return outPath; - }, [filePath, treatOutputFileModifiedTimeAsStart]); + }, [appendFfmpegCommandLog, filePath, treatOutputFileModifiedTimeAsStart]); return { cutMultiple, concatFiles, html5ify, html5ifyDummy, fixInvalidDuration, autoConcatCutSegments,