diff --git a/src/components/input/BaseFileInput.tsx b/src/components/input/BaseFileInput.tsx index 6346b6d..ebc3840 100644 --- a/src/components/input/BaseFileInput.tsx +++ b/src/components/input/BaseFileInput.tsx @@ -41,7 +41,7 @@ export default function BaseFileInput({ } catch (error) { console.error('Error previewing file:', error); } - } + } else setPreview(null); }, [value]); const handleFileChange = (event: React.ChangeEvent) => { @@ -67,11 +67,6 @@ export default function BaseFileInput({ } }; - function handleClear() { - // @ts-ignore - onChange(null); - } - const handleDrop = (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); @@ -213,11 +208,7 @@ export default function BaseFileInput({ )} - + (null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + // FFmpeg only supports a tempo between 0.5 and 2.0, so we chain filters + const computeAudioFilter = (speed: number): string => { + if (speed <= 2 && speed >= 0.5) { + return `atempo=${speed}`; + } + + // Break into supported chunks + const filters: string[] = []; + let remainingSpeed = speed; + while (remainingSpeed > 2.0) { + filters.push('atempo=2.0'); + remainingSpeed /= 2.0; + } + while (remainingSpeed < 0.5) { + filters.push('atempo=0.5'); + remainingSpeed /= 0.5; + } + filters.push(`atempo=${remainingSpeed.toFixed(2)}`); + + return filters.join(','); + }; + + const compute = (optionsValues: InitialValuesType, input: File | null) => { + if (!input) return; + const { newSpeed } = optionsValues; + let ffmpeg: FFmpeg | null = null; + let ffmpegLoaded = false; + + const processVideo = async ( + file: File, + newSpeed: number + ): Promise => { + if (newSpeed === 0) return; + setLoading(true); + + if (!ffmpeg) { + ffmpeg = new FFmpeg(); + } + + if (!ffmpegLoaded) { + await ffmpeg.load({ + wasmURL: + 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm' + }); + ffmpegLoaded = true; + } + + // Write file to FFmpeg FS + const fileName = file.name; + const outputName = 'output.mp4'; + + try { + ffmpeg.writeFile(fileName, await fetchFile(file)); + + const videoFilter = `setpts=${1 / newSpeed}*PTS`; + const audioFilter = computeAudioFilter(newSpeed); + + // Run FFmpeg command + await ffmpeg.exec([ + '-i', + fileName, + '-vf', + videoFilter, + '-filter:a', + audioFilter, + '-c:v', + 'libx264', + '-preset', + 'ultrafast', + '-c:a', + 'aac', + outputName + ]); + + const data = await ffmpeg.readFile(outputName); + + // Create new file from processed data + const blob = new Blob([data], { type: 'video/mp4' }); + const newFile = new File( + [blob], + file.name.replace('.mp4', `-${newSpeed}x.mp4`), + { type: 'video/mp4' } + ); + + // Clean up to free memory + await ffmpeg.deleteFile(fileName); + await ffmpeg.deleteFile(outputName); + + setResult(newFile); + } catch (err) { + console.error(`Failed to process video: ${err}`); + throw err; + } finally { + setLoading(false); + } + }; + + // Here we set the output video + processVideo(input, newSpeed); + }; + + const getGroups: GetGroupsType | null = ({ + values, + updateField + }) => [ + { + title: 'New Video Speed', + component: ( + + updateField('newSpeed', Number(val))} + description="Default multiplier: 2 means 2x faster" + type="number" + /> + + ) + } + ]; + return ( + + } + resultComponent={ + loading ? ( + + ) : ( + + ) + } + initialValues={initialValues} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/video/change-speed/meta.ts b/src/pages/tools/video/change-speed/meta.ts new file mode 100644 index 0000000..ff4f0ad --- /dev/null +++ b/src/pages/tools/video/change-speed/meta.ts @@ -0,0 +1,13 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('video', { + name: 'Change speed', + path: 'change-speed', + icon: 'material-symbols-light:speed-outline', + description: + 'This online utility lets you change the speed of a video. You can speed it up or slow it down.', + shortDescription: 'Quickly change video speed', + keywords: ['change', 'speed'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/video/change-speed/service.ts b/src/pages/tools/video/change-speed/service.ts new file mode 100644 index 0000000..18d6dd6 --- /dev/null +++ b/src/pages/tools/video/change-speed/service.ts @@ -0,0 +1,8 @@ +import { InitialValuesType } from './types'; + +export function main( + input: File | null, + options: InitialValuesType +): File | null { + return input; +} diff --git a/src/pages/tools/video/change-speed/types.ts b/src/pages/tools/video/change-speed/types.ts new file mode 100644 index 0000000..9c74863 --- /dev/null +++ b/src/pages/tools/video/change-speed/types.ts @@ -0,0 +1,3 @@ +export type InitialValuesType = { + newSpeed: number; +}; diff --git a/src/pages/tools/video/index.ts b/src/pages/tools/video/index.ts index 3e6659d..d3507f2 100644 --- a/src/pages/tools/video/index.ts +++ b/src/pages/tools/video/index.ts @@ -1,3 +1,4 @@ +import { tool as videoChangeSpeed } from './change-speed/meta'; import { tool as videoFlip } from './flip/meta'; import { rotate } from '../string/rotate/service'; import { gifTools } from './gif'; @@ -6,6 +7,7 @@ import { tool as rotateVideo } from './rotate/meta'; import { tool as compressVideo } from './compress/meta'; import { tool as loopVideo } from './loop/meta'; import { tool as flipVideo } from './flip/meta'; +import { tool as changeSpeed } from './change-speed/meta'; export const videoTools = [ ...gifTools, @@ -13,5 +15,6 @@ export const videoTools = [ rotateVideo, compressVideo, loopVideo, - flipVideo + flipVideo, + changeSpeed ];