Add modular capture backends, resolution profiles, Wayland support
This commit is contained in:
parent
3b363192f2
commit
80ba9b1b90
16 changed files with 2266 additions and 45 deletions
313
pkg/engine/capture/backends/monitor.go
Normal file
313
pkg/engine/capture/backends/monitor.go
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
// 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 }
|
||||
Loading…
Add table
Add a link
Reference in a new issue