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.
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/PKG-INFO +2 -2
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/README.md +1 -1
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/__init__.py +1 -1
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/record.py +40 -3
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/PKG-INFO +2 -2
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/pyproject.toml +1 -1
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_record.py +31 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/LICENSE +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/__main__.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_bundle.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_compat.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_desktop_runtime.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_devices.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_download.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_enumerate.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_helper.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_log.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_process.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_sigcheck.ps1 +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_sigcheck.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/_winapi.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/agent.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/capture.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/cli.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/config.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/desktop.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/desktop_capture.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/desktop_session.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/display_setup.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/driver.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/driver_manifest.json +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/errors.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/manifest.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/session.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/window.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop/workspace.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/SOURCES.txt +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/dependency_links.txt +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/entry_points.txt +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/requires.txt +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/VirtualDesktop.egg-info/top_level.txt +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/setup.cfg +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_accelerators.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_agent.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_artifact_regression.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_capture.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_cli.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_compat.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_config_and_winapi.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_desktop_isolation.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_destructive_lifecycle.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_display_setup.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_download.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_driver.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_hide_modes.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_integration_windows.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_manifest.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_no_dialogs.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_packaging.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_process.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_virtual_desktop.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_workspace.py +0 -0
- {virtualdesktop-1.6.3 → virtualdesktop-1.6.4}/tests/test_workspace_instances.py +0 -0
- {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
|
+
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` |
|
|
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` |
|
|
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` |
|
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
+
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` |
|
|
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` |
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|