Skip to content

Commit 814b084

Browse files
authored
Add animation creation (#29)
* add ffmpeg * add animation creation * correct sidebar config * fix ffmpeg binaries & animation generation * use dev deps for ci
1 parent d39f13f commit 814b084

File tree

12 files changed

+253
-9
lines changed

12 files changed

+253
-9
lines changed

.github/workflows/build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ jobs:
5151

5252
- name: install frontend dependencies
5353
# If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
54-
run: bun install # change this to npm or pnpm depending on which one you use.
54+
run: bun install --dev # change this to npm or pnpm depending on which one you use.
55+
56+
- name: download ffmpeg
57+
# Use bun to download the ffmpeg sidecar before building so the binary is available
58+
run: bun run download-ffmpeg
5559

5660
- uses: tauri-apps/tauri-action@v0
5761
env:

.github/workflows/publish.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ jobs:
6868

6969
- name: install frontend dependencies
7070
# If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
71-
run: bun install
71+
run: bun install --dev
72+
73+
- name: download ffmpeg
74+
# Use bun to download the ffmpeg sidecar before building so the binary is available
75+
run: bun run download-ffmpeg
7276

7377
- name: build tauri app
7478
uses: tauri-apps/tauri-action@v0

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ node_modules
88
!.env.example
99
vite.config.js.timestamp-*
1010
vite.config.ts.timestamp-*
11+
binaries/

bun.lockb

29.6 KB
Binary file not shown.

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,25 @@
1010
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1111
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
1212
"tauri": "tauri",
13-
"format": "prettier --write . && cd src-tauri && cargo fmt"
13+
"format": "prettier --write . && cd src-tauri && cargo fmt",
14+
"download-ffmpeg": "bun run scripts/download-ffmpeg.ts"
1415
},
1516
"license": "MIT",
1617
"dependencies": {
1718
"@tauri-apps/api": "^2.1.1",
1819
"@tauri-apps/plugin-dialog": "~2.0.1",
1920
"@tauri-apps/plugin-process": "~2.0.0",
20-
"@tauri-apps/plugin-shell": "^2.0.1",
21+
"@tauri-apps/plugin-shell": "^2.3.3",
2122
"@tauri-apps/plugin-updater": "~2.0.0"
2223
},
2324
"devDependencies": {
2425
"@sveltejs/adapter-static": "^3.0.6",
2526
"@sveltejs/kit": "^2.9.0",
2627
"@sveltejs/vite-plugin-svelte": "^4.0.2",
2728
"@tauri-apps/cli": "^2.1.0",
29+
"@types/ffbinaries": "^1.1.5",
30+
"@types/node": "^25.0.0",
31+
"ffbinaries": "^1.1.6",
2832
"prettier": "^3.4.2",
2933
"prettier-plugin-svelte": "^3.3.2",
3034
"svelte": "^5.8.0",

scripts/download-ffmpeg.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import ffbinaries from 'ffbinaries';
2+
import path from 'node:path';
3+
import fs from 'node:fs/promises';
4+
import { chmod } from 'node:fs/promises';
5+
6+
// Define the output directory
7+
const dest = path.join(import.meta.dirname, '../src-tauri/binaries');
8+
9+
// Mapping: ffbinaries platform code -> Tauri target triple filename
10+
const platformMap: Record<string, string> = {
11+
'windows-64': 'ffmpeg-x86_64-pc-windows-msvc.exe',
12+
'osx-64': 'ffmpeg-x86_64-apple-darwin',
13+
'linux-64': 'ffmpeg-x86_64-unknown-linux-gnu',
14+
'linux-arm64': 'ffmpeg-aarch64-unknown-linux-gnu',
15+
};
16+
17+
async function main() {
18+
await fs.mkdir(dest, { recursive: true });
19+
console.log('🚀 Starting FFmpeg download...');
20+
21+
// 1. Download for each platform in parallel
22+
const downloadPromises = Object.entries(platformMap).map(async ([platformCode, targetFilename]) => {
23+
return downloadAndRename(platformCode as ffbinaries.Platform, targetFilename);
24+
});
25+
26+
await Promise.all(downloadPromises);
27+
28+
// 2. Handle M1/M2/M3 Mac (Copy Intel to ARM)
29+
await copyIntelToArm();
30+
31+
console.log('🎉 Done! FFmpeg binaries are ready in src-tauri/binaries/');
32+
}
33+
34+
async function downloadAndRename(platform: ffbinaries.Platform, targetFilename: string) {
35+
const tempDir = path.join(dest, `_temp_${platform}`);
36+
await fs.mkdir(tempDir, { recursive: true });
37+
38+
return new Promise<void>((resolve, reject) => {
39+
ffbinaries.downloadBinaries(['ffmpeg'], {
40+
destination: tempDir,
41+
platform: platform
42+
}, async (err) => {
43+
if (err) {
44+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
45+
return reject(err);
46+
}
47+
48+
try {
49+
// [CRITICAL CHANGE] Hunt for the actual binary file recursively
50+
const binaryName = platform.startsWith('windows') ? 'ffmpeg.exe' : 'ffmpeg';
51+
const foundBinaryPath = await findFileRecursively(tempDir, binaryName);
52+
53+
if (!foundBinaryPath) {
54+
throw new Error(`Binary ${binaryName} not found in extracted folder for ${platform}`);
55+
}
56+
57+
const finalPath = path.join(dest, targetFilename);
58+
59+
// Nuke the destination if it exists (file OR folder)
60+
await fs.rm(finalPath, { recursive: true, force: true });
61+
62+
// Move the specific file found, not the folder
63+
await fs.rename(foundBinaryPath, finalPath);
64+
console.log(` • Ready: ${targetFilename}`);
65+
66+
if (!targetFilename.endsWith('.exe')) {
67+
await chmod(finalPath, 0o755);
68+
}
69+
} catch (e) {
70+
reject(new Error(`Failed to process ${platform}: ${e}`));
71+
} finally {
72+
// Cleanup temp folder
73+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
74+
}
75+
76+
resolve();
77+
});
78+
});
79+
}
80+
81+
// Recursive helper to find the file even if nested in subfolders
82+
async function findFileRecursively(dir: string, filename: string): Promise<string | null> {
83+
const entries = await fs.readdir(dir, { withFileTypes: true });
84+
85+
for (const entry of entries) {
86+
const fullPath = path.join(dir, entry.name);
87+
88+
// If we found the specific file we want
89+
if (entry.isFile() && entry.name === filename) {
90+
return fullPath;
91+
}
92+
93+
// If directory, dive in
94+
if (entry.isDirectory()) {
95+
const found = await findFileRecursively(fullPath, filename);
96+
if (found) return found;
97+
}
98+
}
99+
return null;
100+
}
101+
102+
async function copyIntelToArm() {
103+
const intelMacPath = path.join(dest, 'ffmpeg-x86_64-apple-darwin');
104+
const armMacPath = path.join(dest, 'ffmpeg-aarch64-apple-darwin');
105+
106+
try {
107+
try {
108+
await fs.access(armMacPath);
109+
return;
110+
} catch { /* proceed */ }
111+
112+
await fs.access(intelMacPath);
113+
await fs.rm(armMacPath, { recursive: true, force: true }); // Ensure clean target
114+
await fs.copyFile(intelMacPath, armMacPath);
115+
console.log(` • Copied Intel binary to ${path.basename(armMacPath)} (Runs via Rosetta)`);
116+
} catch (e) {
117+
// Intel file likely missing
118+
}
119+
}
120+
121+
main().catch(console.error);

src-tauri/capabilities/default.json

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"$schema": "../gen/schemas/desktop-schema.json",
33
"identifier": "default",
44
"description": "Capability for the main window",
5-
"windows": ["main"],
5+
"windows": [
6+
"main"
7+
],
68
"permissions": [
79
"core:default",
810
"shell:allow-open",
@@ -13,6 +15,18 @@
1315
"updater:allow-check",
1416
"updater:allow-download-and-install",
1517
"process:allow-restart",
16-
"process:default"
18+
"process:default",
19+
"shell:default",
20+
"shell:allow-execute",
21+
{
22+
"identifier": "shell:allow-execute",
23+
"allow": [
24+
{
25+
"name": "binaries/ffmpeg",
26+
"sidecar": true,
27+
"args": true
28+
}
29+
]
30+
}
1731
]
18-
}
32+
}

src-tauri/src/commands.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use image::{GenericImageView, ImageBuffer};
22
use std::{fs, path::Path};
33
use tauri::{AppHandle, Emitter};
4+
use tauri_plugin_shell::ShellExt;
45

56
use crate::{FilesMap, QuiltStatus, QuiltStatusState};
67

@@ -100,6 +101,7 @@ pub fn make_quilt(
100101
output_folder: String,
101102
columns: isize,
102103
rows: isize,
104+
framerate: isize,
103105
) {
104106
let quilt_size = columns * rows;
105107

@@ -200,4 +202,41 @@ pub fn make_quilt(
200202
},
201203
)
202204
.unwrap();
205+
206+
if framerate > 0 {
207+
app.emit(
208+
"quilt_status",
209+
QuiltStatus {
210+
amount: no_of_quilts,
211+
index: no_of_quilts,
212+
status: QuiltStatusState::CreatingAnimation,
213+
},
214+
)
215+
.unwrap();
216+
let framerate_str = framerate.to_string();
217+
let input_path = std::path::Path::new(&output_folder).join("quilt_%d.png");
218+
let output_path = std::path::Path::new(&output_folder).join("animation.mp4");
219+
let sidebar_command = app.shell().sidecar("ffmpeg").unwrap().args([
220+
"-framerate",
221+
framerate_str.as_str(),
222+
"-i",
223+
input_path.to_str().unwrap(),
224+
"-c:v",
225+
"libx264",
226+
"-pix_fmt",
227+
"yuv420p",
228+
output_path.to_str().unwrap(),
229+
]);
230+
let _ = sidebar_command.spawn().unwrap();
231+
app.emit(
232+
"quilt_status",
233+
QuiltStatus {
234+
amount: no_of_quilts,
235+
index: no_of_quilts,
236+
status: QuiltStatusState::CreatedAnimation,
237+
},
238+
)
239+
.unwrap();
240+
}
241+
// ffmpeg -framerate 24 -i %d.png -c:v libx264 -pix_fmt yuv420p animation.mp4
203242
}

src-tauri/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub struct FilesMap {
1212
enum QuiltStatusState {
1313
InProgress,
1414
Finished,
15+
CreatingAnimation,
16+
CreatedAnimation,
1517
}
1618

1719
#[derive(Clone, Serialize)]

src-tauri/tauri.conf.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
"icons/icon.icns",
3232
"icons/icon.ico"
3333
],
34-
"createUpdaterArtifacts": true
34+
"createUpdaterArtifacts": true,
35+
"externalBin": [
36+
"binaries/ffmpeg"
37+
]
3538
},
3639
"plugins": {
3740
"updater": {

0 commit comments

Comments
 (0)