diff --git a/src/pages/tools/video/index.ts b/src/pages/tools/video/index.ts index 99bb8e7..7cbf9f9 100644 --- a/src/pages/tools/video/index.ts +++ b/src/pages/tools/video/index.ts @@ -1,14 +1,12 @@ -import { tool as videoChangeSpeed } from './change-speed/meta'; -import { tool as videoFlip } from './flip/meta'; -import { rotate } from '../string/rotate/service'; +import { tool as videoToGif } from './video-to-gif/meta'; +import { tool as changeSpeed } from './change-speed/meta'; +import { tool as flipVideo } from './flip/meta'; import { gifTools } from './gif'; import { tool as trimVideo } from './trim/meta'; 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 cropVideo } from './crop-video/meta'; -import { tool as changeSpeed } from './change-speed/meta'; export const videoTools = [ ...gifTools, @@ -18,5 +16,6 @@ export const videoTools = [ loopVideo, flipVideo, cropVideo, - changeSpeed + changeSpeed, + videoToGif ]; diff --git a/src/pages/tools/video/video-to-gif/index.tsx b/src/pages/tools/video/video-to-gif/index.tsx new file mode 100644 index 0000000..a8eba09 --- /dev/null +++ b/src/pages/tools/video/video-to-gif/index.tsx @@ -0,0 +1,176 @@ +import { Box } from '@mui/material'; +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { InitialValuesType } from './types'; +import ToolVideoInput from '@components/input/ToolVideoInput'; +import ToolFileResult from '@components/result/ToolFileResult'; +import SimpleRadio from '@components/options/SimpleRadio'; +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; + +const initialValues: InitialValuesType = { + quality: 'mid', + fps: '10', + scale: '320:-1:flags=bicubic' +}; + +export default function VideoToGif({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const compute = (values: InitialValuesType, input: File | null) => { + if (!input) return; + const { fps, scale } = values; + let ffmpeg: FFmpeg | null = null; + let ffmpegLoaded = false; + + const convertVideoToGif = async ( + file: File, + fps: string, + scale: string + ): Promise => { + 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; + } + + const fileName = file.name; + const outputName = 'output.gif'; + + try { + ffmpeg.writeFile(fileName, await fetchFile(file)); + + await ffmpeg.exec([ + '-i', + fileName, + '-vf', + `fps=${fps},scale=${scale},palettegen`, + 'palette.png' + ]); + + await ffmpeg.exec([ + '-i', + fileName, + '-i', + 'palette.png', + '-filter_complex', + `fps=${fps},scale=${scale}[x];[x][1:v]paletteuse`, + outputName + ]); + + const data = await ffmpeg.readFile(outputName); + + const blob = new Blob([data], { type: 'image/gif' }); + const convertedFile = new File([blob], outputName, { + type: 'image/gif' + }); + + await ffmpeg.deleteFile(fileName); + await ffmpeg.deleteFile(outputName); + + setResult(convertedFile); + } catch (err) { + console.error(`Failed to convert video: ${err}`); + throw err; + } finally { + setLoading(false); + } + }; + + convertVideoToGif(input, fps, scale); + }; + + const getGroups: GetGroupsType | null = ({ + values, + updateField + }) => [ + { + title: 'Set Quality', + component: ( + + { + updateField('quality', 'low'); + updateField('fps', '5'); + updateField('scale', '240:-1:flags=bilinear'); + }} + checked={values.quality === 'low'} + /> + { + updateField('quality', 'mid'); + updateField('fps', '10'); + updateField('scale', '320:-1:flags=bicubic'); + }} + checked={values.quality === 'mid'} + /> + { + updateField('quality', 'high'); + updateField('fps', '15'); + updateField('scale', '480:-1:flags=lanczos'); + }} + checked={values.quality === 'high'} + /> + { + updateField('quality', 'ultra'); + updateField('fps', '15'); + updateField('scale', '640:-1:flags=lanczos'); + }} + checked={values.quality === 'ultra'} + /> + + ) + } + ]; + + 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/video-to-gif/meta.ts b/src/pages/tools/video/video-to-gif/meta.ts new file mode 100644 index 0000000..06f1f3f --- /dev/null +++ b/src/pages/tools/video/video-to-gif/meta.ts @@ -0,0 +1,12 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('video', { + name: 'Video to Gif', + path: 'video-to-gif', + icon: 'fluent:gif-16-regular', + description: 'This online utility lets you convert a short video to gif.', + shortDescription: 'Quickly convert a short video to gif', + keywords: ['video', 'to', 'gif', 'convert'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/video/video-to-gif/types.ts b/src/pages/tools/video/video-to-gif/types.ts new file mode 100644 index 0000000..f07ab51 --- /dev/null +++ b/src/pages/tools/video/video-to-gif/types.ts @@ -0,0 +1,5 @@ +export type InitialValuesType = { + quality: 'mid' | 'high' | 'low' | 'ultra'; + fps: string; + scale: string; +};