# VideoTools Player Module ## Overview The Player module provides rock-solid video playback with frame-accurate capabilities, serving as the foundation for advanced features like enhancement, trimming, and chapter management. ## Architecture Philosophy **Player stability is critical blocker** for all advanced features. The current implementation follows VideoTools' core principles: - **Internal Implementation**: No external player dependencies - **Go-based**: Native integration with existing codebase - **Cross-platform**: Consistent behavior across Linux, Windows, macOS - **Frame-accurate**: Precise seeking and frame extraction - **A/V Sync**: Perfect synchronization without drift - **Extensible**: Clean interfaces for module integration ## Critical Issues Identified (Legacy Implementation) ### 1. Separate A/V Processes - A/V Desync Inevitable **Problem**: Video and audio run in completely separate FFmpeg processes with no synchronization. **Location**: `main.go:10184-10185` ```go func (p *playSession) startLocked(offset float64) { p.runVideo(offset) // Separate process p.runAudio(offset) // Separate process } ``` **Symptoms**: - Gradual A/V drift over time - Stuttering when one process slows down - No way to correct sync when drift occurs ### 2. Command-Line Interface Limitations **Problem**: MPV/VLC controllers use basic CLI without proper IPC or frame extraction. **Location**: `internal/player/mpv_controller.go`, `vlc_controller.go` - No real-time position feedback - No frame extraction capability - Process restart required for control changes ### 3. Frame-Accurate Seeking Problems **Problem**: Seeking restarts entire FFmpeg processes instead of precise seeking. **Location**: `main.go:10018-10028` ```go func (p *playSession) Seek(offset float64) { p.stopLocked() // Kill processes p.startLocked(p.current) // Restart from new position } ``` **Symptoms**: - 100-500ms gap during seek operations - No keyframe awareness - Cannot extract exact frames ### 4. Performance Issues **Problems**: - Frame allocation every frame causes GC pressure - Small audio buffers cause underruns - Volume processing in hot path wastes CPU ## Unified Player Architecture (Solution) ### Core Design Principles 1. **Single FFmpeg Process** - Multiplexed A/V output to maintain perfect sync - Master clock reference for timing - PTS-based synchronization with drift correction 2. **Frame-Accurate Operations** - Seeking to exact frames without restarts - Keyframe extraction for previews - Frame buffer pooling to reduce GC pressure 3. **Hardware Acceleration** - CUDA/VA-API/VideoToolbox integration - Fallback to software decoding - Cross-platform hardware detection 4. **Module Integration** - Clean interfaces for other modules - Frame extraction APIs for enhancement - Chapter detection integration from Author module ## Implementation Strategy ### Phase 1: Foundation (Week 1-2) #### 1.1 Unified FFmpeg Process ```go type UnifiedPlayer struct { cmd *exec.Cmd videoPipe io.Reader audioPipe io.Reader frameBuffer *RingBuffer audioBuffer *RingBuffer syncClock time.Time ptsOffset int64 // Video properties frameRate float64 frameCount int64 duration time.Duration } // Single FFmpeg with A/V sync func (p *UnifiedPlayer) load(path string) error { cmd := exec.Command("ffmpeg", "-i", path, // Video stream "-map", "0:v:0", "-f", "rawvideo", "-pix_fmt", "rgb24", "pipe:4", // Audio stream "-map", "0:a:0", "-f", "s16le", "-ar", "48000", "pipe:5", "-") // Maintain sync internally } ``` #### 1.2 Hardware Acceleration ```go type HardwareBackend struct { Name string // "cuda", "vaapi", "videotoolbox" Available bool Device int Memory int64 } func detectHardwareSupport() []HardwareBackend { var backends []HardwareBackend // NVIDIA CUDA if checkNVML() { backends = append(backends, HardwareBackend{ Name: "cuda", Available: true}) } // Intel VA-API if runtime.GOOS == "linux" && checkVA-API() { backends = append(backends, HardwareBackend{ Name: "vaapi", Available: true}) } // Apple VideoToolbox if runtime.GOOS == "darwin" && checkVideoToolbox() { backends = append(backends, HardwareBackend{ Name: "videotoolbox", Available: true}) } return backends } ``` #### 1.3 Frame Buffer Management ```go type FramePool struct { pool sync.Pool active int maxSize int } func (p *FramePool) get(w, h int) *image.RGBA { if img := p.pool.Get(); img != nil { atomic.AddInt32(&p.active, -1) return img.(*image.RGBA) } if atomic.LoadInt32(&p.active) >= p.maxSize { return image.NewRGBA(image.Rect(0, 0, w, h)) // Fallback } atomic.AddInt32(&p.active, 1) return image.NewRGBA(image.Rect(0, 0, w, h)) } ``` ### Phase 2: Core Features (Week 3-4) #### 2.1 Frame-Accurate Seeking ```go // Frame extraction without restart func (p *Player) SeekToFrame(frame int64) error { seekTime := time.Duration(frame) * time.Second / time.Duration(p.frameRate) // Extract single frame cmd := exec.Command("ffmpeg", "-ss", fmt.Sprintf("%.3f", seekTime.Seconds()), "-i", p.path, "-vframes", "1", "-f", "rawvideo", "-pix_fmt", "rgb24", "-") // Update display immediately frame, err := p.extractFrame(cmd) if err != nil { return err } return p.displayFrame(frame) } ``` #### 2.2 Chapter System Integration ```go // Port scene detection from Author module func (p *Player) DetectScenes(threshold float64) ([]Chapter, error) { cmd := exec.Command("ffmpeg", "-i", p.path, "-vf", fmt.Sprintf("select='gt(scene=%.2f)',metadata=print:file", threshold), "-f", "null", "-") return parseSceneChanges(cmd.Stdout) } // Manual chapter support func (p *Player) AddManualChapter(time time.Duration, title string) error { p.chapters = append(p.chapters, Chapter{ StartTime: time, Title: title, Type: "manual", }) p.updateChapterList() } // Chapter navigation func (p *Player) GoToChapter(index int) error { if index < len(p.chapters) { return p.SeekToTime(p.chapters[index].StartTime) } return nil } ``` #### 2.3 Performance Optimization ```go type SyncManager struct { masterClock time.Time videoPTS int64 audioPTS int64 driftOffset int64 correctionRate float64 } func (s *SyncManager) SyncFrame(frameTime time.Duration) error { now := time.Now() expected := s.masterClock.Add(frameTime) if now.Before(expected) { // We're ahead, wait precisely time.Sleep(expected.Sub(now)) } else if behind := now.Sub(expected); behind > frameDur*2 { // We're way behind, skip this frame logging.Debug(logging.CatPlayer, "dropping frame, %.0fms behind", behind.Seconds()*1000) s.masterClock = now return fmt.Errorf("too far behind, skipping frame") } else { // We're slightly behind, catch up gradually s.masterClock = now.Add(frameDur / 2) } s.masterClock = expected return nil } ``` ### Phase 3: Advanced Features (Week 5-6) #### 3.1 Preview System ```go type PreviewManager struct { player *UnifiedPlayer cache map[int64]*image.RGBA // Frame cache maxSize int } func (p *PreviewManager) GetPreviewFrame(offset time.Duration) (*image.RGBA, error) { frameNum := int64(offset.Seconds() * p.player.FrameRate) if cached, exists := p.cache[frameNum]; exists { return cached, nil } // Extract frame if not cached frame, err := p.player.ExtractFrame(frameNum) if err != nil { return nil, err } // Cache for future use if len(p.cache) >= p.maxSize { p.clearOldestCache() } p.cache[frameNum] = frame return frame, nil } ``` #### 3.2 Error Recovery ```go type ErrorRecovery struct { lastGoodFrame int64 retryCount int maxRetries int } func (e *ErrorRecovery) HandlePlaybackError(err error) error { e.retryCount++ if e.retryCount > e.maxRetries { return fmt.Errorf("max retries exceeded: %w", err) } // Implement recovery strategy if isDecodeError(err) { return e.attemptCodecFallback() } if isBufferError(err) { return e.increaseBufferSize() } return e.retryFromLastGoodFrame() } ``` ## Module Integration Points ### Enhancement Module ```go type EnhancementPlayer interface { // Core playback GetCurrentFrame() int64 ExtractFrame(frame int64) (*image.RGBA, error) ExtractKeyframes() ([]int64, error) // Chapter integration GetChapters() []Chapter AddManualChapter(time time.Duration, title string) error // Content analysis GetVideoInfo() *VideoInfo DetectContent() (ContentType, error) } ``` ### Trim Module ```go type TrimPlayer interface { // Timeline interface GetTimeline() *TimelineWidget SetChapterMarkers([]Chapter) error // Frame-accurate operations TrimToFrames(start, end int64) error GetTrimPreview(start, end int64) (*image.RGBA, error) // Export integration ExportTrimmed(path string) error } ``` ### Author Module Integration ```go // Scene detection integration func (p *Player) ImportSceneChapters(chapters []Chapter) error { p.chapters = append(p.chapters, chapters...) return p.updateChapterList() } ``` ## Performance Monitoring ### Key Metrics ```go type PlayerMetrics struct { FrameDeliveryTime time.Duration // Target: frameDur * 0.8 AudioBufferHealth float64 // Target: > 0.3 (30%) SyncDrift time.Duration // Target: < 10ms CPUMemoryUsage float64 // Target: < 80% FrameDrops int64 // Target: 0 SeekTime time.Duration // Target: < 50ms } func (m *PlayerMetrics) Collect() { // Real-time performance tracking if frameDelivery := time.Since(frameReadStart); frameDelivery > frameDur*1.5 { logging.Warn(logging.CatPlayer, "slow frame delivery: %.1fms", frameDelivery.Seconds()*1000) } if audioBufferFillLevel := audioBuffer.Available() / audioBuffer.Capacity(); audioBufferFillLevel < 0.3 { logging.Warn(logging.CatPlayer, "audio buffer low: %.0f%%", audioBufferFillLevel*100) } } ``` ## Testing Strategy ### Test Matrix | Feature | Test Cases | Success Criteria | |----------|-------------|-----------------| | Playback | 24/30/60fps smooth | No stuttering, <5% frame drops | | Seeking | Frame-accurate | <50ms seek time, exact frame | | A/V Sync | 30+ seconds stable | <10ms drift, no correction needed | | Chapters | Navigation works | Previous/Next jumps correctly | | Hardware | Acceleration detected | GPU utilization when available | | Memory | Stable long-term | No memory leaks, stable usage | | Cross-platform | Consistent behavior | Linux/Windows/macOS parity | ### Stress Testing - Long-duration playback (2+ hours) - Rapid seeking operations (10+ seeks/minute) - Multiple format support (H.264, H.265, VP9, AV1) - Hardware acceleration stress testing - Memory leak detection with runtime/pprof - CPU usage profiling under different loads ## Implementation Timeline **Week 1**: Core unified player architecture **Week 2**: Frame-accurate seeking and chapter integration **Week 3**: Hardware acceleration and performance optimization **Week 4**: Preview system and error recovery **Week 5**: Advanced features (multiple audio tracks, subtitle support) **Week 6**: Cross-platform testing and optimization This player implementation provides the rock-solid foundation needed for all advanced VideoTools features while maintaining cross-platform compatibility and Go-based architecture principles.