From 520a769c74e3701d3451a1940dc4cb8f2532dab3 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Mon, 31 Mar 2025 01:43:03 +0000 Subject: [PATCH] fix: gif speed --- .../tools/video/gif/change-speed/index.tsx | 133 +++++++----------- 1 file changed, 54 insertions(+), 79 deletions(-) diff --git a/src/pages/tools/video/gif/change-speed/index.tsx b/src/pages/tools/video/gif/change-speed/index.tsx index bf10e9d..27bcd4d 100644 --- a/src/pages/tools/video/gif/change-speed/index.tsx +++ b/src/pages/tools/video/gif/change-speed/index.tsx @@ -1,22 +1,16 @@ import { Box } from '@mui/material'; import React, { useState } from 'react'; -import * as Yup from 'yup'; import ToolFileResult from '@components/result/ToolFileResult'; 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 { ToolComponentProps } from '@tools/defineTool'; -import ToolVideoInput from '@components/input/ToolVideoInput'; import ToolImageInput from '@components/input/ToolImageInput'; +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; const initialValues = { - newSpeed: 200 + newSpeed: 2 }; -const validationSchema = Yup.object({ - // splitSeparator: Yup.string().required('The separator is required') -}); export default function ChangeSpeed({ title }: ToolComponentProps) { const [input, setInput] = useState(null); const [result, setResult] = useState(null); @@ -24,82 +18,64 @@ export default function ChangeSpeed({ title }: ToolComponentProps) { const compute = (optionsValues: typeof initialValues, input: File | null) => { if (!input) return; 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 reader = new FileReader(); - reader.readAsArrayBuffer(file); + const processImage = async ( + file: File, + newSpeed: number + ): Promise => { + if (!ffmpeg) { + ffmpeg = new FFmpeg(); + } - reader.onload = async () => { - const arrayBuffer = reader.result; + if (!ffmpegLoaded) { + 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) { - const intArray = new Uint8Array(arrayBuffer); + try { + await ffmpeg.writeFile('input.gif', await fetchFile(file)); - const reader = new GifReader(intArray as Buffer); - const info = reader.frameInfo(0); - const imageDataArr: ImageData[] = new Array(reader.numFrames()) - .fill(0) - .map((_, k) => { - const image = new ImageData(info.width, info.height); + // Use FFmpeg's setpts filter to change the speed + // PTS (Presentation Time Stamp) determines when each frame is shown + // 1/speed changes the PTS - lower value = faster playback + await ffmpeg.exec([ + '-i', + '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; - }); - const gif = new GifWriter( - [], - imageDataArr[0].width, - imageDataArr[0].height, - { loop: 20 } - ); + // Create a new file from the processed data + const blob = new Blob([data], { type: 'image/gif' }); + const newFile = new File( + [blob], + file.name.replace('.gif', `-${newSpeed}x.gif`), + { + type: 'image/gif' + } + ); - imageDataArr.forEach((imageData) => { - const palette = []; - const pixels = new Uint8Array(imageData.width * imageData.height); + // Clean up to free memory + await ffmpeg.deleteFile('input.gif'); + await ffmpeg.deleteFile('output.gif'); - const { data } = imageData; - for (let j = 0, k = 0, jl = data.length; j < jl; j += 4, k++) { - const r = Math.floor(data[j] * 0.1) * 10; - const g = Math.floor(data[j + 1] * 0.1) * 10; - 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); - } - }; + setResult(newFile); + } catch (error) { + console.error('Error processing GIF:', error); + throw error; + } }; processImage(input, newSpeed); @@ -132,8 +108,7 @@ export default function ChangeSpeed({ title }: ToolComponentProps) { updateField('newSpeed', Number(val))} - description={'Default new GIF speed.'} - InputProps={{ endAdornment: ms }} + description={'Default multiplier: 2 means 2x faster'} type={'number'} />