Skip to content

Commit 359bb4d

Browse files
committed
feat(new tool): GIF to MP4
Fix #336
1 parent 5ca56cc commit 359bb4d

7 files changed

Lines changed: 1773 additions & 1718 deletions

File tree

locales/en.yml

Lines changed: 1611 additions & 1717 deletions
Large diffs are not rendered by default.

nginx.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ server {
1111
add_header X-Permitted-Cross-Domain-Policies none;
1212
add_header Cross-Origin-Resource-Policy same-site;
1313
add_header Cross-Origin-Opener-Policy same-origin;
14+
add_header Cross-Origin-Embedder-Policy require-corp;
1415
#add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' http: https: blob:; style-src 'self' 'unsafe-inline' https: http:; img-src 'self' data: blob: https: http:; connect-src data: blob: https: http:; font-src 'self' https: http:; object-src 'none'; base-uri 'self'; form-action 'self';";
1516
location / {
1617
rewrite ^/it-tools/(.*) /$1 break;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
"@date-fns/utc": "^2.1.1",
4747
"@dice-roller/rpg-dice-roller": "^5.5.1",
4848
"@faker-js/faker": "^10.2.0",
49+
"@ffmpeg/ffmpeg": "^0.12.15",
50+
"@ffmpeg/util": "^0.12.2",
4951
"@finegym/fitness-calc": "^1.0.1",
5052
"@fortawesome/fontawesome-svg-core": "^6.7.2",
5153
"@fortawesome/free-solid-svg-icons": "^6.7.2",

pnpm-lock.yaml

Lines changed: 26 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<script setup lang="ts">
2+
import { useI18n } from 'vue-i18n';
3+
import { ref } from 'vue';
4+
import { FFmpeg } from '@ffmpeg/ffmpeg';
5+
6+
import ffmpegClassWorkerUrl from '@ffmpeg/ffmpeg/worker?worker&url';
7+
8+
import { toBlobURL } from '@ffmpeg/util';
9+
10+
const { t } = useI18n();
11+
12+
const logs = ref<string[]>([]);
13+
14+
const ffmpeg = new FFmpeg();
15+
ffmpeg.on('log', ({ message }) => {
16+
logs.value.push(message);
17+
});
18+
19+
const loading = ref(false);
20+
const error = ref('');
21+
const loop = ref('5');
22+
23+
async function loadFFmpeg() {
24+
if (!ffmpeg.loaded) {
25+
const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm';
26+
27+
await ffmpeg.load({
28+
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
29+
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
30+
classWorkerURL: ffmpegClassWorkerUrl,
31+
});
32+
}
33+
}
34+
35+
async function onFileUploaded(gifFile: File) {
36+
loading.value = true;
37+
error.value = '';
38+
39+
try {
40+
await loadFFmpeg();
41+
42+
const inputName = 'input.gif';
43+
const outputName = 'output.mp4';
44+
45+
const buffer = new Uint8Array(await gifFile.arrayBuffer());
46+
if (!buffer) {
47+
return;
48+
}
49+
ffmpeg.writeFile(inputName, buffer);
50+
51+
await ffmpeg.exec([
52+
'-stream_loop', loop.value,
53+
'-i', inputName,
54+
'-movflags', 'faststart',
55+
'-pix_fmt', 'yuv420p',
56+
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
57+
outputName,
58+
]);
59+
60+
const data = await ffmpeg.readFile(outputName);
61+
const blob = new Blob([data], { type: 'video/mp4' });
62+
const url = URL.createObjectURL(blob);
63+
64+
const a = document.createElement('a');
65+
a.href = url;
66+
a.download = `${gifFile.name.replace(/\.gif$/, '')}.mp4`;
67+
a.click();
68+
69+
URL.revokeObjectURL(url);
70+
}
71+
catch (err: any) {
72+
error.value = err.toString();
73+
}
74+
finally {
75+
loading.value = false;
76+
}
77+
}
78+
</script>
79+
80+
<template>
81+
<div>
82+
<n-space justify="center" mb-1>
83+
<n-form-item :label="t('tools.gif-to-mp4.texts.label-stream-loop')" label-placement="left">
84+
<n-input-number-i18n v-model:value="loop" :min="1" />
85+
</n-form-item>
86+
</n-space>
87+
88+
<div style="flex: 0 0 100%">
89+
<div mx-auto max-w-600px>
90+
<c-file-upload
91+
:title="t('tools.gif-to-mp4.texts.title-drag-and-drop-gif-here-or-click-to-select-a-file')"
92+
accept=".gif"
93+
@file-upload="onFileUploaded"
94+
/>
95+
</div>
96+
</div>
97+
98+
<div mt-3 flex justify-center>
99+
<c-alert v-if="error" type="error">
100+
{{ error }}
101+
</c-alert>
102+
<n-spin
103+
v-if="loading"
104+
size="small"
105+
/>
106+
</div>
107+
108+
<c-card :title="t('tools.gif-to-mp4.texts.title-logs')">
109+
<pre>{{ logs.join('\n') }}</pre>
110+
</c-card>
111+
</div>
112+
</template>

src/tools/gif-to-mp4/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { translate as t } from '@/plugins/i18n.plugin';
2+
import { VideoPlus } from '@vicons/tabler';
3+
import { defineTool } from '../tool';
4+
5+
export const tool = defineTool({
6+
name: t('tools.gif-to-mp4.title'),
7+
path: '/gif-to-mp4',
8+
description: t('tools.gif-to-mp4.description'),
9+
keywords: ['gif', 'ffmpeg', 'mp4'],
10+
component: () => import('./gif-to-mp4.vue'),
11+
icon: VideoPlus,
12+
createdAt: new Date('2026-03-15'),
13+
category: 'Images',
14+
externalHTMLContent: 'Download FFMPEG from https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10. All processing done in your browser.',
15+
});

vite.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,10 @@ export default defineConfig({
178178
},
179179
},
180180
},
181+
server: {
182+
headers: {
183+
'Cross-Origin-Opener-Policy': 'same-origin',
184+
'Cross-Origin-Embedder-Policy': 'require-corp',
185+
},
186+
},
181187
});

0 commit comments

Comments
 (0)