Add FPS counter, queue improvements, Compare fixes, and comprehensive documentation
Features: - FPS counter in conversion status showing real-time encoding speed - Job queue now displays FPS, encoding speed (e.g., "1.2x"), and ETA for running conversions - Copy Comparison button exports side-by-side metadata comparison report - Auto-compare checkbox in Convert module - automatically loads Compare view after conversion - Convert Now properly adds job to queue and displays in Job Queue with live stats - Module badge colors in job queue now match main menu tile colors - Fixed fullscreen compare window sizing (reduced player dimensions to prevent overflow) Bug Fixes: - Fixed queue state management - only one job runs at a time (prevents multiple jobs showing "running") - Fixed Compare module slot assignment - single video drops now fill empty slot instead of overwriting - Fixed job queue scroll rubber banding (no longer jumps back to top) - Enhanced crop detection validation for WMV/AVI formats with dimension clamping and bounds checking Documentation: - VT_Player integration notes with API requirements for keyframing and trim features - LosslessCut feature analysis for Trim module inspiration - Video metadata guide covering MP4/MKV custom fields and NFO generation - Trim module design specification - Compare fullscreen mode documentation - Updated VIDEO_PLAYER_FORK.md to mark fork as completed Technical Changes: - Added state tracking for FPS, speed, and ETA (main.go:197-199) - Enhanced queue processJobs() to check for running jobs before starting new ones - Improved Compare module drag-and-drop logic with smart slot assignment (both code paths) - Added deferred scroll position restoration to prevent UI jumping - Job queue Config map now carries conversion stats for display 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
815319b3f5
commit
5b8fc452af
150
docs/COMPARE_FULLSCREEN.md
Normal file
150
docs/COMPARE_FULLSCREEN.md
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# Compare Module - Fullscreen Mode
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The Compare module now includes a **Fullscreen Compare** mode that displays two videos side-by-side in a larger view, optimized for detailed visual comparison.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Current (v0.1)
|
||||||
|
- ✅ Side-by-side fullscreen layout
|
||||||
|
- ✅ Larger video players for better visibility
|
||||||
|
- ✅ Individual playback controls for each video
|
||||||
|
- ✅ File labels showing video names
|
||||||
|
- ✅ Back button to return to regular Compare view
|
||||||
|
- ✅ Pink colored header/footer matching Compare module
|
||||||
|
|
||||||
|
### Planned (Future - requires VT_Player enhancements)
|
||||||
|
- ⏳ **Synchronized playback** - Play/Pause both videos simultaneously
|
||||||
|
- ⏳ **Linked seeking** - Seek to same timestamp in both videos
|
||||||
|
- ⏳ **Frame-by-frame sync** - Step through both videos in lockstep
|
||||||
|
- ⏳ **Volume link** - Adjust volume on both players together
|
||||||
|
- ⏳ **Playback speed sync** - Change speed on both players at once
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Accessing Fullscreen Mode
|
||||||
|
1. Load two videos in the Compare module
|
||||||
|
2. Click the **"Fullscreen Compare"** button
|
||||||
|
3. Videos will display side-by-side in larger players
|
||||||
|
|
||||||
|
### Controls
|
||||||
|
- **Individual players**: Each video has its own play/pause/seek controls
|
||||||
|
- **"Play Both" button**: Placeholder for future synchronized playback
|
||||||
|
- **"Pause Both" button**: Placeholder for future synchronized pause
|
||||||
|
- **"< BACK TO COMPARE"**: Return to regular Compare view
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Visual Quality Comparison
|
||||||
|
Compare encoding settings or compression quality:
|
||||||
|
- Original vs. compressed
|
||||||
|
- Different codec outputs
|
||||||
|
- Before/after color grading
|
||||||
|
- Different resolution scaling
|
||||||
|
|
||||||
|
### Frame-Accurate Comparison
|
||||||
|
When VT_Player sync is implemented:
|
||||||
|
- Compare edits side-by-side
|
||||||
|
- Check for sync issues in re-encodes
|
||||||
|
- Validate frame-accurate cuts
|
||||||
|
- Compare different filter applications
|
||||||
|
|
||||||
|
### A/B Testing
|
||||||
|
Test different processing settings:
|
||||||
|
- Different deinterlacing methods
|
||||||
|
- Upscaling algorithms
|
||||||
|
- Noise reduction levels
|
||||||
|
- Color correction approaches
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- Uses standard `buildVideoPane()` for each side
|
||||||
|
- 640x360 minimum player size (scales with window)
|
||||||
|
- Independent playback state per video
|
||||||
|
- No shared controls between players yet
|
||||||
|
|
||||||
|
### VT_Player API Requirements for Sync
|
||||||
|
For synchronized playback, VT_Player will need:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Playback state access
|
||||||
|
player.IsPlaying() bool
|
||||||
|
player.GetPosition() time.Duration
|
||||||
|
|
||||||
|
// Event callbacks
|
||||||
|
player.OnPlaybackStateChanged(callback func(playing bool))
|
||||||
|
player.OnPositionChanged(callback func(position time.Duration))
|
||||||
|
|
||||||
|
// Synchronized control
|
||||||
|
player.SyncWith(otherPlayer *Player)
|
||||||
|
player.Unsync()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synchronization Strategy
|
||||||
|
When VT_Player supports it:
|
||||||
|
1. **Master-Slave Pattern**: One player is master, other follows
|
||||||
|
2. **Linked Events**: Play/pause/seek events trigger on both
|
||||||
|
3. **Position Polling**: Periodically check for drift and correct
|
||||||
|
4. **Frame-Accurate Sync**: Step both players frame-by-frame together
|
||||||
|
|
||||||
|
## Keyboard Shortcuts (Planned)
|
||||||
|
When implemented in VT_Player:
|
||||||
|
- `Space` - Play/Pause both videos
|
||||||
|
- `J` / `L` - Rewind/Forward both videos
|
||||||
|
- `←` / `→` - Step both videos frame-by-frame
|
||||||
|
- `K` - Pause both videos
|
||||||
|
- `0-9` - Seek to percentage (0% to 90%) in both
|
||||||
|
- `Esc` - Exit fullscreen mode
|
||||||
|
|
||||||
|
## UI Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ < BACK TO COMPARE │ ← Pink header
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Side-by-side fullscreen comparison. Use individual... │
|
||||||
|
│ │
|
||||||
|
│ [▶ Play Both] [⏸ Pause Both] │
|
||||||
|
│ ───────────────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────┬─────────────────────────────┐ │
|
||||||
|
│ │ File 1: video1.mp4 │ File 2: video2.mp4 │ │
|
||||||
|
│ ├─────────────────────────┼─────────────────────────────┤ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ Video Player 1 │ Video Player 2 │ │
|
||||||
|
│ │ (640x360 min) │ (640x360 min) │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ [Play] [Pause] [Seek] │ [Play] [Pause] [Seek] │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └─────────────────────────┴─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
← Pink footer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### v0.2 - Synchronized Playback
|
||||||
|
- Implement master-slave sync between players
|
||||||
|
- Add "Link" toggle button to enable/disable sync
|
||||||
|
- Visual indicator when players are synced
|
||||||
|
|
||||||
|
### v0.3 - Advanced Sync
|
||||||
|
- Offset compensation (e.g., if videos start at different times)
|
||||||
|
- Manual sync adjustment (nudge one video forward/back)
|
||||||
|
- Sync validation indicator (shows if videos are in sync)
|
||||||
|
|
||||||
|
### v0.4 - Comparison Tools
|
||||||
|
- Split-screen view with adjustable divider
|
||||||
|
- A/B quick toggle (show only one at a time)
|
||||||
|
- Difference overlay (highlight changed regions)
|
||||||
|
- Frame difference metrics display
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Fullscreen mode is accessible from regular Compare view
|
||||||
|
- Videos must be loaded before entering fullscreen mode
|
||||||
|
- Synchronized controls are placeholders until VT_Player API is enhanced
|
||||||
|
- Window can be resized freely - players will scale
|
||||||
|
- Each player maintains independent state for now
|
||||||
460
docs/LOSSLESSCUT_INSPIRATION.md
Normal file
460
docs/LOSSLESSCUT_INSPIRATION.md
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
# LosslessCut Features - Inspiration for VideoTools Trim Module
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
LosslessCut is a mature, feature-rich video trimming application built on Electron/React with FFmpeg backend. This document extracts key features and UX patterns that should inspire VideoTools' Trim module development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Core Trim Features to Adopt
|
||||||
|
|
||||||
|
### 1. **Segment-Based Editing** ⭐⭐⭐ (HIGHEST PRIORITY)
|
||||||
|
LosslessCut uses "segments" as first-class citizens rather than simple In/Out points.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
- Each segment has: start time, end time (optional), label, tags, and segment number
|
||||||
|
- Multiple segments can exist on timeline simultaneously
|
||||||
|
- Segments without end time = "markers" (vertical lines on timeline)
|
||||||
|
- Segments can be reordered by drag-drop in segment list
|
||||||
|
|
||||||
|
**Benefits for VideoTools:**
|
||||||
|
- User can mark multiple trim regions in one session
|
||||||
|
- Export all segments at once (batch trim)
|
||||||
|
- Save/load trim projects for later refinement
|
||||||
|
- More flexible than single In/Out point workflow
|
||||||
|
|
||||||
|
**Implementation priority:** HIGH
|
||||||
|
- Start with single segment (In/Out points)
|
||||||
|
- Phase 2: Add multiple segments support
|
||||||
|
- Phase 3: Add segment labels/tags
|
||||||
|
|
||||||
|
**Example workflow:**
|
||||||
|
```
|
||||||
|
1. User loads video
|
||||||
|
2. Finds first good section: 0:30 to 1:45 → Press I, seek, press O → Segment 1 created
|
||||||
|
3. Press + to add new segment
|
||||||
|
4. Finds second section: 3:20 to 5:10 → Segment 2 created
|
||||||
|
5. Export → Creates 2 output files (or 1 merged file if mode set)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Keyboard-First Workflow** ⭐⭐⭐ (HIGHEST PRIORITY)
|
||||||
|
LosslessCut is designed for speed via keyboard shortcuts.
|
||||||
|
|
||||||
|
**Essential shortcuts:**
|
||||||
|
| Key | Action | Notes |
|
||||||
|
|-----|--------|-------|
|
||||||
|
| `SPACE` | Play/Pause | Standard |
|
||||||
|
| `I` | Set segment In point | Industry standard (Adobe, FCP) |
|
||||||
|
| `O` | Set segment Out point | Industry standard |
|
||||||
|
| `←` / `→` | Seek backward/forward | Frame or keyframe stepping |
|
||||||
|
| `,` / `.` | Frame step | Precise frame-by-frame (1 frame) |
|
||||||
|
| `+` | Add new segment | Quick workflow |
|
||||||
|
| `B` | Split segment at cursor | Divide segment into two |
|
||||||
|
| `BACKSPACE` | Delete segment/cutpoint | Quick removal |
|
||||||
|
| `E` | Export | Fast export shortcut |
|
||||||
|
| `C` | Capture screenshot | Snapshot current frame |
|
||||||
|
| Mouse wheel | Seek timeline | Smooth scrubbing |
|
||||||
|
|
||||||
|
**Why keyboard shortcuts matter:**
|
||||||
|
- Professional users edit faster with keyboard
|
||||||
|
- Reduces mouse movement fatigue
|
||||||
|
- Enables "flow state" editing
|
||||||
|
- Standard shortcuts reduce learning curve
|
||||||
|
|
||||||
|
**Implementation for VideoTools:**
|
||||||
|
- Integrate keyboard handling into VT_Player
|
||||||
|
- Show keyboard shortcut overlay (SHIFT+/)
|
||||||
|
- Allow user customization later
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Timeline Zoom** ⭐⭐⭐ (HIGH PRIORITY)
|
||||||
|
Timeline can zoom in/out for precision editing.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
- Zoom slider or mouse wheel on timeline
|
||||||
|
- Zoomed view shows: thumbnails, waveform, keyframes
|
||||||
|
- Timeline scrolls horizontally when zoomed
|
||||||
|
- Zoom follows playhead (keeps current position centered)
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Find exact cut points in long videos
|
||||||
|
- Frame-accurate editing even in 2-hour files
|
||||||
|
- See waveform detail for audio-based cuts
|
||||||
|
|
||||||
|
**Implementation notes:**
|
||||||
|
- VT_Player needs horizontal scrolling timeline widget
|
||||||
|
- Zoom level: 1x (full video) to 100x (extreme detail)
|
||||||
|
- Auto-scroll to keep playhead in view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Waveform Display** ⭐⭐ (MEDIUM PRIORITY)
|
||||||
|
Audio waveform shown on timeline for visual reference.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Shows amplitude over time
|
||||||
|
- Useful for finding speech/silence boundaries
|
||||||
|
- Click waveform to seek
|
||||||
|
- Updates as timeline zooms
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- Trim silence from beginning/end
|
||||||
|
- Find exact start of dialogue
|
||||||
|
- Cut between sentences
|
||||||
|
- Detect audio glitches
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- FFmpeg can generate waveform images: `ffmpeg -i input.mp4 -filter_complex showwavespic output.png`
|
||||||
|
- Display as timeline background
|
||||||
|
- Optional feature (enable/disable)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. **Keyframe Visualization** ⭐⭐ (MEDIUM PRIORITY)
|
||||||
|
Timeline shows video keyframes (I-frames) as markers.
|
||||||
|
|
||||||
|
**Why keyframes matter:**
|
||||||
|
- Lossless copy (`-c copy`) only cuts at keyframes
|
||||||
|
- Cutting between keyframes requires re-encode
|
||||||
|
- Users need visual feedback on keyframe positions
|
||||||
|
|
||||||
|
**How LosslessCut handles it:**
|
||||||
|
- Vertical lines on timeline = keyframes
|
||||||
|
- Color-coded: bright = keyframe, dim = P/B frame
|
||||||
|
- "Smart cut" mode: cuts at keyframe + re-encodes small section
|
||||||
|
|
||||||
|
**Implementation for VideoTools:**
|
||||||
|
- Probe keyframes: `ffprobe -select_streams v -show_frames -show_entries frame=pict_type,pts_time`
|
||||||
|
- Display on timeline
|
||||||
|
- Warn user if cut point not on keyframe (when using `-c copy`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. **Invert Cut Mode** ⭐⭐ (MEDIUM PRIORITY)
|
||||||
|
Yin-yang toggle: Keep segments vs. Remove segments
|
||||||
|
|
||||||
|
**Two modes:**
|
||||||
|
1. **Keep mode** (default): Export marked segments, discard rest
|
||||||
|
2. **Cut mode** (inverted): Remove marked segments, keep rest
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
Video: [────────────────────]
|
||||||
|
Segments: [ SEG1 ] [ SEG2 ]
|
||||||
|
|
||||||
|
Keep mode → Output: SEG1.mp4, SEG2.mp4
|
||||||
|
Cut mode → Output: parts between segments (commercials removed)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- **Keep**: Extract highlights from long recording
|
||||||
|
- **Cut**: Remove commercials from TV recording
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- Simple boolean toggle in UI
|
||||||
|
- Changes FFmpeg command logic
|
||||||
|
- Useful for both workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. **Merge Mode** ⭐⭐ (MEDIUM PRIORITY)
|
||||||
|
Option to merge multiple segments into single output file.
|
||||||
|
|
||||||
|
**Export options:**
|
||||||
|
- **Separate files**: Each segment → separate file
|
||||||
|
- **Merge cuts**: All segments → 1 merged file
|
||||||
|
- **Merge + separate**: Both outputs
|
||||||
|
|
||||||
|
**FFmpeg technique:**
|
||||||
|
```bash
|
||||||
|
# Create concat file listing segments
|
||||||
|
echo "file 'segment1.mp4'" > concat.txt
|
||||||
|
echo "file 'segment2.mp4'" >> concat.txt
|
||||||
|
|
||||||
|
# Merge with concat demuxer
|
||||||
|
ffmpeg -f concat -safe 0 -i concat.txt -c copy merged.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- UI toggle: "Merge segments"
|
||||||
|
- Temp directory for segment exports
|
||||||
|
- Concat demuxer for lossless merge
|
||||||
|
- Clean up temp files after
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. **Manual Timecode Entry** ⭐ (LOW PRIORITY)
|
||||||
|
Type exact timestamps instead of scrubbing.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Click In/Out time → text input appears
|
||||||
|
- Type: `1:23:45.123` or `83.456`
|
||||||
|
- Formats: HH:MM:SS.mmm, MM:SS, seconds
|
||||||
|
- Paste timestamps from clipboard
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- User has exact timestamps from notes
|
||||||
|
- Import cut times from CSV/spreadsheet
|
||||||
|
- Frame-accurate entry (1:23:45.033)
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- Text input next to In/Out displays
|
||||||
|
- Parse various time formats
|
||||||
|
- Validate against video duration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. **Project Files (.llc)** ⭐ (LOW PRIORITY - FUTURE)
|
||||||
|
Save segments to file, resume editing later.
|
||||||
|
|
||||||
|
**LosslessCut project format (JSON5):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"cutSegments": [
|
||||||
|
{
|
||||||
|
"start": 30.5,
|
||||||
|
"end": 105.3,
|
||||||
|
"name": "Opening scene",
|
||||||
|
"tags": { "category": "intro" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": 180.0,
|
||||||
|
"end": 245.7,
|
||||||
|
"name": "Action sequence"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Resume trim session after closing app
|
||||||
|
- Share trim points with team
|
||||||
|
- Version control trim decisions
|
||||||
|
|
||||||
|
**Implementation (later):**
|
||||||
|
- Simple JSON format
|
||||||
|
- Save/load from File menu
|
||||||
|
- Auto-save to temp on changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UX Patterns to Adopt
|
||||||
|
|
||||||
|
### 1. **Timeline Interaction Model**
|
||||||
|
- Click timeline → seek to position
|
||||||
|
- Drag timeline → scrub (live preview)
|
||||||
|
- Mouse wheel → seek forward/backward
|
||||||
|
- Shift+wheel → zoom timeline
|
||||||
|
- Right-click → context menu (set In/Out, add segment, etc.)
|
||||||
|
|
||||||
|
### 2. **Visual Feedback**
|
||||||
|
- **Current time indicator**: Vertical line with triangular markers (top/bottom)
|
||||||
|
- **Segment visualization**: Colored rectangles on timeline
|
||||||
|
- **Hover preview**: Show timestamp on hover
|
||||||
|
- **Segment labels**: Display segment names on timeline
|
||||||
|
|
||||||
|
### 3. **Segment List Panel**
|
||||||
|
LosslessCut shows sidebar with all segments:
|
||||||
|
```
|
||||||
|
┌─ Segments ─────────────────┐
|
||||||
|
│ 1. [00:30 - 01:45] Intro │ ← Selected
|
||||||
|
│ 2. [03:20 - 05:10] Action │
|
||||||
|
│ 3. [07:00 - 09:30] Ending │
|
||||||
|
└────────────────────────────┘
|
||||||
|
```
|
||||||
|
**Features:**
|
||||||
|
- Click segment → select & seek to start
|
||||||
|
- Drag to reorder
|
||||||
|
- Right-click for options (rename, delete, duplicate)
|
||||||
|
|
||||||
|
### 4. **Export Preview Dialog**
|
||||||
|
Before final export, show summary:
|
||||||
|
```
|
||||||
|
┌─ Export Preview ──────────────────────────┐
|
||||||
|
│ Export mode: Separate files │
|
||||||
|
│ Output format: MP4 (same as source) │
|
||||||
|
│ Keyframe mode: Smart cut │
|
||||||
|
│ │
|
||||||
|
│ Segments to export: │
|
||||||
|
│ 1. Intro.mp4 (0:30 - 1:45) → 1.25 min │
|
||||||
|
│ 2. Action.mp4 (3:20 - 5:10) → 1.83 min │
|
||||||
|
│ 3. Ending.mp4 (7:00 - 9:30) → 2.50 min │
|
||||||
|
│ │
|
||||||
|
│ Total output size: ~125 MB │
|
||||||
|
│ │
|
||||||
|
│ [Cancel] [Export] │
|
||||||
|
└───────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Advanced Features (Future Inspiration)
|
||||||
|
|
||||||
|
### 1. **Scene Detection**
|
||||||
|
Auto-create segments at scene changes.
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mp4 -filter_complex \
|
||||||
|
"select='gt(scene,0.4)',metadata=print:file=scenes.txt" \
|
||||||
|
-f null -
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Silence Detection**
|
||||||
|
Auto-trim silent sections.
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mp4 -af silencedetect=noise=-30dB:d=0.5 -f null -
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Black Screen Detection**
|
||||||
|
Find and remove black sections.
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mp4 -vf blackdetect=d=0.5:pix_th=0.10 -f null -
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Chapter Import/Export**
|
||||||
|
- Load MKV/MP4 chapters as segments
|
||||||
|
- Export segments as chapter markers
|
||||||
|
- Useful for DVD/Blu-ray rips
|
||||||
|
|
||||||
|
### 5. **Thumbnail Scrubbing**
|
||||||
|
- Generate thumbnail strip
|
||||||
|
- Show preview on timeline hover
|
||||||
|
- Faster visual navigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Implementation Roadmap for VideoTools
|
||||||
|
|
||||||
|
### Phase 1: Essential Trim (Week 1-2)
|
||||||
|
**Goal:** Basic usable trim functionality
|
||||||
|
- ✅ VT_Player keyframing API (In/Out points)
|
||||||
|
- ✅ Keyboard shortcuts (I, O, Space, ←/→)
|
||||||
|
- ✅ Timeline markers visualization
|
||||||
|
- ✅ Single segment export
|
||||||
|
- ✅ Keep/Cut mode toggle
|
||||||
|
|
||||||
|
### Phase 2: Professional Workflow (Week 3-4)
|
||||||
|
**Goal:** Multi-segment editing
|
||||||
|
- Multiple segments support
|
||||||
|
- Segment list panel
|
||||||
|
- Drag-to-reorder segments
|
||||||
|
- Merge mode
|
||||||
|
- Timeline zoom
|
||||||
|
|
||||||
|
### Phase 3: Visual Enhancements (Week 5-6)
|
||||||
|
**Goal:** Precision editing
|
||||||
|
- Waveform display
|
||||||
|
- Keyframe visualization
|
||||||
|
- Frame-accurate stepping
|
||||||
|
- Manual timecode entry
|
||||||
|
|
||||||
|
### Phase 4: Advanced Features (Week 7+)
|
||||||
|
**Goal:** Power user tools
|
||||||
|
- Project save/load
|
||||||
|
- Scene detection
|
||||||
|
- Silence detection
|
||||||
|
- Export presets
|
||||||
|
- Batch processing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 Key Lessons from LosslessCut
|
||||||
|
|
||||||
|
### 1. **Start Simple, Scale Later**
|
||||||
|
LosslessCut began with basic trim, added features over time. Don't over-engineer initial release.
|
||||||
|
|
||||||
|
### 2. **Keyboard Shortcuts are Critical**
|
||||||
|
Professional users demand keyboard efficiency. Design around keyboard-first workflow.
|
||||||
|
|
||||||
|
### 3. **Visual Feedback Matters**
|
||||||
|
Users need to SEE what they're doing:
|
||||||
|
- Timeline markers
|
||||||
|
- Segment rectangles
|
||||||
|
- Waveforms
|
||||||
|
- Keyframes
|
||||||
|
|
||||||
|
### 4. **Lossless is Tricky**
|
||||||
|
Educate users about keyframes, smart cut, and when re-encode is necessary.
|
||||||
|
|
||||||
|
### 5. **FFmpeg Does the Heavy Lifting**
|
||||||
|
LosslessCut is primarily a UI wrapper around FFmpeg. Focus on great UX, let FFmpeg handle processing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 References
|
||||||
|
|
||||||
|
- **LosslessCut GitHub**: https://github.com/mifi/lossless-cut
|
||||||
|
- **Documentation**: `~/tools/lossless-cut/docs.md`
|
||||||
|
- **Source code**: `~/tools/lossless-cut/src/`
|
||||||
|
- **Keyboard shortcuts**: `~/tools/lossless-cut/README.md` (search "keyboard")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 VideoTools-Specific Considerations
|
||||||
|
|
||||||
|
### Advantages VideoTools Has:
|
||||||
|
1. **Native Go + Fyne**: Faster startup, smaller binary than Electron
|
||||||
|
2. **Integrated workflow**: Trim → Convert → Compare in one app
|
||||||
|
3. **Queue system**: Already have batch processing foundation
|
||||||
|
4. **Smart presets**: Leverage existing quality presets
|
||||||
|
|
||||||
|
### Unique Features to Add:
|
||||||
|
1. **Trim + Convert**: Set In/Out, choose quality preset, export in one step
|
||||||
|
2. **Compare integration**: Auto-load trimmed vs. original for verification
|
||||||
|
3. **Batch trim**: Apply same trim offsets to multiple files (e.g., remove first 30s from all)
|
||||||
|
4. **Smart defaults**: Detect intros/outros and suggest trim points
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Action Items for VT_Player Team
|
||||||
|
|
||||||
|
Based on LosslessCut analysis, VT_Player needs:
|
||||||
|
|
||||||
|
### Essential APIs:
|
||||||
|
1. **Keyframe API**
|
||||||
|
```go
|
||||||
|
SetInPoint(time.Duration)
|
||||||
|
SetOutPoint(time.Duration)
|
||||||
|
GetInPoint() (time.Duration, bool)
|
||||||
|
GetOutPoint() (time.Duration, bool)
|
||||||
|
ClearKeyframes()
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Timeline Visualization**
|
||||||
|
- Draw In/Out markers on timeline
|
||||||
|
- Highlight segment region between markers
|
||||||
|
- Support multiple segments (future)
|
||||||
|
|
||||||
|
3. **Keyboard Shortcuts**
|
||||||
|
- I/O for In/Out points
|
||||||
|
- ←/→ for frame stepping
|
||||||
|
- Space for play/pause
|
||||||
|
- Mouse wheel for seek
|
||||||
|
|
||||||
|
4. **Frame Navigation**
|
||||||
|
```go
|
||||||
|
StepForward() // Next frame
|
||||||
|
StepBackward() // Previous frame
|
||||||
|
GetCurrentFrame() int64
|
||||||
|
SeekToFrame(int64)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Timeline Zoom** (Phase 2)
|
||||||
|
```go
|
||||||
|
SetZoomLevel(float64) // 1.0 to 100.0
|
||||||
|
GetZoomLevel() float64
|
||||||
|
ScrollToTime(time.Duration)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reference Implementation:
|
||||||
|
- Study LosslessCut's Timeline.tsx for zoom logic
|
||||||
|
- Study TimelineSeg.tsx for segment visualization
|
||||||
|
- Study useSegments.tsx for segment state management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document created**: 2025-12-04
|
||||||
|
**Source**: LosslessCut v3.x codebase analysis
|
||||||
|
**Next steps**: Share with VT_Player team, begin Phase 1 implementation
|
||||||
169
docs/TRIM_MODULE_DESIGN.md
Normal file
169
docs/TRIM_MODULE_DESIGN.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# Trim Module Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The Trim module allows users to cut portions of video files using visual keyframe markers. Users can set In/Out points on the timeline and preview the trimmed segment before processing.
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### 1. Visual Timeline Editing
|
||||||
|
- Load video with VT_Player
|
||||||
|
- Set **In Point** (start of keep region) - Press `I` or click button
|
||||||
|
- Set **Out Point** (end of keep region) - Press `O` or click button
|
||||||
|
- Visual markers on timeline showing trim region
|
||||||
|
- Scrub through video to find exact frames
|
||||||
|
|
||||||
|
### 2. Keyframe Controls
|
||||||
|
```
|
||||||
|
[In Point] ←────────────────→ [Out Point]
|
||||||
|
0:10 Keep Region 2:45
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Frame-Accurate Navigation
|
||||||
|
- `←` / `→` - Step backward/forward one frame
|
||||||
|
- `Shift+←` / `Shift+→` - Jump 1 second
|
||||||
|
- `I` - Set In Point at current position
|
||||||
|
- `O` - Set Out Point at current position
|
||||||
|
- `Space` - Play/Pause
|
||||||
|
- `C` - Clear all keyframes
|
||||||
|
|
||||||
|
### 4. Multiple Trim Modes
|
||||||
|
|
||||||
|
#### Mode 1: Keep Region (Default)
|
||||||
|
Keep video between In and Out points, discard rest.
|
||||||
|
```
|
||||||
|
Input: [─────IN════════OUT─────]
|
||||||
|
Output: [════════]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mode 2: Cut Region
|
||||||
|
Remove video between In and Out points, keep rest.
|
||||||
|
```
|
||||||
|
Input: [─────IN════════OUT─────]
|
||||||
|
Output: [─────] [─────]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mode 3: Multiple Segments (Advanced)
|
||||||
|
Define multiple keep/cut regions using segment list.
|
||||||
|
|
||||||
|
## UI Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ < TRIM │ ← Cyan header bar
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────┐ │
|
||||||
|
│ │ Video Player (VT_Player) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Timeline with In/Out markers] │ │
|
||||||
|
│ │ ────I═══════════════O──────── │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Play] [Pause] [In] [Out] [Clear] │ │
|
||||||
|
│ └───────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Trim Mode: ○ Keep Region ○ Cut Region │
|
||||||
|
│ │
|
||||||
|
│ In Point: 00:01:23.456 [Set In] [Clear] │
|
||||||
|
│ Out Point: 00:04:56.789 [Set Out] [Clear] │
|
||||||
|
│ Duration: 00:03:33.333 │
|
||||||
|
│ │
|
||||||
|
│ Output Settings: │
|
||||||
|
│ ┌─────────────────────────────────────┐ │
|
||||||
|
│ │ Format: [Same as source ▼] │ │
|
||||||
|
│ │ Re-encode: [ ] Smart copy (fast) │ │
|
||||||
|
│ │ Quality: [Source quality] │ │
|
||||||
|
│ └─────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Preview Trimmed] [Add to Queue] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
← Cyan footer bar
|
||||||
|
```
|
||||||
|
|
||||||
|
## VT_Player API Requirements
|
||||||
|
|
||||||
|
### Required Methods
|
||||||
|
```go
|
||||||
|
// Keyframe management
|
||||||
|
player.SetInPoint(position time.Duration)
|
||||||
|
player.SetOutPoint(position time.Duration)
|
||||||
|
player.GetInPoint() time.Duration
|
||||||
|
player.GetOutPoint() time.Duration
|
||||||
|
player.ClearKeyframes()
|
||||||
|
|
||||||
|
// Frame-accurate navigation
|
||||||
|
player.StepForward() // Advance one frame
|
||||||
|
player.StepBackward() // Go back one frame
|
||||||
|
player.GetCurrentTime() time.Duration
|
||||||
|
player.GetFrameRate() float64
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
player.ShowMarkers(in, out time.Duration) // Draw on timeline
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Events
|
||||||
|
```go
|
||||||
|
// Keyboard shortcuts
|
||||||
|
- OnKeyPress('I') -> Set In Point
|
||||||
|
- OnKeyPress('O') -> Set Out Point
|
||||||
|
- OnKeyPress('→') -> Step Forward
|
||||||
|
- OnKeyPress('←') -> Step Backward
|
||||||
|
- OnKeyPress('Space') -> Play/Pause
|
||||||
|
- OnKeyPress('C') -> Clear Keyframes
|
||||||
|
```
|
||||||
|
|
||||||
|
## FFmpeg Integration
|
||||||
|
|
||||||
|
### Keep Region Mode
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mp4 -ss 00:01:23.456 -to 00:04:56.789 -c copy output.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cut Region Mode (Complex filter)
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mp4 \
|
||||||
|
-filter_complex "[0:v]split[v1][v2]; \
|
||||||
|
[v1]trim=start=0:end=83.456[v1t]; \
|
||||||
|
[v2]trim=start=296.789[v2t]; \
|
||||||
|
[v1t][v2t]concat=n=2:v=1:a=0[outv]" \
|
||||||
|
-map [outv] output.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Smart Copy (Fast)
|
||||||
|
- Use `-c copy` when no re-encoding needed
|
||||||
|
- Only works at keyframe boundaries
|
||||||
|
- Show warning if In/Out not at keyframes
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Load Video** - Drag video onto Trim tile or use Load button
|
||||||
|
2. **Navigate** - Scrub or use keyboard to find start point
|
||||||
|
3. **Set In** - Press `I` or click "Set In" button
|
||||||
|
4. **Find End** - Navigate to end of region to keep
|
||||||
|
5. **Set Out** - Press `O` or click "Set Out" button
|
||||||
|
6. **Preview** - Click "Preview Trimmed" to see result
|
||||||
|
7. **Queue** - Click "Add to Queue" to process
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
### Precision Considerations
|
||||||
|
- Frame-accurate requires seeking to exact frame boundaries
|
||||||
|
- Display timestamps with millisecond precision (HH:MM:SS.mmm)
|
||||||
|
- VT_Player must handle fractional frame positions
|
||||||
|
- Consider GOP (Group of Pictures) boundaries for smart copy
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Preview shouldn't require full re-encode
|
||||||
|
- Show preview using VT_Player with constrained timeline
|
||||||
|
- Cache preview segments for quick playback testing
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
- Multiple trim regions in single operation
|
||||||
|
- Batch trim multiple files with same In/Out offsets
|
||||||
|
- Save trim presets (e.g., "Remove first 30s and last 10s")
|
||||||
|
- Visual waveform for audio-based trimming
|
||||||
|
- Chapter-aware trimming (trim to chapter boundaries)
|
||||||
|
|
||||||
|
## Module Color
|
||||||
|
**Cyan** - #44DDFF (already defined in modulesList)
|
||||||
612
docs/VIDEO_METADATA_GUIDE.md
Normal file
612
docs/VIDEO_METADATA_GUIDE.md
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
# Video Metadata Guide for VideoTools
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide covers adding custom metadata fields to video files, NFO generation, and integration with VideoTools modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Container Format Metadata Capabilities
|
||||||
|
|
||||||
|
### MP4 / MOV (MPEG-4)
|
||||||
|
**Metadata storage:** Atoms in `moov` container
|
||||||
|
|
||||||
|
**Standard iTunes-compatible tags:**
|
||||||
|
```
|
||||||
|
©nam - Title
|
||||||
|
©ART - Artist
|
||||||
|
©alb - Album
|
||||||
|
©day - Year
|
||||||
|
©gen - Genre
|
||||||
|
©cmt - Comment
|
||||||
|
desc - Description
|
||||||
|
©too - Encoding tool
|
||||||
|
©enc - Encoded by
|
||||||
|
cprt - Copyright
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom tags (with proper keys):**
|
||||||
|
```
|
||||||
|
----:com.apple.iTunes:DIRECTOR - Director
|
||||||
|
----:com.apple.iTunes:PERFORMERS - Performers
|
||||||
|
----:com.apple.iTunes:STUDIO - Studio/Production
|
||||||
|
----:com.apple.iTunes:SERIES - Series name
|
||||||
|
----:com.apple.iTunes:SCENE - Scene number
|
||||||
|
----:com.apple.iTunes:CATEGORIES - Categories/Tags
|
||||||
|
```
|
||||||
|
|
||||||
|
**Setting metadata with FFmpeg:**
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mp4 -c copy \
|
||||||
|
-metadata title="Scene Title" \
|
||||||
|
-metadata artist="Performer Name" \
|
||||||
|
-metadata album="Series Name" \
|
||||||
|
-metadata date="2025" \
|
||||||
|
-metadata genre="Category" \
|
||||||
|
-metadata comment="Scene description" \
|
||||||
|
-metadata description="Full scene info" \
|
||||||
|
output.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom fields:**
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mp4 -c copy \
|
||||||
|
-metadata:s:v:0 custom_field="Custom Value" \
|
||||||
|
output.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MKV (Matroska)
|
||||||
|
**Metadata storage:** Tags element (XML-based)
|
||||||
|
|
||||||
|
**Built-in tag support:**
|
||||||
|
```xml
|
||||||
|
<Tags>
|
||||||
|
<Tag>
|
||||||
|
<Simple>
|
||||||
|
<Name>TITLE</Name>
|
||||||
|
<String>Scene Title</String>
|
||||||
|
</Simple>
|
||||||
|
<Simple>
|
||||||
|
<Name>ARTIST</Name>
|
||||||
|
<String>Performer Name</String>
|
||||||
|
</Simple>
|
||||||
|
<Simple>
|
||||||
|
<Name>DIRECTOR</Name>
|
||||||
|
<String>Director Name</String>
|
||||||
|
</Simple>
|
||||||
|
<Simple>
|
||||||
|
<Name>STUDIO</Name>
|
||||||
|
<String>Production Studio</String>
|
||||||
|
</Simple>
|
||||||
|
<!-- Arbitrary custom tags -->
|
||||||
|
<Simple>
|
||||||
|
<Name>PERFORMERS</Name>
|
||||||
|
<String>Performer 1, Performer 2</String>
|
||||||
|
</Simple>
|
||||||
|
<Simple>
|
||||||
|
<Name>SCENE_NUMBER</Name>
|
||||||
|
<String>EP042</String>
|
||||||
|
</Simple>
|
||||||
|
<Simple>
|
||||||
|
<Name>CATEGORIES</Name>
|
||||||
|
<String>Cat1, Cat2, Cat3</String>
|
||||||
|
</Simple>
|
||||||
|
</Tag>
|
||||||
|
</Tags>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Setting metadata with FFmpeg:**
|
||||||
|
```bash
|
||||||
|
ffmpeg -i input.mkv -c copy \
|
||||||
|
-metadata title="Scene Title" \
|
||||||
|
-metadata artist="Performer Name" \
|
||||||
|
-metadata director="Director" \
|
||||||
|
-metadata studio="Studio Name" \
|
||||||
|
output.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
**Advantages of MKV:**
|
||||||
|
- Unlimited custom tags (any key-value pairs)
|
||||||
|
- Can attach files (NFO, images, scripts)
|
||||||
|
- Hierarchical metadata structure
|
||||||
|
- Best for archival/preservation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MOV (QuickTime)
|
||||||
|
Same as MP4 (both use MPEG-4 structure), but QuickTime supports additional proprietary tags.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 NFO File Format
|
||||||
|
|
||||||
|
NFO (Info) files are plain text/XML files that contain detailed metadata. Common in media libraries (Kodi, Plex, etc.).
|
||||||
|
|
||||||
|
### NFO Format for Movies:
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<movie>
|
||||||
|
<title>Scene Title</title>
|
||||||
|
<originaltitle>Original Title</originaltitle>
|
||||||
|
<sorttitle>Sort Title</sorttitle>
|
||||||
|
<year>2025</year>
|
||||||
|
<releasedate>2025-12-04</releasedate>
|
||||||
|
<plot>Scene description and plot summary</plot>
|
||||||
|
<runtime>45</runtime> <!-- minutes -->
|
||||||
|
<studio>Production Studio</studio>
|
||||||
|
<director>Director Name</director>
|
||||||
|
|
||||||
|
<actor>
|
||||||
|
<name>Performer 1</name>
|
||||||
|
<role>Role 1</role>
|
||||||
|
<thumb>path/to/performer1.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
<actor>
|
||||||
|
<name>Performer 2</name>
|
||||||
|
<role>Role 2</role>
|
||||||
|
</actor>
|
||||||
|
|
||||||
|
<genre>Category 1</genre>
|
||||||
|
<genre>Category 2</genre>
|
||||||
|
|
||||||
|
<tag>Tag1</tag>
|
||||||
|
<tag>Tag2</tag>
|
||||||
|
|
||||||
|
<rating>8.5</rating>
|
||||||
|
<userrating>9.0</userrating>
|
||||||
|
|
||||||
|
<fileinfo>
|
||||||
|
<streamdetails>
|
||||||
|
<video>
|
||||||
|
<codec>h264</codec>
|
||||||
|
<width>1920</width>
|
||||||
|
<height>1080</height>
|
||||||
|
<durationinseconds>2700</durationinseconds>
|
||||||
|
<aspect>1.777778</aspect>
|
||||||
|
</video>
|
||||||
|
<audio>
|
||||||
|
<codec>aac</codec>
|
||||||
|
<channels>2</channels>
|
||||||
|
</audio>
|
||||||
|
</streamdetails>
|
||||||
|
</fileinfo>
|
||||||
|
|
||||||
|
<!-- Custom fields -->
|
||||||
|
<series>Series Name</series>
|
||||||
|
<episode>42</episode>
|
||||||
|
<scene_number>EP042</scene_number>
|
||||||
|
</movie>
|
||||||
|
```
|
||||||
|
|
||||||
|
### NFO Format for TV Episodes:
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<episodedetails>
|
||||||
|
<title>Episode Title</title>
|
||||||
|
<showtitle>Series Name</showtitle>
|
||||||
|
<season>1</season>
|
||||||
|
<episode>5</episode>
|
||||||
|
<aired>2025-12-04</aired>
|
||||||
|
<plot>Episode description</plot>
|
||||||
|
<runtime>30</runtime>
|
||||||
|
<director>Director Name</director>
|
||||||
|
|
||||||
|
<actor>
|
||||||
|
<name>Performer 1</name>
|
||||||
|
<role>Character</role>
|
||||||
|
</actor>
|
||||||
|
|
||||||
|
<studio>Production Studio</studio>
|
||||||
|
<rating>8.0</rating>
|
||||||
|
</episodedetails>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ VideoTools Integration Plan
|
||||||
|
|
||||||
|
### Module: **Metadata Editor** (New Module)
|
||||||
|
**Purpose:** Edit video metadata and generate NFO files
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
1. **Load video** → Extract existing metadata
|
||||||
|
2. **Edit fields** → Standard + custom fields
|
||||||
|
3. **NFO generation** → Auto-generate from metadata
|
||||||
|
4. **Embed metadata** → Write back to video file (lossless remux)
|
||||||
|
5. **Batch metadata** → Apply same metadata to multiple files
|
||||||
|
6. **Templates** → Save/load metadata templates
|
||||||
|
|
||||||
|
**UI Layout:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ < METADATA │ ← Purple header
|
||||||
|
├─────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ File: scene_042.mp4 │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Basic Info ──────────────────────────────┐ │
|
||||||
|
│ │ Title: [________________] │ │
|
||||||
|
│ │ Studio: [________________] │ │
|
||||||
|
│ │ Series: [________________] │ │
|
||||||
|
│ │ Scene #: [____] │ │
|
||||||
|
│ │ Date: [2025-12-04] │ │
|
||||||
|
│ │ Duration: 45:23 (auto) │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Performers ────────────────────────────────┐ │
|
||||||
|
│ │ Performer 1: [________________] [X] │ │
|
||||||
|
│ │ Performer 2: [________________] [X] │ │
|
||||||
|
│ │ [+ Add Performer] │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Categories/Tags ──────────────────────────┐ │
|
||||||
|
│ │ [Tag1] [Tag2] [Tag3] [+ Add] │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Description ────────────────────────────────┐ │
|
||||||
|
│ │ [Multiline text area for plot/description] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Custom Fields ────────────────────────────┐ │
|
||||||
|
│ │ Director: [________________] │ │
|
||||||
|
│ │ IMDB ID: [________________] │ │
|
||||||
|
│ │ Custom 1: [________________] │ │
|
||||||
|
│ │ [+ Add Field] │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Generate NFO] [Embed in Video] [Save Template]│
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Implementation Details
|
||||||
|
|
||||||
|
### 1. Reading Metadata
|
||||||
|
**Using FFprobe:**
|
||||||
|
```bash
|
||||||
|
ffprobe -v quiet -print_format json -show_format input.mp4
|
||||||
|
|
||||||
|
# Output includes:
|
||||||
|
{
|
||||||
|
"format": {
|
||||||
|
"filename": "input.mp4",
|
||||||
|
"tags": {
|
||||||
|
"title": "Scene Title",
|
||||||
|
"artist": "Performer Name",
|
||||||
|
"album": "Series Name",
|
||||||
|
"date": "2025",
|
||||||
|
"genre": "Category",
|
||||||
|
"comment": "Description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Go implementation:**
|
||||||
|
```go
|
||||||
|
type VideoMetadata struct {
|
||||||
|
Title string
|
||||||
|
Studio string
|
||||||
|
Series string
|
||||||
|
SceneNumber string
|
||||||
|
Date string
|
||||||
|
Performers []string
|
||||||
|
Director string
|
||||||
|
Categories []string
|
||||||
|
Description string
|
||||||
|
CustomFields map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func probeMetadata(path string) (*VideoMetadata, error) {
|
||||||
|
cmd := exec.Command("ffprobe",
|
||||||
|
"-v", "quiet",
|
||||||
|
"-print_format", "json",
|
||||||
|
"-show_format",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Format struct {
|
||||||
|
Tags map[string]string `json:"tags"`
|
||||||
|
} `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
json.Unmarshal(output, &result)
|
||||||
|
|
||||||
|
metadata := &VideoMetadata{
|
||||||
|
Title: result.Format.Tags["title"],
|
||||||
|
Studio: result.Format.Tags["studio"],
|
||||||
|
Series: result.Format.Tags["album"],
|
||||||
|
Date: result.Format.Tags["date"],
|
||||||
|
Categories: strings.Split(result.Format.Tags["genre"], ", "),
|
||||||
|
Description: result.Format.Tags["comment"],
|
||||||
|
CustomFields: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Writing Metadata
|
||||||
|
**Using FFmpeg (lossless remux):**
|
||||||
|
```go
|
||||||
|
func embedMetadata(inputPath string, metadata *VideoMetadata, outputPath string) error {
|
||||||
|
args := []string{
|
||||||
|
"-i", inputPath,
|
||||||
|
"-c", "copy", // Lossless copy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add standard tags
|
||||||
|
if metadata.Title != "" {
|
||||||
|
args = append(args, "-metadata", fmt.Sprintf("title=%s", metadata.Title))
|
||||||
|
}
|
||||||
|
if metadata.Studio != "" {
|
||||||
|
args = append(args, "-metadata", fmt.Sprintf("studio=%s", metadata.Studio))
|
||||||
|
}
|
||||||
|
if metadata.Series != "" {
|
||||||
|
args = append(args, "-metadata", fmt.Sprintf("album=%s", metadata.Series))
|
||||||
|
}
|
||||||
|
if metadata.Date != "" {
|
||||||
|
args = append(args, "-metadata", fmt.Sprintf("date=%s", metadata.Date))
|
||||||
|
}
|
||||||
|
if len(metadata.Categories) > 0 {
|
||||||
|
args = append(args, "-metadata", fmt.Sprintf("genre=%s", strings.Join(metadata.Categories, ", ")))
|
||||||
|
}
|
||||||
|
if metadata.Description != "" {
|
||||||
|
args = append(args, "-metadata", fmt.Sprintf("comment=%s", metadata.Description))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom fields
|
||||||
|
for key, value := range metadata.CustomFields {
|
||||||
|
args = append(args, "-metadata", fmt.Sprintf("%s=%s", key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, outputPath)
|
||||||
|
|
||||||
|
cmd := exec.Command("ffmpeg", args...)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Generating NFO
|
||||||
|
```go
|
||||||
|
func generateNFO(metadata *VideoMetadata, videoPath string) (string, error) {
|
||||||
|
nfo := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<movie>
|
||||||
|
<title>` + escapeXML(metadata.Title) + `</title>
|
||||||
|
<studio>` + escapeXML(metadata.Studio) + `</studio>
|
||||||
|
<series>` + escapeXML(metadata.Series) + `</series>
|
||||||
|
<year>` + metadata.Date + `</year>
|
||||||
|
<plot>` + escapeXML(metadata.Description) + `</plot>
|
||||||
|
`
|
||||||
|
|
||||||
|
// Add performers
|
||||||
|
for _, performer := range metadata.Performers {
|
||||||
|
nfo += ` <actor>
|
||||||
|
<name>` + escapeXML(performer) + `</name>
|
||||||
|
</actor>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add categories/genres
|
||||||
|
for _, category := range metadata.Categories {
|
||||||
|
nfo += ` <genre>` + escapeXML(category) + `</genre>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom fields
|
||||||
|
for key, value := range metadata.CustomFields {
|
||||||
|
nfo += ` <` + key + `>` + escapeXML(value) + `</` + key + `>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
nfo += `</movie>`
|
||||||
|
|
||||||
|
// Save to file (same name as video + .nfo extension)
|
||||||
|
nfoPath := strings.TrimSuffix(videoPath, filepath.Ext(videoPath)) + ".nfo"
|
||||||
|
return nfoPath, os.WriteFile(nfoPath, []byte(nfo), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeXML(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "&", "&")
|
||||||
|
s = strings.ReplaceAll(s, "<", "<")
|
||||||
|
s = strings.ReplaceAll(s, ">", ">")
|
||||||
|
s = strings.ReplaceAll(s, "\"", """)
|
||||||
|
s = strings.ReplaceAll(s, "'", "'")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Attaching NFO to MKV
|
||||||
|
MKV supports embedded attachments (like NFO files):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Attach NFO file to MKV
|
||||||
|
mkvpropedit video.mkv --add-attachment scene_info.nfo --attachment-mime-type text/plain --attachment-name "scene_info.nfo"
|
||||||
|
|
||||||
|
# Or with FFmpeg (re-mux required)
|
||||||
|
ffmpeg -i input.mkv -i scene_info.nfo -c copy \
|
||||||
|
-attach scene_info.nfo -metadata:s:t:0 mimetype=text/plain \
|
||||||
|
output.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
**Go implementation:**
|
||||||
|
```go
|
||||||
|
func attachNFOtoMKV(mkvPath string, nfoPath string) error {
|
||||||
|
cmd := exec.Command("mkvpropedit", mkvPath,
|
||||||
|
"--add-attachment", nfoPath,
|
||||||
|
"--attachment-mime-type", "text/plain",
|
||||||
|
"--attachment-name", filepath.Base(nfoPath),
|
||||||
|
)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Metadata Templates
|
||||||
|
|
||||||
|
Allow users to save metadata templates for batch processing.
|
||||||
|
|
||||||
|
**Template JSON:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Studio XYZ Default Template",
|
||||||
|
"fields": {
|
||||||
|
"studio": "Studio XYZ",
|
||||||
|
"series": "Series Name",
|
||||||
|
"categories": ["Category1", "Category2"],
|
||||||
|
"custom_fields": {
|
||||||
|
"director": "John Doe",
|
||||||
|
"producer": "Jane Smith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
1. User creates template with common studio/series info
|
||||||
|
2. Load template when editing new video
|
||||||
|
3. Only fill in unique fields (title, performers, date, scene #)
|
||||||
|
4. Batch apply template to multiple files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Use Cases
|
||||||
|
|
||||||
|
### 1. Adult Content Library
|
||||||
|
```
|
||||||
|
Title: "Scene Title"
|
||||||
|
Studio: "Production Studio"
|
||||||
|
Series: "Series Name - Season 2"
|
||||||
|
Scene Number: "EP042"
|
||||||
|
Performers: ["Performer A", "Performer B"]
|
||||||
|
Director: "Director Name"
|
||||||
|
Categories: ["Category1", "Category2", "Category3"]
|
||||||
|
Date: "2025-12-04"
|
||||||
|
Description: "Full scene description and plot"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Personal Video Archive
|
||||||
|
```
|
||||||
|
Title: "Birthday Party 2025"
|
||||||
|
Event: "John's 30th Birthday"
|
||||||
|
Location: "Los Angeles, CA"
|
||||||
|
People: ["John", "Sarah", "Mike", "Emily"]
|
||||||
|
Date: "2025-06-15"
|
||||||
|
Description: "John's surprise birthday party"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Movie Collection
|
||||||
|
```
|
||||||
|
Title: "Movie Title"
|
||||||
|
Original Title: "原題"
|
||||||
|
Director: "Christopher Nolan"
|
||||||
|
Year: "2024"
|
||||||
|
IMDB ID: "tt1234567"
|
||||||
|
Actors: ["Actor 1", "Actor 2"]
|
||||||
|
Genre: ["Sci-Fi", "Thriller"]
|
||||||
|
Rating: "8.5/10"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Integration with Existing Modules
|
||||||
|
|
||||||
|
### Convert Module
|
||||||
|
- **Checkbox**: "Preserve metadata" (default: on)
|
||||||
|
- **Checkbox**: "Copy metadata from source" (default: on)
|
||||||
|
- Allow adding/editing metadata before conversion
|
||||||
|
|
||||||
|
### Inspect Module
|
||||||
|
- **Add tab**: "Metadata" to view/edit metadata
|
||||||
|
- Show both standard and custom fields
|
||||||
|
- Quick edit without re-encoding
|
||||||
|
|
||||||
|
### Compare Module
|
||||||
|
- **Add**: "Compare Metadata" button
|
||||||
|
- Show metadata diff between two files
|
||||||
|
- Highlight differences
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Basic Metadata Support (Week 1)
|
||||||
|
- Read metadata with ffprobe
|
||||||
|
- Display in Inspect module
|
||||||
|
- Edit basic fields (title, artist, date, comment)
|
||||||
|
- Write metadata with FFmpeg (lossless)
|
||||||
|
|
||||||
|
### Phase 2: NFO Generation (Week 2)
|
||||||
|
- NFO file generation
|
||||||
|
- Save alongside video file
|
||||||
|
- Load NFO and populate fields
|
||||||
|
- Template system
|
||||||
|
|
||||||
|
### Phase 3: Advanced Metadata (Week 3)
|
||||||
|
- Custom fields support
|
||||||
|
- Performers list
|
||||||
|
- Categories/tags
|
||||||
|
- Metadata Editor module UI
|
||||||
|
|
||||||
|
### Phase 4: Batch & Templates (Week 4)
|
||||||
|
- Metadata templates
|
||||||
|
- Batch apply to multiple files
|
||||||
|
- MKV attachment support (embed NFO)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 References
|
||||||
|
|
||||||
|
### FFmpeg Metadata Documentation
|
||||||
|
- https://ffmpeg.org/ffmpeg-formats.html#Metadata
|
||||||
|
- https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
||||||
|
|
||||||
|
### NFO Format Standards
|
||||||
|
- Kodi NFO: https://kodi.wiki/view/NFO_files
|
||||||
|
- Plex Agents: https://support.plex.tv/articles/
|
||||||
|
|
||||||
|
### Matroska Tags
|
||||||
|
- https://www.matroska.org/technical/specs/tagging/index.html
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Summary
|
||||||
|
|
||||||
|
**Yes, you can absolutely store custom metadata in video files!**
|
||||||
|
|
||||||
|
**Best format for rich metadata:** MKV (unlimited custom tags + file attachments)
|
||||||
|
|
||||||
|
**Most compatible:** MP4/MOV (iTunes tags work in QuickTime, VLC, etc.)
|
||||||
|
|
||||||
|
**Recommended approach for VideoTools:**
|
||||||
|
1. Support both embedded metadata (in video file) AND sidecar NFO files
|
||||||
|
2. MKV: Embed NFO as attachment + metadata tags
|
||||||
|
3. MP4: Metadata tags + separate .nfo file
|
||||||
|
4. Allow users to choose what metadata to embed
|
||||||
|
5. Generate NFO for media center compatibility (Kodi, Plex, Jellyfin)
|
||||||
|
|
||||||
|
**Next steps:**
|
||||||
|
1. Add basic metadata reading to `probeVideo()` function
|
||||||
|
2. Add metadata display to Inspect module
|
||||||
|
3. Create Metadata Editor module
|
||||||
|
4. Implement NFO generation
|
||||||
|
5. Add metadata templates
|
||||||
|
|
||||||
|
This would be a killer feature for VideoTools! 🚀
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
# Video Player Fork Plan
|
# Video Player Fork Plan
|
||||||
|
|
||||||
|
## Status: COMPLETED ✅
|
||||||
|
**VT_Player has been forked as a separate project for independent development.**
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
The video player component will be extracted into a separate project to allow independent development and improvement of video playback controls while keeping VideoTools focused on video processing.
|
The video player component has been extracted into a separate project (VT_Player) to allow independent development and improvement of video playback controls while keeping VideoTools focused on video processing.
|
||||||
|
|
||||||
## Current Player Integration
|
## Current Player Integration
|
||||||
The player is currently embedded in VideoTools at:
|
The player is used in VideoTools at:
|
||||||
- `internal/player/` - Player implementation
|
- Convert module - Video preview and playback
|
||||||
- `main.go` - Player state and controls in Convert module
|
- Compare module - Side-by-side video comparison (as of dev13)
|
||||||
|
- Inspect module - Single video playback with metadata (as of dev13)
|
||||||
- Preview frame display
|
- Preview frame display
|
||||||
- Playback controls (play/pause, seek, volume)
|
- Playback controls (play/pause, seek, volume)
|
||||||
|
|
||||||
|
|
@ -19,17 +23,20 @@ The player is currently embedded in VideoTools at:
|
||||||
- Can be used by other projects
|
- Can be used by other projects
|
||||||
|
|
||||||
### 2. Improved Controls
|
### 2. Improved Controls
|
||||||
Current limitations to address:
|
Features to develop in VT_Player:
|
||||||
|
- **Keyframing** - Mark in/out points for trimming and chapter creation
|
||||||
- Tighten up video controls
|
- Tighten up video controls
|
||||||
- Better seek bar with thumbnails on hover
|
- Better seek bar with thumbnails on hover
|
||||||
- Improved timeline scrubbing
|
- Improved timeline scrubbing
|
||||||
- Keyboard shortcuts for playback
|
- Keyboard shortcuts for playback
|
||||||
- Frame-accurate stepping
|
- Frame-accurate stepping (←/→ keys for frame-by-frame)
|
||||||
- Playback speed controls
|
- Playback speed controls (0.25x to 2x)
|
||||||
- Better volume control UI
|
- Better volume control UI
|
||||||
|
- Timeline markers for chapters
|
||||||
|
- Visual in/out point indicators
|
||||||
|
|
||||||
### 3. Clean API
|
### 3. Clean API
|
||||||
The forked player should expose a clean API:
|
VT_Player should expose a clean API for VideoTools integration:
|
||||||
```go
|
```go
|
||||||
type Player interface {
|
type Player interface {
|
||||||
Load(path string) error
|
Load(path string) error
|
||||||
|
|
@ -38,36 +45,42 @@ type Player interface {
|
||||||
Seek(position time.Duration)
|
Seek(position time.Duration)
|
||||||
GetFrame(position time.Duration) (image.Image, error)
|
GetFrame(position time.Duration) (image.Image, error)
|
||||||
SetVolume(level float64)
|
SetVolume(level float64)
|
||||||
|
|
||||||
|
// Keyframing support for Trim/Chapter modules
|
||||||
|
SetInPoint(position time.Duration)
|
||||||
|
SetOutPoint(position time.Duration)
|
||||||
|
GetInPoint() time.Duration
|
||||||
|
GetOutPoint() time.Duration
|
||||||
|
ClearKeyframes()
|
||||||
|
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Migration Strategy
|
## VT_Player Development Strategy
|
||||||
|
|
||||||
### Phase 1: Extract to Separate Module
|
### Phase 1: Core Player Features ✅
|
||||||
1. Create new repository: `github.com/yourusername/fyne-videoplayer`
|
- [x] Basic playback controls (play/pause/seek)
|
||||||
2. Copy `internal/player/` to new repo
|
- [x] Volume control
|
||||||
3. Extract player dependencies
|
- [x] Frame preview display
|
||||||
4. Create clean API surface
|
- [x] Integration with VideoTools modules
|
||||||
5. Add comprehensive tests
|
|
||||||
|
|
||||||
### Phase 2: Update VideoTools
|
### Phase 2: Enhanced Controls (Current Focus)
|
||||||
1. Import fyne-videoplayer as dependency
|
Priority features for Trim/Chapter module integration:
|
||||||
2. Replace internal/player with external package
|
- [ ] **Keyframe markers** - Set In/Out points visually on timeline
|
||||||
3. Update player instantiation
|
- [ ] **Frame-accurate stepping** - ←/→ keys for frame-by-frame navigation
|
||||||
4. Verify all playback features work
|
- [ ] **Visual timeline with markers** - Show In/Out points on seek bar
|
||||||
5. Remove old internal/player code
|
- [ ] **Keyboard shortcuts** - I (in), O (out), Space (play/pause), ←/→ (step)
|
||||||
|
- [ ] **Export keyframe data** - Return In/Out timestamps to VideoTools
|
||||||
|
|
||||||
### Phase 3: Enhance Player (Post-Fork)
|
### Phase 3: Advanced Features (Future)
|
||||||
Features to add after fork:
|
|
||||||
- [ ] Thumbnail preview on seek bar hover
|
- [ ] Thumbnail preview on seek bar hover
|
||||||
- [ ] Frame-accurate stepping (←/→ keys)
|
|
||||||
- [ ] Playback speed controls (0.25x to 2x)
|
- [ ] Playback speed controls (0.25x to 2x)
|
||||||
- [ ] Improved volume slider
|
- [ ] Improved volume slider with visual feedback
|
||||||
- [ ] Keyboard shortcuts (Space, K, J, L, etc.)
|
- [ ] Chapter markers on timeline
|
||||||
- [ ] Timeline markers
|
|
||||||
- [ ] Subtitle support
|
- [ ] Subtitle support
|
||||||
- [ ] Multi-audio track switching
|
- [ ] Multi-audio track switching
|
||||||
|
- [ ] Zoom timeline for precision editing
|
||||||
|
|
||||||
## Technical Considerations
|
## Technical Considerations
|
||||||
|
|
||||||
|
|
@ -89,18 +102,37 @@ Player must work on:
|
||||||
- Low CPU usage during playback
|
- Low CPU usage during playback
|
||||||
- Fast seeking
|
- Fast seeking
|
||||||
|
|
||||||
## Timeline
|
## VideoTools Module Integration
|
||||||
1. **Week 1-2**: Extract player code, create repo, clean API
|
|
||||||
2. **Week 3**: Integration testing, update VideoTools
|
### Modules Using VT_Player
|
||||||
3. **Week 4+**: Enhanced controls and features
|
1. **Convert Module** - Preview video before conversion
|
||||||
|
2. **Compare Module** - Side-by-side video playback for comparison
|
||||||
|
3. **Inspect Module** - Single video playback with detailed metadata
|
||||||
|
4. **Trim Module** (planned) - Keyframe-based trimming with In/Out points
|
||||||
|
5. **Chapter Module** (planned) - Mark chapter points on timeline
|
||||||
|
|
||||||
|
### Integration Requirements for Trim/Chapter
|
||||||
|
The Trim and Chapter modules will require:
|
||||||
|
- Keyframe API to set In/Out points
|
||||||
|
- Visual markers on timeline showing trim regions
|
||||||
|
- Frame-accurate seeking for precise cuts
|
||||||
|
- Ability to export timestamp data for FFmpeg commands
|
||||||
|
- Preview of trimmed segment before processing
|
||||||
|
|
||||||
## Benefits
|
## Benefits
|
||||||
- **VideoTools**: Leaner codebase, focus on processing
|
- **VideoTools**: Leaner codebase, focus on video processing
|
||||||
- **Player**: Independent evolution, reusable component
|
- **VT_Player**: Independent evolution, reusable component, dedicated feature development
|
||||||
- **Users**: Better video controls, more reliable playback
|
- **Users**: Professional-grade video controls, precise editing capabilities
|
||||||
- **Developers**: Easier to contribute to either project
|
- **Developers**: Easier to contribute, clear separation of concerns
|
||||||
|
|
||||||
|
## Development Philosophy
|
||||||
|
- **VT_Player**: Focus on playback, navigation, and visual controls
|
||||||
|
- **VideoTools**: Focus on video processing, encoding, and batch operations
|
||||||
|
- Clean API boundary allows independent versioning
|
||||||
|
- VT_Player features can be tested independently before VideoTools integration
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Keep player dependency minimal in VideoTools
|
- VT_Player repo: Separate project with independent development cycle
|
||||||
- Player should be optional - frame display can work without playback
|
- VideoTools will import VT_Player as external dependency
|
||||||
- Consider using player in Compare module for side-by-side playback (future)
|
- Keyframing features are priority for Trim/Chapter module development
|
||||||
|
- Compare module demonstrates successful multi-player integration
|
||||||
|
|
|
||||||
373
docs/VT_PLAYER_INTEGRATION_NOTES.md
Normal file
373
docs/VT_PLAYER_INTEGRATION_NOTES.md
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
# VT_Player Integration Notes for Lead Developer
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
|
||||||
|
**VideoTools Repository**: https://git.leaktechnologies.dev/Leak_Technologies/VideoTools.git
|
||||||
|
**VT_Player**: Forked video player component for independent development
|
||||||
|
|
||||||
|
VT_Player was forked from VideoTools to enable dedicated development of video playback controls and features without impacting the main VideoTools codebase.
|
||||||
|
|
||||||
|
## Current Integration Points
|
||||||
|
|
||||||
|
### VideoTools Modules Using VT_Player
|
||||||
|
|
||||||
|
1. **Convert Module** - Preview video before/during conversion
|
||||||
|
2. **Compare Module** - Side-by-side video comparison (2 players)
|
||||||
|
3. **Inspect Module** - Single video playback with metadata display
|
||||||
|
4. **Compare Fullscreen** - Larger side-by-side view (planned: synchronized playback)
|
||||||
|
|
||||||
|
### Current VT_Player Usage Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
// VideoTools calls buildVideoPane() which creates player
|
||||||
|
videoPane := buildVideoPane(state, fyne.NewSize(320, 180), videoSource, updateCallback)
|
||||||
|
|
||||||
|
// buildVideoPane internally:
|
||||||
|
// - Creates player.Controller
|
||||||
|
// - Sets up playback controls
|
||||||
|
// - Returns fyne.CanvasObject with player UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Priority Features Needed in VT_Player
|
||||||
|
|
||||||
|
### 1. Keyframing API (HIGHEST PRIORITY)
|
||||||
|
**Required for**: Trim Module, Chapter Module
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Proposed API
|
||||||
|
type KeyframeController interface {
|
||||||
|
// Set keyframe markers
|
||||||
|
SetInPoint(position time.Duration) error
|
||||||
|
SetOutPoint(position time.Duration) error
|
||||||
|
ClearInPoint()
|
||||||
|
ClearOutPoint()
|
||||||
|
ClearAllKeyframes()
|
||||||
|
|
||||||
|
// Get keyframe data
|
||||||
|
GetInPoint() (time.Duration, bool) // Returns position and hasInPoint
|
||||||
|
GetOutPoint() (time.Duration, bool)
|
||||||
|
GetSegmentDuration() time.Duration // Duration between In and Out
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
ShowKeyframeMarkers(show bool) // Toggle marker visibility on timeline
|
||||||
|
HighlightSegment(in, out time.Duration) // Highlight region between markers
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case**: User scrubs video, presses `I` to set In point, scrubs to end, presses `O` to set Out point. Visual markers show on timeline. VideoTools reads timestamps for FFmpeg trim command.
|
||||||
|
|
||||||
|
### 2. Frame-Accurate Navigation (HIGH PRIORITY)
|
||||||
|
**Required for**: Trim Module, Compare sync
|
||||||
|
|
||||||
|
```go
|
||||||
|
type FrameNavigationController interface {
|
||||||
|
// Step through video frame-by-frame
|
||||||
|
StepForward() error // Advance exactly 1 frame
|
||||||
|
StepBackward() error // Go back exactly 1 frame
|
||||||
|
|
||||||
|
// Frame info
|
||||||
|
GetCurrentFrame() int64 // Current frame number
|
||||||
|
GetFrameAtTime(time.Duration) int64 // Frame number at timestamp
|
||||||
|
GetTimeAtFrame(int64) time.Duration // Timestamp of frame number
|
||||||
|
GetTotalFrames() int64
|
||||||
|
|
||||||
|
// Seek to exact frame
|
||||||
|
SeekToFrame(frameNum int64) error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case**: User finds exact frame for cut point using arrow keys (←/→), sets In/Out markers precisely.
|
||||||
|
|
||||||
|
### 3. Synchronized Playback API (MEDIUM PRIORITY)
|
||||||
|
**Required for**: Compare Fullscreen, Compare Module sync
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SyncController interface {
|
||||||
|
// Link two players together
|
||||||
|
SyncWith(otherPlayer player.Controller) error
|
||||||
|
Unsync()
|
||||||
|
IsSynced() bool
|
||||||
|
GetSyncMaster() player.Controller
|
||||||
|
|
||||||
|
// Callbacks for sync events
|
||||||
|
OnPlayStateChanged(callback func(playing bool))
|
||||||
|
OnPositionChanged(callback func(position time.Duration))
|
||||||
|
|
||||||
|
// Sync with offset (for videos that don't start at same time)
|
||||||
|
SetSyncOffset(offset time.Duration)
|
||||||
|
GetSyncOffset() time.Duration
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case**: Compare module loads two videos. User clicks "Play Both" button. Both players play in sync. When one player is paused/seeked, other follows.
|
||||||
|
|
||||||
|
### 4. Playback Speed Control (MEDIUM PRIORITY)
|
||||||
|
**Required for**: Trim Module, general UX improvement
|
||||||
|
|
||||||
|
```go
|
||||||
|
type PlaybackSpeedController interface {
|
||||||
|
SetPlaybackSpeed(speed float64) error // 0.25x to 2.0x
|
||||||
|
GetPlaybackSpeed() float64
|
||||||
|
GetSupportedSpeeds() []float64 // [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case**: User slows playback to 0.25x to find exact frame for trim point.
|
||||||
|
|
||||||
|
## Integration Architecture
|
||||||
|
|
||||||
|
### Current Pattern
|
||||||
|
```
|
||||||
|
VideoTools (main.go)
|
||||||
|
└─> buildVideoPane()
|
||||||
|
└─> player.New()
|
||||||
|
└─> player.Controller interface
|
||||||
|
└─> Returns fyne.CanvasObject
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proposed Enhanced Pattern
|
||||||
|
```
|
||||||
|
VideoTools (main.go)
|
||||||
|
└─> buildVideoPane()
|
||||||
|
└─> player.NewEnhanced()
|
||||||
|
├─> player.Controller (basic playback)
|
||||||
|
├─> player.KeyframeController (trim support)
|
||||||
|
├─> player.FrameNavigationController (frame stepping)
|
||||||
|
├─> player.SyncController (multi-player sync)
|
||||||
|
└─> player.PlaybackSpeedController (speed control)
|
||||||
|
└─> Returns fyne.CanvasObject
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
- Keep existing `player.Controller` interface unchanged
|
||||||
|
- Add new optional interfaces
|
||||||
|
- VideoTools checks if player implements enhanced interfaces:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if keyframer, ok := player.(KeyframeController); ok {
|
||||||
|
// Use keyframe features
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Requirements
|
||||||
|
|
||||||
|
### 1. Timeline Visual Enhancements
|
||||||
|
|
||||||
|
Current timeline needs:
|
||||||
|
- **In/Out Point Markers**: Visual indicators (⬇️ symbols or colored bars)
|
||||||
|
- **Segment Highlight**: Show region between In and Out with different color
|
||||||
|
- **Frame Number Display**: Show current frame number alongside timestamp
|
||||||
|
- **Marker Drag Support**: Allow dragging markers to adjust In/Out points
|
||||||
|
|
||||||
|
### 2. Keyboard Shortcuts
|
||||||
|
|
||||||
|
Essential shortcuts for VT_Player:
|
||||||
|
|
||||||
|
| Key | Action | Notes |
|
||||||
|
|-----|--------|-------|
|
||||||
|
| `Space` | Play/Pause | Standard |
|
||||||
|
| `←` | Step backward 1 frame | Frame-accurate |
|
||||||
|
| `→` | Step forward 1 frame | Frame-accurate |
|
||||||
|
| `Shift+←` | Jump back 1 second | Quick navigation |
|
||||||
|
| `Shift+→` | Jump forward 1 second | Quick navigation |
|
||||||
|
| `I` | Set In Point | Trim support |
|
||||||
|
| `O` | Set Out Point | Trim support |
|
||||||
|
| `C` | Clear keyframes | Reset markers |
|
||||||
|
| `K` | Pause | Video editor standard |
|
||||||
|
| `J` | Rewind | Video editor standard |
|
||||||
|
| `L` | Fast forward | Video editor standard |
|
||||||
|
| `0-9` | Seek to % | 0=start, 5=50%, 9=90% |
|
||||||
|
|
||||||
|
### 3. Performance Considerations
|
||||||
|
|
||||||
|
- **Frame stepping**: Must be instant, no lag
|
||||||
|
- **Keyframe display**: Update timeline without stuttering
|
||||||
|
- **Sync**: Maximum 1-frame drift between synced players
|
||||||
|
- **Memory**: Don't load entire video into RAM for frame navigation
|
||||||
|
|
||||||
|
### 4. FFmpeg Integration
|
||||||
|
|
||||||
|
VT_Player should expose frame-accurate timestamps that VideoTools can use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: VideoTools gets In=83.456s, Out=296.789s from VT_Player
|
||||||
|
ffmpeg -ss 83.456 -to 296.789 -i input.mp4 -c copy output.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
Frame-accurate seeking requires:
|
||||||
|
- Seek to nearest keyframe before target
|
||||||
|
- Decode frames until exact target reached
|
||||||
|
- Display correct frame with minimal latency
|
||||||
|
|
||||||
|
## Data Flow Examples
|
||||||
|
|
||||||
|
### Trim Module Workflow
|
||||||
|
```
|
||||||
|
1. User loads video in Trim module
|
||||||
|
2. VideoTools creates VT_Player with keyframe support
|
||||||
|
3. User navigates with arrow keys (VT_Player handles frame stepping)
|
||||||
|
4. User presses 'I' → VT_Player sets In point marker
|
||||||
|
5. User navigates to end point
|
||||||
|
6. User presses 'O' → VT_Player sets Out point marker
|
||||||
|
7. User clicks "Preview Trim" → VT_Player plays segment between markers
|
||||||
|
8. User clicks "Add to Queue"
|
||||||
|
9. VideoTools reads keyframes: in = player.GetInPoint(), out = player.GetOutPoint()
|
||||||
|
10. VideoTools builds FFmpeg command with timestamps
|
||||||
|
11. FFmpeg trims video
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compare Sync Workflow
|
||||||
|
```
|
||||||
|
1. User loads 2 videos in Compare module
|
||||||
|
2. VideoTools creates 2 VT_Player instances
|
||||||
|
3. User clicks "Play Both"
|
||||||
|
4. VideoTools calls: player1.SyncWith(player2)
|
||||||
|
5. VideoTools calls: player1.Play()
|
||||||
|
6. VT_Player automatically plays player2 in sync
|
||||||
|
7. User pauses player1 → VT_Player pauses player2
|
||||||
|
8. User seeks player1 → VT_Player seeks player2 to same position
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
VT_Player should include tests for:
|
||||||
|
|
||||||
|
1. **Keyframe Accuracy**
|
||||||
|
- Set In/Out points, verify exact timestamps returned
|
||||||
|
- Clear markers, verify they're removed
|
||||||
|
- Test edge cases (In > Out, negative times, beyond duration)
|
||||||
|
|
||||||
|
2. **Frame Navigation**
|
||||||
|
- Step forward/backward through entire video
|
||||||
|
- Verify frame numbers are sequential
|
||||||
|
- Test at video start (can't go back) and end (can't go forward)
|
||||||
|
|
||||||
|
3. **Sync Reliability**
|
||||||
|
- Play two videos for 30 seconds, verify max drift < 1 frame
|
||||||
|
- Pause/seek operations propagate correctly
|
||||||
|
- Unsync works properly
|
||||||
|
|
||||||
|
4. **Performance**
|
||||||
|
- Frame step latency < 50ms
|
||||||
|
- Timeline marker updates < 16ms (60fps)
|
||||||
|
- Memory usage stable during long playback sessions
|
||||||
|
|
||||||
|
## Communication Protocol
|
||||||
|
|
||||||
|
### VideoTools → VT_Player
|
||||||
|
|
||||||
|
VideoTools will request features through interface methods:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Example: VideoTools wants to enable trim mode
|
||||||
|
if trimmer, ok := player.(TrimController); ok {
|
||||||
|
trimmer.EnableTrimMode(true)
|
||||||
|
trimmer.OnInPointSet(func(t time.Duration) {
|
||||||
|
// Update VideoTools UI to show In point timestamp
|
||||||
|
})
|
||||||
|
trimmer.OnOutPointSet(func(t time.Duration) {
|
||||||
|
// Update VideoTools UI to show Out point timestamp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VT_Player → VideoTools
|
||||||
|
|
||||||
|
VT_Player communicates state changes through callbacks:
|
||||||
|
|
||||||
|
```go
|
||||||
|
player.OnPlaybackStateChanged(func(playing bool) {
|
||||||
|
// VideoTools updates UI (play button ↔ pause button)
|
||||||
|
})
|
||||||
|
|
||||||
|
player.OnPositionChanged(func(position time.Duration) {
|
||||||
|
// VideoTools updates position display
|
||||||
|
})
|
||||||
|
|
||||||
|
player.OnKeyframeSet(func(markerType string, position time.Duration) {
|
||||||
|
// VideoTools logs keyframe for FFmpeg command
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
### Phase 1: Core API (Immediate)
|
||||||
|
- Define interfaces for keyframe, frame nav, sync
|
||||||
|
- Implement basic keyframe markers (In/Out points)
|
||||||
|
- Add frame stepping (←/→ keys)
|
||||||
|
- Document API for VideoTools integration
|
||||||
|
|
||||||
|
### Phase 2: Visual Enhancements (Week 2)
|
||||||
|
- Enhanced timeline with marker display
|
||||||
|
- Segment highlighting between In/Out
|
||||||
|
- Frame number display
|
||||||
|
- Keyboard shortcuts
|
||||||
|
|
||||||
|
### Phase 3: Sync Features (Week 3)
|
||||||
|
- Implement synchronized playback API
|
||||||
|
- Master-slave pattern for linked players
|
||||||
|
- Offset compensation for non-aligned videos
|
||||||
|
|
||||||
|
### Phase 4: Advanced Features (Week 4+)
|
||||||
|
- Playback speed control
|
||||||
|
- Timeline zoom for precision editing
|
||||||
|
- Thumbnail preview on hover
|
||||||
|
- Chapter markers
|
||||||
|
|
||||||
|
## Notes for VT_Player Developer
|
||||||
|
|
||||||
|
1. **Keep backward compatibility**: Existing VideoTools code using basic player.Controller should continue working
|
||||||
|
|
||||||
|
2. **Frame-accurate is critical**: Trim module requires exact frame positioning. Off-by-one frame errors are unacceptable.
|
||||||
|
|
||||||
|
3. **Performance over features**: Frame stepping must be instant. Users will hold arrow keys to scrub through video.
|
||||||
|
|
||||||
|
4. **Visual feedback matters**: Keyframe markers must be immediately visible. Timeline updates should be smooth.
|
||||||
|
|
||||||
|
5. **Cross-platform testing**: VT_Player must work on Linux (GNOME/X11/Wayland), macOS, and Windows
|
||||||
|
|
||||||
|
6. **FFmpeg integration**: VT_Player doesn't run FFmpeg, but must provide precise timestamps that VideoTools can pass to FFmpeg
|
||||||
|
|
||||||
|
7. **Minimize dependencies**: Keep VT_Player focused on playback/navigation. VideoTools handles video processing.
|
||||||
|
|
||||||
|
## Questions to Consider
|
||||||
|
|
||||||
|
1. **Keyframe storage**: Should keyframes be stored in VT_Player or passed back to VideoTools immediately?
|
||||||
|
|
||||||
|
2. **Sync drift handling**: If synced players drift apart, which one is "correct"? Should we periodically resync?
|
||||||
|
|
||||||
|
3. **Frame stepping during playback**: Can user step frame-by-frame while video is playing, or must they pause first?
|
||||||
|
|
||||||
|
4. **Memory management**: For long videos (hours), how do we efficiently support frame-accurate navigation without excessive memory?
|
||||||
|
|
||||||
|
5. **Hardware acceleration**: Should frame stepping use GPU decoding, or is CPU sufficient for single frames?
|
||||||
|
|
||||||
|
## Current VideoTools Status
|
||||||
|
|
||||||
|
### Working Modules
|
||||||
|
- ✅ Convert - Video conversion with preview
|
||||||
|
- ✅ Compare - Side-by-side comparison (basic)
|
||||||
|
- ✅ Inspect - Single video with metadata
|
||||||
|
- ✅ Compare Fullscreen - Larger view (sync placeholders added)
|
||||||
|
|
||||||
|
### Planned Modules Needing VT_Player Features
|
||||||
|
- ⏳ Trim - **Needs**: Keyframing + frame navigation
|
||||||
|
- ⏳ Chapter - **Needs**: Multiple keyframe markers on timeline
|
||||||
|
- ⏳ Merge - May need synchronized preview of multiple clips
|
||||||
|
|
||||||
|
### Auto-Compare Feature (NEW)
|
||||||
|
- ✅ Checkbox in Convert module: "Compare After"
|
||||||
|
- ✅ After conversion completes, automatically loads:
|
||||||
|
- File 1 (Original) = source video
|
||||||
|
- File 2 (Converted) = output video
|
||||||
|
- ✅ User can immediately inspect conversion quality
|
||||||
|
|
||||||
|
## Contact & Coordination
|
||||||
|
|
||||||
|
For questions about VideoTools integration:
|
||||||
|
- Review this document
|
||||||
|
- Check `/docs/VIDEO_PLAYER_FORK.md` for fork strategy
|
||||||
|
- Check `/docs/TRIM_MODULE_DESIGN.md` for detailed trim module requirements
|
||||||
|
- Check `/docs/COMPARE_FULLSCREEN.md` for sync requirements
|
||||||
|
|
||||||
|
VideoTools will track VT_Player changes and update integration code as new features become available.
|
||||||
|
|
@ -330,6 +330,22 @@ func (q *Queue) processJobs() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if there's already a running job (only process one at a time)
|
||||||
|
hasRunningJob := false
|
||||||
|
for _, job := range q.jobs {
|
||||||
|
if job.Status == JobStatusRunning {
|
||||||
|
hasRunningJob = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a job is already running, wait and check again later
|
||||||
|
if hasRunningJob {
|
||||||
|
q.mu.Unlock()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Find highest priority pending job
|
// Find highest priority pending job
|
||||||
var nextJob *Job
|
var nextJob *Job
|
||||||
highestPriority := -1
|
highestPriority := -1
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
"fyne.io/fyne/v2/canvas"
|
||||||
|
|
@ -49,11 +50,13 @@ func (m *MonoTheme) Size(name fyne.ThemeSizeName) float32 {
|
||||||
// ModuleTile is a clickable tile widget for module selection
|
// ModuleTile is a clickable tile widget for module selection
|
||||||
type ModuleTile struct {
|
type ModuleTile struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
label string
|
label string
|
||||||
color color.Color
|
color color.Color
|
||||||
enabled bool
|
enabled bool
|
||||||
onTapped func()
|
onTapped func()
|
||||||
onDropped func([]fyne.URI)
|
onDropped func([]fyne.URI)
|
||||||
|
flashing bool
|
||||||
|
draggedOver bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewModuleTile creates a new module tile
|
// NewModuleTile creates a new module tile
|
||||||
|
|
@ -72,15 +75,40 @@ func NewModuleTile(label string, col color.Color, enabled bool, tapped func(), d
|
||||||
// DraggedOver implements desktop.Droppable interface
|
// DraggedOver implements desktop.Droppable interface
|
||||||
func (m *ModuleTile) DraggedOver(pos fyne.Position) {
|
func (m *ModuleTile) DraggedOver(pos fyne.Position) {
|
||||||
logging.Debug(logging.CatUI, "DraggedOver tile=%s enabled=%v pos=%v", m.label, m.enabled, pos)
|
logging.Debug(logging.CatUI, "DraggedOver tile=%s enabled=%v pos=%v", m.label, m.enabled, pos)
|
||||||
|
if m.enabled {
|
||||||
|
m.draggedOver = true
|
||||||
|
m.Refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DraggedOut is called when drag leaves the tile
|
||||||
|
func (m *ModuleTile) DraggedOut() {
|
||||||
|
logging.Debug(logging.CatUI, "DraggedOut tile=%s", m.label)
|
||||||
|
m.draggedOver = false
|
||||||
|
m.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dropped implements desktop.Droppable interface
|
// Dropped implements desktop.Droppable interface
|
||||||
func (m *ModuleTile) Dropped(pos fyne.Position, items []fyne.URI) {
|
func (m *ModuleTile) Dropped(pos fyne.Position, items []fyne.URI) {
|
||||||
|
fmt.Printf("[DROPTILE] Dropped on tile=%s enabled=%v itemCount=%d\n", m.label, m.enabled, len(items))
|
||||||
logging.Debug(logging.CatUI, "Dropped on tile=%s enabled=%v items=%v", m.label, m.enabled, items)
|
logging.Debug(logging.CatUI, "Dropped on tile=%s enabled=%v items=%v", m.label, m.enabled, items)
|
||||||
|
// Reset dragged over state
|
||||||
|
m.draggedOver = false
|
||||||
|
|
||||||
if m.enabled && m.onDropped != nil {
|
if m.enabled && m.onDropped != nil {
|
||||||
|
fmt.Printf("[DROPTILE] Calling callback for %s\n", m.label)
|
||||||
logging.Debug(logging.CatUI, "Calling onDropped callback for %s", m.label)
|
logging.Debug(logging.CatUI, "Calling onDropped callback for %s", m.label)
|
||||||
|
// Trigger flash animation
|
||||||
|
m.flashing = true
|
||||||
|
m.Refresh()
|
||||||
|
// Reset flash after 300ms
|
||||||
|
time.AfterFunc(300*time.Millisecond, func() {
|
||||||
|
m.flashing = false
|
||||||
|
m.Refresh()
|
||||||
|
})
|
||||||
m.onDropped(items)
|
m.onDropped(items)
|
||||||
} else {
|
} else {
|
||||||
|
fmt.Printf("[DROPTILE] Drop IGNORED on %s: enabled=%v hasCallback=%v\n", m.label, m.enabled, m.onDropped != nil)
|
||||||
logging.Debug(logging.CatUI, "Drop ignored: enabled=%v hasCallback=%v", m.enabled, m.onDropped != nil)
|
logging.Debug(logging.CatUI, "Drop ignored: enabled=%v hasCallback=%v", m.enabled, m.onDropped != nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +173,22 @@ func (r *moduleTileRenderer) MinSize() fyne.Size {
|
||||||
|
|
||||||
func (r *moduleTileRenderer) Refresh() {
|
func (r *moduleTileRenderer) Refresh() {
|
||||||
r.bg.FillColor = r.tile.color
|
r.bg.FillColor = r.tile.color
|
||||||
|
|
||||||
|
// Apply visual feedback based on state
|
||||||
|
if r.tile.flashing {
|
||||||
|
// Flash animation - white outline
|
||||||
|
r.bg.StrokeColor = color.White
|
||||||
|
r.bg.StrokeWidth = 3
|
||||||
|
} else if r.tile.draggedOver {
|
||||||
|
// Dragging over - cyan/blue outline to indicate drop zone
|
||||||
|
r.bg.StrokeColor = color.NRGBA{R: 0, G: 200, B: 255, A: 255}
|
||||||
|
r.bg.StrokeWidth = 3
|
||||||
|
} else {
|
||||||
|
// Normal state
|
||||||
|
r.bg.StrokeColor = GridColor
|
||||||
|
r.bg.StrokeWidth = 1
|
||||||
|
}
|
||||||
|
|
||||||
r.bg.Refresh()
|
r.bg.Refresh()
|
||||||
r.label.Text = r.tile.label
|
r.label.Text = r.tile.label
|
||||||
r.label.Refresh()
|
r.label.Refresh()
|
||||||
|
|
|
||||||
|
|
@ -34,18 +34,25 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
||||||
)
|
)
|
||||||
|
|
||||||
var tileObjects []fyne.CanvasObject
|
var tileObjects []fyne.CanvasObject
|
||||||
for _, mod := range modules {
|
for i := range modules {
|
||||||
modID := mod.ID // Capture for closure
|
mod := modules[i] // Create new variable for this iteration
|
||||||
|
modID := mod.ID // Capture for closure
|
||||||
var tapFunc func()
|
var tapFunc func()
|
||||||
var dropFunc func([]fyne.URI)
|
var dropFunc func([]fyne.URI)
|
||||||
if mod.Enabled {
|
if mod.Enabled {
|
||||||
|
// Create new closure with properly captured modID
|
||||||
|
id := modID // Explicit capture
|
||||||
tapFunc = func() {
|
tapFunc = func() {
|
||||||
onModuleClick(modID)
|
onModuleClick(id)
|
||||||
}
|
}
|
||||||
dropFunc = func(items []fyne.URI) {
|
dropFunc = func(items []fyne.URI) {
|
||||||
onModuleDrop(modID, items)
|
fmt.Printf("[MAINMENU] dropFunc called for module=%s itemCount=%d\n", id, len(items))
|
||||||
|
logging.Debug(logging.CatUI, "MainMenu dropFunc called for module=%s itemCount=%d", id, len(items))
|
||||||
|
onModuleDrop(id, items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fmt.Printf("[MAINMENU] Creating tile for module=%s enabled=%v hasDropFunc=%v\n", modID, mod.Enabled, dropFunc != nil)
|
||||||
|
logging.Debug(logging.CatUI, "Creating tile for module=%s enabled=%v hasDropFunc=%v", modID, mod.Enabled, dropFunc != nil)
|
||||||
tileObjects = append(tileObjects, buildModuleTile(mod, tapFunc, dropFunc))
|
tileObjects = append(tileObjects, buildModuleTile(mod, tapFunc, dropFunc))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,22 @@ func getStatusText(job *queue.Job) string {
|
||||||
if job.StartedAt != nil {
|
if job.StartedAt != nil {
|
||||||
elapsed = fmt.Sprintf(" | Elapsed: %s", time.Since(*job.StartedAt).Round(time.Second))
|
elapsed = fmt.Sprintf(" | Elapsed: %s", time.Since(*job.StartedAt).Round(time.Second))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Status: Running | Progress: %.1f%%%s", job.Progress, elapsed)
|
|
||||||
|
// Add FPS and speed info if available in Config
|
||||||
|
var extras string
|
||||||
|
if job.Config != nil {
|
||||||
|
if fps, ok := job.Config["fps"].(float64); ok && fps > 0 {
|
||||||
|
extras += fmt.Sprintf(" | %.0f fps", fps)
|
||||||
|
}
|
||||||
|
if speed, ok := job.Config["speed"].(float64); ok && speed > 0 {
|
||||||
|
extras += fmt.Sprintf(" | %.2fx", speed)
|
||||||
|
}
|
||||||
|
if etaDuration, ok := job.Config["eta"].(time.Duration); ok && etaDuration > 0 {
|
||||||
|
extras += fmt.Sprintf(" | ETA %s", etaDuration.Round(time.Second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Status: Running | Progress: %.1f%%%s%s", job.Progress, elapsed, extras)
|
||||||
case queue.JobStatusPaused:
|
case queue.JobStatusPaused:
|
||||||
return "Status: Paused"
|
return "Status: Paused"
|
||||||
case queue.JobStatusCompleted:
|
case queue.JobStatusCompleted:
|
||||||
|
|
@ -259,23 +274,23 @@ func buildModuleBadge(t queue.JobType) fyne.CanvasObject {
|
||||||
return container.NewMax(bg, container.NewCenter(label))
|
return container.NewMax(bg, container.NewCenter(label))
|
||||||
}
|
}
|
||||||
|
|
||||||
// moduleColor maps job types to distinct colors for quick visual scanning.
|
// moduleColor maps job types to distinct colors matching the main module colors
|
||||||
func moduleColor(t queue.JobType) color.Color {
|
func moduleColor(t queue.JobType) color.Color {
|
||||||
switch t {
|
switch t {
|
||||||
case queue.JobTypeConvert:
|
case queue.JobTypeConvert:
|
||||||
return color.RGBA{R: 76, G: 232, B: 112, A: 255} // green
|
return color.RGBA{R: 139, G: 68, B: 255, A: 255} // Violet (#8B44FF)
|
||||||
case queue.JobTypeMerge:
|
case queue.JobTypeMerge:
|
||||||
return color.RGBA{R: 68, G: 136, B: 255, A: 255} // blue
|
return color.RGBA{R: 68, G: 136, B: 255, A: 255} // Blue (#4488FF)
|
||||||
case queue.JobTypeTrim:
|
case queue.JobTypeTrim:
|
||||||
return color.RGBA{R: 255, G: 193, B: 7, A: 255} // amber
|
return color.RGBA{R: 68, G: 221, B: 255, A: 255} // Cyan (#44DDFF)
|
||||||
case queue.JobTypeFilter:
|
case queue.JobTypeFilter:
|
||||||
return color.RGBA{R: 160, G: 86, B: 255, A: 255} // purple
|
return color.RGBA{R: 68, G: 255, B: 136, A: 255} // Green (#44FF88)
|
||||||
case queue.JobTypeUpscale:
|
case queue.JobTypeUpscale:
|
||||||
return color.RGBA{R: 255, G: 138, B: 101, A: 255} // coral
|
return color.RGBA{R: 170, G: 255, B: 68, A: 255} // Yellow-Green (#AAFF44)
|
||||||
case queue.JobTypeAudio:
|
case queue.JobTypeAudio:
|
||||||
return color.RGBA{R: 255, G: 215, B: 64, A: 255} // gold
|
return color.RGBA{R: 255, G: 215, B: 68, A: 255} // Yellow (#FFD744)
|
||||||
case queue.JobTypeThumb:
|
case queue.JobTypeThumb:
|
||||||
return color.RGBA{R: 102, G: 217, B: 239, A: 255} // teal
|
return color.RGBA{R: 255, G: 136, B: 68, A: 255} // Orange (#FF8844)
|
||||||
default:
|
default:
|
||||||
return color.Gray{Y: 180}
|
return color.Gray{Y: 180}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user