fix: gif speed

This commit is contained in:
Ibrahima G. Coulibaly
2025-03-31 01:43:03 +00:00
parent 2a3642d14a
commit 520a769c74

View File

@@ -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>