iso-bot/pkg/engine/capture/backends/monitor.go

313 lines
No EOL
9.6 KiB
Go

// Monitor capture for full screen or monitor region capture (cross-platform).
package backends
import (
"fmt"
"image"
"runtime"
"git.cloonar.com/openclawd/iso-bot/pkg/engine/capture"
)
// MonitorConfig holds configuration for monitor capture.
type MonitorConfig struct {
// MonitorIndex specifies which monitor to capture (0-based).
// -1 captures the primary monitor.
MonitorIndex int `yaml:"monitor_index"`
// Region defines a specific area to capture.
// If nil, captures the entire monitor.
Region *capture.Region `yaml:"region"`
// IncludeCursor captures the mouse cursor.
IncludeCursor bool `yaml:"include_cursor"`
// Method specifies the capture method.
// Options: "auto", "gdi", "dxgi", "x11", "wayland"
Method string `yaml:"method"`
}
// MonitorSource captures from a monitor or screen region.
type MonitorSource struct {
config MonitorConfig
monitors []MonitorInfo
activeIndex int
width int
height int
impl MonitorCaptureImpl
}
// MonitorInfo describes a display monitor.
type MonitorInfo struct {
Index int
Name string
X int
Y int
Width int
Height int
Primary bool
ScaleFactor float64
}
// MonitorCaptureImpl defines platform-specific capture implementation.
type MonitorCaptureImpl interface {
Initialize() error
CaptureMonitor(monitor MonitorInfo, region *capture.Region) (image.Image, error)
Close() error
}
// NewMonitorSource creates a monitor capture source.
func NewMonitorSource(configMap map[string]interface{}) (capture.Source, error) {
var config MonitorConfig
// Extract config from map
if index, ok := configMap["monitor_index"].(int); ok {
config.MonitorIndex = index
} else {
config.MonitorIndex = -1 // Primary monitor
}
if regionMap, ok := configMap["region"].(map[string]interface{}); ok {
region := &capture.Region{}
if x, ok := regionMap["x"].(int); ok {
region.X = x
}
if y, ok := regionMap["y"].(int); ok {
region.Y = y
}
if width, ok := regionMap["width"].(int); ok {
region.Width = width
}
if height, ok := regionMap["height"].(int); ok {
region.Height = height
}
config.Region = region
}
if cursor, ok := configMap["include_cursor"].(bool); ok {
config.IncludeCursor = cursor
} else {
config.IncludeCursor = true
}
if method, ok := configMap["method"].(string); ok {
config.Method = method
} else {
config.Method = "auto"
}
source := &MonitorSource{
config: config,
}
// Initialize platform-specific implementation
if err := source.initImpl(); err != nil {
return nil, fmt.Errorf("failed to initialize monitor capture: %w", err)
}
return source, nil
}
// Name returns a description of this capture source.
func (m *MonitorSource) Name() string {
if m.config.MonitorIndex >= 0 {
return fmt.Sprintf("Monitor %d", m.config.MonitorIndex)
}
return "Primary Monitor"
}
// Capture grabs a single frame from the monitor.
func (m *MonitorSource) Capture() (image.Image, error) {
if m.impl == nil {
return nil, fmt.Errorf("monitor capture not initialized")
}
monitor, err := m.getTargetMonitor()
if err != nil {
return nil, err
}
return m.impl.CaptureMonitor(monitor, m.config.Region)
}
// CaptureRegion grabs a sub-region of the monitor.
func (m *MonitorSource) CaptureRegion(r capture.Region) (image.Image, error) {
if m.impl == nil {
return nil, fmt.Errorf("monitor capture not initialized")
}
monitor, err := m.getTargetMonitor()
if err != nil {
return nil, err
}
return m.impl.CaptureMonitor(monitor, &r)
}
// Size returns the monitor dimensions.
func (m *MonitorSource) Size() (width, height int) {
if m.config.Region != nil {
return m.config.Region.Width, m.config.Region.Height
}
return m.width, m.height
}
// Close releases monitor capture resources.
func (m *MonitorSource) Close() error {
if m.impl != nil {
return m.impl.Close()
}
return nil
}
// initImpl initializes the platform-specific capture implementation.
func (m *MonitorSource) initImpl() error {
var impl MonitorCaptureImpl
var err error
// Choose implementation based on platform and method
switch {
case m.config.Method == "auto":
impl, err = m.createAutoImpl()
case m.config.Method == "gdi" && runtime.GOOS == "windows":
impl, err = NewGDICapture(m.config)
case m.config.Method == "dxgi" && runtime.GOOS == "windows":
impl, err = NewDXGICapture(m.config)
case m.config.Method == "x11" && runtime.GOOS == "linux":
impl, err = NewX11MonitorCapture(m.config)
case m.config.Method == "wayland" && runtime.GOOS == "linux":
impl, err = NewWaylandMonitorCapture(m.config)
default:
return fmt.Errorf("unsupported capture method %q for platform %s", m.config.Method, runtime.GOOS)
}
if err != nil {
return err
}
m.impl = impl
return m.impl.Initialize()
}
// createAutoImpl selects the best implementation for the current platform.
func (m *MonitorSource) createAutoImpl() (MonitorCaptureImpl, error) {
switch runtime.GOOS {
case "windows":
// Prefer DXGI for better performance, fallback to GDI
if impl, err := NewDXGICapture(m.config); err == nil {
return impl, nil
}
return NewGDICapture(m.config)
case "linux":
// Try Wayland first, fallback to X11
if impl, err := NewWaylandMonitorCapture(m.config); err == nil {
return impl, nil
}
return NewX11MonitorCapture(m.config)
case "darwin":
return NewMacOSCapture(m.config)
default:
return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
}
// getTargetMonitor returns the monitor to capture based on configuration.
func (m *MonitorSource) getTargetMonitor() (MonitorInfo, error) {
if len(m.monitors) == 0 {
return MonitorInfo{}, fmt.Errorf("no monitors detected")
}
if m.config.MonitorIndex == -1 {
// Find primary monitor
for _, monitor := range m.monitors {
if monitor.Primary {
return monitor, nil
}
}
// If no primary found, use first monitor
return m.monitors[0], nil
}
if m.config.MonitorIndex >= len(m.monitors) {
return MonitorInfo{}, fmt.Errorf("monitor index %d out of range (have %d monitors)", m.config.MonitorIndex, len(m.monitors))
}
return m.monitors[m.config.MonitorIndex], nil
}
// enumerateMonitors discovers available monitors.
func (m *MonitorSource) enumerateMonitors() error {
// TODO: Implement monitor enumeration for each platform
// This would use platform-specific APIs to discover monitors
m.monitors = []MonitorInfo{
{
Index: 0,
Name: "Primary Display",
X: 0,
Y: 0,
Width: 1920,
Height: 1080,
Primary: true,
ScaleFactor: 1.0,
},
}
return nil
}
// Platform-specific capture implementations (stubs for now)
// NewGDICapture creates a GDI-based capture implementation for Windows.
func NewGDICapture(config MonitorConfig) (MonitorCaptureImpl, error) {
// TODO: Implement GDI capture using GetDC/BitBlt
return &GDICapture{config: config}, nil
}
// NewDXGICapture creates a DXGI-based capture implementation for Windows.
func NewDXGICapture(config MonitorConfig) (MonitorCaptureImpl, error) {
// TODO: Implement DXGI Desktop Duplication API capture
return &DXGICapture{config: config}, nil
}
// NewX11MonitorCapture creates an X11-based capture implementation for Linux.
func NewX11MonitorCapture(config MonitorConfig) (MonitorCaptureImpl, error) {
// TODO: Implement X11 root window capture using XGetImage
return &X11MonitorCapture{config: config}, nil
}
// NewWaylandMonitorCapture creates a Wayland-based capture implementation for Linux.
func NewWaylandMonitorCapture(config MonitorConfig) (MonitorCaptureImpl, error) {
// TODO: Implement Wayland capture using PipeWire/xdg-desktop-portal
return &WaylandMonitorCapture{config: config}, nil
}
// NewMacOSCapture creates a macOS capture implementation.
func NewMacOSCapture(config MonitorConfig) (MonitorCaptureImpl, error) {
// TODO: Implement macOS capture using CGDisplayCreateImage
return &MacOSCapture{config: config}, nil
}
// Stub implementations
type GDICapture struct{ config MonitorConfig }
func (g *GDICapture) Initialize() error { return fmt.Errorf("GDI capture not implemented") }
func (g *GDICapture) CaptureMonitor(monitor MonitorInfo, region *capture.Region) (image.Image, error) { return nil, fmt.Errorf("GDI capture not implemented") }
func (g *GDICapture) Close() error { return nil }
type DXGICapture struct{ config MonitorConfig }
func (d *DXGICapture) Initialize() error { return fmt.Errorf("DXGI capture not implemented") }
func (d *DXGICapture) CaptureMonitor(monitor MonitorInfo, region *capture.Region) (image.Image, error) { return nil, fmt.Errorf("DXGI capture not implemented") }
func (d *DXGICapture) Close() error { return nil }
type X11MonitorCapture struct{ config MonitorConfig }
func (x *X11MonitorCapture) Initialize() error { return fmt.Errorf("X11 monitor capture not implemented") }
func (x *X11MonitorCapture) CaptureMonitor(monitor MonitorInfo, region *capture.Region) (image.Image, error) { return nil, fmt.Errorf("X11 monitor capture not implemented") }
func (x *X11MonitorCapture) Close() error { return nil }
type WaylandMonitorCapture struct{ config MonitorConfig }
func (w *WaylandMonitorCapture) Initialize() error { return fmt.Errorf("Wayland monitor capture not implemented") }
func (w *WaylandMonitorCapture) CaptureMonitor(monitor MonitorInfo, region *capture.Region) (image.Image, error) { return nil, fmt.Errorf("Wayland monitor capture not implemented") }
func (w *WaylandMonitorCapture) Close() error { return nil }
type MacOSCapture struct{ config MonitorConfig }
func (m *MacOSCapture) Initialize() error { return fmt.Errorf("macOS capture not implemented") }
func (m *MacOSCapture) CaptureMonitor(monitor MonitorInfo, region *capture.Region) (image.Image, error) { return nil, fmt.Errorf("macOS capture not implemented") }
func (m *MacOSCapture) Close() error { return nil }