framesource 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. frame_processors/__init__.py +37 -0
  2. frame_source/__init__.py +50 -0
  3. framesource/__init__.py +93 -0
  4. framesource/_msmf_config.py +26 -0
  5. framesource/_version.py +24 -0
  6. framesource/discovery.py +109 -0
  7. framesource/errors.py +82 -0
  8. framesource/factory.py +290 -0
  9. framesource/processors/__init__.py +34 -0
  10. framesource/processors/equirectangular360_processor.py +577 -0
  11. framesource/processors/fisheye2equirectangular_processor.py +328 -0
  12. framesource/processors/frame_processor.py +30 -0
  13. framesource/processors/hyperspectral_processor.py +23 -0
  14. framesource/processors/realsense_depth_processor.py +90 -0
  15. framesource/py.typed +0 -0
  16. framesource/sources/__init__.py +40 -0
  17. framesource/sources/audiospectrogram_capture.py +1068 -0
  18. framesource/sources/basler_capture.py +477 -0
  19. framesource/sources/folder_capture.py +920 -0
  20. framesource/sources/genicam_capture.py +681 -0
  21. framesource/sources/huateng_capture.py +245 -0
  22. framesource/sources/ipcamera_capture.py +254 -0
  23. framesource/sources/mvsdk.py +2454 -0
  24. framesource/sources/realsense_capture.py +565 -0
  25. framesource/sources/screen_capture.py +800 -0
  26. framesource/sources/video_capture_base.py +560 -0
  27. framesource/sources/video_file_capture.py +259 -0
  28. framesource/sources/webcam_capture.py +511 -0
  29. framesource/sources/ximea_capture.py +299 -0
  30. framesource/threading_utils.py +790 -0
  31. framesource-0.3.0.dist-info/METADATA +787 -0
  32. framesource-0.3.0.dist-info/RECORD +37 -0
  33. framesource-0.3.0.dist-info/WHEEL +5 -0
  34. framesource-0.3.0.dist-info/licenses/LICENSE +21 -0
  35. framesource-0.3.0.dist-info/scm_file_list.json +276 -0
  36. framesource-0.3.0.dist-info/scm_version.json +8 -0
  37. framesource-0.3.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,800 @@
1
+ import logging
2
+ import threading
3
+ import time
4
+ import warnings
5
+ from typing import Any, Optional
6
+
7
+ import cv2
8
+ import numpy as np
9
+
10
+ from .video_capture_base import VideoCaptureBase
11
+
12
+ try:
13
+ import mss
14
+ except ImportError:
15
+ mss = None
16
+ logging.warning("mss is not installed. Install it with 'pip install mss' to use ScreenCapture.")
17
+
18
+ import platform
19
+
20
+ SYSTEM = platform.system()
21
+
22
+ # Windows support
23
+ try:
24
+ import win32con
25
+ import win32gui
26
+
27
+ WINDOWS_AVAILABLE = True
28
+ except ImportError:
29
+ win32gui = None
30
+ win32con = None
31
+ WINDOWS_AVAILABLE = False
32
+ if SYSTEM == "Windows":
33
+ logging.warning(
34
+ "pywin32 is not installed. Install it with 'pip install pywin32' to discover windows."
35
+ )
36
+
37
+ # macOS support
38
+ try:
39
+ if SYSTEM == "Darwin":
40
+ from Quartz import (
41
+ CGWindowListCopyWindowInfo,
42
+ kCGNullWindowID,
43
+ kCGWindowBounds,
44
+ kCGWindowLayer,
45
+ kCGWindowListOptionOnScreenOnly,
46
+ kCGWindowName,
47
+ kCGWindowNumber,
48
+ kCGWindowOwnerName,
49
+ )
50
+
51
+ MACOS_AVAILABLE = True
52
+ else:
53
+ MACOS_AVAILABLE = False
54
+ except ImportError:
55
+ MACOS_AVAILABLE = False
56
+ if SYSTEM == "Darwin":
57
+ logging.warning(
58
+ "pyobjc-framework-Quartz is not installed. Install it with "
59
+ "'pip install pyobjc-framework-Quartz' to discover windows on macOS."
60
+ )
61
+
62
+ # Linux support
63
+ try:
64
+ if SYSTEM == "Linux":
65
+ import subprocess
66
+
67
+ LINUX_AVAILABLE = True
68
+ else:
69
+ LINUX_AVAILABLE = False
70
+ except ImportError:
71
+ LINUX_AVAILABLE = False
72
+
73
+ # Configure logging
74
+ logger = logging.getLogger(__name__)
75
+
76
+
77
+ class ScreenCapture(VideoCaptureBase):
78
+ """
79
+ Capture class for grabbing frames from a region of the screen.
80
+ Args:
81
+ x (int): Top-left x coordinate
82
+ y (int): Top-left y coordinate
83
+ w (int): Width of region
84
+ h (int): Height of region
85
+ fps (float): Target FPS (default 30)
86
+ """
87
+
88
+ has_discovery = True
89
+ supports_exposure = False # Screen pixels have no controllable exposure
90
+ supports_gain = False # Screen pixels have no controllable gain
91
+
92
+ def __init__(
93
+ self,
94
+ x: int = 0,
95
+ y: int = 0,
96
+ w: int = 640,
97
+ h: int = 480,
98
+ fps: float = 30.0,
99
+ *,
100
+ hwnd: Optional[Any] = None,
101
+ **kwargs,
102
+ ):
103
+ """Initialize the screen-region capture.
104
+
105
+ Args:
106
+ x: Left edge of the capture region (pixels from the left).
107
+ y: Top edge of the capture region (pixels from the top).
108
+ w: Width of the capture region in pixels.
109
+ h: Height of the capture region in pixels.
110
+ fps: Target frame rate in frames per second.
111
+ hwnd: Optional native window handle to track (set automatically by
112
+ :meth:`from_window`).
113
+ **kwargs: Additional passthrough options stored on ``self.config``.
114
+ """
115
+ # ``hwnd`` was previously accepted through ``**kwargs``; forward it
116
+ # unchanged so ``self.config`` keeps carrying it when provided.
117
+ if hwnd is not None:
118
+ kwargs["hwnd"] = hwnd
119
+ super().__init__(**kwargs)
120
+ self.x = x
121
+ self.y = y
122
+ self.w = w
123
+ self.h = h
124
+ self.fps = fps
125
+ self.monitor = {"top": y, "left": x, "width": w, "height": h}
126
+ self._thread_local = threading.local()
127
+ self.is_connected = False
128
+ self.time_of_last_frame = 0.0
129
+ self.hwnd = hwnd # Optional window handle for tracking
130
+
131
+ @classmethod
132
+ def from_window(cls, window_id: Any, fps: float = 30.0, **kwargs):
133
+ """
134
+ Create a ScreenCapture instance configured to capture a specific window.
135
+
136
+ Args:
137
+ window_id: Window identifier (hwnd on Windows, window number on macOS,
138
+ window ID on Linux)
139
+ fps: Target frame rate
140
+ **kwargs: Additional parameters
141
+
142
+ Returns:
143
+ ScreenCapture: Configured instance
144
+ """
145
+ if SYSTEM == "Windows":
146
+ if not WINDOWS_AVAILABLE:
147
+ raise RuntimeError(
148
+ "Window capture requires pywin32. Install with 'pip install pywin32'"
149
+ )
150
+
151
+ try:
152
+ rect = win32gui.GetWindowRect(window_id)
153
+ left, top, right, bottom = rect
154
+ width = right - left
155
+ height = bottom - top
156
+
157
+ instance = cls(x=left, y=top, w=width, h=height, fps=fps, hwnd=window_id, **kwargs)
158
+ return instance
159
+
160
+ except Exception as e:
161
+ raise RuntimeError(
162
+ f"Failed to get window dimensions for hwnd {window_id}: {e}"
163
+ ) from e
164
+
165
+ elif SYSTEM == "Darwin":
166
+ if not MACOS_AVAILABLE:
167
+ raise RuntimeError(
168
+ "Window capture requires pyobjc-framework-Quartz. "
169
+ "Install with 'pip install pyobjc-framework-Quartz'"
170
+ )
171
+
172
+ try:
173
+ # Get window info
174
+ window_list = CGWindowListCopyWindowInfo(
175
+ kCGWindowListOptionOnScreenOnly, kCGNullWindowID
176
+ )
177
+ for window in window_list:
178
+ if window.get(kCGWindowNumber) == window_id:
179
+ bounds = window.get(kCGWindowBounds)
180
+ left = int(bounds["X"])
181
+ top = int(bounds["Y"])
182
+ width = int(bounds["Width"])
183
+ height = int(bounds["Height"])
184
+
185
+ instance = cls(
186
+ x=left, y=top, w=width, h=height, fps=fps, hwnd=window_id, **kwargs
187
+ )
188
+ return instance
189
+
190
+ raise RuntimeError(f"Window {window_id} not found")
191
+
192
+ except Exception as e:
193
+ raise RuntimeError(
194
+ f"Failed to get window dimensions for window {window_id}: {e}"
195
+ ) from e
196
+
197
+ elif SYSTEM == "Linux":
198
+ if not LINUX_AVAILABLE:
199
+ raise RuntimeError("Window capture requires wmctrl or xdotool")
200
+
201
+ try:
202
+ # Try to get window geometry using xdotool
203
+ result = subprocess.run(
204
+ ["xdotool", "getwindowgeometry", "--shell", str(window_id)],
205
+ capture_output=True,
206
+ text=True,
207
+ check=True,
208
+ )
209
+
210
+ # Parse output
211
+ geometry = {}
212
+ for line in result.stdout.strip().split("\n"):
213
+ if "=" in line:
214
+ key, value = line.split("=", 1)
215
+ geometry[key] = int(value)
216
+
217
+ left = geometry.get("X", 0)
218
+ top = geometry.get("Y", 0)
219
+ width = geometry.get("WIDTH", 640)
220
+ height = geometry.get("HEIGHT", 480)
221
+
222
+ instance = cls(x=left, y=top, w=width, h=height, fps=fps, hwnd=window_id, **kwargs)
223
+ return instance
224
+
225
+ except subprocess.CalledProcessError:
226
+ # Try wmctrl as fallback
227
+ try:
228
+ result = subprocess.run(
229
+ ["wmctrl", "-lG"], capture_output=True, text=True, check=True
230
+ )
231
+
232
+ for line in result.stdout.strip().split("\n"):
233
+ parts = line.split(None, 7)
234
+ if len(parts) >= 7:
235
+ win_id = parts[0]
236
+ if win_id == hex(window_id) or win_id == str(window_id):
237
+ left = int(parts[2])
238
+ top = int(parts[3])
239
+ width = int(parts[4])
240
+ height = int(parts[5])
241
+
242
+ instance = cls(
243
+ x=left,
244
+ y=top,
245
+ w=width,
246
+ h=height,
247
+ fps=fps,
248
+ hwnd=window_id,
249
+ **kwargs,
250
+ )
251
+ return instance
252
+
253
+ raise RuntimeError(f"Window {window_id} not found in wmctrl output")
254
+
255
+ except Exception as e:
256
+ raise RuntimeError(f"Failed to get window dimensions using wmctrl: {e}") from e
257
+
258
+ except Exception as e:
259
+ raise RuntimeError(
260
+ f"Failed to get window dimensions for window {window_id}: {e}"
261
+ ) from e
262
+
263
+ else:
264
+ raise RuntimeError(f"Window capture not supported on {SYSTEM}")
265
+
266
+ def connect(self) -> bool:
267
+ if mss is None:
268
+ logger.error("mss is not installed. Cannot use ScreenCapture.")
269
+ return False
270
+ self.is_connected = True
271
+ logger.info(
272
+ f"ScreenCapture connected to region x={self.x}, y={self.y}, w={self.w}, h={self.h}"
273
+ )
274
+ return True
275
+
276
+ def disconnect(self) -> bool:
277
+ self.is_connected = False
278
+ logger.info("ScreenCapture disconnected.")
279
+ return True
280
+
281
+ def _get_sct(self):
282
+ if not hasattr(self._thread_local, "sct") or self._thread_local.sct is None:
283
+ if mss is None:
284
+ raise RuntimeError("mss is not installed. Cannot create screen capture.")
285
+ self._thread_local.sct = mss.mss()
286
+ return self._thread_local.sct
287
+
288
+ def _read_implementation(self) -> tuple[bool, Optional[np.ndarray]]:
289
+ if not self.is_connected:
290
+ return False, None
291
+ sct = self._get_sct()
292
+ # Real-time playback control
293
+ if self.fps > 0:
294
+ frame_duration = 1.0 / self.fps
295
+ now = time.time()
296
+ elapsed = now - self.time_of_last_frame
297
+ if elapsed < frame_duration:
298
+ time.sleep(frame_duration - elapsed)
299
+ self.time_of_last_frame = time.time()
300
+ img = np.array(sct.grab(self.monitor))
301
+ # Convert BGRA to BGR
302
+ frame = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
303
+ return True, frame
304
+
305
+ def set_exposure(self, value: float) -> bool:
306
+ logger.warning("Exposure control not applicable for screen capture.")
307
+ return False
308
+
309
+ def get_exposure(self) -> Optional[float]:
310
+ return None
311
+
312
+ def set_gain(self, value: float) -> bool:
313
+ logger.warning("Gain control not applicable for screen capture.")
314
+ return False
315
+
316
+ def get_gain(self) -> Optional[float]:
317
+ return None
318
+
319
+ def enable_auto_exposure(self, enable: bool = True) -> bool:
320
+ logger.warning("Auto exposure control not applicable for screen capture.")
321
+ return False
322
+
323
+ def get_frame_size(self) -> Optional[tuple[int, int]]:
324
+ return (self.w, self.h)
325
+
326
+ def set_frame_size(self, width: int, height: int) -> bool:
327
+ self.w = width
328
+ self.h = height
329
+ self.monitor["width"] = width
330
+ self.monitor["height"] = height
331
+ return True
332
+
333
+ def get_fps(self) -> Optional[float]:
334
+ return self.fps
335
+
336
+ def set_fps(self, fps: float) -> bool:
337
+ self.fps = fps
338
+ return True
339
+
340
+ @classmethod
341
+ def discover(cls) -> list:
342
+ """
343
+ Discover available screen capture sources (monitors/displays and windows).
344
+
345
+ Returns:
346
+ list: List of dictionaries containing screen/window information.
347
+ Each dict contains:
348
+ - For monitors: {'type': 'monitor', 'index': int, 'name': str, 'width': int,
349
+ 'height': int, 'left': int, 'top': int}
350
+ - For windows: {'type': 'window', 'id': str/int, 'name': str, 'title': str,
351
+ 'width': int, 'height': int, 'left': int, 'top': int, 'is_visible': bool}
352
+ """
353
+ devices = []
354
+
355
+ # Discover monitors
356
+ if mss is None:
357
+ logger.warning("mss module not available. Cannot discover screen sources.")
358
+ else:
359
+ try:
360
+ with mss.mss() as sct:
361
+ # Get all monitors (index 0 is typically all monitors combined)
362
+ monitors = sct.monitors
363
+
364
+ for i, monitor in enumerate(monitors):
365
+ device_data = {
366
+ "type": "monitor",
367
+ "index": i,
368
+ "id": f"monitor_{i}",
369
+ "name": f"Monitor {i}" if i > 0 else "All Monitors",
370
+ "width": monitor["width"],
371
+ "height": monitor["height"],
372
+ "left": monitor["left"],
373
+ "top": monitor["top"],
374
+ }
375
+ devices.append(device_data)
376
+ logger.info(f"Found screen source: {device_data}")
377
+
378
+ except Exception as e:
379
+ logger.error(f"Error discovering screen sources: {e}")
380
+
381
+ # Discover windows - platform specific
382
+ if SYSTEM == "Windows":
383
+ devices.extend(cls._discover_windows_windows())
384
+ elif SYSTEM == "Darwin":
385
+ devices.extend(cls._discover_windows_macos())
386
+ elif SYSTEM == "Linux":
387
+ devices.extend(cls._discover_windows_linux())
388
+ else:
389
+ logger.info(f"Window discovery not supported on {SYSTEM}")
390
+
391
+ return devices
392
+
393
+ @classmethod
394
+ def _discover_windows_windows(cls) -> list:
395
+ """Discover windows on Windows using win32gui."""
396
+ if not WINDOWS_AVAILABLE:
397
+ logger.info("Window discovery not available (pywin32 not installed)")
398
+ return []
399
+
400
+ windows = []
401
+
402
+ try:
403
+
404
+ def window_callback(hwnd, extra):
405
+ """Callback function to enumerate windows"""
406
+ if not win32gui.IsWindowVisible(hwnd):
407
+ return
408
+
409
+ # Get window title
410
+ title = win32gui.GetWindowText(hwnd)
411
+
412
+ # Skip windows without titles or with empty titles
413
+ if not title or len(title.strip()) == 0:
414
+ return
415
+
416
+ # Get window rectangle
417
+ try:
418
+ rect = win32gui.GetWindowRect(hwnd)
419
+ left, top, right, bottom = rect
420
+ width = right - left
421
+ height = bottom - top
422
+
423
+ # Skip windows that are too small (likely not real windows)
424
+ if width < 50 or height < 50:
425
+ return
426
+
427
+ # Get class name for additional context
428
+ try:
429
+ class_name = win32gui.GetClassName(hwnd)
430
+ except Exception:
431
+ class_name = "Unknown"
432
+
433
+ window_data = {
434
+ "type": "window",
435
+ "hwnd": hwnd,
436
+ "id": f"window_{hwnd}",
437
+ "name": title,
438
+ "title": title,
439
+ "class_name": class_name,
440
+ "width": width,
441
+ "height": height,
442
+ "left": left,
443
+ "top": top,
444
+ "is_visible": True,
445
+ }
446
+ windows.append(window_data)
447
+
448
+ except Exception as e:
449
+ logger.debug(f"Error getting window rect for hwnd {hwnd}: {e}")
450
+
451
+ # Enumerate all windows
452
+ win32gui.EnumWindows(window_callback, None)
453
+
454
+ # Sort windows by title for consistent ordering
455
+ windows.sort(key=lambda w: w["title"].lower())
456
+
457
+ logger.info(f"Found {len(windows)} visible windows")
458
+
459
+ except Exception as e:
460
+ logger.error(f"Error discovering windows: {e}")
461
+
462
+ return windows
463
+
464
+ @classmethod
465
+ def _discover_windows_macos(cls) -> list:
466
+ """Discover windows on macOS using Quartz."""
467
+ if not MACOS_AVAILABLE:
468
+ logger.info("Window discovery not available (pyobjc-framework-Quartz not installed)")
469
+ return []
470
+
471
+ windows = []
472
+
473
+ try:
474
+ # Get list of all on-screen windows
475
+ window_list = CGWindowListCopyWindowInfo(
476
+ kCGWindowListOptionOnScreenOnly, kCGNullWindowID
477
+ )
478
+
479
+ for window in window_list:
480
+ # Get window properties
481
+ window_layer = window.get(kCGWindowLayer, 0)
482
+
483
+ # Skip desktop and other background windows (layer > 0)
484
+ if window_layer != 0:
485
+ continue
486
+
487
+ window_name = window.get(kCGWindowName, "")
488
+ owner_name = window.get(kCGWindowOwnerName, "")
489
+ window_number = window.get(kCGWindowNumber, 0)
490
+ bounds = window.get(kCGWindowBounds, {})
491
+
492
+ # Skip windows without names
493
+ if not window_name or len(window_name.strip()) == 0:
494
+ continue
495
+
496
+ # Get dimensions
497
+ left = int(bounds.get("X", 0))
498
+ top = int(bounds.get("Y", 0))
499
+ width = int(bounds.get("Width", 0))
500
+ height = int(bounds.get("Height", 0))
501
+
502
+ # Skip windows that are too small
503
+ if width < 50 or height < 50:
504
+ continue
505
+
506
+ window_data = {
507
+ "type": "window",
508
+ "window_number": window_number,
509
+ "id": f"window_{window_number}",
510
+ "name": window_name,
511
+ "title": window_name,
512
+ "owner": owner_name,
513
+ "width": width,
514
+ "height": height,
515
+ "left": left,
516
+ "top": top,
517
+ "is_visible": True,
518
+ }
519
+ windows.append(window_data)
520
+
521
+ # Sort windows by title for consistent ordering
522
+ windows.sort(key=lambda w: w["title"].lower())
523
+
524
+ logger.info(f"Found {len(windows)} visible windows")
525
+
526
+ except Exception as e:
527
+ logger.error(f"Error discovering windows on macOS: {e}")
528
+
529
+ return windows
530
+
531
+ @classmethod
532
+ def _discover_windows_linux(cls) -> list:
533
+ """Discover windows on Linux using wmctrl or xdotool."""
534
+ if not LINUX_AVAILABLE:
535
+ return []
536
+
537
+ windows = []
538
+
539
+ # Try wmctrl first (more reliable)
540
+ try:
541
+ result = subprocess.run(["wmctrl", "-lGp"], capture_output=True, text=True, check=True)
542
+
543
+ for line in result.stdout.strip().split("\n"):
544
+ parts = line.split(None, 8)
545
+ if len(parts) >= 9:
546
+ win_id = parts[0]
547
+ desktop = parts[1]
548
+ pid = parts[2]
549
+ left = int(parts[3])
550
+ top = int(parts[4])
551
+ width = int(parts[5])
552
+ height = int(parts[6])
553
+ title = parts[8] if len(parts) > 8 else ""
554
+
555
+ # Skip windows without titles
556
+ if not title or len(title.strip()) == 0:
557
+ continue
558
+
559
+ # Skip windows that are too small
560
+ if width < 50 or height < 50:
561
+ continue
562
+
563
+ # Convert hex window ID to int
564
+ try:
565
+ window_id_int = int(win_id, 16)
566
+ except Exception:
567
+ window_id_int = win_id
568
+
569
+ window_data = {
570
+ "type": "window",
571
+ "window_id": window_id_int,
572
+ "id": f"window_{win_id}",
573
+ "name": title,
574
+ "title": title,
575
+ "desktop": desktop,
576
+ "pid": pid,
577
+ "width": width,
578
+ "height": height,
579
+ "left": left,
580
+ "top": top,
581
+ "is_visible": True,
582
+ }
583
+ windows.append(window_data)
584
+
585
+ # Sort windows by title for consistent ordering
586
+ windows.sort(key=lambda w: w["title"].lower())
587
+
588
+ logger.info(f"Found {len(windows)} visible windows using wmctrl")
589
+
590
+ except (subprocess.CalledProcessError, FileNotFoundError):
591
+ # Try xdotool as fallback
592
+ try:
593
+ result = subprocess.run(
594
+ ["xdotool", "search", "--onlyvisible", "--name", ".*"],
595
+ capture_output=True,
596
+ text=True,
597
+ check=True,
598
+ )
599
+
600
+ window_ids = result.stdout.strip().split("\n")
601
+
602
+ for win_id in window_ids:
603
+ if not win_id:
604
+ continue
605
+
606
+ try:
607
+ # Get window name
608
+ name_result = subprocess.run(
609
+ ["xdotool", "getwindowname", win_id],
610
+ capture_output=True,
611
+ text=True,
612
+ check=True,
613
+ )
614
+ title = name_result.stdout.strip()
615
+
616
+ if not title or len(title.strip()) == 0:
617
+ continue
618
+
619
+ # Get window geometry
620
+ geom_result = subprocess.run(
621
+ ["xdotool", "getwindowgeometry", "--shell", win_id],
622
+ capture_output=True,
623
+ text=True,
624
+ check=True,
625
+ )
626
+
627
+ # Parse geometry
628
+ geometry = {}
629
+ for line in geom_result.stdout.strip().split("\n"):
630
+ if "=" in line:
631
+ key, value = line.split("=", 1)
632
+ try:
633
+ geometry[key] = int(value)
634
+ except Exception:
635
+ geometry[key] = value
636
+
637
+ left = geometry.get("X", 0)
638
+ top = geometry.get("Y", 0)
639
+ width = geometry.get("WIDTH", 0)
640
+ height = geometry.get("HEIGHT", 0)
641
+
642
+ # Skip windows that are too small
643
+ if width < 50 or height < 50:
644
+ continue
645
+
646
+ window_data = {
647
+ "type": "window",
648
+ "window_id": int(win_id),
649
+ "id": f"window_{win_id}",
650
+ "name": title,
651
+ "title": title,
652
+ "width": width,
653
+ "height": height,
654
+ "left": left,
655
+ "top": top,
656
+ "is_visible": True,
657
+ }
658
+ windows.append(window_data)
659
+
660
+ except Exception as e:
661
+ logger.debug(f"Error getting info for window {win_id}: {e}")
662
+ continue
663
+
664
+ # Sort windows by title for consistent ordering
665
+ windows.sort(key=lambda w: w["title"].lower())
666
+
667
+ logger.info(f"Found {len(windows)} visible windows using xdotool")
668
+
669
+ except (subprocess.CalledProcessError, FileNotFoundError):
670
+ logger.warning(
671
+ "Neither wmctrl nor xdotool available. "
672
+ "Install one to discover windows on Linux."
673
+ )
674
+
675
+ return windows
676
+
677
+ @classmethod
678
+ def get_config_schema(cls) -> dict[str, Any]:
679
+ """Get configuration schema for screen capture"""
680
+ warnings.warn(
681
+ "get_config_schema() is deprecated and will be removed in a future release; "
682
+ "UI form schemas belong in the consuming application.",
683
+ DeprecationWarning,
684
+ stacklevel=2,
685
+ )
686
+ return {
687
+ "title": "Screen Capture Configuration",
688
+ "description": "Configure screen/monitor capture settings",
689
+ "fields": [
690
+ {
691
+ "name": "x",
692
+ "label": "X Position",
693
+ "type": "number",
694
+ "min": 0,
695
+ "max": 5000,
696
+ "placeholder": "0",
697
+ "description": "Left edge of capture area (pixels from left)",
698
+ "required": False,
699
+ "default": 0,
700
+ },
701
+ {
702
+ "name": "y",
703
+ "label": "Y Position",
704
+ "type": "number",
705
+ "min": 0,
706
+ "max": 5000,
707
+ "placeholder": "0",
708
+ "description": "Top edge of capture area (pixels from top)",
709
+ "required": False,
710
+ "default": 0,
711
+ },
712
+ {
713
+ "name": "w",
714
+ "label": "Width",
715
+ "type": "number",
716
+ "min": 160,
717
+ "max": 5000,
718
+ "placeholder": "640",
719
+ "description": "Width of capture area in pixels",
720
+ "required": False,
721
+ "default": 640,
722
+ },
723
+ {
724
+ "name": "h",
725
+ "label": "Height",
726
+ "type": "number",
727
+ "min": 120,
728
+ "max": 5000,
729
+ "placeholder": "480",
730
+ "description": "Height of capture area in pixels",
731
+ "required": False,
732
+ "default": 480,
733
+ },
734
+ {
735
+ "name": "fps",
736
+ "label": "Frame Rate (FPS)",
737
+ "type": "number",
738
+ "min": 1,
739
+ "max": 60,
740
+ "placeholder": "30",
741
+ "description": "Frames per second for screen capture",
742
+ "required": False,
743
+ "default": 30,
744
+ },
745
+ ],
746
+ }
747
+
748
+
749
+ if __name__ == "__main__":
750
+ # Example usage
751
+
752
+ sources = ScreenCapture.discover()
753
+ print(f"\nDiscovered {len(sources)} screen capture sources:\n")
754
+
755
+ # Display monitors
756
+ monitors = [s for s in sources if s.get("type") == "monitor"]
757
+ if monitors:
758
+ print(f"Monitors ({len(monitors)}):")
759
+ for screen in monitors:
760
+ print(
761
+ f" - {screen['name']} (#{screen['index']}): "
762
+ f"{screen['width']}x{screen['height']} at ({screen['left']}, {screen['top']})"
763
+ )
764
+
765
+ # Display windows
766
+ windows = [s for s in sources if s.get("type") == "window"]
767
+ if windows:
768
+ print(f"\nWindows ({len(windows)}):")
769
+ for _i, window in enumerate(windows[:20]): # Show first 20 windows
770
+ print(
771
+ f" - {window['title'][:60]:60s} | "
772
+ f"{window['width']}x{window['height']} at ({window['left']}, {window['top']})"
773
+ )
774
+ if len(windows) > 20:
775
+ print(f" ... and {len(windows) - 20} more windows")
776
+
777
+ print("\n" + "=" * 80)
778
+ print("Example: Capture from a specific monitor")
779
+ print("=" * 80)
780
+
781
+ camera = ScreenCapture(x=100, y=100, w=800, h=600, fps=30)
782
+ if camera.connect():
783
+ print("Screen capture connected successfully.")
784
+ print(f"Frame size: {camera.get_frame_size()}")
785
+ print(f"FPS: {camera.get_fps()}")
786
+
787
+ # Read a few frames
788
+ frame_count = 0
789
+ while camera.is_connected and frame_count < 100:
790
+ ret, frame = camera.read()
791
+ if ret:
792
+ cv2.imshow("Screen Capture", frame) # type: ignore
793
+ frame_count += 1
794
+ if cv2.waitKey(1) & 0xFF == ord("q"):
795
+ break
796
+
797
+ camera.stop()
798
+ camera.disconnect()
799
+ else:
800
+ print("Failed to connect to screen capture.")