VirtualDesktop 1.6.3__tar.gz → 1.6.4__tar.gz

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 (64) hide show
  1. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/PKG-INFO +2 -2
  2. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/README.md +1 -1
  3. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/__init__.py +1 -1
  4. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/record.py +40 -3
  5. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/PKG-INFO +2 -2
  6. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/pyproject.toml +1 -1
  7. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_record.py +31 -0
  8. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/LICENSE +0 -0
  9. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/__main__.py +0 -0
  10. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_bundle.py +0 -0
  11. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_compat.py +0 -0
  12. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_desktop_runtime.py +0 -0
  13. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_devices.py +0 -0
  14. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_download.py +0 -0
  15. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_enumerate.py +0 -0
  16. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_helper.py +0 -0
  17. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_log.py +0 -0
  18. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_process.py +0 -0
  19. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_sigcheck.ps1 +0 -0
  20. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_sigcheck.py +0 -0
  21. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_winapi.py +0 -0
  22. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/agent.py +0 -0
  23. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/capture.py +0 -0
  24. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/cli.py +0 -0
  25. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/config.py +0 -0
  26. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/desktop.py +0 -0
  27. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/desktop_capture.py +0 -0
  28. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/desktop_session.py +0 -0
  29. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/display_setup.py +0 -0
  30. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/driver.py +0 -0
  31. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/driver_manifest.json +0 -0
  32. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/errors.py +0 -0
  33. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/manifest.py +0 -0
  34. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/session.py +0 -0
  35. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/window.py +0 -0
  36. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/workspace.py +0 -0
  37. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/SOURCES.txt +0 -0
  38. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/dependency_links.txt +0 -0
  39. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/entry_points.txt +0 -0
  40. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/requires.txt +0 -0
  41. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/top_level.txt +0 -0
  42. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/setup.cfg +0 -0
  43. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_accelerators.py +0 -0
  44. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_agent.py +0 -0
  45. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_artifact_regression.py +0 -0
  46. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_capture.py +0 -0
  47. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_cli.py +0 -0
  48. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_compat.py +0 -0
  49. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_config_and_winapi.py +0 -0
  50. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_desktop_isolation.py +0 -0
  51. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_destructive_lifecycle.py +0 -0
  52. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_display_setup.py +0 -0
  53. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_download.py +0 -0
  54. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_driver.py +0 -0
  55. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_hide_modes.py +0 -0
  56. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_integration_windows.py +0 -0
  57. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_manifest.py +0 -0
  58. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_no_dialogs.py +0 -0
  59. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_packaging.py +0 -0
  60. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_process.py +0 -0
  61. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_virtual_desktop.py +0 -0
  62. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_workspace.py +0 -0
  63. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_workspace_instances.py +0 -0
  64. {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_workspace_move.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: VirtualDesktop
3
- Version: 1.6.3
3
+ Version: 1.6.4
4
4
  Summary: win10-win11 虚拟桌面
5
5
  Author: VirtualDesktop contributors
6
6
  License: MIT License
@@ -572,7 +572,7 @@ with space.record_stream("run.gif", target=window, fps=8, duration=5) as stream:
572
572
 
573
573
  | 参数 | 含义 |
574
574
  |---|---|
575
- | `fps` | 目标帧率(必填,默认 10)。低于目标会被丢弃并计入 `report().dropped` |
575
+ | `fps` | 目标帧率(默认 10)。**范围 1–90**,超出会被钳制:`report().fps` 报实际使用的值,`report().fps_requested` 只在被钳制时给出原值,`report().fps_clamped` 为布尔。低于目标会被丢弃并计入 `report().dropped`;`report().measured_fps` 是达成帧率(跟不上目标时低于 `fps`) |
576
576
  | `duration` | 秒;到点自动停止,`stopped_by="duration"` |
577
577
  | `max_frames` | 帧数上限;到点自动停止,`stopped_by="max_frames"` |
578
578
  | — | 不传 `duration` 时有一个 **1 小时硬上限**(`MAX_DURATION`):录制线程持有 `Recorder` 引用,被丢弃且未 `stop()` 的对象无法回收,没有上限就会一直录下去(并带着 ffmpeg 子进程)。正常用法请始终用 `with` |
@@ -515,7 +515,7 @@ with space.record_stream("run.gif", target=window, fps=8, duration=5) as stream:
515
515
 
516
516
  | 参数 | 含义 |
517
517
  |---|---|
518
- | `fps` | 目标帧率(必填,默认 10)。低于目标会被丢弃并计入 `report().dropped` |
518
+ | `fps` | 目标帧率(默认 10)。**范围 1–90**,超出会被钳制:`report().fps` 报实际使用的值,`report().fps_requested` 只在被钳制时给出原值,`report().fps_clamped` 为布尔。低于目标会被丢弃并计入 `report().dropped`;`report().measured_fps` 是达成帧率(跟不上目标时低于 `fps`) |
519
519
  | `duration` | 秒;到点自动停止,`stopped_by="duration"` |
520
520
  | `max_frames` | 帧数上限;到点自动停止,`stopped_by="max_frames"` |
521
521
  | — | 不传 `duration` 时有一个 **1 小时硬上限**(`MAX_DURATION`):录制线程持有 `Recorder` 引用,被丢弃且未 `stop()` 的对象无法回收,没有上限就会一直录下去(并带着 ffmpeg 子进程)。正常用法请始终用 `with` |
@@ -72,7 +72,7 @@ from .record import (
72
72
  supported_formats,
73
73
  )
74
74
 
75
- __version__ = "1.6.3"
75
+ __version__ = "1.6.4"
76
76
 
77
77
  __all__ = [
78
78
  "__version__",
@@ -60,6 +60,8 @@ __all__ = [
60
60
  "GIF_FORMATS",
61
61
  "IMAGE_FORMATS",
62
62
  "MAX_DURATION",
63
+ "MAX_FPS",
64
+ "MIN_FPS",
63
65
  "Recorder",
64
66
  "RecordingReport",
65
67
  "VIDEO_FORMATS",
@@ -86,6 +88,12 @@ _PIPE_PIXEL_FORMAT = "rgb24"
86
88
  #: explicit larger ``duration``.
87
89
  MAX_DURATION = 3600.0
88
90
 
91
+ #: Frame-rate range a recording is held to. ``fps`` outside it is clamped (and reported), because
92
+ #: the alternative is either a refusal over a cosmetic parameter or a file whose real rate differs
93
+ #: from the one the caller asked for without saying so.
94
+ MAX_FPS = 90.0
95
+ MIN_FPS = 1.0
96
+
89
97
 
90
98
  def supported_formats(ffmpeg: Optional[str] = None) -> Dict[str, str]:
91
99
  """Which extensions this machine can write, and what writes them."""
@@ -114,6 +122,7 @@ class RecordingReport:
114
122
  path: str
115
123
  format: str
116
124
  frames: int
125
+ #: The frame rate actually used (``fps`` after clamping).
117
126
  fps: float
118
127
  duration: float
119
128
  size: Optional[Tuple[int, int]] = None
@@ -122,11 +131,20 @@ class RecordingReport:
122
131
  dropped: int = 0
123
132
  error: str = ""
124
133
  backend: str = ""
134
+ #: The ``fps`` the caller asked for, when it differed from the one actually used.
135
+ fps_requested: Optional[float] = None
136
+ #: Frames per second actually achieved, which is below ``fps`` whenever frames were dropped.
137
+ measured_fps: float = 0.0
125
138
 
126
139
  @property
127
140
  def ok(self) -> bool:
128
141
  return not self.error and self.frames > 0
129
142
 
143
+ @property
144
+ def fps_clamped(self) -> bool:
145
+ """Whether ``fps`` was adjusted into ``[MIN_FPS, MAX_FPS]``."""
146
+ return self.fps_requested is not None and self.fps_requested != self.fps
147
+
130
148
  def to_dict(self) -> dict:
131
149
  return {
132
150
  "target": self.target,
@@ -135,6 +153,9 @@ class RecordingReport:
135
153
  "backend": self.backend,
136
154
  "frames": self.frames,
137
155
  "fps": round(self.fps, 3),
156
+ "measured_fps": round(self.measured_fps, 3),
157
+ "fps_requested": self.fps_requested,
158
+ "fps_clamped": self.fps_clamped,
138
159
  "duration": round(self.duration, 3),
139
160
  "size": list(self.size) if self.size else None,
140
161
  "bytes": self.bytes_written,
@@ -146,9 +167,11 @@ class RecordingReport:
146
167
  def describe(self) -> str:
147
168
  if self.error:
148
169
  return f"{self.target} -> {self.path}: {self.error}"
170
+ clamped = f" (asked {self.fps_requested:g})" if self.fps_clamped else ""
149
171
  return (
150
172
  f"{self.target} -> {self.path} ({self.format}, {self.frames} frames, "
151
- f"{self.duration:.1f}s, {self.fps:.1f} fps, {self.bytes_written / 1024:.0f} KB)"
173
+ f"{self.duration:.1f}s, {self.fps:g} fps{clamped}, "
174
+ f"{self.bytes_written / 1024:.0f} KB)"
152
175
  )
153
176
 
154
177
 
@@ -325,7 +348,18 @@ class Recorder:
325
348
  self.path = Path(path)
326
349
  self.source = source
327
350
  self.target = target
328
- self.fps = float(fps)
351
+ # Clamp into the supported range rather than refusing or silently producing something else.
352
+ # Below MIN_FPS a recording is a slideshow; above MAX_FPS the capture path is the bottleneck
353
+ # anyway (PrintWindow measured at ~23 fps on this class of machine), so a higher number would
354
+ # only make the recorder drop frames and misrepresent the result.
355
+ self.fps_requested = float(fps)
356
+ self.fps = min(max(float(fps), MIN_FPS), MAX_FPS)
357
+ self.fps_clamped = self.fps != self.fps_requested
358
+ if self.fps_clamped:
359
+ log.info(
360
+ "fps %.3f is outside [%s, %s]; recording at %.3f",
361
+ self.fps_requested, MIN_FPS, MAX_FPS, self.fps,
362
+ )
329
363
  self.duration = duration
330
364
  self.max_frames = max_frames
331
365
  self.format = _resolve_format(self.path) if format is None else (
@@ -405,7 +439,8 @@ class Recorder:
405
439
  path=str(self.path),
406
440
  format=self.format,
407
441
  frames=self._frames,
408
- fps=self._frames / elapsed if elapsed > 0 else 0.0,
442
+ fps=self.fps,
443
+ measured_fps=self._frames / elapsed if elapsed > 0 else 0.0,
409
444
  duration=elapsed,
410
445
  size=self._size,
411
446
  bytes_written=bytes_written,
@@ -413,6 +448,8 @@ class Recorder:
413
448
  dropped=self._dropped,
414
449
  error=error,
415
450
  backend=self._backend,
451
+ # Only reported when it differed: an absent value means "you got what you asked for".
452
+ fps_requested=self.fps_requested if self.fps_clamped else None,
416
453
  )
417
454
 
418
455
  def __enter__(self) -> "Recorder":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: VirtualDesktop
3
- Version: 1.6.3
3
+ Version: 1.6.4
4
4
  Summary: win10-win11 虚拟桌面
5
5
  Author: VirtualDesktop contributors
6
6
  License: MIT License
@@ -572,7 +572,7 @@ with space.record_stream("run.gif", target=window, fps=8, duration=5) as stream:
572
572
 
573
573
  | 参数 | 含义 |
574
574
  |---|---|
575
- | `fps` | 目标帧率(必填,默认 10)。低于目标会被丢弃并计入 `report().dropped` |
575
+ | `fps` | 目标帧率(默认 10)。**范围 1–90**,超出会被钳制:`report().fps` 报实际使用的值,`report().fps_requested` 只在被钳制时给出原值,`report().fps_clamped` 为布尔。低于目标会被丢弃并计入 `report().dropped`;`report().measured_fps` 是达成帧率(跟不上目标时低于 `fps`) |
576
576
  | `duration` | 秒;到点自动停止,`stopped_by="duration"` |
577
577
  | `max_frames` | 帧数上限;到点自动停止,`stopped_by="max_frames"` |
578
578
  | — | 不传 `duration` 时有一个 **1 小时硬上限**(`MAX_DURATION`):录制线程持有 `Recorder` 引用,被丢弃且未 `stop()` 的对象无法回收,没有上限就会一直录下去(并带着 ffmpeg 子进程)。正常用法请始终用 `with` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "VirtualDesktop"
7
- version = "1.6.3"
7
+ version = "1.6.4"
8
8
  description = "win10-win11 虚拟桌面"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -115,6 +115,37 @@ def test_fps_must_be_positive():
115
115
  assert excinfo.value.code == "bad-fps"
116
116
 
117
117
 
118
+ def test_fps_is_clamped_into_the_supported_range():
119
+ """``fps`` outside ``[MIN_FPS, MAX_FPS]`` is clamped, and the request is still reported.
120
+
121
+ Clamping rather than refusing keeps a cosmetic parameter from failing a recording, and reporting
122
+ ``fps_requested`` keeps it from silently producing a file at a rate the caller did not ask for.
123
+ """
124
+ from VirtualDesktop.record import MAX_FPS, MIN_FPS
125
+
126
+ assert (MIN_FPS, MAX_FPS) == (1.0, 90.0)
127
+
128
+ cases = [
129
+ (0.5, MIN_FPS, True),
130
+ (MIN_FPS, MIN_FPS, False),
131
+ (30.0, 30.0, False),
132
+ (MAX_FPS, MAX_FPS, False),
133
+ (MAX_FPS * 2, MAX_FPS, True),
134
+ (1e6, MAX_FPS, True),
135
+ ]
136
+ for requested, expected, clamped in cases:
137
+ recorder = Recorder("unused.gif", lambda: None, fps=requested)
138
+ assert recorder.fps == expected, (requested, recorder.fps)
139
+ assert recorder.fps_clamped is clamped, (requested, recorder.fps_clamped)
140
+ report = recorder.report()
141
+ assert report.fps == expected
142
+ assert report.fps_clamped is clamped
143
+ # The original request is reported only when it was overridden: an absent value means the
144
+ # caller got exactly what they asked for.
145
+ assert report.fps_requested == (float(requested) if clamped else None)
146
+ assert report.to_dict()["fps_clamped"] is clamped
147
+
148
+
118
149
  # -- window recording -----------------------------------------------------------------
119
150
 
120
151
 
File without changes
File without changes