// Package capture provides screen capture from various sources. // // Supports capturing from: // - Local window (by title or handle) // - VM display (VNC, Spice, or VM window on host) // - Full screen / monitor region // // The capture interface is source-agnostic — the engine doesn't care // where the frames come from. package capture import ( "image" "time" ) // Region defines a rectangular area to capture. type Region struct { X, Y, Width, Height int } // Source represents a capture source (window, VM, screen, etc.) type Source interface { // Name returns a human-readable description of the source. Name() string // Capture grabs a single frame. Capture() (image.Image, error) // CaptureRegion grabs a sub-region of the source. CaptureRegion(r Region) (image.Image, error) // Size returns the source dimensions. Size() (width, height int) // Close releases resources. Close() error } // Stats tracks capture performance metrics. type Stats struct { FrameCount uint64 AvgCaptureMs float64 FPS float64 LastCapture time.Time } // Manager handles screen capture with performance tracking. type Manager struct { source Source stats Stats } // NewManager creates a capture manager with the given source. func NewManager(source Source) *Manager { return &Manager{source: source} } // Capture grabs a frame and updates performance stats. func (m *Manager) Capture() (image.Image, error) { start := time.Now() frame, err := m.source.Capture() if err != nil { return nil, err } elapsed := time.Since(start) m.stats.FrameCount++ m.stats.LastCapture = start // Rolling average alpha := 0.1 ms := float64(elapsed.Microseconds()) / 1000.0 if m.stats.FrameCount == 1 { m.stats.AvgCaptureMs = ms } else { m.stats.AvgCaptureMs = m.stats.AvgCaptureMs*(1-alpha) + ms*alpha } if m.stats.AvgCaptureMs > 0 { m.stats.FPS = 1000.0 / m.stats.AvgCaptureMs } return frame, nil } // CaptureRegion grabs a sub-region. func (m *Manager) CaptureRegion(r Region) (image.Image, error) { return m.source.CaptureRegion(r) } // Stats returns current capture performance stats. func (m *Manager) Stats() Stats { return m.stats } // Close releases the capture source. func (m *Manager) Close() error { return m.source.Close() } // Source returns the underlying capture source. func (m *Manager) Source() Source { return m.source } // Size returns the source dimensions. func (m *Manager) Size() (width, height int) { return m.source.Size() } // SetSource swaps the capture source. // This is useful for development when uploading new screenshots. func (m *Manager) SetSource(newSource Source) error { // Close the old source if m.source != nil { if err := m.source.Close(); err != nil { // Log but don't fail - we still want to switch sources // log.Printf("Warning: failed to close old capture source: %v", err) } } // Switch to new source m.source = newSource // Reset stats for the new source m.stats = Stats{} return nil }