Skip to content

Commit

Permalink
implement speedup/slowdown
Browse files Browse the repository at this point in the history
closes #1712
  • Loading branch information
mifi committed Sep 17, 2023
1 parent 65d674f commit fdbccfa
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
15 changes: 12 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -733,6 +739,7 @@ const App = memo(() => {
setHideCanvasPreview(false);
setExportConfirmVisible(false);
setMergedOutFileName();
setOutputPlaybackRateState(1);

cancelRenderThumbnails();
}, [cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setDeselectedSegmentIds, cancelRenderThumbnails]);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2469,6 +2476,8 @@ const App = memo(() => {
isFileOpened={isFileOpened}
darkMode={darkMode}
setDarkMode={setDarkMode}
outputPlaybackRate={outputPlaybackRate}
setOutputPlaybackRate={setOutputPlaybackRate}
/>
</div>

Expand Down
15 changes: 13 additions & 2 deletions src/BottomBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -383,7 +390,11 @@ const BottomBar = memo(({
))}
</Select>

{detectedFps != null && <div title={t('Video FPS')} style={{ color: 'var(--gray11)', fontSize: '.7em', marginLeft: 6 }}>{detectedFps.toFixed(3)}</div>}
{detectedFps != null && (
<div title={t('Video FPS')} role="button" onClick={handleChangePlaybackRateClick} style={{ color: 'var(--gray11)', fontSize: '.7em', marginLeft: 6 }}>{(detectedFps * outputPlaybackRate).toFixed(3)}</div>
)}

<IoMdSpeedometer title={t('Change FPS')} style={{ padding: '0 .2em', fontSize: '1.3em' }} role="button" onClick={handleChangePlaybackRateClick} />
</>
)}

Expand Down
30 changes: 30 additions & 0 deletions src/dialogs/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -605,3 +605,33 @@ export async function openConcatFinishedToast({ filePath, warnings, notices }) {

await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 });
}

export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) {
const fps = detectedFps || 1;
const currentFps = fps * outputPlaybackRate;

function parseValue(v) {
const newFps = parseFloat(v);
if (!Number.isNaN(newFps)) {
return newFps / fps;
}
return undefined;
}

const { value } = await Swal.fire({
title: i18n.t('Change FPS'),
input: 'text',
inputValue: currentFps.toFixed(5),
text: i18n.t('This option lets you losslessly change the speed at which media players will play back the exported file. For example if you double the FPS, the playback speed will double (and duration will halve), however all the frames will be intact and played back (but faster). Be careful not to set it too high, as the player might not be able to keep up (playback CPU usage will increase proportionally to the speed!)'),
showCancelButton: true,
inputValidator: (v) => {
const parsed = parseValue(v);
if (parsed != null) return undefined;
return i18n.t('Please enter a valid number.');
},
});

if (!value) return undefined;

return parseValue(value);
}
8 changes: 6 additions & 2 deletions src/hooks/useFfmpegOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,15 @@ const tryDeleteFiles = async (paths) => pMap(paths, (path) => {
unlink(path).catch((err) => console.error('Failed to delete', path, err));
}, { concurrency: 5 });

function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput }) {
function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate }) {
const shouldSkipExistingFile = useCallback(async (path) => {
const skip = !enableOverwriteOutput && await pathExists(path);
if (skip) console.log('Not overwriting existing file', path);
return skip;
}, [enableOverwriteOutput]);

const getOutputPlaybackRateArgs = useCallback(() => (outputPlaybackRate !== 1 ? ['-itsscale', 1 / outputPlaybackRate] : []), [outputPlaybackRate]);

const concatFiles = useCallback(async ({ paths, outDir, outPath, metadataFromPath, includeAllStreams, streams, outFormat, ffmpegExperimental, onProgress = () => {}, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, videoTimebase, appendFfmpegCommandLog }) => {
if (await shouldSkipExistingFile(outPath)) return { haveExcludedStreams: false };

Expand Down Expand Up @@ -279,6 +281,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// No progress if we set loglevel warning :(
// '-loglevel', 'warning',

...getOutputPlaybackRateArgs(outputPlaybackRate),

...inputArgs,

...mapStreamsArgs,
Expand Down Expand Up @@ -317,7 +321,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
logStdoutStderr(result);

await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(videoDuration) ? videoDuration : undefined, treatOutputFileModifiedTimeAsStart });
}, [filePath, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);
}, [filePath, getOutputPlaybackRateArgs, outputPlaybackRate, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]);

const cutMultiple = useCallback(async ({
outputDir, customOutDir, segments, outSegFileNames, videoDuration, rotation, detectedFps,
Expand Down

0 comments on commit fdbccfa

Please sign in to comment.