Skip to content

Commit 76d615e

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 291821e + 0a675f8 commit 76d615e

File tree

5 files changed

+160
-1
lines changed

5 files changed

+160
-1
lines changed

src/pages/tools/video/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,12 @@ import { gifTools } from './gif';
33
import { tool as trimVideo } from './trim/meta';
44
import { tool as rotateVideo } from './rotate/meta';
55
import { tool as compressVideo } from './compress/meta';
6+
import { tool as loopVideo } from './loop/meta';
67

7-
export const videoTools = [...gifTools, trimVideo, rotateVideo, compressVideo];
8+
export const videoTools = [
9+
...gifTools,
10+
trimVideo,
11+
rotateVideo,
12+
compressVideo,
13+
loopVideo
14+
];
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Box } from '@mui/material';
2+
import { useState } from 'react';
3+
import ToolContent from '@components/ToolContent';
4+
import { ToolComponentProps } from '@tools/defineTool';
5+
import { GetGroupsType } from '@components/options/ToolOptions';
6+
import { CardExampleType } from '@components/examples/ToolExamples';
7+
import { loopVideo } from './service';
8+
import { InitialValuesType } from './types';
9+
import ToolVideoInput from '@components/input/ToolVideoInput';
10+
import ToolFileResult from '@components/result/ToolFileResult';
11+
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
12+
import { updateNumberField } from '@utils/string';
13+
import * as Yup from 'yup';
14+
15+
const initialValues: InitialValuesType = {
16+
loops: 2
17+
};
18+
19+
const validationSchema = Yup.object({
20+
loops: Yup.number().min(1, 'Number of loops must be greater than 1')
21+
});
22+
23+
export default function Loop({ title, longDescription }: ToolComponentProps) {
24+
const [input, setInput] = useState<File | null>(null);
25+
const [result, setResult] = useState<File | null>(null);
26+
const [loading, setLoading] = useState(false);
27+
28+
const compute = async (values: InitialValuesType, input: File | null) => {
29+
if (!input) return;
30+
try {
31+
setLoading(true);
32+
const resultFile = await loopVideo(input, values);
33+
await setResult(resultFile);
34+
} catch (error) {
35+
console.error(error);
36+
} finally {
37+
setLoading(false);
38+
}
39+
};
40+
41+
const getGroups: GetGroupsType<InitialValuesType> | null = ({
42+
values,
43+
updateField
44+
}) => [
45+
{
46+
title: 'Loops',
47+
component: (
48+
<Box>
49+
<TextFieldWithDesc
50+
onOwnChange={(value) =>
51+
updateNumberField(value, 'loops', updateField)
52+
}
53+
value={values.loops}
54+
label={'Number of Loops'}
55+
/>
56+
</Box>
57+
)
58+
}
59+
];
60+
return (
61+
<ToolContent
62+
title={title}
63+
input={input}
64+
inputComponent={
65+
<ToolVideoInput
66+
value={input}
67+
onChange={setInput}
68+
accept={['video/*']}
69+
/>
70+
}
71+
resultComponent={
72+
loading ? (
73+
<ToolFileResult
74+
value={null}
75+
title={'Looping Video'}
76+
loading={true}
77+
extension={''}
78+
/>
79+
) : (
80+
<ToolFileResult
81+
value={result}
82+
title={'Looped Video'}
83+
extension={'mp4'}
84+
/>
85+
)
86+
}
87+
initialValues={initialValues}
88+
validationSchema={validationSchema}
89+
getGroups={getGroups}
90+
setInput={setInput}
91+
compute={compute}
92+
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
93+
/>
94+
);
95+
}

src/pages/tools/video/loop/meta.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineTool } from '@tools/defineTool';
2+
import { lazy } from 'react';
3+
4+
export const tool = defineTool('video', {
5+
name: 'Loop Video',
6+
path: 'loop',
7+
icon: 'ic:baseline-loop',
8+
description:
9+
'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.',
10+
shortDescription: 'Loop videos multiple times',
11+
keywords: ['loop', 'video', 'repeat', 'duplicate', 'sequence', 'playback'],
12+
component: lazy(() => import('./index'))
13+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { InitialValuesType } from './types';
2+
import { FFmpeg } from '@ffmpeg/ffmpeg';
3+
import { fetchFile } from '@ffmpeg/util';
4+
5+
const ffmpeg = new FFmpeg();
6+
7+
export async function loopVideo(
8+
input: File,
9+
options: InitialValuesType
10+
): Promise<File> {
11+
if (!ffmpeg.loaded) {
12+
await ffmpeg.load({
13+
wasmURL:
14+
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
15+
});
16+
}
17+
18+
const inputName = 'input.mp4';
19+
const outputName = 'output.mp4';
20+
await ffmpeg.writeFile(inputName, await fetchFile(input));
21+
22+
const args = [];
23+
const loopCount = options.loops - 1;
24+
25+
if (loopCount <= 0) {
26+
return input;
27+
}
28+
29+
args.push('-stream_loop', loopCount.toString());
30+
args.push('-i', inputName);
31+
args.push('-c:v', 'libx264', '-preset', 'ultrafast', outputName);
32+
33+
await ffmpeg.exec(args);
34+
35+
const loopedData = await ffmpeg.readFile(outputName);
36+
return await new File(
37+
[new Blob([loopedData], { type: 'video/mp4' })],
38+
`${input.name.replace(/\.[^/.]+$/, '')}_looped.mp4`,
39+
{ type: 'video/mp4' }
40+
);
41+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type InitialValuesType = {
2+
loops: number;
3+
};

0 commit comments

Comments
 (0)