Files
gui-video-clipper/docs/superpowers/specs/2026-09-21-video-clipper-design.md

350 lines
23 KiB
Markdown
Raw Normal View History

# Video Clipper — Design Spec
**Date:** 2026-09-21
**Status:** Draft
**Stack:** Tauri v2 + Svelte (frontend) + Rust (backend) + ffmpeg + yt-dlp
## Overview
A macOS GUI app for creating clips from online videos (primarily YouTube) without requiring the user to manually download the video first. The app provides a timeline-based interface for scrubbing through video, marking clip boundaries, and exporting clips via ffmpeg.
Think "LosslessCut, but paste a URL instead of opening a file."
## Core User Flow
1. User pastes a YouTube (or other supported) URL.
2. App resolves the URL via yt-dlp → retrieves metadata and a direct stream URL.
3. Video begins playing immediately via the stream URL (HTML5 `<video>` element).
4. A background download of the full video starts simultaneously.
5. User scrubs the timeline, marks clip in/out points.
6. User exports clips → ffmpeg operates on the local (downloaded) file.
## Architecture
Three layers:
### 1. Tauri/Rust Backend
Manages all subprocess orchestration, file I/O, and state persistence. Communicates with the frontend via Tauri commands (request/response) and Tauri events (push notifications like download progress).
**Components:**
| Component | Responsibility |
|---|---|
| `DependencyManager` | Check the full dependency chain on `$PATH`: ffmpeg/ffprobe, yt-dlp, a JavaScript runtime (Deno preferred, Node.js fallback), and the `bgutil-ytdlp-pot-provider` yt-dlp plugin. On first launch or when anything is missing, shows a step-by-step **Setup Wizard** that walks the user through installing each dependency one at a time. See the Dependency Chain section. |
| `VideoResolver` | Takes a URL, calls `yt-dlp --dump-json <url>` for metadata and `yt-dlp -g <url>` for the direct stream URL. Returns a `VideoMetadata` struct. |
| `DownloadManager` | Runs `yt-dlp -o <temp_path> <url>` in the background. Emits progress events to the frontend. Manages temp file lifecycle (cleanup on cancel/error). |
| `KeyframeIndex` | After download completes, runs `ffprobe -select_streams v -show_entries frame=pts_time,flags -of csv <file>` to extract keyframe positions. Returns a sorted list of keyframe timestamps. |
| `ClipExporter` | Runs ffmpeg for clip extraction. Supports two modes: lossless (stream copy, keyframe-aligned) and precise (re-encode, exact frames). Handles both individual and merged export. |
| `WaveformGenerator` | After download completes, extracts audio peak data from the video file via ffmpeg. Computes ~8000 peak samples for the full video duration and sends them to the frontend for Canvas rendering. |
| `ThumbnailExtractor` | After download completes, extracts frames at computed intervals via ffmpeg (1/sec for <10 min, 1/2s for 10–60 min, 1/5s for >60 min). Outputs small JPEGs (160×90px) packed into sprite sheets for efficient rendering. |
| `PreferencesStore` | Persists user preferences (output directory, window geometry, cookie source) via `tauri-plugin-store`. |
### 2. Svelte Frontend
Renders the video player, custom timeline, clip management panel, and export dialog.
**Components:**
| Component | Responsibility |
|---|---|
| `UrlInput` | Text field for URL entry. Detects paste, shows loading spinner during resolution, displays errors. |
| `VideoPlayer` | Wraps the HTML5 `<video>` element. Exposes play/pause/seek API. Stays synced with the Timeline component via a shared Svelte store. |
| `Timeline` | Canvas-based timeline with four visual layers (top to bottom): thumbnail strip, waveform, clip markers, controls. Renders: playhead (current position), clip regions (colored rectangles with draggable start/end handles), keyframe markers (after index is built), thumbnail frames (after extraction), audio waveform (after extraction). Supports click-to-seek, drag-to-scrub, zoom via scroll wheel or slider. |
| `TransportControls` | Play/pause, frame step (±1 frame), keyframe jump, seek ±1s/±5s buttons. Wired to keyboard shortcuts. |
| `ClipList` | Scrollable panel listing all clips. Each row shows: color swatch, label (editable), start time (editable), end time (editable), delete button. Clicking a row selects the clip and jumps the playhead to its start. |
| `ExportDialog` | Modal dialog for export configuration. See Export Flow section. |
| `StatusBar` | Bottom bar showing: download progress (percentage + ETA), active export progress, tool availability status. |
| `SetupWizard` | Step-by-step modal shown on first launch or when dependencies are missing. Walks the user through installing each dependency one at a time with green checkmarks for what's already present. See the Dependency Chain section. |
### 3. External CLI Tools
- **yt-dlp:** URL resolution, metadata extraction, stream URL retrieval, video downloading. All yt-dlp invocations include the user's configured cookie source (see Cookie/Auth section).
- **ffmpeg:** Clip extraction (both lossless and re-encode modes), clip merging, waveform peak extraction, thumbnail frame extraction.
- **ffprobe:** Keyframe position extraction (part of the ffmpeg package).
- **deno** (or **node**): JavaScript runtime required by the PO Token provider plugin for YouTube playback.
- **bgutil-ytdlp-pot-provider:** yt-dlp plugin that automatically generates per-video Proof-of-Origin tokens required by YouTube.
## Data Model
```typescript
interface Clip {
id: string;
startTime: number; // seconds (float, e.g. 83.450)
endTime: number; // seconds (float)
label: string; // auto-generated "Clip 1", "Clip 2", editable by user
color: string; // auto-assigned from a palette for visual distinction
}
interface VideoSession {
url: string;
title: string;
duration: number; // seconds
fps: number; // frames per second, from yt-dlp metadata
thumbnailUrl: string | null;
streamUrl: string; // direct stream URL for preview playback
localFilePath: string | null; // set once background download completes
downloadProgress: number; // 0.0 - 1.0
keyframePositions: number[]; // populated after download + ffprobe
waveformPeaks: number[]; // normalized audio peaks (0.0–1.0), ~8000 samples, populated after download + ffmpeg
thumbnailSpritesheets: ThumbnailSpritesheet[]; // populated after download + ffmpeg
clips: Clip[];
}
interface ThumbnailSpritesheet {
filePath: string; // path to the spritesheet image
startIndex: number; // index of first thumbnail in this sheet
count: number; // number of thumbnails in this sheet
thumbWidth: number; // pixel width of each thumbnail (e.g. 160)
thumbHeight: number; // pixel height of each thumbnail (e.g. 90)
columns: number; // thumbnails per row in the spritesheet
intervalSeconds: number; // time interval between thumbnails
}
type CookieSource =
| { type: "browser"; browser: "firefox" }
| { type: "file"; path: string }
| { type: "none" };
interface UserPreferences {
outputDirectory: string; // default: home directory (~/)
windowBounds: { x: number; y: number; width: number; height: number } | null;
cookieSource: CookieSource; // default: { type: "browser", browser: "firefox" }
}
```
## Timeline & Interaction Design
### Layout
```
┌─────────────────────────────────────────────────────────┐
│ [Paste URL...] [⚙ Prefs] │
├─────────────────────────────────────────────────────────┤
│ │
│ VIDEO PLAYER │
│ (16:9 aspect ratio) │
│ │
├─────────────────────────────────────────────────────────┤
│ [◄K] [◄|] [▶/❚❚] [|►] [K►] 00:03:42 / 00:12:35 │
│ [I] [O] │
├─────────────────────────────────────────────────────────┤
│ ▼ playhead │
│ ┃ │
│ ┃ [thumb] [thumb] [thumb] [thumb] [thumb] [thumb] │ ← Thumbnail strip
│ ┃ ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿ │ ← Waveform
│ ┃ ▓▓▓▓▓▓▓clip 1▓▓▓▓▓▓▓▓▓ ▓▓▓clip 2▓▓▓ │ ← Clip markers
│ ┃ │
│ [🔍−] ═══════════●══════════ [🔍+] zoom slider │ ← Controls
├─────────────────────────────────────────────────────────┤
│ CLIPS │
│ ┌──────────────────────────────────────────┐ │
│ │ Clip 1 │ 01:23.450 → 02:45.120 │ [✕] │ │
│ │ Clip 2 │ 05:10.000 → 05:55.800 │ [✕] │ │
│ └──────────────────────────────────────────┘ │
│ │
│ [Export Selected] [Export All] │
└─────────────────────────────────────────────────────────┘
```
The timeline area has four visual layers (top to bottom):
1. **Thumbnail strip** — Video frame thumbnails at adaptive density based on zoom level.
2. **Waveform** — Audio amplitude visualization.
3. **Clip markers** — Colored regions with draggable handles, overlaying the above.
4. **Controls** — Zoom slider and playhead position.
Before the background download completes, the thumbnail and waveform layers show a static placeholder (subtle gradient bar with "Generating…" label). They fade in with real data once extraction finishes.
### Timeline Interactions
- **Scrub:** Click anywhere on the timeline to jump. Click-and-drag to scrub continuously.
- **Mark in-point:** Press `I` or click `[I]` → if a clip is currently selected, updates that clip's start time. Otherwise, begins a new clip by recording a pending in-point at the current playhead position.
- **Mark out-point:** Press `O` or click `[O]` → if a clip is selected, updates that clip's end time. If there is a pending in-point (from a previous `I` press), creates a new clip spanning from the pending in-point to the current playhead. If neither, does nothing.
- **Adjust boundaries:** Drag the left/right edges of any clip region. Cursor changes to a resize handle near edges.
- **Fine-tune in clip list:** Click timestamp values in the clip list to edit them numerically.
- **Select clip:** Click a clip region on the timeline or its row in the clip list. Playhead jumps to its start.
- **Delete clip:** Click `[✕]`, or select and press `Delete`.
- **Zoom:** Scroll wheel on the timeline, or use the zoom slider. Zooming centers on the playhead.
### Keyboard Shortcuts
| Key | Action |
|---|---|
| `Space` | Play / Pause |
| `,` (comma) | Step back 1 frame |
| `.` (period) | Step forward 1 frame |
| `Shift+,` | Jump to previous keyframe |
| `Shift+.` | Jump to next keyframe |
| `←` / `→` | Seek ±5 seconds |
| `Shift+←` / `Shift+→` | Seek ±1 second |
| `I` | Set in-point (clip start) |
| `O` | Set out-point (clip end) |
| `J` / `K` / `L` | Shuttle: reverse / pause / forward |
| `Delete` | Remove selected clip |
| `Cmd+E` | Open export dialog |
| `Cmd+A` | Select all clips |
### Frame Stepping Implementation
- Frame stepping uses the video's FPS from yt-dlp metadata. Step = seek by `1/fps` seconds.
- Keyframe jumping requires the keyframe index, which is built after the background download completes via ffprobe.
- Until the keyframe index is ready, keyframe jump buttons are disabled with a tooltip: "Available after download completes."
## Export Flow
### Export Dialog
```
┌─────────── Export Clips ───────────┐
│ │
│ Clips to export: │
│ ○ Selected clip (Clip 2) │
│ ● All clips (3) │
│ │
│ Output mode: │
│ ● Individual files │
│ ○ Merged into one video │
│ │
│ Cut mode: │
│ ● Lossless (fast, keyframe-aligned)│
│ ○ Precise (re-encode, exact frames)│
│ │
│ Format: [MP4 (H.264) ▾] │
│ │
│ Save to: [~/ 📁] │
│ │
│ Naming: {title} - Clip {n} │
│ │
│ [Cancel] [Export] │
└─────────────────────────────────────┘
```
### Export Behavior
- **Lossless mode:** Runs `ffmpeg -ss <start> -to <end> -i <file> -c copy <output>`. Fast, no quality loss, but cuts align to nearest keyframe.
- **Precise mode:** Runs `ffmpeg -ss <start> -to <end> -i <file> -c:v libx264 -c:a aac <output>`. Slower, re-encodes, but frame-accurate.
- **Merged export:** Concatenates clips using ffmpeg's concat demuxer. Clips are ordered by their start time.
- **Format:** For lossless, determined by source container (remux). For precise, user picks MP4/MKV/WebM.
- **Save location:** Defaults to `~/` on first run. User picks a directory via native macOS folder picker. The choice persists across sessions. Always overridable per-export.
- **Naming:** Auto-generated: `{video title} - Clip 1.mp4`, `{video title} - Clip 2.mp4`. Merged: `{video title} - Merged.mp4`. Existing files are not silently overwritten — append ` (2)`, ` (3)`, etc.
### Export Prerequisites
Export requires the background download to be complete (ffmpeg operates on the local file, not the stream URL). If the download is still in progress when the user clicks Export:
- Show: "Download at 72% — export will start automatically when ready."
- User can wait or cancel.
- Export begins automatically once the download finishes.
## Dependency Chain & Setup Wizard
YouTube's 2026 anti-bot measures require more than just ffmpeg and yt-dlp. The full dependency chain:
| Dependency | Purpose | Install Method | Check |
|---|---|---|---|
| `ffmpeg` (+ `ffprobe`) | Clip extraction, waveform generation, thumbnail extraction, keyframe indexing | `brew install ffmpeg` | `which ffmpeg` |
| `yt-dlp` | URL resolution, metadata, streaming, downloading | `brew install yt-dlp` | `which yt-dlp` |
| `deno` (preferred) or `node` | JavaScript runtime for PO Token generation | `brew install deno` | `which deno \|\| which node` |
| `bgutil-ytdlp-pot-provider` | yt-dlp plugin: automatic per-video Proof-of-Origin token generation for YouTube | `python3 -m pip install -U bgutil-ytdlp-pot-provider` | Run `yt-dlp -v --simulate "https://www.youtube.com/watch?v=dQw4w9WgXcQ"` and check for `GetPOT` in `[debug] Extractor Plugins:` output |
### Setup Wizard UX
On first launch (or when any dependency is missing), the app shows a step-by-step Setup Wizard modal:
1. Each dependency is listed as a row with a status indicator: ✅ (installed), ❌ (missing), or ⏳ (installing).
2. The wizard checks each dependency in order. For the first missing item, it shows a description of what it does and an **[Install]** button.
3. Clicking **[Install]** runs the appropriate install command. Output is shown in a scrollable log area within the wizard.
4. After successful install, the status updates to ✅ and the wizard advances to the next item.
5. A **[Skip]** button is available for each step (the app will work for non-YouTube sources without the PO token chain, and without cookies for public content).
6. Once all critical dependencies are present (at minimum: ffmpeg + yt-dlp), the wizard closes.
The wizard also runs a quick self-check on subsequent launches. If everything is present, it's invisible. If something was uninstalled or broken, it resurfaces.
## Cookie / Authentication
YouTube requires session cookies for private, members-only, and age-restricted content. Even for public content, cookies can help avoid rate limiting and bot detection.
### Cookie Source Configuration
In the Preferences panel, the user configures their cookie source:
- **Default: Firefox** (`--cookies-from-browser firefox`). Firefox is the only browser where `--cookies-from-browser` reliably works — Chromium browsers (Chrome 127+) encrypt cookies with a process-bound key that yt-dlp can't decrypt.
- **Fallback: cookies.txt file** — User points to a Netscape-format cookies.txt file (exported via a browser extension). Useful if Firefox isn't available.
- **None** — No cookies. Works for public, non-restricted content on non-YouTube sites.
If the user selects Firefox but Firefox isn't installed, the app shows a note: "Firefox is recommended for reliable YouTube authentication. Alternatively, import a cookies.txt file."
### Integration with yt-dlp
Every yt-dlp invocation (metadata, stream URL, download) includes the configured cookie flag:
- Firefox: `--cookies-from-browser firefox`
- cookies.txt: `--cookies /path/to/cookies.txt`
- None: no cookie flag
## Waveform Visualization
### Generation
After the background download completes, the `WaveformGenerator` backend component extracts audio peak data:
1. Run ffmpeg to decode the audio track and compute amplitude peaks at a fixed sample count (~8000 samples for the full video duration).
2. Normalize peaks to a 0.0–1.0 range.
3. Send the peak array to the frontend via a Tauri event.
The exact ffmpeg pipeline: decode audio → resample to a low rate matching the desired sample count → extract absolute amplitude values → normalize.
### Display
- **Before download completes:** Static placeholder — a subtle striped gradient bar with a centered "Generating waveform…" label. Clearly communicates that data is coming.
- **After extraction:** Replace the placeholder with the real waveform, rendered on the Canvas as a filled amplitude graph. Smooth fade-in transition.
- The waveform responds to zoom: at higher zoom levels, more detail is visible. At lower zoom levels, peaks are aggregated (max value per visible pixel).
## Thumbnail Strip
### Generation
After the background download completes, the `ThumbnailExtractor` backend component extracts video frames:
1. Compute the extraction interval based on video duration:
- Short videos (<10 min): 1 frame per second
- Medium videos (10–60 min): 1 frame per 2 seconds
- Long videos (>60 min): 1 frame per 5 seconds
2. Run ffmpeg to extract frames at the computed interval, scaled to 160×90px (16:9).
3. Pack extracted frames into sprite sheets (e.g. 10×10 grids = 100 thumbnails per sheet) for efficient loading and rendering.
4. Send sprite sheet metadata (file paths, dimensions, interval) to the frontend via a Tauri event.
### Display
- **Before download completes:** Static placeholder — a subtle gradient bar with "Generating thumbnails…" label. Matches the waveform placeholder style.
- **After extraction:** Replace the placeholder with real thumbnails, drawn from sprite sheets onto the Canvas.
- **Adaptive density:** The timeline shows ~1 thumbnail per 100px of visible width. Zooming in reveals more granular frames (showing frames closer together in time); zooming out shows fewer (skipping intermediate frames). The extraction provides enough frames to support all reasonable zoom levels; the Canvas renderer picks which frames to display based on the current visible time range and pixel width.
## Edge Cases
| Scenario | Behavior |
|---|---|
| Stream URL expires during preview | Detect playback error on `<video>`, re-resolve via yt-dlp, resume at same position. |
| Age-restricted / auth-required video | If no cookie source is configured, show error: "This video requires authentication. Configure cookies in Preferences." If cookies are configured but still fail, show the yt-dlp error with guidance. |
| Overlapping clips | Allowed. Each clip is independent. In merged export, clips are sorted by start time. If clip B starts before clip A ends, clip B's start is trimmed to clip A's end to avoid duplicate frames. |
| Very long video (>2 hours) | No special limits. Timeline zoom handles precision. |
| Any dependency missing on startup | Setup Wizard appears, walking user through installing each missing dependency step by step. See Dependency Chain section. |
| PO Token provider not working | yt-dlp may still work for some content without the PO token, but YouTube will likely block playback. Show a warning: "YouTube access may be limited. Check the PO Token provider in Preferences." |
| Waveform/thumbnail generation fails | Non-fatal. Log the ffmpeg error, keep the placeholder visible with an unobtrusive "Failed to generate" message. All other features continue working. |
| Export fails mid-way | Show ffmpeg error output in a dismissable panel. Partial files cleaned up. |
| Non-YouTube URL | yt-dlp supports 1000+ sites. App tries any URL. Shows "Unsupported URL" if yt-dlp fails. |
| Network disconnection during download | Download pauses. App shows "Network unavailable" status. Resumes automatically when connection returns (yt-dlp handles resume via `-c`). |
| App closed with active download | Clean up temp files. No persistence of partial downloads between sessions. |
## Future Features (Not in MVP)
These are explicitly out of scope for the initial version but should be kept in mind during architecture so they don't require rewrites:
- **Local file support** — Drag & drop a local `.mp4`/`.mkv`/`.webm` file. Skip the yt-dlp step, go straight to playback + timeline.
- **Video quality/resolution selection** — After URL resolution, show available formats and let the user pick (720p, 1080p, 4K, etc.) before downloading.
- **Save/load clip projects** — Persist clip data (URL, timestamps, labels) to a project file. Reopen later to continue editing or re-export.
- **Batch processing** — Queue multiple URLs, each with their own clips.
- **Custom ffmpeg flags** — Power-user option to pass arbitrary ffmpeg arguments during export.
- **Cross-platform** — Tauri supports Windows/Linux. The architecture doesn't preclude this, but macOS is the target for MVP.
- **Additional cookie browsers** — Support `--cookies-from-browser` for browsers beyond Firefox (if Chromium cookie encryption issues are resolved upstream).
- **Progressive waveform** — Build waveform data in real-time via Web Audio API as the stream plays, rather than waiting for download.