Files
video-edit/PROJECT_CONTEXT.md
Mike Johnston 0d9c00ea29
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m11s
update readme and context
2026-02-09 21:16:22 -05:00

427 lines
14 KiB
Markdown

# 📘 Project Context for AI Assistants
This document provides technical context for AI assistants working on this video editor project. It covers architecture decisions, implementation patterns, and key code locations.
---
## 🏗️ Architecture Overview
### Tech Stack
- **Backend**: Flask 3.0 (Python 3.11+)
- **Video Engine**: FFmpeg with libx264 encoder
- **Frontend**: Vanilla JavaScript (no frameworks)
- **Deployment**: Python local / Docker / Docker Compose
### Request Flow
```
1. User uploads video → /upload endpoint
2. Server assigns UUID, stores in uploads/ dir
3. User edits via UI → sends params to /process
4. FFmpeg processes with filters → outputs/ dir
5. User downloads → /download/<file_id> endpoint
6. Cleanup → /cleanup/<file_id> removes temp files
```
### File Structure
```
c:\Projects\editor/
├── app.py # Flask backend, FFmpeg integration
├── templates/
│ └── index.html # Single-page frontend app
├── requirements.txt # Python dependencies
├── uploads/ # Temporary uploaded files (created on first run)
├── outputs/ # Processed video outputs (created on first run)
├── Dockerfile # Container image definition
├── docker-compose.yml # Local development compose
├── prod-compose.yml # Production registry-pull compose
├── .gitea/
│ └── workflows/
│ └── rebuild-prod.yaml # CI/CD build and deployment
├── README.md # User documentation
└── PROJECT_CONTEXT.md # This file (technical context)
```
---
## 🔑 Key Implementation Details
### 1. Video Processing (`app.py`)
#### FFmpeg Filter Chain
The app builds a complex FFmpeg filter chain dynamically based on user selections:
```python
# Base filters array
filters = []
# Rotation (if not 0°)
if rotate and rotate != '0':
# transpose values: 1=90°CW, 2=90°CCW, 3=180° (using 2,2)
if rotate == '90':
filters.append('transpose=1')
elif rotate == '180':
filters.append('transpose=2,transpose=2')
elif rotate == '270':
filters.append('transpose=2')
# Crop (if specified)
if crop_w and crop_h:
filters.append(f'crop={crop_w}:{crop_h}:{crop_x}:{crop_y}')
# Scale with even dimension enforcement
if scale_percentage < 100:
# trunc(value/2)*2 ensures dimensions are divisible by 2 (H.264 requirement)
filters.append(f'scale=trunc(iw*{scale_percentage/100}/2)*2:trunc(ih*{scale_percentage/100}/2)*2')
# Final filter string
filter_str = ','.join(filters)
```
**Critical**: The `trunc(iw*percentage/100/2)*2` pattern ensures output dimensions are always even numbers. H.264 encoder requires width and height divisible by 2.
#### File ID System
- UUID4 generates unique IDs for each upload
- `original_filenames` dict maps UUID → original filename
- Downloads use: `{original_name}_processed_{timestamp}.mp4`
- Timestamp format: `YYYYMMDD_HHMMSS` (e.g., `20240315_143022`)
#### CRF Quality Control
- CRF (Constant Rate Factor) range: 18-32
- Lower CRF = higher quality, larger file
- Default: 23 (balanced)
- Passed directly to FFmpeg: `-crf {quality}`
### 2. Visual Crop Selection (`index.html`)
#### Canvas Overlay Technique
```javascript
// Create canvas overlay matching video display size
cropCanvas.width = video.offsetWidth;
cropCanvas.height = video.offsetHeight;
// Calculate scale factors for actual video pixels
const scaleX = video.videoWidth / video.offsetWidth;
const scaleY = video.videoHeight / video.offsetHeight;
// Mouse drag draws rectangle on canvas
// Convert canvas coordinates → actual video pixels
const actualX = Math.round(rectX * scaleX);
const actualY = Math.round(rectY * scaleY);
const actualW = Math.round(rectW * scaleX);
const actualH = Math.round(rectH * scaleY);
```
**Why this works**: Video element display size ≠ actual video resolution. Scale factors convert screen pixels to video pixels for accurate crop commands.
#### Dynamic Button Creation
The crop button is dynamically injected into video-info after video loads:
```javascript
function setupCropButton(video, canvas) {
const videoInfoDiv = document.getElementById('video-info');
// Create button HTML
const cropButtonHtml = `
<button id="draw-crop-btn" class="btn btn-primary">
🎯 Click to Draw Crop Area
</button>
`;
// Inject after codec paragraph
videoInfoDiv.innerHTML += cropButtonHtml;
// Add event listener to new button
document.getElementById('draw-crop-btn').addEventListener('click', () => {
enableCropDrawing(video, canvas);
});
}
```
**Caution**: Using `innerHTML +=` recreates all child elements. Event listeners must be re-attached after injection.
### 3. Trim Controls
#### Video Position Capture
Instead of complex timeline scrubbing, uses simple current position capture:
```javascript
// "Set Start Here" button
video.currentTime // Gets current playback position in seconds
// Display position in real-time
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 100);
return `${mins}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`;
}
```
**Design Decision**: Timeline scrubbers added complexity without improving UX. Position-based buttons are more intuitive.
### 4. Scale and Dimension Display
#### Live Output Resolution Preview
```javascript
// Calculate what output dimensions will be
let outputW = videoWidth;
let outputH = videoHeight;
// If cropped, use crop dimensions
if (cropW && cropH) {
outputW = cropW;
outputH = cropH;
}
// Apply scale percentage
outputW = Math.round(outputW * (scalePercentage / 100));
outputH = Math.round(outputH * (scalePercentage / 100));
// Force even dimensions (H.264 compatibility)
if (outputW % 2 !== 0) outputW++;
if (outputH % 2 !== 0) outputH++;
// Display to user
document.getElementById('output-resolution').textContent = `${outputW}x${outputH}`;
```
**Important**: Frontend preview must match backend FFMPEG logic exactly, or users see incorrect dimensions.
---
## 🎨 UI/UX Patterns
### Feature Ordering Logic
Features are ordered by typical editing workflow:
1. **Trim** - Select video segment (temporal)
2. **Crop** - Select video area (spatial)
3. **Output Scale** - Apply to trimmed/cropped result
4. **Compression Quality** - Final output quality
5. **Quick Actions** - Rotate/mute (can apply anytime)
### Reset Functionality
Complete reset clears:
- All input values (trim, crop, scale, quality)
- Canvas drawings (crop rectangle)
- Video preview (remove src)
- Button states (rotate, mute)
- Display texts (resolution, filename, time)
- Section visibility (hide edit/download sections)
```javascript
// Comprehensive reset
video.src = '';
cropCanvas.width = 0;
cropCanvas.height = 0;
document.querySelectorAll('input[type="number"]').forEach(input => input.value = '');
document.getElementById('scale-slider').value = 100;
document.getElementById('mute-audio').classList.remove('active');
// ... etc
```
### Button State Management
Visual feedback for toggles:
```javascript
// Mute audio toggle
muteBtn.classList.toggle('active');
muteBtn.textContent = muted ? '🔇 Audio Muted' : '🔊 Mute Audio';
// Rotation state
document.getElementById('rotation-display').textContent =
rotationAngle === 0 ? '' : `Rotated ${rotationAngle}°`;
```
---
## 🐳 Deployment Configurations
### Local Development
```bash
# Python
python app.py # Runs on localhost:5000
# Docker
docker-compose up --build
```
### Production (CI/CD)
`.gitea/workflows/rebuild-prod.yaml`:
1. Triggered on push to main
2. Builds Docker image with FFmpeg
3. Pushes to `reg.dev.nervesocket.com/video-editor:latest`
4. Calls Portainer API to redeploy container
5. Uses production compose pulling from registry
**Registry**: Private registry at `reg.dev.nervesocket.com`
**Deployment**: Portainer API with webhook ID `df5c509c-8002-463e-b009-3b05b8a1e2c3`
---
## 🧩 Common Extension Patterns
### Adding New FFmpeg Filter
1. Add UI control in `index.html` (slider, button, input)
2. Send parameter with form submission (line ~400)
3. Extract in `app.py` process_video: `request.form.get('param_name')`
4. Add filter to filters array: `filters.append('filter_name=value')`
5. Ensure filter order is logical (crop before scale, etc.)
### Adding New Quick Action
1. Create button in Quick Actions section
2. Add click handler setting global variable
3. Send parameter in POST to /process
4. Handle in backend with FFmpeg flag or filter
5. Add reset logic to resetApp()
### Handling New Video Codec Output
Currently hardcoded to H.264/AAC. To add codec selection:
1. Add codec dropdown in UI
2. Pass codec parameter to backend
3. Conditional FFmpeg args based on codec:
```python
if codec == 'h264':
cmd.extend(['-c:v', 'libx264', '-crf', quality])
elif codec == 'h265':
cmd.extend(['-c:v', 'libx265', '-crf', quality])
```
---
## 🔍 Debugging Tips
### FFmpeg Command Inspection
The app prints full FFmpeg command to console:
```python
print("FFmpeg command:", ' '.join(cmd))
subprocess.run(cmd, check=True)
```
Copy this command and run manually to debug encoding issues.
### Canvas Coordinate Debugging
Add console logs in mouse handlers:
```javascript
console.log('Canvas coords:', x, y);
console.log('Video coords:', Math.round(x * scaleX), Math.round(y * scaleY));
```
### Processing Failures
Common issues:
- **Odd dimensions**: Backend now handles with trunc(), shouldn't occur
- **Invalid crop**: Crop exceeds video bounds (validate in frontend)
- **Missing FFmpeg**: Docker image includes it, but local Python needs manual install
- **Unsupported format**: Check FFmpeg support with `ffmpeg -formats`
---
## 📦 Dependencies
### Python (requirements.txt)
```
Flask==3.0.0
Werkzeug==3.0.1
```
### System
- **FFmpeg**: Must be installed and in PATH
- On Debian/Ubuntu: `apt-get install ffmpeg`
- On Windows: Download from ffmpeg.org
- Docker: Installed in Dockerfile
### FFmpeg Codecs Used
- **Video**: libx264 (H.264/AVC)
- **Audio**: aac (AAC)
- **Filters**: crop, scale, transpose
---
## 🚨 Critical Implementation Notes
### 1. H.264 Even Dimension Requirement
**Must enforce**: Width and height divisible by 2
- Backend: `trunc(iw*scale/2)*2`
- Frontend: `if (dim % 2 !== 0) dim++;`
- Violation causes FFmpeg error: "width not divisible by 2"
### 2. File Cleanup
Currently, files are NOT auto-deleted. Manual cleanup endpoint exists but:
- `/cleanup/<file_id>` removes files
- Consider adding auto-cleanup timer (e.g., 1 hour after upload)
- Or cron job to clean old files
### 3. Memory Usage
`original_filenames` dict grows indefinitely in memory. For production:
- Persist to Redis/database
- Or tie to file existence (check disk, rebuild dict on startup)
### 4. Security Considerations
Current implementation is for trusted users:
- No file size limits (could DOS server)
- No rate limiting
- No authentication
- FFmpeg runs with user permissions (subprocess)
- For public deployment, add nginx upload limits, Flask rate limiting, user sessions
### 5. Browser Compatibility
- **Canvas API**: IE not supported (use Chrome/Firefox/Edge)
- **Video Preview**: Requires browser codec support (H.264 widely supported)
- **File API**: Drag-and-drop requires modern browser
---
## 🎯 Design Philosophy Explained
### Why No Framework?
- **Simplicity**: Single HTML file, no build process
- **Learning**: Clear vanilla JS patterns
- **Deployment**: No npm, no bundling, just Python + FFmpeg
- **Performance**: No framework overhead for simple app
### Why Percentage-Based Scaling?
- **Intuitive**: "50% smaller" clearer than "calculate dimensions"
- **Flexible**: Works regardless of input resolution
- **Safer**: Avoids invalid dimensions (too large/small)
- **Chaining**: Works naturally after crop (scale the crop, not original)
### Why CRF Instead of Bitrate?
- **Quality-based**: Maintains consistent quality across different content
- **Simpler**: One slider, no need to understand bitrate implications
- **Better results**: Variable bitrate adapts to video complexity
- **Industry standard**: CRF 18-23 is professional range
---
## 🔮 Future Enhancement Considerations
If extending this project, consider:
1. **Multiple Video Support**: Concatenation, side-by-side
2. **Audio Controls**: Volume adjustment, audio replacement
3. **Filters**: Brightness, contrast, saturation, blur
4. **Watermarks**: Text or image overlays
5. **Format Selection**: WebM, GIF, HEVC output options
6. **Batch Processing**: Queue multiple jobs
7. **Progress Bar**: Real-time FFmpeg encoding progress (parse stderr)
8. **Undo/Redo**: State management for edit history
9. **Presets**: Save common settings as named presets
10. **Cloud Storage**: S3/Azure integration for large files
---
## 📞 Getting Help
When asking for assistance:
1. **Describe the feature**: User-facing behavior first
2. **Specify location**: Which file/function needs changes
3. **FFmpeg context**: If encoding-related, mention codec/format
4. **Show errors**: Full error messages help diagnose issues
5. **Test command**: If FFmpeg fails, what command would work manually?
Example good request:
> "I want to add a brightness filter. Users should have a slider in the UI from -1.0 to 1.0. This should apply the FFmpeg `eq=brightness=X` filter before the scale filter. The parameter should be called 'brightness' and default to 0.0."
---
**Last Updated**: 2024-03-15 (after adding compression quality slider and filename preservation)
**Current Version**: Fully functional simple video editor with 7 main features (trim, crop, scale, quality, rotation, mute, visual controls)