diff --git a/README.md b/README.md index bb5123e0..17b60b59 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ The main feature is lossless trimming and cutting of video and audio files, whic - Customizable keyboard hotkeys - Black scene detection, silent audio detection, and scene change detection - Divide timeline into segments with length L or into N segments or even randomized segments! +- Speed up / slow down video or audio file ([changing FPS](https://github.com/mifi/lossless-cut/issues/1712)) - [Basic CLI support](cli.md) ## Example lossless use cases diff --git a/issues.md b/issues.md index 4e3a9176..a07bd3d8 100644 --- a/issues.md +++ b/issues.md @@ -2,8 +2,6 @@ - **Can LosslessCut crop, resize, stretch, mirror, overlay text/images, watermark, blur, redact, re-encode, create GIF, slideshow, burn subtitles, color grading, fade/transition between video clips, fade/combine/mix/merge audio tracks or change audio volume?** - No, these are all lossy operations (meaning you *have* to re-encode the file), but in the future I may start to implement such features. [See this issue for more information.](https://github.com/mifi/lossless-cut/issues/372) -- Can i speed-up/slow-down video? - - Not yet, but see this issue: [#1712](https://github.com/mifi/lossless-cut/issues/1712) - Can LosslessCut be batched/automated using a CLI or API? - While it was never designed for advanced batching/automation, it does have a [basic CLI](./cli.md), and there are a few feature requests regarding this: [#980](https://github.com/mifi/lossless-cut/issues/980) [#868](https://github.com/mifi/lossless-cut/issues/868). - Is there a keyboard shortcut to do X? diff --git a/src/App.jsx b/src/App.jsx index 648f610b..39851fd9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -149,6 +149,7 @@ const App = memo(() => { const [exportConfirmVisible, setExportConfirmVisible] = useState(false); const [cacheBuster, setCacheBuster] = useState(0); const [customMergedOutFileName, setMergedOutFileName] = useState(); + const [outputPlaybackRate, setOutputPlaybackRateState] = useState(1); const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState(); @@ -203,6 +204,11 @@ const App = memo(() => { const videoRef = useRef(); + const setOutputPlaybackRate = useCallback((v) => { + setOutputPlaybackRateState(v); + if (videoRef.current) videoRef.current.playbackRate = v; + }, []); + const isFileOpened = !!filePath; const onOutputFormatUserChange = useCallback((newFormat) => { @@ -733,6 +739,7 @@ const App = memo(() => { setHideCanvasPreview(false); setExportConfirmVisible(false); setMergedOutFileName(); + setOutputPlaybackRateState(1); cancelRenderThumbnails(); }, [cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setDeselectedSegmentIds, cancelRenderThumbnails]); @@ -751,7 +758,7 @@ const App = memo(() => { const { concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration, - } = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput }); + } = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate }); const html5ifyAndLoad = useCallback(async (cod, fp, speed, hv, ha) => { const usesDummyVideo = ['fastest-audio', 'fastest-audio-remux', 'fastest'].includes(speed); @@ -855,12 +862,12 @@ const App = memo(() => { if (Math.abs(commandedTimeRef.current - video.currentTime) > 1) video.currentTime = commandedTimeRef.current; - if (resetPlaybackRate) video.playbackRate = 1; + if (resetPlaybackRate) video.playbackRate = outputPlaybackRate; video.play().catch((err) => { showPlaybackFailedMessage(); console.error(err); }); - }, [filePath, playing]); + }, [filePath, outputPlaybackRate, playing]); const togglePlay = useCallback(({ resetPlaybackRate, playbackMode } = {}) => { playbackModeRef.current = undefined; @@ -2469,6 +2476,8 @@ const App = memo(() => { isFileOpened={isFileOpened} darkMode={darkMode} setDarkMode={setDarkMode} + outputPlaybackRate={outputPlaybackRate} + setOutputPlaybackRate={setOutputPlaybackRate} /> diff --git a/src/BottomBar.jsx b/src/BottomBar.jsx index fb010d46..b7a0227b 100644 --- a/src/BottomBar.jsx +++ b/src/BottomBar.jsx @@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { motion } from 'framer-motion'; import { MdRotate90DegreesCcw } from 'react-icons/md'; import { useTranslation } from 'react-i18next'; -import { IoIosCamera, IoMdKey } from 'react-icons/io'; +import { IoIosCamera, IoMdKey, IoMdSpeedometer } from 'react-icons/io'; import { FaYinYang, FaTrashAlt, FaStepBackward, FaStepForward, FaCaretLeft, FaCaretRight, FaPause, FaPlay, FaImages, FaKey, FaSun } from 'react-icons/fa'; import { GiSoundWaves } from 'react-icons/gi'; // import useTraceUpdate from 'use-trace-update'; @@ -22,6 +22,7 @@ import { getSegColor as getSegColorRaw } from './util/colors'; import { useSegColors } from './contexts'; import { formatDuration, parseDuration, isExactDurationMatch } from './util/duration'; import useUserSettings from './hooks/useUserSettings'; +import { askForPlaybackRate } from './dialogs'; const { clipboard } = window.require('electron'); @@ -144,6 +145,7 @@ const BottomBar = memo(({ keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe, detectedFps, isFileOpened, selectedSegments, darkMode, setDarkMode, toggleEnableThumbnails, toggleWaveformMode, waveformMode, showThumbnails, + outputPlaybackRate, setOutputPlaybackRate, }) => { const { t } = useTranslation(); const { getSegColor } = useSegColors(); @@ -192,6 +194,11 @@ const BottomBar = memo(({ checkAppPath(); }, []); + const handleChangePlaybackRateClick = useCallback(async () => { + const newRate = await askForPlaybackRate({ detectedFps, outputPlaybackRate }); + if (newRate != null) setOutputPlaybackRate(newRate); + }, [detectedFps, outputPlaybackRate, setOutputPlaybackRate]); + function renderJumpCutpointButton(direction) { const newIndex = currentSegIndexSafe + direction; const seg = cutSegments[newIndex]; @@ -383,7 +390,11 @@ const BottomBar = memo(({ ))} - {detectedFps != null &&