Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 19 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,26 @@ Press your app preview videos into App Store perfection.
- Convert videos to macOS App Store format (1920×1080)
- Convert videos to iOS App Store format (886×1920)
- Add silent audio track (fixes common Apple upload rejections)
- Fast server-side processing with FFmpeg
- **100% client-side processing** - your video never leaves your browser
- Desktop-only (video processing requires desktop browser)

## Requirements

### System Requirements

- **Node.js** 18.x or higher
- **FFmpeg** installed on the server (required for video processing)
- **Node.js** 18.x or higher (for development/hosting)
- **Modern browser** with SharedArrayBuffer support (Chrome, Firefox, Edge)

### Video Requirements

- Format: MP4
- Duration: 15-30 seconds
- Max file size: 500MB
- Duration: 15-30 seconds (App Store limit)

## Server Requirements
## How It Works

This application requires FFmpeg to be installed on the server.
Ciderpress uses [ffmpeg.wasm](https://github.com/ffmpegwasm/ffmpeg.wasm) to process videos entirely in your browser using WebAssembly. No server-side processing required.

### macOS

```bash
brew install ffmpeg
ffmpeg -version
```

### Ubuntu/Debian

```bash
sudo apt update
sudo apt install -y ffmpeg
ffmpeg -version
```

### CentOS/RHEL/Fedora

```bash
sudo dnf install -y ffmpeg ffmpeg-devel
ffmpeg -version
```

### Windows

1. Download FFmpeg from [ffmpeg.org/download.html](https://www.ffmpeg.org/download.html)
2. Extract to `C:\ffmpeg`
3. Add `C:\ffmpeg\bin` to system PATH
4. Verify: `ffmpeg -version`
The first time you convert a video, the app downloads the FFmpeg WASM core (~31MB). This is cached by your browser for future use.

## Development

Expand Down Expand Up @@ -89,14 +61,13 @@ bun run build
- **Animation:** Motion (Framer Motion)
- **Linting:** Biome
- **Testing:** Vitest + React Testing Library
- **Video Processing:** FFmpeg (server-side)
- **Video Processing:** ffmpeg.wasm (client-side WebAssembly)

## Project Structure

```
src/
├── app/ # Next.js App Router
│ ├── api/convert/ # Video conversion API endpoint
│ └── page.tsx # Main page
├── components/
│ ├── providers/ # React context providers
Expand All @@ -110,22 +81,22 @@ src/

## Deployment

Deploy to any Node.js platform that supports Next.js:
Deploy to any static hosting or Node.js platform:

- Vercel
- Railway
- Render
- DigitalOcean App Platform
- AWS (EC2, ECS, Lambda)
- Vercel (recommended)
- Netlify
- Cloudflare Pages
- Any static host

**Important:** Ensure FFmpeg is installed on your deployment server. The app will return a 503 error if FFmpeg is not available.
No special server configuration required since all video processing happens client-side.

### Environment Variables
### Required Headers

No environment variables are required for basic operation. For production, consider:
The app requires these security headers for SharedArrayBuffer support (already configured in `next.config.mjs`):

```env
NODE_ENV=production
```
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
```

## License
Expand Down
8 changes: 8 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable Turbopack (default in Next.js 16)
turbopack: {},

// Set security headers for SharedArrayBuffer support
// Enable WebAssembly support for ffmpeg.wasm
webpack: (config) => {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
};
return config;
},

// Set security headers for SharedArrayBuffer support (required for ffmpeg.wasm)
async headers() {
return [
{
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
"packageManager": "bun@1.2.20",
"scripts": {
"dev": "next dev",
"build": "next build",
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start",
"lint": "biome lint ./src",
"format": "biome format --write ./src",
Expand All @@ -20,6 +20,8 @@
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
Expand Down
175 changes: 0 additions & 175 deletions src/app/api/convert/route.ts

This file was deleted.

37 changes: 30 additions & 7 deletions src/components/terminal-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,42 @@ export default function TerminalContent({
return null;
}

const textClassName = cn(
"break-words w-full",
message.type === "prompt" && "text-cyan-400",
message.type === "info" && "text-neutral-400",
message.type === "success" && "text-emerald-400",
message.type === "error" && "text-red-400",
);

// Live messages render instantly and update in real-time
if (message.live) {
// Auto-complete live messages so the queue keeps moving
if (!isComplete) {
handleMessageComplete(index);
}

return (
<div key={`${messageVersion}-${index}-live`}>
<span className={cn("text-sm font-mono tracking-tight", textClassName)}>
{message.text}
</span>
{message.buttons && (
<AnimatedSpan delay={TERMINAL_TIMING.BUTTON_APPEAR_DELAY_MS}>
{renderButtons(message.buttons, index)}
</AnimatedSpan>
)}
</div>
);
}

return (
<div key={`${messageVersion}-${index}`}>
<TypingAnimation
delay={0} // No delay - we control start via activeMessageIndex
duration={TERMINAL_TIMING.TYPING_SPEED_MS}
onComplete={() => handleMessageComplete(index)}
className={cn(
"break-words w-full",
message.type === "prompt" && "text-cyan-400",
message.type === "info" && "text-neutral-400",
message.type === "success" && "text-emerald-400",
message.type === "error" && "text-red-400",
)}
className={textClassName}
>
{message.text}
</TypingAnimation>
Expand Down
Loading