mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-19 05:59:34 +02:00
fix: gif speed
This commit is contained in:
@@ -1,22 +1,16 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { FrameOptions, GifReader, GifWriter } from 'omggif';
|
|
||||||
import { gifBinaryToFile } from '@utils/gif';
|
|
||||||
import ToolContent from '@components/ToolContent';
|
import ToolContent from '@components/ToolContent';
|
||||||
import { ToolComponentProps } from '@tools/defineTool';
|
import { ToolComponentProps } from '@tools/defineTool';
|
||||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
|
||||||
import ToolImageInput from '@components/input/ToolImageInput';
|
import ToolImageInput from '@components/input/ToolImageInput';
|
||||||
|
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||||
|
import { fetchFile } from '@ffmpeg/util';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
newSpeed: 200
|
newSpeed: 2
|
||||||
};
|
};
|
||||||
const validationSchema = Yup.object({
|
|
||||||
// splitSeparator: Yup.string().required('The separator is required')
|
|
||||||
});
|
|
||||||
export default function ChangeSpeed({ title }: ToolComponentProps) {
|
export default function ChangeSpeed({ title }: ToolComponentProps) {
|
||||||
const [input, setInput] = useState<File | null>(null);
|
const [input, setInput] = useState<File | null>(null);
|
||||||
const [result, setResult] = useState<File | null>(null);
|
const [result, setResult] = useState<File | null>(null);
|
||||||
@@ -24,82 +18,64 @@ export default function ChangeSpeed({ title }: ToolComponentProps) {
|
|||||||
const compute = (optionsValues: typeof initialValues, input: File | null) => {
|
const compute = (optionsValues: typeof initialValues, input: File | null) => {
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
const { newSpeed } = optionsValues;
|
const { newSpeed } = optionsValues;
|
||||||
|
// Initialize FFmpeg once in your component/app
|
||||||
|
let ffmpeg: FFmpeg | null = null;
|
||||||
|
let ffmpegLoaded = false;
|
||||||
|
|
||||||
const processImage = async (file: File, newSpeed: number) => {
|
const processImage = async (
|
||||||
const reader = new FileReader();
|
file: File,
|
||||||
reader.readAsArrayBuffer(file);
|
newSpeed: number
|
||||||
|
): Promise<File> => {
|
||||||
|
if (!ffmpeg) {
|
||||||
|
ffmpeg = new FFmpeg();
|
||||||
|
}
|
||||||
|
|
||||||
reader.onload = async () => {
|
if (!ffmpegLoaded) {
|
||||||
const arrayBuffer = reader.result;
|
await ffmpeg.load({
|
||||||
|
wasmURL:
|
||||||
|
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
|
||||||
|
});
|
||||||
|
ffmpegLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (arrayBuffer instanceof ArrayBuffer) {
|
try {
|
||||||
const intArray = new Uint8Array(arrayBuffer);
|
await ffmpeg.writeFile('input.gif', await fetchFile(file));
|
||||||
|
|
||||||
const reader = new GifReader(intArray as Buffer);
|
// Use FFmpeg's setpts filter to change the speed
|
||||||
const info = reader.frameInfo(0);
|
// PTS (Presentation Time Stamp) determines when each frame is shown
|
||||||
const imageDataArr: ImageData[] = new Array(reader.numFrames())
|
// 1/speed changes the PTS - lower value = faster playback
|
||||||
.fill(0)
|
await ffmpeg.exec([
|
||||||
.map((_, k) => {
|
'-i',
|
||||||
const image = new ImageData(info.width, info.height);
|
'input.gif',
|
||||||
|
'-filter:v',
|
||||||
|
`setpts=${1 / newSpeed}*PTS`,
|
||||||
|
'-f',
|
||||||
|
'gif',
|
||||||
|
'output.gif'
|
||||||
|
]);
|
||||||
|
|
||||||
reader.decodeAndBlitFrameRGBA(k, image.data);
|
// Read the result
|
||||||
|
const data = await ffmpeg.readFile('output.gif');
|
||||||
|
|
||||||
return image;
|
// Create a new file from the processed data
|
||||||
});
|
const blob = new Blob([data], { type: 'image/gif' });
|
||||||
const gif = new GifWriter(
|
const newFile = new File(
|
||||||
[],
|
[blob],
|
||||||
imageDataArr[0].width,
|
file.name.replace('.gif', `-${newSpeed}x.gif`),
|
||||||
imageDataArr[0].height,
|
{
|
||||||
{ loop: 20 }
|
type: 'image/gif'
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
imageDataArr.forEach((imageData) => {
|
// Clean up to free memory
|
||||||
const palette = [];
|
await ffmpeg.deleteFile('input.gif');
|
||||||
const pixels = new Uint8Array(imageData.width * imageData.height);
|
await ffmpeg.deleteFile('output.gif');
|
||||||
|
|
||||||
const { data } = imageData;
|
setResult(newFile);
|
||||||
for (let j = 0, k = 0, jl = data.length; j < jl; j += 4, k++) {
|
} catch (error) {
|
||||||
const r = Math.floor(data[j] * 0.1) * 10;
|
console.error('Error processing GIF:', error);
|
||||||
const g = Math.floor(data[j + 1] * 0.1) * 10;
|
throw error;
|
||||||
const b = Math.floor(data[j + 2] * 0.1) * 10;
|
}
|
||||||
const color = (r << 16) | (g << 8) | (b << 0);
|
|
||||||
|
|
||||||
const index = palette.indexOf(color);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
pixels[k] = palette.length;
|
|
||||||
palette.push(color);
|
|
||||||
} else {
|
|
||||||
pixels[k] = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force palette to be power of 2
|
|
||||||
|
|
||||||
let powof2 = 1;
|
|
||||||
while (powof2 < palette.length) powof2 <<= 1;
|
|
||||||
palette.length = powof2;
|
|
||||||
|
|
||||||
const delay = newSpeed / 10; // Delay in hundredths of a sec (100 = 1s)
|
|
||||||
const options: FrameOptions = {
|
|
||||||
palette,
|
|
||||||
delay
|
|
||||||
};
|
|
||||||
gif.addFrame(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
imageData.width,
|
|
||||||
imageData.height,
|
|
||||||
// @ts-ignore
|
|
||||||
pixels,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const newFile = gifBinaryToFile(gif.getOutputBuffer(), file.name);
|
|
||||||
|
|
||||||
setResult(newFile);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
processImage(input, newSpeed);
|
processImage(input, newSpeed);
|
||||||
@@ -132,8 +108,7 @@ export default function ChangeSpeed({ title }: ToolComponentProps) {
|
|||||||
<TextFieldWithDesc
|
<TextFieldWithDesc
|
||||||
value={values.newSpeed}
|
value={values.newSpeed}
|
||||||
onOwnChange={(val) => updateField('newSpeed', Number(val))}
|
onOwnChange={(val) => updateField('newSpeed', Number(val))}
|
||||||
description={'Default new GIF speed.'}
|
description={'Default multiplier: 2 means 2x faster'}
|
||||||
InputProps={{ endAdornment: <Typography>ms</Typography> }}
|
|
||||||
type={'number'}
|
type={'number'}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
Reference in New Issue
Block a user