mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-06 08:54:57 +05:30
feat: crop video init
This commit is contained in:
parent
5c26d9b54f
commit
45af1b0735
7 changed files with 403 additions and 60 deletions
121
.idea/workspace.xml
generated
121
.idea/workspace.xml
generated
|
|
@ -5,9 +5,13 @@
|
|||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: readme img and fix broken link">
|
||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/crop-video.service.test.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/index.tsx" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/meta.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/types.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/Hero.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/Hero.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools-by-category/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools-by-category/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/ToolContent.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolContent.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/index.ts" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -24,7 +28,7 @@
|
|||
<option name="PUSH_AUTO_UPDATE" value="true" />
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="chesterkxng" />
|
||||
<entry key="$PROJECT_DIR$" value="main" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
|
|
@ -159,56 +163,57 @@
|
|||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"Docker.Dockerfile build.executor": "Run",
|
||||
"Docker.Dockerfile.executor": "Run",
|
||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||
"Playwright.JoinText Component.executor": "Run",
|
||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"Vitest.compute function (1).executor": "Run",
|
||||
"Vitest.compute function.executor": "Run",
|
||||
"Vitest.mergeText.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||
"Vitest.parsePageRanges.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||
"Vitest.replaceText function.executor": "Run",
|
||||
"Vitest.timeBetweenDates.executor": "Run",
|
||||
"git-widget-placeholder": "main",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.dev.executor": "Run",
|
||||
"npm.lint.executor": "Run",
|
||||
"npm.prebuild.executor": "Run",
|
||||
"npm.script:create:tool.executor": "Run",
|
||||
"npm.test.executor": "Run",
|
||||
"npm.test:e2e.executor": "Run",
|
||||
"npm.test:e2e:run.executor": "Run",
|
||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||
"project.structure.last.edited": "Problems",
|
||||
"project.structure.proportion": "0.0",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"Docker.Dockerfile build.executor": "Run",
|
||||
"Docker.Dockerfile.executor": "Run",
|
||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||
"Playwright.JoinText Component.executor": "Run",
|
||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"Vitest.compute function (1).executor": "Run",
|
||||
"Vitest.compute function.executor": "Run",
|
||||
"Vitest.crop-video.executor": "Run",
|
||||
"Vitest.mergeText.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||
"Vitest.parsePageRanges.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||
"Vitest.replaceText function.executor": "Run",
|
||||
"Vitest.timeBetweenDates.executor": "Run",
|
||||
"git-widget-placeholder": "crop-video",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.dev.executor": "Run",
|
||||
"npm.lint.executor": "Run",
|
||||
"npm.prebuild.executor": "Run",
|
||||
"npm.script:create:tool.executor": "Run",
|
||||
"npm.test.executor": "Run",
|
||||
"npm.test:e2e.executor": "Run",
|
||||
"npm.test:e2e:run.executor": "Run",
|
||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||
"project.structure.last.edited": "Problems",
|
||||
"project.structure.proportion": "0.0",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="ReactDesignerToolWindowState">
|
||||
<option name="myId2Visible">
|
||||
<map>
|
||||
|
|
@ -234,17 +239,17 @@
|
|||
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\categories" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager" selected="npm.dev">
|
||||
<configuration name="calculateTimeBetweenDates" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
|
||||
<component name="RunManager" selected="Vitest.crop-video">
|
||||
<configuration name="crop-video" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
|
||||
<node-interpreter value="project" />
|
||||
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
|
||||
<working-dir value="$PROJECT_DIR$" />
|
||||
<vitest-options value="--run" />
|
||||
<envs />
|
||||
<scope-kind value="SUITE" />
|
||||
<test-file value="$PROJECT_DIR$/src/pages/tools/time/time-between-dates/time-between-dates.service.test.ts" />
|
||||
<test-file value="$PROJECT_DIR$/src/pages/tools/video/crop-video/crop-video.service.test.ts" />
|
||||
<test-names>
|
||||
<test-name value="calculateTimeBetweenDates" />
|
||||
<test-name value="crop-video" />
|
||||
</test-names>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
|
|
@ -306,18 +311,18 @@
|
|||
</configuration>
|
||||
<list>
|
||||
<item itemvalue="npm.dev" />
|
||||
<item itemvalue="Vitest.calculateTimeBetweenDates" />
|
||||
<item itemvalue="Vitest.crop-video" />
|
||||
<item itemvalue="Vitest.timeBetweenDates" />
|
||||
<item itemvalue="Vitest.parsePageRanges" />
|
||||
<item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" />
|
||||
</list>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="Vitest.crop-video" />
|
||||
<item itemvalue="npm.dev" />
|
||||
<item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" />
|
||||
<item itemvalue="Vitest.parsePageRanges" />
|
||||
<item itemvalue="Vitest.timeBetweenDates" />
|
||||
<item itemvalue="Vitest.calculateTimeBetweenDates" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const FormikListenerComponent = <T,>({
|
|||
if (exception instanceof Error) showSnackBar(exception.message, 'error');
|
||||
else console.error(exception);
|
||||
}
|
||||
}, [values, input, showSnackBar]);
|
||||
}, [compute, values, input, showSnackBar]);
|
||||
|
||||
useEffect(() => {
|
||||
onValuesChange?.(values);
|
||||
|
|
|
|||
86
src/pages/tools/video/crop-video/crop-video.service.test.ts
Normal file
86
src/pages/tools/video/crop-video/crop-video.service.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cropVideo } from './service';
|
||||
|
||||
// Mock FFmpeg
|
||||
vi.mock('@ffmpeg/ffmpeg', () => {
|
||||
return {
|
||||
FFmpeg: vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
loaded: false,
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
writeFile: vi.fn().mockResolvedValue(undefined),
|
||||
exec: vi.fn().mockResolvedValue(undefined),
|
||||
readFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3]))
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// Mock fetchFile
|
||||
vi.mock('@ffmpeg/util', () => {
|
||||
return {
|
||||
fetchFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3]))
|
||||
};
|
||||
});
|
||||
|
||||
describe('crop-video', () => {
|
||||
let mockFile: File;
|
||||
let mockVideoInfo: { width: number; height: number };
|
||||
|
||||
beforeEach(() => {
|
||||
mockFile = new File(['test'], 'test.mp4', { type: 'video/mp4' });
|
||||
mockVideoInfo = { width: 1280, height: 720 };
|
||||
|
||||
// Reset global File constructor
|
||||
global.File = vi.fn().mockImplementation((bits, name, options) => {
|
||||
return new File(bits, name, options);
|
||||
});
|
||||
});
|
||||
|
||||
it('should crop a video with valid parameters', async () => {
|
||||
const options = {
|
||||
width: 640,
|
||||
height: 360,
|
||||
x: 0,
|
||||
y: 0,
|
||||
maintainAspectRatio: false
|
||||
};
|
||||
|
||||
const result = await cropVideo(mockFile, options, mockVideoInfo);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name).toContain('_cropped');
|
||||
expect(result.type).toBe('video/mp4');
|
||||
});
|
||||
|
||||
it('should throw error if videoInfo is not provided', async () => {
|
||||
const options = {
|
||||
width: 640,
|
||||
height: 360,
|
||||
x: 0,
|
||||
y: 0,
|
||||
maintainAspectRatio: false
|
||||
};
|
||||
|
||||
await expect(cropVideo(mockFile, options, null)).rejects.toThrow(
|
||||
'Video information is required'
|
||||
);
|
||||
});
|
||||
|
||||
it('should adjust crop dimensions to fit within video bounds', async () => {
|
||||
const options = {
|
||||
width: 2000, // Larger than video width
|
||||
height: 1000, // Larger than video height
|
||||
x: 500,
|
||||
y: 500,
|
||||
maintainAspectRatio: false
|
||||
};
|
||||
|
||||
const result = await cropVideo(mockFile, options, mockVideoInfo);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
// We can't test the actual crop dimensions since FFmpeg is mocked
|
||||
// but we can verify the file was created
|
||||
expect(result.name).toContain('_cropped');
|
||||
});
|
||||
});
|
||||
223
src/pages/tools/video/crop-video/index.tsx
Normal file
223
src/pages/tools/video/crop-video/index.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import { Box, Checkbox, FormControlLabel } from '@mui/material';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { updateNumberField } from '@utils/string';
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile } from '@ffmpeg/util';
|
||||
import { debounce } from 'lodash';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { InitialValuesType } from './types';
|
||||
|
||||
const ffmpeg = new FFmpeg();
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
width: 640,
|
||||
height: 360,
|
||||
x: 0,
|
||||
y: 0,
|
||||
maintainAspectRatio: true
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
width: Yup.number()
|
||||
.min(1, 'Width must be at least 1px')
|
||||
.required('Width is required'),
|
||||
height: Yup.number()
|
||||
.min(1, 'Height must be at least 1px')
|
||||
.required('Height is required'),
|
||||
x: Yup.number()
|
||||
.min(0, 'X position must be positive')
|
||||
.required('X position is required'),
|
||||
y: Yup.number()
|
||||
.min(0, 'Y position must be positive')
|
||||
.required('Y position is required')
|
||||
});
|
||||
|
||||
export default function CropVideo({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
const [videoInfo, setVideoInfo] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
} | null>(null);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
|
||||
// Get video dimensions when a video is loaded
|
||||
useEffect(() => {
|
||||
if (input) {
|
||||
const video = document.createElement('video');
|
||||
video.onloadedmetadata = () => {
|
||||
console.log('loadedmetadata', video.videoWidth, video.videoHeight);
|
||||
setVideoInfo({
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight
|
||||
});
|
||||
};
|
||||
video.src = URL.createObjectURL(input);
|
||||
} else {
|
||||
setVideoInfo(null);
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
const compute = async (
|
||||
optionsValues: InitialValuesType,
|
||||
input: File | null
|
||||
) => {
|
||||
if (!input || !videoInfo) return;
|
||||
try {
|
||||
setIsProcessing(true);
|
||||
|
||||
// Ensure values are within video bounds
|
||||
const cropWidth = Math.min(
|
||||
optionsValues.width,
|
||||
videoInfo.width - optionsValues.x
|
||||
);
|
||||
const cropHeight = Math.min(
|
||||
optionsValues.height,
|
||||
videoInfo.height - optionsValues.y
|
||||
);
|
||||
const cropX = Math.min(optionsValues.x, videoInfo.width - cropWidth);
|
||||
const cropY = Math.min(optionsValues.y, videoInfo.height - cropHeight);
|
||||
|
||||
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';
|
||||
|
||||
// Load file into FFmpeg's virtual filesystem
|
||||
await ffmpeg.writeFile(inputName, await fetchFile(input));
|
||||
|
||||
// Run FFmpeg command to crop video
|
||||
// The crop filter format is: crop=width:height:x:y
|
||||
await ffmpeg.exec([
|
||||
'-i',
|
||||
inputName,
|
||||
'-vf',
|
||||
`crop=${cropWidth}:${cropHeight}:${cropX}:${cropY}`,
|
||||
'-c:a',
|
||||
'copy',
|
||||
outputName
|
||||
]);
|
||||
|
||||
// Retrieve the processed file
|
||||
const croppedData = await ffmpeg.readFile(outputName);
|
||||
const croppedBlob = new Blob([croppedData], { type: 'video/mp4' });
|
||||
const croppedFile = new File(
|
||||
[croppedBlob],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_cropped.mp4`,
|
||||
{
|
||||
type: 'video/mp4'
|
||||
}
|
||||
);
|
||||
|
||||
setResult(croppedFile);
|
||||
} catch (error) {
|
||||
console.error('Error cropping video:', error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedCompute = useCallback(debounce(compute, 1000), [videoInfo]);
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField,
|
||||
setFieldValue
|
||||
}) => [
|
||||
{
|
||||
title: 'Crop Settings',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'width', updateField)
|
||||
}
|
||||
value={values.width}
|
||||
label={'Width (pixels)'}
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
helperText={videoInfo ? `Original width: ${videoInfo.width}px` : ''}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'height', updateField)
|
||||
}
|
||||
value={values.height}
|
||||
label={'Height (pixels)'}
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
helperText={
|
||||
videoInfo ? `Original height: ${videoInfo.height}px` : ''
|
||||
}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) => updateNumberField(value, 'x', updateField)}
|
||||
value={values.x}
|
||||
label={'X Position (pixels)'}
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) => updateNumberField(value, 'y', updateField)}
|
||||
value={values.y}
|
||||
label={'Y Position (pixels)'}
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.maintainAspectRatio}
|
||||
onChange={(e) => {
|
||||
setFieldValue('maintainAspectRatio', e.target.checked);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Maintain aspect ratio"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['video/mp4', 'video/webm', 'video/ogg']}
|
||||
title={'Input Video'}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Cropped Video'}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
loading={isProcessing}
|
||||
/>
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={debouncedCompute}
|
||||
setInput={setInput}
|
||||
validationSchema={validationSchema}
|
||||
toolInfo={{
|
||||
title: 'How to crop a video',
|
||||
description:
|
||||
'Video cropping allows you to remove unwanted outer areas from your video frames. Specify the width, height, X position, and Y position to define the crop region. The X and Y positions determine the top-left corner of the crop area.'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
15
src/pages/tools/video/crop-video/meta.ts
Normal file
15
src/pages/tools/video/crop-video/meta.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Crop Video',
|
||||
path: 'crop-video',
|
||||
icon: 'mdi:crop',
|
||||
description:
|
||||
'This online tool lets you crop videos by specifying width, height, and position coordinates. Remove unwanted areas from your videos and keep only the parts you need. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Crop videos by specifying width, height and position',
|
||||
keywords: ['crop', 'video', 'resize', 'trim', 'edit', 'ffmpeg'],
|
||||
longDescription:
|
||||
'Video cropping is the process of removing unwanted outer areas from a video frame. This tool allows you to specify exact dimensions and coordinates to crop your video, helping you focus on the important parts of your footage or adjust aspect ratios.',
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
7
src/pages/tools/video/crop-video/types.ts
Normal file
7
src/pages/tools/video/crop-video/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export type InitialValuesType = {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
maintainAspectRatio: boolean;
|
||||
};
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
import { tool as videoCropVideo } from './crop-video/meta';
|
||||
import { rotate } from '../string/rotate/service';
|
||||
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';
|
||||
|
||||
export const videoTools = [...gifTools, trimVideo, rotateVideo, compressVideo];
|
||||
export const videoTools = [
|
||||
...gifTools,
|
||||
trimVideo,
|
||||
rotateVideo,
|
||||
compressVideo,
|
||||
videoCropVideo
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue