feat: video convert to gif

This commit is contained in:
NajiAli3010 2025-06-14 23:24:41 +05:00
commit 229efa2447
7 changed files with 225 additions and 1 deletions

View file

@ -0,0 +1,133 @@
import { Box } from '@mui/material';
import React, { useCallback, useState } from 'react';
import * as Yup from 'yup';
import { debounce } from 'lodash';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import { GetGroupsType } from '@components/options/ToolOptions';
import ToolVideoInput from '@components/input/ToolVideoInput';
import ToolFileResult from '@components/result/ToolFileResult';
import { convertToGif } from './service';
import { InitialValuesType } from './types';
import SimpleRadio from '@components/options/SimpleRadio';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
const initialValues: InitialValuesType = {
resolution: '720p',
frameRate: 30
};
const validationSchema = Yup.object({
resolution: Yup.string().required('Resolution is required'),
frameRate: Yup.number().min(1).max(60).required('Frame rate is required')
});
const resolutionOptions = [
{ value: '720p', label: '720p (1280x720)' },
{ value: '360p', label: '360p (640x360)' }
];
export default function ConvertToGif({ title }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const compute = async (
optionsValues: InitialValuesType,
input: File | null
) => {
if (!input) return;
setLoading(true);
try {
const gifFile = await convertToGif(input, optionsValues);
setResult(gifFile);
} catch (error) {
console.error('Error converting to GIF:', error);
setResult(null);
} finally {
setLoading(false);
}
};
const debouncedCompute = useCallback(debounce(compute, 1000), []);
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Resolution',
component: (
<Box>
{resolutionOptions.map((option) => (
<SimpleRadio
key={option.value}
title={option.label}
checked={values.resolution === option.value}
onClick={() => updateField('resolution', option.value)}
/>
))}
</Box>
)
},
{
title: 'Frame Rate',
component: (
<Box sx={{ mb: 2 }}>
<Slider
min={1}
max={60}
value={values.frameRate}
onChange={(value) => {
updateField(
'frameRate',
typeof value === 'number' ? value : value[0]
);
}}
marks={{
10: '10fps',
30: '30fps (Default)',
60: '60fps'
}}
style={{ width: '90%' }}
/>
</Box>
)
}
];
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolVideoInput
value={input}
onChange={setInput}
title={'Input Video'}
/>
}
resultComponent={
<ToolFileResult
title="Converted GIF"
value={result}
extension="gif"
loading={loading}
loadingText="Converting video to GIF..."
/>
}
initialValues={initialValues}
getGroups={getGroups}
compute={debouncedCompute}
setInput={setInput}
validationSchema={validationSchema}
toolInfo={{
title: 'Convert Video to GIF',
description:
'Easily convert short videos to GIF format with customizable resolution and frame rate.'
}}
/>
);
}

View file

@ -0,0 +1,12 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('video', {
name: 'Convert to GIF',
path: 'convert-to-gif',
icon: 'mdi:file-gif-box',
description: 'Convert short videos to GIF format easily and quickly.',
shortDescription: 'Convert videos to GIFs.',
keywords: ['video', 'gif', 'convert'],
component: lazy(() => import('./index'))
});

View file

@ -0,0 +1,26 @@
import { convertToGif } from './service';
import { InitialValuesType } from './types';
describe('convertToGif', () => {
it('should convert video to GIF successfully', async () => {
const mockVideoFile = new File(['mock'], 'mock.mp4', { type: 'video/mp4' });
const mockOptions: InitialValuesType = {
resolution: '720p',
frameRate: 30
};
const result = await convertToGif(mockVideoFile, mockOptions);
expect(result).toBe('converted-video.gif');
});
it('should throw an error for invalid input', async () => {
const mockOptions: InitialValuesType = {
resolution: '720p',
frameRate: 30
};
await expect(convertToGif(null as any, mockOptions)).rejects.toThrow(
'Invalid input or options'
);
});
});

View file

@ -0,0 +1,46 @@
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';
import { InitialValuesType } from './types';
const ffmpeg = new FFmpeg();
export async function convertToGif(
input: File,
options: InitialValuesType
): Promise<File> {
if (!ffmpeg.loaded) {
await ffmpeg.load({
wasmURL:
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
});
}
const inputName = 'input.mp4';
const outputName = 'output.gif';
await ffmpeg.writeFile(inputName, await fetchFile(input));
const resolution = options.resolution === '720p' ? '1280x720' : '640x360';
const args = [
'-i',
inputName,
'-vf',
`fps=${options.frameRate},scale=${resolution}:flags=lanczos`,
outputName
];
try {
await ffmpeg.exec(args);
} catch (error) {
console.error('FFmpeg execution failed:', error);
throw new Error('GIF conversion failed.');
}
const outputData = await ffmpeg.readFile(outputName);
return new File(
[new Blob([outputData], { type: 'image/gif' })],
`${input.name.replace(/\.[^/.]+$/, '')}_converted.gif`,
{ type: 'image/gif' }
);
}

View file

@ -0,0 +1,4 @@
export type InitialValuesType = {
resolution: string;
frameRate: number;
};

View file

@ -9,6 +9,7 @@ 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';
import { tool as convertToGif } from './convert-to-gif/meta';
export const videoTools = [
...gifTools,
@ -18,5 +19,6 @@ export const videoTools = [
loopVideo,
flipVideo,
cropVideo,
changeSpeed
changeSpeed,
convertToGif
];

View file

@ -11,6 +11,7 @@ import { csvTools } from '../pages/tools/csv';
import { timeTools } from '../pages/tools/time';
import { IconifyIcon } from '@iconify/react';
import { pdfTools } from '../pages/tools/pdf';
import { tool as videoConvertToGif } from '../pages/tools/video/convert-to-gif/meta';
const toolCategoriesOrder: ToolCategory[] = [
'image-generic',