From 2b7ffb49f69a4d33838f43e18c92024fdcf5d9c4 Mon Sep 17 00:00:00 2001 From: Yousef Mahmoud Date: Sun, 20 Apr 2025 11:14:41 +0200 Subject: [PATCH] feat(loop): add loop video tool to video tools list --- src/pages/tools/video/index.ts | 9 ++- src/pages/tools/video/loop/index.tsx | 95 +++++++++++++++++++++++++++ src/pages/tools/video/loop/meta.ts | 13 ++++ src/pages/tools/video/loop/service.ts | 41 ++++++++++++ src/pages/tools/video/loop/types.ts | 3 + 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/video/loop/index.tsx create mode 100644 src/pages/tools/video/loop/meta.ts create mode 100644 src/pages/tools/video/loop/service.ts create mode 100644 src/pages/tools/video/loop/types.ts diff --git a/src/pages/tools/video/index.ts b/src/pages/tools/video/index.ts index 07405b1..ca3e413 100644 --- a/src/pages/tools/video/index.ts +++ b/src/pages/tools/video/index.ts @@ -3,5 +3,12 @@ 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'; -export const videoTools = [...gifTools, trimVideo, rotateVideo, compressVideo]; +export const videoTools = [ + ...gifTools, + trimVideo, + rotateVideo, + compressVideo, + loopVideo +]; diff --git a/src/pages/tools/video/loop/index.tsx b/src/pages/tools/video/loop/index.tsx new file mode 100644 index 0000000..80cf2be --- /dev/null +++ b/src/pages/tools/video/loop/index.tsx @@ -0,0 +1,95 @@ +import { Box } from '@mui/material'; +import { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { CardExampleType } from '@components/examples/ToolExamples'; +import { loopVideo } from './service'; +import { InitialValuesType } from './types'; +import ToolVideoInput from '@components/input/ToolVideoInput'; +import ToolFileResult from '@components/result/ToolFileResult'; +import TextFieldWithDesc from '@components/options/TextFieldWithDesc'; +import { updateNumberField } from '@utils/string'; +import * as Yup from 'yup'; + +const initialValues: InitialValuesType = { + loops: 1 +}; + +const validationSchema = Yup.object({ + loops: Yup.number().min(1, 'Number of loops must be greater than 1') +}); + +export default function Loop({ title, longDescription }: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const compute = async (values: InitialValuesType, input: File | null) => { + if (!input) return; + try { + setLoading(true); + const resultFile = await loopVideo(input, values); + await setResult(resultFile); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const getGroups: GetGroupsType | null = ({ + values, + updateField + }) => [ + { + title: 'Loops', + component: ( + + + updateNumberField(value, 'loops', updateField) + } + value={values.loops} + label={'Number of Loops'} + /> + + ) + } + ]; + return ( + + } + resultComponent={ + loading ? ( + + ) : ( + + ) + } + initialValues={initialValues} + validationSchema={validationSchema} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/video/loop/meta.ts b/src/pages/tools/video/loop/meta.ts new file mode 100644 index 0000000..37d09c2 --- /dev/null +++ b/src/pages/tools/video/loop/meta.ts @@ -0,0 +1,13 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('video', { + name: 'Loop Video', + path: 'loop', + icon: 'ic:baseline-loop', + description: + 'This online utility lets you loop videos by specifying the number of repetitions. You can preview the looped video before processing. Supports common video formats like MP4, WebM, and OGG.', + shortDescription: 'Loop videos multiple times', + keywords: ['loop', 'video', 'repeat', 'duplicate', 'sequence', 'playback'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/video/loop/service.ts b/src/pages/tools/video/loop/service.ts new file mode 100644 index 0000000..294a85a --- /dev/null +++ b/src/pages/tools/video/loop/service.ts @@ -0,0 +1,41 @@ +import { InitialValuesType } from './types'; +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; + +const ffmpeg = new FFmpeg(); + +export async function loopVideo( + input: File, + options: InitialValuesType +): Promise { + 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.mp4'; + await ffmpeg.writeFile(inputName, await fetchFile(input)); + + const args = []; + const loopCount = options.loops - 1; + + if (loopCount <= 0) { + return input; + } + + args.push('-stream_loop', loopCount.toString()); + args.push('-i', inputName); + args.push('-c:v', 'libx264', '-preset', 'ultrafast', outputName); + + await ffmpeg.exec(args); + + const loopedData = await ffmpeg.readFile(outputName); + return await new File( + [new Blob([loopedData], { type: 'video/mp4' })], + `${input.name.replace(/\.[^/.]+$/, '')}_looped.mp4`, + { type: 'video/mp4' } + ); +} diff --git a/src/pages/tools/video/loop/types.ts b/src/pages/tools/video/loop/types.ts new file mode 100644 index 0000000..1eaf708 --- /dev/null +++ b/src/pages/tools/video/loop/types.ts @@ -0,0 +1,3 @@ +export type InitialValuesType = { + loops: number; +};