# Stash Analysis and Solution Plan ## ๐Ÿ” **Real Problem Analysis** ### **The Core Issue: FFmpeg Process Management, Not Duration Protection** After analyzing the docs and your Stash discovery, the real problem is **NOT** duration corruption or progress bar jitter. The real problem is: 1. **โŒ Poor FFmpeg Process Management**: Our current system doesn't handle FFmpeg processes robustly 2. **โŒ No Seek-Optimized Transcoding**: We don't restart FFmpeg with `--ss` for seeking like Stash does 3. **โŒ Resource Leaks**: FFmpeg processes may not terminate properly, causing system issues 4. **โŒ Inefficient Seeking**: Seeking requires transcoding from the beginning instead of from the seek point ### **What We Were Fixing vs. What We Should Fix** | What We Were Fixing | What We Should Fix | |---------------------|-------------------| | โŒ Duration corruption during transcoding | โœ… **FFmpeg process lifecycle management** | | โŒ Progress bar jitter | โœ… **Seek-optimized transcoding with `--ss`** | | โŒ Anti-jitter mechanisms | โœ… **Process cleanup and resource management** | | โŒ Complex duration validation | โœ… **Simple, reliable FFmpeg process handling** | ## ๐ŸŽฏ **Stash's Brilliant Approach** ### **Key Discovery: Process Restart on Seek** From your `stash.md` analysis, Stash does something incredibly smart: ``` PID 559: ffmpeg -ss 29.848541666666662 -i video.avi ... (seek to 29.8s) PID 711: ffmpeg -ss 77.374375 -i video.avi ... (seek to 77.3s) PID 741: ffmpeg -ss 103.31072916666666 -i video.avi ... (seek to 103.3s) ``` **Every seek starts a NEW FFmpeg process with `--ss` (start time) parameter!** ### **Why This is Brilliant** 1. **๐ŸŽฏ Instant Seeking**: No need to wait for transcoding from beginning 2. **๐Ÿ”„ Clean Process Management**: Each seek = new process, old process can be killed 3. **๐Ÿ’พ Memory Efficiency**: No need to buffer from start to seek point 4. **โšก Performance**: Direct start at desired position 5. **๐Ÿงน Resource Cleanup**: Easy to kill old processes ### **Stash's Process Flow** ``` User seeks to 30s โ†’ Kill current FFmpeg โ†’ Start new FFmpeg with -ss 30 User seeks to 60s โ†’ Kill current FFmpeg โ†’ Start new FFmpeg with -ss 60 User seeks to 90s โ†’ Kill current FFmpeg โ†’ Start new FFmpeg with -ss 90 ``` ## ๐Ÿ—๏ธ **Proposed Solution Architecture** ### **Phase 1: Robust FFmpeg Process Management** #### **1.1 Process Registry System** ```typescript // lib/ffmpeg/process-registry.ts class FFmpegProcessRegistry { private processes = new Map(); // Register new process register(videoId: string, seekTime: number, process: ChildProcess, command: string[]) { const key = `${videoId}_${seekTime}`; // Kill existing process for this video if different seek time this.killExisting(videoId, seekTime); this.processes.set(key, { process, startTime: new Date(), seekTime, videoId, command }); } // Kill existing process for video (different seek time) private killExisting(videoId: string, newSeekTime: number) { for (const [key, entry] of this.processes.entries()) { if (entry.videoId === videoId && entry.seekTime !== newSeekTime) { console.log(`[FFMPEG] Killing existing process for ${videoId} (seek: ${entry.seekTime}s)`); this.killProcess(key); } } } // Kill specific process killProcess(key: string) { const entry = this.processes.get(key); if (entry && !entry.process.killed) { entry.process.kill('SIGKILL'); this.processes.delete(key); } } // Kill all processes for a video killAllForVideo(videoId: string) { for (const [key, entry] of this.processes.entries()) { if (entry.videoId === videoId) { this.killProcess(key); } } } // Get process info getProcessInfo(videoId: string) { return Array.from(this.processes.entries()) .filter(([_, entry]) => entry.videoId === videoId) .map(([key, entry]) => ({ key, seekTime: entry.seekTime, uptime: Date.now() - entry.startTime.getTime(), command: entry.command })); } } ``` #### **1.2 Enhanced Transcoding API** ```typescript // pages/api/stream/[id]/transcode.ts export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { id } = req.query; const { seek } = req.query; // New: seek time parameter const seekTime = seek ? parseFloat(seek as string) : 0; // Kill existing process for this video ffmpegRegistry.killAllForVideo(id as string); // Build FFmpeg command with seek const ffmpegArgs = [ '-hide_banner', '-v', 'error', ...(seekTime > 0 ? ['-ss', seekTime.toString()] : []), // Add seek if specified '-i', videoPath, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'veryfast', '-crf', '25', '-sc_threshold', '0', '-movflags', 'frag_keyframe+empty_moov', '-ac', '2', '-f', 'mp4', 'pipe:1' ]; console.log(`[FFMPEG] Starting transcoding for video ${id} with seek: ${seekTime}s`); console.log(`[FFMPEG] Command: ffmpeg ${ffmpegArgs.join(' ')}`); const ffmpeg = spawn('ffmpeg', ffmpegArgs); // Register process ffmpegRegistry.register(id as string, seekTime, ffmpeg, ffmpegArgs); // Handle cleanup req.on('close', () => { console.log(`[FFMPEG] Client disconnected, killing process for video ${id}`); ffmpegRegistry.killAllForVideo(id as string); }); // Stream response res.setHeader('Content-Type', 'video/mp4'); res.setHeader('X-Content-Duration', videoDuration.toString()); res.setHeader('X-Seek-Time', seekTime.toString()); ffmpeg.stdout.pipe(res); } ``` ### **Phase 2: Frontend Seek Integration** #### **2.1 Enhanced Video Player** ```typescript // components/video-viewer.tsx const handleSeek = (newTime: number) => { if (videoRef.current) { console.log(`[PLAYER] Seeking to ${newTime}s`); // Kill current transcoding process const videoId = getVideoId(); if (videoId && isTranscoding) { console.log(`[PLAYER] Killing current transcoding process`); // This will trigger a new transcoding process with seek } // Update video source with seek parameter const seekUrl = `/api/stream/${videoId}/transcode?seek=${newTime}`; videoRef.current.src = seekUrl; videoRef.current.load(); // Set current time immediately for UI responsiveness setCurrentTime(newTime); // Start new transcoding from seek point setIsTranscoding(true); } }; ``` #### **2.2 Process Monitoring** ```typescript // hooks/use-ffmpeg-monitor.ts export const useFFmpegMonitor = (videoId: string) => { const [processInfo, setProcessInfo] = useState([]); useEffect(() => { const interval = setInterval(async () => { try { const response = await fetch(`/api/ffmpeg/status?videoId=${videoId}`); const data = await response.json(); setProcessInfo(data.processes); } catch (error) { console.error('Failed to fetch FFmpeg status:', error); } }, 2000); return () => clearInterval(interval); }, [videoId]); return processInfo; }; ``` ### **Phase 3: Advanced Features** #### **3.1 Preemptive Process Management** ```typescript // lib/ffmpeg/preemptive-manager.ts class PreemptiveFFmpegManager { // Start transcoding slightly ahead of current playback startPreemptiveTranscoding(videoId: string, currentTime: number) { const aheadTime = currentTime + 30; // 30 seconds ahead // Start background process const ffmpeg = spawn('ffmpeg', [ '-ss', aheadTime.toString(), '-i', videoPath, // ... other args ]); // Register as preemptive (lower priority) ffmpegRegistry.register(videoId, aheadTime, ffmpeg, [], { preemptive: true }); } } ``` #### **3.2 Quality Adaptation** ```typescript // lib/ffmpeg/quality-manager.ts class QualityManager { // Adapt quality based on system load getOptimalQuality(systemLoad: number, availableMemory: number) { if (systemLoad > 0.8 || availableMemory < 1000000000) { return { crf: 28, preset: 'ultrafast' }; // Lower quality, faster } else if (systemLoad > 0.5) { return { crf: 25, preset: 'veryfast' }; // Medium quality } else { return { crf: 23, preset: 'fast' }; // Higher quality } } } ``` ## ๐Ÿ“‹ **Implementation Plan** ### **Week 1: Foundation** - [ ] Implement `FFmpegProcessRegistry` class - [ ] Add seek parameter to transcoding API - [ ] Basic process cleanup on client disconnect ### **Week 2: Integration** - [ ] Update video player to use seek-optimized transcoding - [ ] Implement process monitoring in frontend - [ ] Add process status API endpoint ### **Week 3: Advanced Features** - [ ] Preemptive transcoding for smooth playback - [ ] Quality adaptation based on system load - [ ] Process health monitoring and auto-cleanup ### **Week 4: Testing & Optimization** - [ ] Load testing with multiple concurrent streams - [ ] Memory leak detection and prevention - [ ] Performance optimization ## ๐ŸŽฏ **Expected Results** ### **Before (Current Issues)** - โŒ Seeking requires transcoding from beginning - โŒ FFmpeg processes may leak resources - โŒ Poor performance on seek operations - โŒ Complex duration protection (unnecessary) ### **After (Stash-like Solution)** - โœ… **Instant seeking** with `--ss` parameter - โœ… **Clean process management** - one process per seek - โœ… **Resource efficiency** - no unnecessary buffering - โœ… **Simple architecture** - focus on process management, not duration protection - โœ… **Professional streaming experience** like Stash ## ๐Ÿ”ง **Key Implementation Details** ### **1. Process Lifecycle** ``` Seek Request โ†’ Kill Old Process โ†’ Start New Process with -ss โ†’ Stream Response ``` ### **2. Resource Management** - **Memory**: Kill old processes immediately - **CPU**: One active process per video - **Network**: Direct streaming without buffering ### **3. Error Handling** - **Process failures**: Auto-restart with exponential backoff - **Network issues**: Kill process, let client retry - **System overload**: Reduce quality, limit concurrent processes ## ๐Ÿš€ **Why This Approach is Better** 1. **๐ŸŽฏ Solves Real Problem**: Process management, not duration corruption 2. **๐Ÿ”„ Follows Stash Pattern**: Proven approach from successful application 3. **โšก Performance**: Instant seeking, no unnecessary transcoding 4. **๐Ÿงน Clean Architecture**: Simple, maintainable code 5. **๐Ÿ“ˆ Scalable**: Easy to add features like quality adaptation This approach transforms our video player from a basic transcoder into a **professional streaming solution** that rivals Stash's performance and reliability. ## ๐ŸŽจ **UI Issues That Still Need Solving** ### **The Remaining Problems** Even with solid FFmpeg process management, we still have these critical UI issues: 1. **โŒ Progress Bar Jumping Backwards**: During buffering, the progress bar jumps backward as new data arrives 2. **โŒ Wrong Duration Display**: Shows buffered duration instead of real video duration 3. **โŒ Poor User Experience**: Users can't trust the progress bar or seek to accurate positions ### **Why These Issues Persist** These UI issues are **separate from** the FFmpeg process management problem: - **FFmpeg Process Management**: Solves seeking performance and resource leaks - **UI Progress Issues**: Caused by how the browser handles video metadata and buffering events ### **Focused UI Solution Strategy** #### **1. Duration Source Priority** ```typescript // Priority order for duration (highest to lowest) const getDuration = async (videoId: string) => { // 1. Database-stored duration (most reliable) const dbDuration = await getStoredDuration(videoId); if (dbDuration > 0) return dbDuration; // 2. HTTP headers from transcoding endpoint const headerDuration = await getHeaderDuration(videoId); if (headerDuration > 0) return headerDuration; // 3. Video element metadata (least reliable - can be buffered duration) return null; // Let video element handle it }; ``` #### **2. Progress Bar Anti-Jitter** ```typescript // Simple, effective anti-jitter logic const useStableProgress = (videoRef: RefObject, realDuration: number) => { const [currentTime, setCurrentTime] = useState(0); const lastStableTime = useRef(0); const handleTimeUpdate = () => { if (!videoRef.current) return; const newTime = videoRef.current.currentTime; // Only allow forward progress or very small backward adjustments if (newTime >= lastStableTime.current - 0.1) { setCurrentTime(newTime); lastStableTime.current = newTime; } else { console.log(`[PROGRESS] Blocked backward jump: ${newTime}s -> ${lastStableTime.current}s`); } }; return { currentTime, handleTimeUpdate }; }; ``` #### **3. Duration Protection** ```typescript // Protect against buffered duration corruption const useProtectedDuration = (videoId: string) => { const [duration, setDuration] = useState(0); const hasRealDuration = useRef(false); useEffect(() => { // Get real duration from database first const fetchRealDuration = async () => { const realDuration = await getDuration(videoId); if (realDuration > 0) { setDuration(realDuration); hasRealDuration.current = true; console.log(`[DURATION] Set real duration: ${realDuration}s`); } }; fetchRealDuration(); }, [videoId]); // Block duration updates from video metadata if we have real duration const handleDurationChange = (newDuration: number) => { if (hasRealDuration.current) { console.log(`[DURATION] Blocked metadata duration: ${newDuration}s (using stored: ${duration}s)`); return; // Keep the real duration } // Only accept duration if it's significantly larger (not buffered duration) if (newDuration > duration * 2.0) { setDuration(newDuration); console.log(`[DURATION] Updated duration: ${newDuration}s`); } }; return { duration, handleDurationChange }; }; ``` ### **Implementation Priority** 1. **๐Ÿ”ฅ High Priority**: Fix duration display (show real duration, not buffered) 2. **๐Ÿ”ฅ High Priority**: Fix progress bar backward jumps 3. **โšก Medium Priority**: Integrate with FFmpeg process management 4. **๐ŸŽจ Low Priority**: Enhanced progress bar UI (buffer visualization, etc.) ### **Why This Approach Works** - **๐ŸŽฏ Focused**: Solves specific UI problems without overcomplicating - **๐Ÿ”„ Compatible**: Works alongside the FFmpeg process management solution - **โšก Simple**: Uses proven patterns (database-first duration, anti-jitter logic) - **๐Ÿงช Testable**: Each component can be tested independently ### **Expected UI Results** - โœ… **Accurate Duration**: Always shows real video duration (e.g., 9 minutes, not 6 seconds) - โœ… **Stable Progress**: No more backward jumps during buffering - โœ… **Reliable Seeking**: Seek bar represents actual video timeline - โœ… **Professional Feel**: Progress bar behaves like YouTube/Netflix