Skip to content

Commit 1989cf1

Browse files
authored
Merge pull request #2 from Nackapatz/claude/video-merger-migration-bw4wib
Add editing/export features, redesign UI, and add cross-platform scripts
2 parents 73959ad + 377b1e3 commit 1989cf1

23 files changed

Lines changed: 1889 additions & 248 deletions

README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,31 @@ A browser-first MP4 merger built with Vite, React, TypeScript, `@dnd-kit`, and `
5959

6060
## Features
6161

62-
- **Drag & Drop Upload** – Add multiple `.mp4` files via picker, drop zone, or paste.
62+
- **Drag & Drop Upload** – Add multiple video files (`.mp4`, `.mov`, `.webm`, `.mkv`) via picker, drop zone, or paste.
6363
- **Smart Auto-Sort** – Clips are automatically ordered by file modification time.
6464
- **Clip Inspector** – See duration, resolution, aspect ratio, and audio stream info per clip.
6565
- **Sortable Timeline** – Reorder clips with mouse or keyboard via `@dnd-kit`.
6666
- **Trim Segments** – Cut in/out points on any clip before merging.
67-
- **Flexible Output** – Choose from common output aspect ratios (16:9, 9:16, 1:1, 4:3, etc.).
67+
- **Split Clip** – Split a clip into two independent clips at the playhead.
68+
- **Duplicate & Rename** – Duplicate any clip, or double-click its name to rename it.
69+
- **Undo / Redo** – Step back and forward through timeline edits (`Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z`).
70+
- **Per-Clip Volume & Mute** – Adjust or silence individual clips.
71+
- **Per-Clip Speed** – Slow down or speed up a clip from 0.5x–2x.
72+
- **Fade In / Out** – Add audio+video fades per clip.
73+
- **Color Adjustment** – Brightness, contrast, and saturation per clip.
74+
- **Watermark Overlay** – Burn in a logo image at any corner or center, with adjustable opacity.
75+
- **Background Music** – Mix in a looping music track under the merged audio.
76+
- **GIF Export** – Convert the merged result to an animated GIF.
77+
- **Flexible Output** – Choose from common output aspect ratios (16:9, 9:16, 1:1) plus ready-made social presets (Instagram Reel, TikTok, YouTube Shorts, Instagram Story, Square Post).
6878
- **Aspect Ratio Fit** – Mismatched source frames are placed over a blurred background to fill the canvas.
6979
- **Audio Normalization** – Clips without audio receive a silent AAC track so mixed projects merge cleanly.
7080
- **Hybrid Engine** – Falls back to native backend FFmpeg for larger projects; browser FFmpeg for everything else.
71-
- **100% Private** – Files are processed locally in the browser. Nothing is uploaded to a server.
81+
- **Autosave & Restore** – Project settings are autosaved locally; reload the app and restore/relink clips.
82+
- **Live ETA** – See an estimated time remaining while merging.
83+
- **Error Log Export** – Download a text log of everything that went wrong during a session.
84+
- **Keyboard Shortcuts Cheat Sheet** – Press `?` for a quick reference.
85+
- **Light & Dark Theme** – Toggle from the toolbar; respects your OS preference by default.
86+
- **100% Private** – Files are processed locally in the browser by default. Nothing is uploaded to a server unless you opt into backend mode.
7287
- **Modern UI** – React 18 + Lucide icons, accessible keyboard interactions.
7388

7489
---
@@ -171,6 +186,18 @@ ffprobe -version
171186

172187
## Getting Started
173188

189+
### Quick start scripts
190+
191+
Cross-platform launcher and build scripts are included so you don't need to remember npm commands:
192+
193+
| Platform | Start dev servers | Build for production |
194+
| --------------- | ------------------ | --------------------- |
195+
| Windows | `start.bat` | `build.bat` |
196+
| macOS | `start.command` | `build.command` |
197+
| Linux / macOS | `./start.sh` | `./build.sh` |
198+
199+
Each script checks for Node.js, runs `npm install` if `node_modules` is missing, and then starts the dev servers or produces a production build in `dist/`.
200+
174201
Start both the frontend and backend dev servers in one command:
175202

176203
```bash

build.bat

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@echo off
2+
setlocal
3+
cd /d "%~dp0"
4+
5+
where node >nul 2>nul
6+
if errorlevel 1 (
7+
echo Node.js is required but was not found on PATH. Install it from https://nodejs.org/ and try again.
8+
pause
9+
exit /b 1
10+
)
11+
12+
if not exist "node_modules" (
13+
echo Installing dependencies...
14+
call npm install
15+
if errorlevel 1 (
16+
echo npm install failed.
17+
pause
18+
exit /b 1
19+
)
20+
)
21+
22+
echo Building Video Merger for production...
23+
call npm run build
24+
if errorlevel 1 (
25+
echo Build failed.
26+
pause
27+
exit /b 1
28+
)
29+
30+
echo Build complete. Output is in the "dist" folder.
31+
pause

build.command

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
cd "$(dirname "${BASH_SOURCE[0]}")"
3+
./build.sh

build.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
cd "$(dirname "${BASH_SOURCE[0]}")"
4+
5+
if ! command -v node >/dev/null 2>&1; then
6+
echo "Node.js is required but was not found on PATH. Install it from https://nodejs.org/ and try again."
7+
exit 1
8+
fi
9+
10+
if [ ! -d "node_modules" ]; then
11+
echo "Installing dependencies..."
12+
npm install
13+
fi
14+
15+
echo "Building Video Merger for production..."
16+
npm run build
17+
18+
echo "Build complete. Output is in the \"dist\" folder."

src/backend/ffmpegCommands.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { ClipRotation, OutputSettings, TrimSegment, VideoMetadata } from "../shared/types";
1+
import type { ClipRotation, ColorAdjust, OutputSettings, TrimSegment, VideoMetadata } from "../shared/types";
2+
import { defaultColorAdjust } from "../shared/types";
23
import {
34
buildIndexedAudioFilter,
45
createRemuxSegmentsArgs,
@@ -11,6 +12,73 @@ export interface BackendClip {
1112
metadata: VideoMetadata;
1213
rotation: ClipRotation;
1314
trimSegments?: TrimSegment[];
15+
volume: number;
16+
muted: boolean;
17+
speed: number;
18+
fadeIn: number;
19+
fadeOut: number;
20+
colorAdjust: ColorAdjust;
21+
}
22+
23+
export interface ClipEffects {
24+
volume: number;
25+
muted: boolean;
26+
speed: number;
27+
fadeIn: number;
28+
fadeOut: number;
29+
colorAdjust: ColorAdjust;
30+
}
31+
32+
export function parseClipEffects(rawEffects: unknown, length: number): ClipEffects[] {
33+
const fallback = (): ClipEffects[] =>
34+
Array.from({ length }, () => ({
35+
volume: 1,
36+
muted: false,
37+
speed: 1,
38+
fadeIn: 0,
39+
fadeOut: 0,
40+
colorAdjust: { ...defaultColorAdjust }
41+
}));
42+
43+
if (typeof rawEffects !== "string" || length === 0) {
44+
return fallback();
45+
}
46+
47+
try {
48+
const parsed = JSON.parse(rawEffects) as unknown;
49+
if (!Array.isArray(parsed) || parsed.length !== length) {
50+
return fallback();
51+
}
52+
return parsed.map((value) => normalizeClipEffects(value));
53+
} catch {
54+
return fallback();
55+
}
56+
}
57+
58+
function normalizeClipEffects(value: unknown): ClipEffects {
59+
const entry = (value && typeof value === "object" ? value : {}) as Partial<ClipEffects>;
60+
const colorAdjustRaw = (entry.colorAdjust ?? {}) as Partial<ColorAdjust>;
61+
62+
return {
63+
volume: clampNumber(entry.volume, 0, 2, 1),
64+
muted: entry.muted === true,
65+
speed: clampNumber(entry.speed, 0.5, 2, 1),
66+
fadeIn: clampNumber(entry.fadeIn, 0, 30, 0),
67+
fadeOut: clampNumber(entry.fadeOut, 0, 30, 0),
68+
colorAdjust: {
69+
brightness: clampNumber(colorAdjustRaw.brightness, -1, 1, 0),
70+
contrast: clampNumber(colorAdjustRaw.contrast, 0, 3, 1),
71+
saturation: clampNumber(colorAdjustRaw.saturation, 0, 3, 1)
72+
}
73+
};
74+
}
75+
76+
function clampNumber(value: unknown, min: number, max: number, fallback: number): number {
77+
const numeric = typeof value === "number" ? value : Number(value);
78+
if (!Number.isFinite(numeric)) {
79+
return fallback;
80+
}
81+
return Math.min(max, Math.max(min, numeric));
1482
}
1583

1684
export { buildIndexedAudioFilter, createRemuxSegmentsArgs, createSegmentArgs, getVideoBitrate };

0 commit comments

Comments
 (0)